Skip to content

Commit 1818ed2

Browse files
authored
Add .NET-based tooling to construct RPM packages (#15191)
1 parent 12f9567 commit 1818ed2

File tree

16 files changed

+1606
-12
lines changed

16 files changed

+1606
-12
lines changed

src/Microsoft.DotNet.Build.Tasks.Installers/build/installer.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
<UsingTask TaskName="CreateControlFile"
2222
AssemblyFile="$(MicrosoftDotNetBuildTasksInstallersTaskAssembly)" />
2323
<UsingTask TaskName="CreateDebPackage"
24-
AssemblyFile="$(MicrosoftDotNetBuildTasksInstallersTaskAssembly)" />
24+
AssemblyFile="$(MicrosoftDotNetBuildTasksInstallersTaskAssembly)" />
25+
<UsingTask TaskName="CreateRpmPackage"
26+
AssemblyFile="$(MicrosoftDotNetBuildTasksInstallersTaskAssembly)" />
2527

2628
</Project>

src/Microsoft.DotNet.Build.Tasks.Installers/build/installer.singlerid.targets

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,13 @@
8080
BuildInParallel="$(BuildInParallel)" />
8181
</Target>
8282

83+
<PropertyGroup>
84+
<_CreateRpmTargets Condition="'$(UseArcadeRpmTooling)' != 'true'">FindFPM;CreateRpmUsingFpm</_CreateRpmTargets>
85+
<_CreateRpmTargets Condition="'$(UseArcadeRpmTooling)' == 'true'">CreateRpm</_CreateRpmTargets>
86+
</PropertyGroup>
87+
8388
<Target Name="GenerateDeb" DependsOnTargets="CreateDeb" />
84-
<Target Name="GenerateRpm" DependsOnTargets="FindFPM;CreateRpm" />
89+
<Target Name="GenerateRpm" DependsOnTargets="$(_CreateRpmTargets)" />
8590
<Target Name="GenerateMsi" DependsOnTargets="CreateWixInstaller" />
8691
<Target Name="GenerateCrossArchMsi" DependsOnTargets="CreateCrossArchWixInstaller" />
8792
<Target Name="GeneratePkg" DependsOnTargets="CreatePkg" />
@@ -292,7 +297,7 @@
292297

293298
<!-- Create any symlinks -->
294299
<MakeDir Directories="$(_LayoutDataRoot)%(DebSymlink.RelativeDir)" Condition="'%(DebSymlink.Identity)' != ''" />
295-
<Exec Command="ln -s %(DebSymlink.LinkTarget) %(DebSymlink.FileName)%(DebSymlink.Extension)" WorkingDirectory="$(_LayoutDataRoot)%(DebSymlink.RelativeDir)" Condition="'%(DebSymlink.Identity)' != ''" />
300+
<Exec Command="ln -sf %(DebSymlink.LinkTarget) %(DebSymlink.FileName)%(DebSymlink.Extension)" WorkingDirectory="$(_LayoutDataRoot)%(DebSymlink.RelativeDir)" Condition="'%(DebSymlink.Identity)' != ''" />
296301

297302
<Exec Command="tar -C '$(_LayoutControlRoot)' -czf '$(_LayoutDirectory)/control.tar.gz' ."
298303
IgnoreExitCode="true"
@@ -314,6 +319,104 @@
314319
Create RPM package.
315320
-->
316321
<Target Name="CreateRpm"
322+
DependsOnTargets="
323+
_GetInstallerProperties;
324+
_CreateInstallerLayout;
325+
GetRpmInstallerJsonProperties"
326+
Returns="$(_InstallerFile)">
327+
328+
<PropertyGroup>
329+
<_LayoutDirectory>$(IntermediateOutputPath)installer/rpmLayout/</_LayoutDirectory>
330+
<_LayoutDataRoot>$(_LayoutDirectory)root/</_LayoutDataRoot>
331+
<_LayoutPackageRoot>$(_LayoutDataRoot)$(LinuxInstallRoot)</_LayoutPackageRoot>
332+
<_LayoutDocs>$(_LayoutDataRoot)/usr/share/doc</_LayoutDocs>
333+
</PropertyGroup>
334+
335+
<RemoveDir Condition="Exists('$(_InstallerIntermediatesDir)')" Directories="$(_InstallerIntermediatesDir)" />
336+
<MakeDir Directories="$(_InstallerIntermediatesDir)" />
337+
338+
<!-- Create empty layout. -->
339+
<RemoveDir Condition="Exists('$(_LayoutDirectory)')" Directories="$(_LayoutDirectory)" />
340+
<MakeDir Directories="$(_LayoutDirectory)" />
341+
<MakeDir Directories="$(_LayoutPackageRoot)" />
342+
<MakeDir Directories="$(_LayoutDocs)" />
343+
344+
<MSBuild Projects="$(MSBuildProjectFullPath)"
345+
Targets="PublishToDisk"
346+
Properties="OutputPath=$([MSBuild]::EnsureTrailingSlash('$(_LayoutPackageRoot)'))"
347+
RemoveProperties="@(_GlobalPropertiesToRemoveForPublish)" />
348+
349+
<Copy
350+
SourceFiles="@(Manpage)"
351+
DestinationFiles="@(Manpage->'$(_LayoutDocs)/%(RecursiveDir)%(Filename)%(Extension)')" />
352+
353+
<!-- Write copyright file in the debian format. There's no defined format for RPM copyright files. -->
354+
<ItemGroup>
355+
<_CopyrightLine Include="Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/" />
356+
<_CopyrightLine Include="%0AFiles: *" />
357+
<_CopyrightLine Include="Copyright: $(_PackageCopyright)" />
358+
<_CopyrightLine Include="Comment: Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license." />
359+
<_CopyrightLine Include="License: MIT and ASL 2.0 and BSD" />
360+
<_CopyrightLine Include="%0ALicense: $([System.IO.File]::ReadAllText('$(LicenseFile)'))" />
361+
</ItemGroup>
362+
363+
<WriteLinesToFile File="$(_LayoutDocs)/$(VersionedInstallerName)/copyright" Lines="@(_CopyrightLine)" />
364+
365+
<!-- We always own the directory for the copyright file. -->
366+
<ItemGroup>
367+
<_GeneratedFileOwnedDirectory Include="$(_LayoutDocs)/$(VersionedInstallerName)" />
368+
</ItemGroup>
369+
370+
<!-- Create any symlinks -->
371+
<MakeDir Directories="$(_LayoutDataRoot)%(DebSymlink.RelativeDir)" Condition="'%(DebSymlink.Identity)' != ''" />
372+
<Exec Command="ln -sf %(DebSymlink.LinkTarget) %(DebSymlink.FileName)%(DebSymlink.Extension)" WorkingDirectory="$(_LayoutDataRoot)%(DebSymlink.RelativeDir)" Condition="'%(DebSymlink.Identity)' != ''" />
373+
374+
<!--
375+
Compress all files and directories that we output.
376+
We'll filter out any directories we don't own in CreateRpmPackage.
377+
-->
378+
<PropertyGroup>
379+
<_FindAllPayloadFilesCommand>find . -depth ! -wholename '.'</_FindAllPayloadFilesCommand>
380+
</PropertyGroup>
381+
<Exec Command="$(_FindAllPayloadFilesCommand) -print | cpio -H newc -o --quiet > '$(_LayoutDirectory)/payload.cpio'"
382+
WorkingDirectory="$(_LayoutDataRoot)"
383+
EchoOff="true"
384+
StandardOutputImportance="Low" />
385+
386+
<!-- For each filesystem entry, execute the 'file' command. We need to both shell-escape and MSBuild-escape the semicolon -->
387+
<Exec Command="$(_FindAllPayloadFilesCommand) -exec file {} \%3B"
388+
ConsoleToMsBuild="true"
389+
EchoOff="true"
390+
StandardOutputImportance="Low"
391+
WorkingDirectory="$(_LayoutDataRoot)">
392+
<Output TaskParameter="ConsoleOutput" ItemName="_RawFileKindInfo" />
393+
</Exec>
394+
395+
<CreateRpmPackage
396+
OutputRpmPackagePath="$(_InstallerFile)"
397+
Vendor=".NET Foundation"
398+
Packager=".NET Team &lt;[email protected]&gt;"
399+
PackageName="$(VersionedInstallerName)"
400+
PackageVersion="$(InstallerPackageVersion)"
401+
PackageRelease="$(InstallerPackageRelease)"
402+
PackageOS="$(TargetRuntimeOS)"
403+
PackageArchitecture="$(InstallerTargetArchitecture)"
404+
Payload="$(_LayoutDirectory)/payload.cpio"
405+
RawPayloadFileKinds="@(_RawFileKindInfo)"
406+
Requires="@(LinuxPackageDependency)"
407+
Conflicts="@(RpmConflicts)"
408+
OwnedDirectories="@(RpmOwnedDirectory);@(_GeneratedFileOwnedDirectory)"
409+
ChangelogLines="- https://github.com/dotnet/core/tree/master/release-notes"
410+
License="MIT and ASL 2.0 and BSD"
411+
Summary="$(_ShortDescription)"
412+
Description="$(_PackageLongDescription)"
413+
PackageUrl="https://github.com/dotnet/core"
414+
/>
415+
416+
<Message Text="$(MSBuildProjectName) -> $(_InstallerFile)" Importance="high" />
417+
</Target>
418+
419+
<Target Name="CreateRpmUsingFpm"
317420
DependsOnTargets="
318421
_GetInstallerProperties;
319422
_CreateInstallerLayout;
@@ -367,10 +470,13 @@
367470
UseHardlinksIfPossible="False" />
368471

369472
<Message Text="$(MSBuildProjectName) -> $(_InstallerFile)" Importance="high" />
473+
</Target>
370474

475+
<Target Name="_BuildMarinerRpms"
476+
AfterTargets="GenerateRpm"
477+
Condition="'$(CreateRPMForCblMariner)' == 'true'">
371478
<!-- CBL-Mariner 1.0 -->
372-
<Copy Condition="'$(CreateRPMForCblMariner)' == 'true'"
373-
SourceFiles="@(GeneratedRpmFiles)"
479+
<Copy SourceFiles="$(_InstallerFile)"
374480
DestinationFiles="$(_InstallerFileCblMariner)"
375481
OverwriteReadOnlyFiles="True"
376482
SkipUnchangedFiles="False"
@@ -379,8 +485,7 @@
379485
<Message Text="$(MSBuildProjectName) -> $(_InstallerFileCblMariner)" Importance="high" />
380486

381487
<!-- CBL-Mariner 2.0 -->
382-
<Copy Condition="'$(CreateRPMForCblMariner)' == 'true'"
383-
SourceFiles="@(GeneratedRpmFiles)"
488+
<Copy SourceFiles="$(_InstallerFile)"
384489
DestinationFiles="$(_InstallerFileCblMariner2)"
385490
OverwriteReadOnlyFiles="True"
386491
SkipUnchangedFiles="False"

src/Microsoft.DotNet.Build.Tasks.Installers/src/ArReader.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,10 @@ internal sealed class ArReader(Stream stream, bool leaveOpen) : IDisposable
6969
throw new InvalidDataException("Invalid archive magic");
7070
}
7171

72-
MemoryStream dataStream = new MemoryStream();
73-
for (ulong remaining = size; remaining > 0; remaining -= int.MaxValue)
74-
{
75-
stream.CopyTo(dataStream, (int)Math.Min(remaining, int.MaxValue));
76-
}
72+
byte[] data = new byte[size];
73+
ReadExactly(data, 0, checked((int)size));
74+
75+
MemoryStream dataStream = new MemoryStream(data);
7776

7877
if (size % 2 == 1)
7978
{
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.IO.Compression;
8+
using System.Linq;
9+
using System.Runtime.CompilerServices;
10+
using System.Text;
11+
using System.Threading.Tasks;
12+
13+
namespace Microsoft.DotNet.Build.Tasks.Installers
14+
{
15+
internal sealed class CpioEntry(ulong inode, string name, ulong timestamp, ulong ownerID, ulong groupID, uint mode, ushort numberOfLinks, ulong devMajor, ulong devMinor, ulong rdevMajor, ulong rdevMinor, MemoryStream dataStream)
16+
{
17+
public const uint FileKindMask = 0xF000;
18+
19+
public const uint RegularFile = 0x8000;
20+
21+
public const uint SymbolicLink = 0xA000;
22+
23+
public const uint Directory = 0x4000;
24+
25+
public ulong Inode { get; } = inode;
26+
public string Name { get; } = name;
27+
public ulong Timestamp { get; } = timestamp;
28+
public ulong OwnerID { get; } = ownerID;
29+
public ulong GroupID { get; } = groupID;
30+
public uint Mode { get; } = mode;
31+
public MemoryStream DataStream { get; } = dataStream;
32+
public ushort NumberOfLinks { get; } = numberOfLinks;
33+
public ulong DevMajor { get; } = devMajor;
34+
public ulong DevMinor { get; } = devMinor;
35+
public ulong RDevMajor { get; } = rdevMajor;
36+
public ulong RDevMinor { get; } = rdevMinor;
37+
38+
public CpioEntry WithName(string name)
39+
{
40+
return new CpioEntry(Inode, name, Timestamp, OwnerID, GroupID, Mode, NumberOfLinks, DevMajor, DevMinor, RDevMajor, RDevMinor, DataStream);
41+
}
42+
}
43+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Globalization;
7+
using System.IO;
8+
using System.Linq;
9+
using System.Runtime.CompilerServices;
10+
using System.Text;
11+
using System.Threading.Tasks;
12+
13+
#nullable enable
14+
15+
namespace Microsoft.DotNet.Build.Tasks.Installers
16+
{
17+
internal sealed class CpioReader(Stream stream, bool leaveOpen) : IDisposable
18+
{
19+
public CpioEntry? GetNextEntry()
20+
{
21+
if (stream.Position == stream.Length)
22+
{
23+
return null;
24+
}
25+
26+
byte[] magic = new byte[6];
27+
stream.ReadExactly(magic, 0, 6);
28+
if (!magic.AsSpan().SequenceEqual("070701"u8))
29+
{
30+
throw new InvalidDataException("Invalid header magic");
31+
}
32+
33+
byte[] byteBuffer = new byte[8];
34+
35+
stream.ReadExactly(byteBuffer, 0, 8);
36+
ulong inode = ulong.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
37+
38+
stream.ReadExactly(byteBuffer, 0, 8);
39+
uint mode = uint.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
40+
41+
stream.ReadExactly(byteBuffer, 0, 8);
42+
ulong ownerID = ulong.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
43+
44+
stream.ReadExactly(byteBuffer, 0, 8);
45+
ulong groupID = ulong.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
46+
47+
stream.ReadExactly(byteBuffer, 0, 8);
48+
ushort numberOfLinks = ushort.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
49+
50+
stream.ReadExactly(byteBuffer, 0, 8);
51+
ulong timestamp = ulong.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
52+
53+
stream.ReadExactly(byteBuffer, 0, 8);
54+
ulong size = ulong.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
55+
56+
stream.ReadExactly(byteBuffer, 0, 8);
57+
ulong devMajor = ulong.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
58+
59+
stream.ReadExactly(byteBuffer, 0, 8);
60+
ulong devMinor = ulong.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
61+
62+
stream.ReadExactly(byteBuffer, 0, 8);
63+
ulong rdevMajor = ulong.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
64+
65+
stream.ReadExactly(byteBuffer, 0, 8);
66+
ulong rdevMinor = ulong.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
67+
68+
stream.ReadExactly(byteBuffer, 0, 8);
69+
int namesize = int.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
70+
71+
// Ignore checksum
72+
stream.ReadExactly(byteBuffer, 0, 8);
73+
_ = int.Parse(Encoding.ASCII.GetString(byteBuffer), NumberStyles.HexNumber);
74+
75+
byte[] nameBuffer = new byte[namesize];
76+
stream.ReadExactly(nameBuffer, 0, namesize);
77+
string name = Encoding.ASCII.GetString(nameBuffer, 0, namesize - 1);
78+
79+
stream.AlignReadTo(4);
80+
81+
byte[] data = new byte[size];
82+
stream.ReadExactly(data, 0, (int)size);
83+
MemoryStream dataStream = new(data);
84+
85+
stream.AlignReadTo(4);
86+
87+
if (name == "TRAILER!!!")
88+
{
89+
// We've reached the end of the archive.
90+
return null;
91+
}
92+
93+
return new CpioEntry(
94+
inode,
95+
name,
96+
timestamp,
97+
ownerID,
98+
groupID,
99+
mode,
100+
numberOfLinks,
101+
devMajor,
102+
devMinor,
103+
rdevMajor,
104+
rdevMinor,
105+
dataStream);
106+
}
107+
108+
public void Dispose()
109+
{
110+
if (!leaveOpen)
111+
{
112+
stream.Dispose();
113+
}
114+
}
115+
}
116+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Globalization;
7+
using System.IO;
8+
using System.Linq;
9+
using System.Runtime.CompilerServices;
10+
using System.Text;
11+
using System.Threading.Tasks;
12+
13+
#nullable enable
14+
15+
namespace Microsoft.DotNet.Build.Tasks.Installers
16+
{
17+
internal sealed class CpioWriter(Stream stream, bool leaveOpen) : IDisposable
18+
{
19+
public void WriteNextEntry(CpioEntry entry)
20+
{
21+
stream.Write("070701"u8.ToArray());
22+
23+
using StreamWriter writer = new(stream, Encoding.ASCII, bufferSize: -1, leaveOpen: true);
24+
25+
writer.Write(entry.Inode.ToString("x8"));
26+
writer.Write(entry.Mode.ToString("x8"));
27+
writer.Write(entry.OwnerID.ToString("x8"));
28+
writer.Write(entry.GroupID.ToString("x8"));
29+
writer.Write(entry.NumberOfLinks.ToString("x8"));
30+
writer.Write(entry.Timestamp.ToString("x8"));
31+
writer.Write(entry.DataStream.Length.ToString("x8"));
32+
writer.Write(entry.DevMajor.ToString("x8"));
33+
writer.Write(entry.DevMinor.ToString("x8"));
34+
writer.Write(entry.RDevMajor.ToString("x8"));
35+
writer.Write(entry.RDevMinor.ToString("x8"));
36+
writer.Write((entry.Name.Length + 1).ToString("x8")); // This field should include the null terminator.
37+
writer.Flush();
38+
stream.Write("00000000"u8.ToArray()); // Check field
39+
writer.Write(entry.Name);
40+
writer.Flush();
41+
stream.WriteByte(0);
42+
stream.AlignWriteTo(4);
43+
entry.DataStream.CopyTo(stream);
44+
stream.AlignWriteTo(4);
45+
}
46+
47+
public void Dispose()
48+
{
49+
CpioEntry trailerEntry = new(0, "TRAILER!!!", 0, 0, 0, 0, 0, 0, 0, 0, 0, new MemoryStream());
50+
WriteNextEntry(trailerEntry);
51+
if (!leaveOpen)
52+
{
53+
stream.Dispose();
54+
}
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)