Merge pull request #135 from qmfrederik/features/bin-symlink

Install a symlink to the main executable in /usr/local/bin
This commit is contained in:
Frederik Carlier
2019-12-02 17:49:26 +01:00
committed by GitHub
12 changed files with 162 additions and 9 deletions

View File

@@ -20,7 +20,7 @@ namespace Packaging.Targets.Tests
public void FromDirectoryTest()
{
ArchiveBuilder builder = new ArchiveBuilder();
var entries = builder.FromDirectory("archive", "/opt/demo", Array.Empty<ITaskItem>());
var entries = builder.FromDirectory("archive", null, "/opt/demo", Array.Empty<ITaskItem>());
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);
}
/// <summary>
/// Tests the <see cref="ArchiveBuilder.FromDirectory(string, string, ITaskItem[])"/> method
/// </summary>
[Fact]
public void FromDirectoryWithAppHost()
{
ArchiveBuilder builder = new ArchiveBuilder();
var entries = builder.FromDirectory("archive", "demo", "/opt/demo", Array.Empty<ITaskItem>());
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);
}
}
}

View File

@@ -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
/// <returns>
/// A list of <see cref="ArchiveEntry"/> objects representing the data in the directory.
/// </returns>
public List<ArchiveEntry> FromDirectory(string directory, string prefix, ITaskItem[] metadata)
public List<ArchiveEntry> FromDirectory(string directory, string appHost, string prefix, ITaskItem[] metadata)
{
List<ArchiveEntry> value = new List<ArchiveEntry>();
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<byte>(),
});
}
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;
}

View File

@@ -63,6 +63,13 @@ namespace Packaging.Targets
public string Priority { get; set; }
/// <summary>
/// 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.
/// </summary>
public string AppHost { get; set; }
/// <summary>
/// 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);

View File

@@ -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<byte>()));
}
/// <summary>
/// Adds a symlink entry to a <see cref="CpioFile"/>.
/// </summary>
/// <param name="entry">
/// The symlink entry to add.
/// </param>
/// <param name="cpioFile">
/// The <see cref="CpioFile"/> to which to add the entry.
/// </param>
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)));
}
/// <summary>
/// Adds a file entry to a <see cref="CpioFile"/>.
/// </summary>

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -80,6 +80,13 @@ namespace Packaging.Targets
/// </summary>
public string RpmPackageArchitecture { get; set; }
/// <summary>
/// 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.
/// </summary>
public string AppHost { get; set; }
/// <summary>
/// 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();

View File

@@ -40,6 +40,7 @@ namespace Packaging.Targets
ArchiveBuilder archiveBuilder = new ArchiveBuilder();
var archiveEntries = archiveBuilder.FromDirectory(
this.PublishDir,
null,
this.Prefix,
this.Content);

View File

@@ -23,6 +23,8 @@
<PackageMaintainer Condition="'$(PackageMaintainer)' == '' AND '$(Authors)' == ''">Anonymous &lt;noreply@example.com&gt;</PackageMaintainer>
<PackageDescription Condition="'$(PackageDescription)' == '' AND '$(Description)' != ''">$(Description)</PackageDescription>
<PackageDescription Condition="'$(PackageDescription)' == '' AND '$(Description)' == ''">$(PackageName) version $(PackageVersion) - $(Release)</PackageDescription>
<SymlinkAppHostInBin Condition="'$(SymlinkAppHostInBin)' == ''">true</SymlinkAppHostInBin>
<AppHost Condition="'$(AppHost)' == '' AND '$(SymlinkAppHostInBin)' == 'true'">$(AssemblyName)$(_NativeExecutableExtension)</AppHost>
</PropertyGroup>
</Target>
@@ -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)"

View File

@@ -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")

View File

@@ -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")