Files
duplicati/ReleaseBuilder/CliCommand/Build.CreatePackage.cs
T

331 lines
16 KiB
C#

using System.IO.Compression;
namespace ReleaseBuilder.CliCommand;
public static partial class Build
{
/// <summary>
/// Implementations for the package builds
/// </summary>
private static class CreatePackage
{
/// <summary>
/// Builds the packages for the specified build targets.
/// </summary>
/// <param name="baseDir">The base directory.</param>
/// <param name="buildRoot">The build root directory.</param>
/// <param name="buildTargets">The build targets.</param>
/// <param name="keepBuilds">A flag indicating whether to keep the build files.</param>
/// <param name="rtcfg">The runtime configuration.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task BuildPackages(string baseDir, string buildRoot, IEnumerable<PackageTarget> buildTargets, bool keepBuilds, RuntimeConfig rtcfg)
{
var packagesToBuild = buildTargets.Distinct().ToList();
if (packagesToBuild.Count == 1)
Console.WriteLine($"Building single package: {packagesToBuild.First().PackageTargetString}");
else
Console.WriteLine($"Building {packagesToBuild.Count} packages");
foreach (var target in packagesToBuild)
{
Console.WriteLine($"Building {target.PackageTargetString} ...");
await BuildPackage(baseDir, buildRoot, target, rtcfg, keepBuilds);
Console.WriteLine("Completed!");
}
}
/// <summary>
/// Builds the package for the given target
/// </summary>
/// <param name="baseDir">The source folder base</param>
/// <param name="releaseInfo">The release info to use</param>
/// <param name="rtcfg">The runtime configuration</param>
/// <param name="keepBuilds">A flag that allows re-using existing builds</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
static async Task BuildPackage(string baseDir, string buildRoot, PackageTarget target, RuntimeConfig rtcfg, bool keepBuilds)
{
var packageFolder = Path.Combine(buildRoot, "packages");
if (!Directory.Exists(packageFolder))
Directory.CreateDirectory(packageFolder);
var packageFile = Path.Combine(packageFolder, $"{rtcfg.ReleaseInfo.ReleaseName}-{target.PackageTargetString}");
if (File.Exists(packageFile))
{
if (keepBuilds)
{
Console.WriteLine($"Package file already exists, skipping package build for {target.PackageTargetString}");
return;
}
File.Delete(packageFile);
}
var tempFile = Path.Combine(packageFolder, $"tmp-{rtcfg.ReleaseInfo.ReleaseName}-{target.PackageTargetString}");
if (File.Exists(tempFile))
File.Delete(tempFile);
switch (target.Package)
{
case PackageType.Zip:
await BuildZipPackage(buildRoot, tempFile, target, rtcfg);
break;
case PackageType.MSI:
await BuildMsiPackage(baseDir, buildRoot, tempFile, target, rtcfg);
break;
case PackageType.DMG:
await BuildMacDmgPackage(baseDir, buildRoot, tempFile, target, rtcfg);
break;
case PackageType.MacPkg:
await BuildMacPkgPackage(baseDir, buildRoot, tempFile, target, rtcfg);
break;
case PackageType.Deb:
await BuildDebPackage(baseDir, buildRoot, tempFile, target, rtcfg);
break;
// case PackageType.SynologySpk:
// await BuildZipPackage(buildRoot, tempFile, target, rtcfg);
// await SignSynologyPackage(Path.Combine(outputFolder, target.PackageTargetString), rtcfg);
// break;
default:
throw new Exception($"Unsupported package type: {target.Package}");
}
File.Move(tempFile, packageFile);
}
/// <summary>
/// Builds a zip package asynchronously.
/// </summary>
/// <param name="buildRoot">The output folder where the zip package will be created.</param>
/// <param name="zipFile">The zip file to generate.</param>
/// <param name="target">The package target.</param>
/// <param name="rtcfg">The runtime configuration.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
static async Task BuildZipPackage(string buildRoot, string zipFile, PackageTarget target, RuntimeConfig rtcfg)
{
if (File.Exists(zipFile))
File.Delete(zipFile);
using (ZipArchive zip = ZipFile.Open(zipFile, ZipArchiveMode.Create))
{
foreach (var f in Directory.EnumerateFiles(Path.Combine(buildRoot, target.BuildTargetString), "*", SearchOption.AllDirectories))
{
var entry = zip.CreateEntry(Path.GetRelativePath(buildRoot, f), CompressionLevel.Optimal);
using (var stream = entry.Open())
using (var file = File.OpenRead(f))
await file.CopyToAsync(stream);
}
}
}
/// <summary>
/// Builds an MSI package asynchronously.
/// </summary>
/// <param name="baseDir">The source base directory.</param>
/// <param name="buildRoot">The root directory of the build.</param>
/// <param name="msiFile">The MSI file to generate.</param>
/// <param name="target">The package target.</param>
/// <param name="rtcfg">The runtime configuration.</param>
/// <returns>A task representing the asynchronous operation.</returns>
static async Task BuildMsiPackage(string baseDir, string buildRoot, string msiFile, PackageTarget target, RuntimeConfig rtcfg)
{
var installerDir = Path.Combine(baseDir, "Installer", "Windows");
var binFiles = Path.Combine(installerDir, "binfiles.wxs");
var sourceFiles = Path.Combine(buildRoot, target.BuildTargetString);
if (!sourceFiles.EndsWith(Path.DirectorySeparatorChar))
sourceFiles += Path.DirectorySeparatorChar;
File.WriteAllText(binFiles, WixHeatBuilder.CreateWixFilelist(sourceFiles));
await ProcessHelper.Execute(new[] {
Program.Configuration.Commands.Wix!,
"--define", $"HarvestPath={sourceFiles}",
"--arch", target.ArchString,
"--output", msiFile,
Path.Combine(installerDir, "Shortcuts.wxs"),
binFiles,
Path.Combine(installerDir, "Duplicati.wxs")
}, workingDirectory: buildRoot);
if (rtcfg.UseAuthenticodeSigning)
await rtcfg.AuthenticodeSign(msiFile);
}
/// <summary>
/// Builds a DMG package asynchronously.
/// </summary>
/// <param name="baseDir">The source base directory.</param>
/// <param name="buildRoot">The root directory of the build.</param>
/// <param name="dmgFile">The DMG file to generate.</param>
/// <param name="target">The package target.</param>
/// <param name="rtcfg">The runtime configuration.</param>
/// <returns>A task representing the asynchronous operation.</returns>
static async Task BuildMacDmgPackage(string baseDir, string buildRoot, string dmgFile, PackageTarget target, RuntimeConfig rtcfg)
{
var mountDir = Path.Combine(buildRoot, "mount");
if (Directory.Exists(mountDir))
{
await ProcessHelper.Execute([
"hdiutil", "detach", mountDir, "-quiet", "-force",
], workingDirectory: buildRoot, codeIsError: _ => false);
Directory.Delete(mountDir, false);
}
Directory.CreateDirectory(mountDir);
var installerDir = Path.Combine(baseDir, "Installer", "MacOS");
var compressedDmg = Path.Combine(installerDir, "template.dmg.bz2");
if (!File.Exists(compressedDmg))
throw new FileNotFoundException($"Compressed dmg template file not found: {compressedDmg}");
// Remove the bz2
var templateDmg = Path.Combine(buildRoot, Path.GetFileNameWithoutExtension(compressedDmg));
if (File.Exists(templateDmg))
File.Delete(templateDmg);
// Decompress the dmg
using (var fs = File.Create(templateDmg))
await ProcessHelper.ExecuteWithOutput([
"bzip2", "--decompress", "--keep", "--quiet", "--stdout", compressedDmg
], fs, workingDirectory: buildRoot);
if (!File.Exists(templateDmg))
throw new FileNotFoundException($"Decompressed dmg template file not found: {templateDmg}");
await ProcessHelper.ExecuteAll([
["hdiutil", "resize", "-size", "300M", templateDmg],
["hdiutil", "attach", templateDmg, "-noautoopen", "-quiet", "-mountpoint", mountDir]
], workingDirectory: buildRoot);
// Change the dmg name
var dmgname = $"Duplicati {rtcfg.ReleaseInfo.ReleaseName}";
Console.WriteLine($"Setting dmg name to {dmgname}");
await ProcessHelper.Execute([
"diskutil", "quiet", "rename", mountDir, dmgname
], workingDirectory: mountDir);
// Make the Duplicati.app structure, root folder should exist
var appFolder = Path.Combine(mountDir, MacOSAppName);
if (Directory.Exists(appFolder))
Directory.Delete(appFolder, true);
// Place the prepared folder
EnvHelper.CopyDirectory(Path.Combine(buildRoot, $"{target.BuildTargetString}-{MacOSAppName}"), appFolder, recursive: true);
// Set permissions inside DMG file
if (!OperatingSystem.IsWindows())
await EnvHelper.Chown(appFolder, "root", "admin", true);
// Unmount the dmg and compress
await ProcessHelper.ExecuteAll([
["hdiutil", "detach", mountDir, "-quiet", "-force"],
["hdiutil", "convert", templateDmg, "-quiet", "-format", "UDZO", "-imagekey", "zlib-level=9", "-o", dmgFile]
], workingDirectory: buildRoot);
// Clean up
File.Delete(templateDmg);
Directory.Delete(mountDir, false);
if (rtcfg.UseCodeSignSigning)
await rtcfg.Codesign(dmgFile, Path.Combine(installerDir, "Entitlements.plist"));
}
/// <summary>
/// Builds the Mac package asynchronously.
/// </summary>
/// <param name="baseDir">The base directory.</param>
/// <param name="buildRoot">The build root directory.</param>
/// <param name="pkgFile">The package file path.</param>
/// <param name="target">The package target.</param>
/// <param name="rtcfg">The runtime configuration.</param>
/// <returns>A task representing the asynchronous operation.</returns>
static async Task BuildMacPkgPackage(string baseDir, string buildRoot, string pkgFile, PackageTarget target, RuntimeConfig rtcfg)
{
var tmpFolder = Path.Combine(buildRoot, "tmp-pkg");
if (Directory.Exists(tmpFolder))
Directory.Delete(tmpFolder, true);
Directory.CreateDirectory(tmpFolder);
var installerDir = Path.Combine(baseDir, "Installer", "MacOS");
var appFolder = Path.Combine(tmpFolder, MacOSAppName);
if (Directory.Exists(appFolder))
Directory.Delete(appFolder, true);
// Place the prepared folder
EnvHelper.CopyDirectory(Path.Combine(buildRoot, $"{target.BuildTargetString}-{MacOSAppName}"), appFolder, recursive: true);
// Copy the source script files
var scripts = new[] { "daemon", "daemon-scripts", "app-scripts" };
// Copy scripts
foreach (var s in scripts)
EnvHelper.CopyDirectory(Path.Combine(installerDir, s), Path.Combine(tmpFolder, s), recursive: true);
// Set permissions
if (!OperatingSystem.IsWindows())
{
await EnvHelper.Chown(appFolder, "root", "admin", true);
foreach (var f in Directory.EnumerateFiles(Path.Combine(tmpFolder, "daemon"), "*.launchagent.plist", SearchOption.AllDirectories))
await EnvHelper.Chown(f, "root", "wheel", false);
var filemode = EnvHelper.GetUnixFileMode("+x");
var allscripts = scripts.Select(x => Path.Combine(tmpFolder, x)).Where(Directory.Exists).SelectMany(x => Directory.EnumerateFiles(x, "*", SearchOption.AllDirectories));
foreach (var x in allscripts)
if (File.Exists(x))
EnvHelper.AddFilemode(x, filemode);
}
var pkgAppFile = Path.Combine(tmpFolder, $"{rtcfg.ReleaseInfo.ReleaseName}-DuplicatiApp.pkg");
if (File.Exists(pkgAppFile))
File.Delete(pkgAppFile);
var pkgDaemonFile = Path.Combine(tmpFolder, $"{rtcfg.ReleaseInfo.ReleaseName}-DuplicatiDaemon.pkg");
if (File.Exists(pkgDaemonFile))
File.Delete(pkgDaemonFile);
var distributionFile = Path.Combine(tmpFolder, "Distribution.xml");
File.WriteAllText(distributionFile,
File.ReadAllText(Path.Combine(installerDir, "Distribution.xml"))
.Replace("DuplicatiApp.pkg", Path.GetFileName(pkgAppFile))
.Replace("DuplicatiDaemon.pkg", Path.GetFileName(pkgDaemonFile))
);
// Make the pkg files
await ProcessHelper.ExecuteAll([
["pkgbuild", "--analyze", "--root", appFolder, "--install-location", "/Applications/Duplicati.app", "InstallerComponent.plist"],
["pkgbuild", "--scripts", Path.Combine(tmpFolder, "app-scripts"), "--identifier", "com.duplicati.app", "--root", appFolder, "--install-location", "/Applications/Duplicati.app", "--component-plist", "InstallerComponent.plist", pkgAppFile],
["pkgbuild", "--scripts", Path.Combine(tmpFolder, "daemon-scripts"), "--identifier", "com.duplicati.app.daemon", "--root", Path.Combine(tmpFolder, "daemon"), "--install-location", "/Library/LaunchAgents", pkgDaemonFile],
["productbuild", "--distribution", distributionFile, "--package-path", ".", "--resources", ".", pkgFile]
], workingDirectory: tmpFolder);
// Clean up
Directory.Delete(tmpFolder, true);
// Sign the pkg file
if (rtcfg.UseCodeSignSigning)
await rtcfg.Productsign(pkgFile);
}
/// <summary>
/// Builds a DEB package using Docker
/// </summary>
/// <param name="baseDir">The base directory.</param>
/// <param name="buildRoot">The build root directory.</param>
/// <param name="debFile">The DEB file to generate.</param>
/// <param name="target">The package target.</param>
/// <param name="rtcfg">The runtime configuration.</param>
/// <returns>A task representing the asynchronous operation.</returns>
static Task BuildDebPackage(string baseDir, string buildRoot, string debFile, PackageTarget target, RuntimeConfig rtcfg)
{
throw new NotImplementedException();
}
}
}