diff --git a/Packaging.Targets.Tests/ArchiveBuilderTests.cs b/Packaging.Targets.Tests/ArchiveBuilderTests.cs index 05c5695..0556ca2 100644 --- a/Packaging.Targets.Tests/ArchiveBuilderTests.cs +++ b/Packaging.Targets.Tests/ArchiveBuilderTests.cs @@ -20,7 +20,7 @@ namespace Packaging.Targets.Tests public void FromDirectoryTest() { ArchiveBuilder builder = new ArchiveBuilder(); - var entries = builder.FromDirectory("archive", "/opt/demo", Array.Empty()); + var entries = builder.FromDirectory("archive", null, "/opt/demo", Array.Empty()); Assert.Equal(2, entries.Count); @@ -68,7 +68,7 @@ namespace Packaging.Targets.Tests var taskItem = new TaskItem("script.sh", metadata); var taskItems = new ITaskItem[] { taskItem }; - var entries = builder.FromDirectory("archive", "/opt/demo", taskItems); + var entries = builder.FromDirectory("archive", null, "/opt/demo", taskItems); Assert.Equal(2, entries.Count); @@ -100,5 +100,56 @@ namespace Packaging.Targets.Tests Assert.Equal("/bin/script.sh", script.TargetPathWithFinalSlash); Assert.Equal(ArchiveEntryType.None, script.Type); } + + /// + /// Tests the method + /// + [Fact] + public void FromDirectoryWithAppHost() + { + ArchiveBuilder builder = new ArchiveBuilder(); + var entries = builder.FromDirectory("archive", "demo", "/opt/demo", Array.Empty()); + + Assert.Equal(3, entries.Count); + + var readme = entries[0]; + Assert.Equal("root", readme.Group); + Assert.Equal(1L, readme.Inode); + Assert.False(readme.IsAscii); + Assert.Equal(string.Empty, readme.LinkTo); + Assert.Equal(LinuxFileMode.S_IROTH | LinuxFileMode.S_IRGRP | LinuxFileMode.S_IRUSR | LinuxFileMode.S_IFREG, readme.Mode); + Assert.Equal("root", readme.Owner); + Assert.False(readme.RemoveOnUninstall); + Assert.Equal(Path.Combine("archive", "README.md"), readme.SourceFilename); + Assert.Equal("/opt/demo/README.md", readme.TargetPath); + Assert.Equal("/opt/demo/README.md", readme.TargetPathWithFinalSlash); + Assert.Equal(ArchiveEntryType.None, readme.Type); + + var script = entries[1]; + Assert.Equal("root", script.Group); + Assert.Equal(2L, script.Inode); + Assert.False(script.IsAscii); + Assert.Equal(string.Empty, script.LinkTo); + Assert.Equal(LinuxFileMode.S_IROTH | LinuxFileMode.S_IRGRP | LinuxFileMode.S_IRUSR | LinuxFileMode.S_IFREG, script.Mode); + Assert.Equal("root", script.Owner); + Assert.False(script.RemoveOnUninstall); + Assert.Equal(Path.Combine("archive", "script.sh"), script.SourceFilename); + Assert.Equal("/opt/demo/script.sh", script.TargetPath); + Assert.Equal("/opt/demo/script.sh", script.TargetPathWithFinalSlash); + Assert.Equal(ArchiveEntryType.None, script.Type); + + var symlink = entries[2]; + Assert.Equal("root", symlink.Group); + Assert.Equal(3L, symlink.Inode); + Assert.False(symlink.IsAscii); + Assert.Equal("/opt/demo/demo", symlink.LinkTo); + Assert.Equal(LinuxFileMode.S_IXOTH | LinuxFileMode.S_IROTH | LinuxFileMode.S_IXGRP | LinuxFileMode.S_IRGRP | LinuxFileMode.S_IXUSR | LinuxFileMode.S_IWUSR | LinuxFileMode.S_IRUSR | LinuxFileMode.S_IFLNK, symlink.Mode); + Assert.Equal("root", symlink.Owner); + Assert.False(symlink.RemoveOnUninstall); + Assert.Null(symlink.SourceFilename); + Assert.Equal("/usr/local/bin/demo", symlink.TargetPath); + Assert.Equal("/usr/local/bin/demo", symlink.TargetPathWithFinalSlash); + Assert.Equal(ArchiveEntryType.None, symlink.Type); + } } } diff --git a/Packaging.Targets/ArchiveBuilder.cs b/Packaging.Targets/ArchiveBuilder.cs index 09120ba..6310ac5 100644 --- a/Packaging.Targets/ArchiveBuilder.cs +++ b/Packaging.Targets/ArchiveBuilder.cs @@ -111,8 +111,8 @@ namespace Packaging.Targets } else if (entry.Mode.HasFlag(LinuxFileMode.S_IFLNK)) { - using (var fileStrema = file.Open()) - using (var reader = new StreamReader(fileStrema, Encoding.UTF8)) + using (var fileStream = file.Open()) + using (var reader = new StreamReader(fileStream, Encoding.UTF8)) { entry.LinkTo = reader.ReadToEnd(); } @@ -188,10 +188,28 @@ namespace Packaging.Targets /// /// A list of objects representing the data in the directory. /// - public List FromDirectory(string directory, string prefix, ITaskItem[] metadata) + public List FromDirectory(string directory, string appHost, string prefix, ITaskItem[] metadata) { List value = new List(); this.AddDirectory(directory, string.Empty, prefix, value, metadata); + + // Add a symlink to appHost, if available + if (appHost != null) + { + value.Add( + new ArchiveEntry() + { + Mode = LinuxFileMode.S_IXOTH | LinuxFileMode.S_IROTH | LinuxFileMode.S_IXGRP | LinuxFileMode.S_IRGRP | LinuxFileMode.S_IXUSR | LinuxFileMode.S_IWUSR | LinuxFileMode.S_IRUSR | LinuxFileMode.S_IFLNK, + Modified = DateTimeOffset.UtcNow, + Group = "root", + Owner = "root", + TargetPath = $"/usr/local/bin/{appHost}", + LinkTo = $"{prefix}/{appHost}", + Inode = this.inode++, + Sha256 = Array.Empty(), + }); + } + return value; } @@ -244,7 +262,7 @@ namespace Packaging.Targets { if (fileName.StartsWith(".") || fileStream.Length == 0) { - // Skip hidden and empty files - this would case rmplint errors. + // Skip hidden and empty files - this would case rpmlint errors. return; } diff --git a/Packaging.Targets/DebTask.cs b/Packaging.Targets/DebTask.cs index b660d4d..dbb570a 100644 --- a/Packaging.Targets/DebTask.cs +++ b/Packaging.Targets/DebTask.cs @@ -63,6 +63,13 @@ namespace Packaging.Targets public string Priority { get; set; } + /// + /// Gets or sets the path to the app host. This is a native executable which loads + /// the .NET Core runtime, and invokes the manage assembly. On Linux, it is symlinked + /// into ${prefix}/bin. + /// + public string AppHost { get; set; } + /// /// Gets or sets a list of empty folders to create when /// installing this package. @@ -188,6 +195,7 @@ namespace Packaging.Targets ArchiveBuilder archiveBuilder = new ArchiveBuilder(); var archiveEntries = archiveBuilder.FromDirectory( this.PublishDir, + this.AppHost, this.Prefix, this.Content); diff --git a/Packaging.Targets/IO/CpioFileCreator.cs b/Packaging.Targets/IO/CpioFileCreator.cs index 1bbd28d..ada0014 100644 --- a/Packaging.Targets/IO/CpioFileCreator.cs +++ b/Packaging.Targets/IO/CpioFileCreator.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; namespace Packaging.Targets.IO { @@ -43,6 +44,10 @@ namespace Packaging.Targets.IO { this.AddDirectory(entry, cpioFile); } + else if (entry.Mode.HasFlag(LinuxFileMode.S_IFLNK)) + { + this.AddSymlink(entry, cpioFile); + } else { this.AddFile(entry, cpioFile); @@ -92,6 +97,45 @@ namespace Packaging.Targets.IO cpioFile.Write(directoryHeader, targetPath, new MemoryStream(Array.Empty())); } + /// + /// Adds a symlink entry to a . + /// + /// + /// The symlink entry to add. + /// + /// + /// The to which to add the entry. + /// + public void AddSymlink(ArchiveEntry entry, CpioFile cpioFile) + { + var targetPath = entry.TargetPath; + + if (!targetPath.StartsWith(".")) + { + targetPath = "." + targetPath; + } + + CpioHeader cpioHeader = new CpioHeader() + { + Check = 0, + DevMajor = 1, + DevMinor = 0, + FileSize = entry.FileSize, + Gid = 0, // root + Uid = 0, // root + Ino = entry.Inode, + FileMode = entry.Mode, + LastModified = entry.Modified, + NameSize = (uint)entry.TargetPath.Length + 1, + Nlink = 1, + RDevMajor = 0, + RDevMinor = 0, + Signature = "070701", + }; + + cpioFile.Write(cpioHeader, targetPath, new MemoryStream(Encoding.UTF8.GetBytes(entry.LinkTo))); + } + /// /// Adds a file entry to a . /// diff --git a/Packaging.Targets/IO/TarFileCreator.cs b/Packaging.Targets/IO/TarFileCreator.cs index cadd6d8..db2eebd 100644 --- a/Packaging.Targets/IO/TarFileCreator.cs +++ b/Packaging.Targets/IO/TarFileCreator.cs @@ -107,7 +107,7 @@ namespace Packaging.Targets.IO } else { - type = TarTypeFlag.LnkType; + type = TarTypeFlag.SymType; } bool dispose = false; @@ -137,7 +137,7 @@ namespace Packaging.Targets.IO GroupId = 0, UserId = 0, GroupName = entry.Group, - LinkName = string.Empty, + LinkName = isLink ? entry.LinkTo : string.Empty, Prefix = string.Empty, TypeFlag = type, UserName = entry.Owner, diff --git a/Packaging.Targets/Rpm/RpmMetadata.cs b/Packaging.Targets/Rpm/RpmMetadata.cs index 642b94b..52ebf98 100644 --- a/Packaging.Targets/Rpm/RpmMetadata.cs +++ b/Packaging.Targets/Rpm/RpmMetadata.cs @@ -615,7 +615,7 @@ namespace Packaging.Targets.Rpm modes[i] = (short)file.Mode; rdevs[i] = file.Rdev; mtimes[i] = (int)file.ModifiedTime.ToUnixTimeSeconds(); - md5s[i] = BitConverter.ToString(file.MD5Hash).Replace("-", string.Empty).ToLowerInvariant(); + md5s[i] = file.MD5Hash == null ? string.Empty : BitConverter.ToString(file.MD5Hash).Replace("-", string.Empty).ToLowerInvariant(); linkTos[i] = file.LinkTo; usernames[i] = file.UserName; groupnames[i] = file.GroupName; diff --git a/Packaging.Targets/Rpm/RpmPackageCreator.cs b/Packaging.Targets/Rpm/RpmPackageCreator.cs index de2330d..9184fa8 100644 --- a/Packaging.Targets/Rpm/RpmPackageCreator.cs +++ b/Packaging.Targets/Rpm/RpmPackageCreator.cs @@ -524,6 +524,16 @@ namespace Packaging.Targets.Rpm size = 0x1000; } + if (entry.Mode.HasFlag(LinuxFileMode.S_IFLNK)) + { + // RPM uses cpio archives. cpio archives store the target of a symlink as + // the file content (as opposed to tar, which has a dedicated field for this). + // As a result, the file size is equal to the length of the path of the link + // target. + // https://bugzilla.redhat.com/show_bug.cgi?id=1224555 has some background info. + size = (uint)Encoding.UTF8.GetByteCount(entry.LinkTo); + } + RpmFile file = new RpmFile() { Size = (int)size, diff --git a/Packaging.Targets/RpmTask.cs b/Packaging.Targets/RpmTask.cs index 194e742..5f7e043 100644 --- a/Packaging.Targets/RpmTask.cs +++ b/Packaging.Targets/RpmTask.cs @@ -80,6 +80,13 @@ namespace Packaging.Targets /// public string RpmPackageArchitecture { get; set; } + /// + /// Gets or sets the path to the app host. This is a native executable which loads + /// the .NET Core runtime, and invokes the manage assembly. On Linux, it is symlinked + /// into ${prefix}/bin. + /// + public string AppHost { get; set; } + /// /// Gets or sets a list of empty folders to create when /// installing this package. @@ -263,10 +270,12 @@ namespace Packaging.Targets ArchiveBuilder archiveBuilder = new ArchiveBuilder(); var archiveEntries = archiveBuilder.FromDirectory( this.PublishDir, + this.AppHost, this.Prefix, this.Content); archiveEntries.AddRange(archiveBuilder.FromLinuxFolders(this.LinuxFolders)); + archiveEntries = archiveEntries .OrderBy(e => e.TargetPathWithFinalSlash, StringComparer.Ordinal) .ToList(); diff --git a/Packaging.Targets/TarballTask.cs b/Packaging.Targets/TarballTask.cs index a027ef1..2b4225d 100644 --- a/Packaging.Targets/TarballTask.cs +++ b/Packaging.Targets/TarballTask.cs @@ -40,6 +40,7 @@ namespace Packaging.Targets ArchiveBuilder archiveBuilder = new ArchiveBuilder(); var archiveEntries = archiveBuilder.FromDirectory( this.PublishDir, + null, this.Prefix, this.Content); diff --git a/Packaging.Targets/build/Packaging.Targets.targets b/Packaging.Targets/build/Packaging.Targets.targets index 3353874..dcadbc3 100644 --- a/Packaging.Targets/build/Packaging.Targets.targets +++ b/Packaging.Targets/build/Packaging.Targets.targets @@ -23,6 +23,8 @@ Anonymous <noreply@example.com> $(Description) $(PackageName) version $(PackageVersion) - $(Release) + true + $(AssemblyName)$(_NativeExecutableExtension) @@ -90,6 +92,7 @@ Version="$(PackageVersion)" Release="$(Release)" PackageName="$(PackageName)" + AppHost="$(AppHost)" Content="@(Content)" LinuxFolders="@(LinuxFolder)" RpmDotNetDependencies="@(RpmDotNetDependency)" @@ -158,6 +161,7 @@ Prefix="$(Prefix)" Version="$(PackageVersion)" PackageName="$(PackageName)" + AppHost="$(AppHost)" Content="@(Content)" LinuxFolders="@(LinuxFolder)" DebDependencies="@(DebDependency)" diff --git a/molecule/framework-dependent/molecule/default/tests/test_default.py b/molecule/framework-dependent/molecule/default/tests/test_default.py index e6eb247..c81eff3 100644 --- a/molecule/framework-dependent/molecule/default/tests/test_default.py +++ b/molecule/framework-dependent/molecule/default/tests/test_default.py @@ -8,3 +8,7 @@ testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( def test_package_is_runnable(host): host.run_test("/usr/share/framework-dependent-app/framework-dependent-app") + + +def test_package_symlink_is_runnable(host): + host.run_test("/usr/local/bin/framework-dependent-app") diff --git a/molecule/self-contained/molecule/default/tests/test_default.py b/molecule/self-contained/molecule/default/tests/test_default.py index 7d230fe..795f49b 100644 --- a/molecule/self-contained/molecule/default/tests/test_default.py +++ b/molecule/self-contained/molecule/default/tests/test_default.py @@ -8,3 +8,7 @@ testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( def test_package_is_runnable(host): host.run_test("/usr/share/self-contained-app/self-contained-app") + + +def test_package_symlink_is_runnable(host): + host.run_test("/usr/local/bin/self-contained-app")