File: src\CreateRpmPackage.cs
Web Access
Project: src\src\Microsoft.DotNet.Build.Tasks.Installers\Microsoft.DotNet.Build.Tasks.Installers.csproj (Microsoft.DotNet.Build.Tasks.Installers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Build.Framework;
 
namespace Microsoft.DotNet.Build.Tasks.Installers.src
{
    /// <summary>
    /// Create a .deb package from a control file and a data file.
    /// </summary>
    /// <remarks>
    /// Implements the format specified in https://manpages.debian.org/bookworm/dpkg-dev/deb.5.en.html
    /// </remarks>
    public sealed class CreateRpmPackage : BuildTask
    {
        private static readonly DateTime UnixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
 
        [Required]
        public string OutputRpmPackagePath { get; set; } = "";
        [Required]
        public string Vendor { get; set; } = "";
        [Required]
        public string Packager { get; set; } = "";
        [Required]
        public string PackageName { get; set; } = "";
        [Required]
        public string PackageVersion { get; set; } = "";
        [Required]
        public string PackageRelease { get; set; } = "";
        [Required]
        public string PackageOS { get; set; } = "";
        [Required]
        public string PackageArchitecture { get; set; } = "";
        [Required]
        public string Payload { get; set; } = "";
        /// <summary>
        ///  A list of file kinds that are included in the payload. Each item is the output from the 'file'
        ///  tool on the path of the file in the payload.
        /// </summary>
        [Required]
        public ITaskItem[] RawPayloadFileKinds { get; set; } = [];
        [Required]
        public ITaskItem[] Requires { get; set; } = [];
        [Required]
        public ITaskItem[] Conflicts { get; set; } = [];
        [Required]
        public ITaskItem[] OwnedDirectories { get; set; } = [];
        [Required]
        public ITaskItem[] ChangelogLines { get; set; } = [];
        [Required]
        public string License { get; set; } = "";
        [Required]
        public string Summary { get; set; } = "";
        [Required]
        public string Description { get; set; } = "";
        [Required]
        public string PackageUrl { get; set; } = "";
        [Required]
        public ITaskItem[] Scripts { get; set; } = [];
 
        public override bool Execute()
        {
            var arch = PackageArchitecture switch
            {
                "x86" => Architecture.X86,
                "x64" => Architecture.X64,
                "arm" => Architecture.Arm,
                "arm64" => Architecture.Arm64,
#if NET
                "armv6" => Architecture.Armv6,
                "s390x" => Architecture.S390x,
                "ppc64le" => Architecture.Ppc64le,
                "riscv64" => Architecture.RiscV64,
                "loongarch64"  => Architecture.LoongArch64,
#endif
                _ => throw new ArgumentException($"Unknown architecture: {PackageArchitecture}")
            };
            RpmBuilder builder = new(PackageName, PackageVersion, PackageRelease, arch, OSPlatform.Create(PackageOS))
            {
                Vendor = Vendor,
                Packager = Packager,
                License = License,
                Summary = Summary,
                Description = Description,
                Url = PackageUrl
            };
 
            foreach (ITaskItem require in Requires)
            {
                builder.AddRequiredCapability(require.ItemSpec, require.GetMetadata("Version"));
            }
 
            foreach (ITaskItem conflict in Conflicts)
            {
                builder.AddConflict(conflict.ItemSpec);
            }
 
            foreach (ITaskItem changelogLine in ChangelogLines)
            {
                builder.AddChangelogLine(Packager, changelogLine.ItemSpec);
            }
 
            builder.AddProvidedCapability(PackageName, PackageVersion);
            builder.AddProvidedCapability($"{PackageName}({RpmBuilder.GetRpmHeaderArchitecture(arch)})", PackageVersion);
 
            HashSet<string> ownedDirectories = new(OwnedDirectories.Select(d => d.ItemSpec));
 
            foreach (ITaskItem script in Scripts)
            {
                builder.AddScript(script.GetMetadata("Kind"), File.ReadAllText(script.ItemSpec));
            }
 
            using (CpioReader reader = new(File.OpenRead(Payload), leaveOpen: false))
            {
                Dictionary<string, string> filePathToKind = RawPayloadFileKinds.Select(k => k.ItemSpec.Split(':')).ToDictionary(k => k[0], k => k[1].Trim());
                for (CpioEntry entry = reader.GetNextEntry(); entry is not null; entry = reader.GetNextEntry())
                {
                    if ((entry.Mode & CpioEntry.FileKindMask) == CpioEntry.Directory)
                    {
                        // Only include directories we own.
                        if (!ownedDirectories.Contains(entry.Name))
                        {
                            continue;
                        }
                    }
                    // RPM requires the CPIO entries to be rooted in a relative root of './'.
                    // The cpio tool doesn't want to do this, so we do it here.
                    entry = entry.WithName($"./{entry.Name}");
                    string kind = filePathToKind[entry.Name];
                    // Symlinks may be broken when we are assembling the package, but will not be when we install the package.
                    // Update the file kinds to not say "broken symlink".
                    kind = kind.Replace("broken symlink", "symlink");
                    builder.AddFile(entry, kind);
                }
            }
 
            RpmPackage package = builder.Build();
 
            using FileStream rpmPackageStream = new(OutputRpmPackagePath, FileMode.Create, FileAccess.Write);
 
            package.WriteTo(rpmPackageStream);
            return true;
        }
    }
}