Refactor build code into steps
This commit is contained in:
Vendored
+3
-1
@@ -19,7 +19,9 @@
|
||||
"--targets", "osx-x64-gui.dmg",
|
||||
"--targets", "osx-arm64-gui.dmg",
|
||||
"--targets", "osx-x64-gui.pkg",
|
||||
"--targets", "osx-arm64-gui.pkg",
|
||||
"--targets", "osx-arm64-gui.pkg",
|
||||
"--targets", "linux-x64-gui.deb",
|
||||
"--targets", "linux-x64-cli.deb",
|
||||
"--keep-build", "true"
|
||||
],
|
||||
"env": {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ReleaseBuilder.CliCommand;
|
||||
|
||||
public static partial class Build
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper methods cleaning and signing build outputs
|
||||
/// </summary>
|
||||
private static class PostCompile
|
||||
{
|
||||
/// <summary>
|
||||
/// Prepares a target directory with fixes that are done post-build, but before making the individual packages
|
||||
/// </summary>
|
||||
/// <param name="baseDir">The source directory</param>
|
||||
/// <param name="buildDir">The output build directory to modify</param>
|
||||
/// <param name="os">The target operating system</param>
|
||||
/// <param name="arch">The target architecture</param>
|
||||
/// <param name="rtcfg">The runtime config</param>
|
||||
/// <param name="keepBuilds">A flag that allows re-using existing builds</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
public static async Task PrepareTargetDirectory(string baseDir, string buildDir, OSType os, ArchType arch, RuntimeConfig rtcfg, bool keepBuilds)
|
||||
{
|
||||
await RemoveUnwantedFiles(os, buildDir);
|
||||
|
||||
switch (os)
|
||||
{
|
||||
case OSType.Windows:
|
||||
await SignWindowsExecutables(buildDir, rtcfg);
|
||||
break;
|
||||
|
||||
case OSType.MacOS:
|
||||
await SetExecutableFlags(buildDir, rtcfg);
|
||||
await MakeSymlinks(buildDir);
|
||||
await BundleMacOSApplication(baseDir, buildDir, rtcfg, keepBuilds);
|
||||
break;
|
||||
|
||||
case OSType.Linux:
|
||||
await SetExecutableFlags(buildDir, rtcfg);
|
||||
await MakeSymlinks(buildDir);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list of folders that are unwanted for a given OS target
|
||||
/// </summary>
|
||||
/// <param name="os">The OS to get unwanted folders for</param>
|
||||
/// <returns>The unwanted folders</returns>
|
||||
static string[] UnwantedFolders(OSType os)
|
||||
=> os switch
|
||||
{
|
||||
OSType.Windows => ["lvm-scripts"],
|
||||
OSType.MacOS => ["lvm-scripts", "win-tools"],
|
||||
OSType.Linux => ["win-tools"],
|
||||
_ => throw new Exception($"Not supported os: {os}")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A list of files that are unwanted for a given OS target
|
||||
/// </summary>
|
||||
/// <param name="os">The OS to get unwanted files for</param>
|
||||
/// <returns>The files that are unwanted</returns>
|
||||
static string[] UnwantedFiles(OSType os)
|
||||
=> os switch
|
||||
{
|
||||
OSType.Windows => [],
|
||||
OSType.MacOS => [Path.Combine("utility-scripts", "DuplicatiVerify.ps1")],
|
||||
OSType.Linux => [Path.Combine("utility-scripts", "DuplicatiVerify.ps1")],
|
||||
_ => throw new Exception($"Not supported os: {os}")
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The unwanted filenames
|
||||
/// </summary>
|
||||
/// <param name="os">The operating system to get the unwanted filenames for</param>
|
||||
/// <returns>The list of unwanted filenames</returns>
|
||||
static IEnumerable<string> UnwantedFileGlobExps(OSType os)
|
||||
=> new[] {
|
||||
"Thumbs.db",
|
||||
"desktop.ini",
|
||||
".DS_Store",
|
||||
"*.bak",
|
||||
"*.pdb",
|
||||
"*.mdb",
|
||||
"._*",
|
||||
os == OSType.Windows ? "*.sh" : "*.bat"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns a regular expression mapping files that are not wanted in the build folders
|
||||
/// </summary>
|
||||
/// <param name="os">The operating system to get the unwanted filenames for</param>
|
||||
/// <returns>A regular expression for matching unwanted filenames</returns>
|
||||
static Regex UnwantedFilePatterns(OSType os)
|
||||
=> new Regex(@$"^({string.Join("|", UnwantedFileGlobExps(os).Select(x => x.Replace(".", "\\.").Replace("*", ".*")))})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Removes unwanted contents from the build folders
|
||||
/// </summary>
|
||||
/// <param name="os">The operating system the folder is targeting</param>
|
||||
/// <param name="buildDir">The directory where the build output is placed</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static Task RemoveUnwantedFiles(OSType os, string buildDir)
|
||||
{
|
||||
foreach (var d in UnwantedFolders(os).Select(x => Path.Combine(buildDir, x)))
|
||||
if (Directory.Exists(d))
|
||||
Directory.Delete(d, true);
|
||||
|
||||
foreach (var f in UnwantedFiles(os).Select(x => Path.Combine(buildDir, x)))
|
||||
if (File.Exists(f))
|
||||
File.Delete(f);
|
||||
|
||||
var patterns = UnwantedFilePatterns(os);
|
||||
foreach (var f in Directory.EnumerateFiles(buildDir, "*", SearchOption.AllDirectories).Where(x => patterns.IsMatch(Path.GetFileName(x))))
|
||||
if (File.Exists(f))
|
||||
File.Delete(f);
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Introduces symbolic links for executables that have a different name
|
||||
/// </summary>
|
||||
/// <param name="buildDir">The build path to use</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static Task MakeSymlinks(string buildDir)
|
||||
{
|
||||
foreach (var k in ExecutableRenames)
|
||||
if (File.Exists(Path.Combine(buildDir, k.Key)) && !File.Exists(Path.Combine(buildDir, k.Value)))
|
||||
File.CreateSymbolicLink(Path.Combine(buildDir, k.Value), Path.Combine(".", k.Key));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the executable flags for the build output
|
||||
/// </summary>
|
||||
/// <param name="buildDir">The build directory</param>
|
||||
/// <param name="rtcfg">The runtime configuration</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static Task SetExecutableFlags(string buildDir, RuntimeConfig rtcfg)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
// Mark executables with the execute flag
|
||||
var executables = rtcfg.ExecutableBinaries.Select(x => Path.Combine(buildDir, x))
|
||||
.Concat(Directory.EnumerateFiles(buildDir, "*.sh", SearchOption.AllDirectories));
|
||||
var filemode = EnvHelper.GetUnixFileMode("+x");
|
||||
foreach (var x in executables)
|
||||
if (File.Exists(x))
|
||||
EnvHelper.AddFilemode(x, filemode);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the MacOS folder structure by moving all files into a .app folder
|
||||
/// </summary>
|
||||
/// <param name="baseDir">The source folder</param>
|
||||
/// <param name="buildDir">The MacOS build output</param>
|
||||
/// <param name="rtcfg">The runtime configuration</param>
|
||||
/// <param name="keepBuilds">A flag that allows re-using existing builds</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static async Task BundleMacOSApplication(string baseDir, string buildDir, RuntimeConfig rtcfg, bool keepBuilds)
|
||||
{
|
||||
var buildroot = Path.GetDirectoryName(buildDir) ?? throw new Exception("Bad build dir");
|
||||
// Create target .app folder
|
||||
var appDir = Path.Combine(
|
||||
buildroot,
|
||||
$"{Path.GetFileName(buildDir)}-{MacOSAppName}"
|
||||
);
|
||||
|
||||
if (Directory.Exists(appDir))
|
||||
{
|
||||
if (keepBuilds)
|
||||
{
|
||||
Console.WriteLine("App folder already exsists, skipping MacOS application build");
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.Delete(appDir, true);
|
||||
}
|
||||
|
||||
// Prepare the .app folder structure
|
||||
var tmpApp = Path.Combine(buildroot, "tmpapp", MacOSAppName);
|
||||
|
||||
var folders = new[] {
|
||||
Path.Combine("Contents"),
|
||||
Path.Combine("Contents", "MacOS"),
|
||||
Path.Combine("Contents", "Resources"),
|
||||
};
|
||||
|
||||
if (Directory.Exists(tmpApp))
|
||||
Directory.Delete(tmpApp, true);
|
||||
|
||||
Directory.CreateDirectory(tmpApp);
|
||||
foreach (var f in folders)
|
||||
Directory.CreateDirectory(Path.Combine(tmpApp, f));
|
||||
|
||||
// Copy the primary contents into the binary folder
|
||||
var binDir = Path.Combine(tmpApp, "Contents", "MacOS");
|
||||
EnvHelper.CopyDirectory(buildDir, binDir, recursive: true);
|
||||
|
||||
// Patch the plist and place the icon from the resources
|
||||
var installerDir = Path.Combine(baseDir, "Installer", "MacOS");
|
||||
|
||||
var plist = File.ReadAllText(Path.Combine(installerDir, "app-resources", "Info.plist"))
|
||||
.Replace("!LONG_VERSION!", rtcfg.ReleaseInfo.ReleaseName)
|
||||
.Replace("!SHORT_VERSION!", rtcfg.ReleaseInfo.Version.ToString());
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(tmpApp, "Contents", "Info.plist"),
|
||||
plist
|
||||
);
|
||||
|
||||
File.Copy(
|
||||
Path.Combine(installerDir, "app-resources", "Duplicati.icns"),
|
||||
Path.Combine(tmpApp, "Contents", "Resources", "Duplicati.icns"),
|
||||
overwrite: true
|
||||
);
|
||||
|
||||
// Inject the launch agent
|
||||
EnvHelper.CopyDirectory(
|
||||
Path.Combine(installerDir, "daemon"),
|
||||
Path.Combine(tmpApp, "Contents", "Resources"),
|
||||
recursive: true
|
||||
);
|
||||
|
||||
// Inject the uninstall.sh script
|
||||
File.Copy(
|
||||
Path.Combine(installerDir, "uninstall.sh"),
|
||||
Path.Combine(tmpApp, "Contents", "MacOS", "uninstall.sh"),
|
||||
overwrite: true
|
||||
);
|
||||
|
||||
if (rtcfg.UseCodeSignSigning)
|
||||
{
|
||||
var entitlementFile = Path.Combine(installerDir, "Entitlements.plist");
|
||||
foreach (var f in Directory.EnumerateFiles(binDir, "*", SearchOption.AllDirectories))
|
||||
await rtcfg.Codesign(f, entitlementFile);
|
||||
|
||||
await rtcfg.Codesign(Path.Combine(tmpApp), entitlementFile);
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
foreach (var f in Directory.EnumerateFiles(binDir, "*.launchagent.plist", SearchOption.TopDirectoryOnly))
|
||||
File.SetUnixFileMode(f, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead);
|
||||
|
||||
Directory.Move(tmpApp, appDir);
|
||||
Directory.Delete(Path.GetDirectoryName(tmpApp) ?? throw new Exception("Unexpected empty path"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs all .exe and .dll files with Authenticode
|
||||
/// </summary>
|
||||
/// <param name="buildDir">The folder to sign files in</param>
|
||||
/// <param name="rtcfg">The runtime config</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static async Task SignWindowsExecutables(string buildDir, RuntimeConfig rtcfg)
|
||||
{
|
||||
var cfg = Program.Configuration;
|
||||
if (!rtcfg.UseAuthenticodeSigning)
|
||||
return;
|
||||
|
||||
var filenames = Directory.EnumerateFiles(buildDir, "Duplicati.*", SearchOption.TopDirectoryOnly)
|
||||
.Where(x => x.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || x.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
Console.WriteLine($"Performing Authenticode signing of {filenames.Count} files");
|
||||
|
||||
foreach (var file in filenames)
|
||||
await rtcfg.AuthenticodeSign(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
namespace ReleaseBuilder.CliCommand;
|
||||
|
||||
public static partial class Build
|
||||
{
|
||||
/// <summary>
|
||||
/// Main compilation of projects
|
||||
/// </summary>
|
||||
private static class Compile
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the projects listed in <paramref name="sourceProjects"/> for the distinct <paramref name="buildTargets"/>
|
||||
/// </summary>
|
||||
/// <param name="baseDir">The base solution folder</param>
|
||||
/// <param name="buildDir">The folder where builds should be placed</param>
|
||||
/// <param name="sourceProjects">The projects to build</param>
|
||||
/// <param name="windowsOnlyProjects">Projects that are only for the Windows targets</param>
|
||||
/// <param name="guiProjects">Projects that are only needed for GUI builds</param>
|
||||
/// <param name="buildTargets">The targets to build</param>
|
||||
/// <param name="releaseInfo">The release info to use for the build</param>
|
||||
/// <param name="keepBuilds">A flag that allows re-using existing builds</param>
|
||||
/// <param name="rtcfg">The runtime configuration</param>
|
||||
/// <returns>A task that completes when the build is done</returns>
|
||||
public static async Task BuildProjects(string baseDir, string buildDir, IEnumerable<string> sourceProjects, IEnumerable<string> windowsOnlyProjects, IEnumerable<string> guiProjects, IEnumerable<PackageTarget> buildTargets, ReleaseInfo releaseInfo, bool keepBuilds, RuntimeConfig rtcfg)
|
||||
{
|
||||
// For tracing, create a log folder and store all logs there
|
||||
var logFolder = Path.Combine(buildDir, "logs");
|
||||
Directory.CreateDirectory(logFolder);
|
||||
|
||||
// Get the unique build targets (ignoring the package type)
|
||||
var buildArchTargets = buildTargets.DistinctBy(x => (x.OS, x.Arch, x.Interface)).ToArray();
|
||||
|
||||
if (buildArchTargets.Length == 1)
|
||||
Console.WriteLine($"Building single release: {buildArchTargets.First().BuildTargetString}");
|
||||
else
|
||||
Console.WriteLine($"Building {buildArchTargets.Length} versions");
|
||||
|
||||
foreach (var target in buildArchTargets)
|
||||
{
|
||||
var outputFolder = Path.Combine(buildDir, target.BuildTargetString);
|
||||
|
||||
// Faster iteration for debugging is to keep the build folder
|
||||
if (keepBuilds && Directory.Exists(outputFolder))
|
||||
{
|
||||
Console.WriteLine($"Skipping build as output exists for {target.BuildTargetString}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var tmpfolder = Path.Combine(buildDir, target.BuildTargetString + "-tmp");
|
||||
Console.WriteLine($"Building {target.BuildTargetString} ...");
|
||||
|
||||
foreach (var proj in sourceProjects)
|
||||
{
|
||||
if (target.OS != OSType.Windows && windowsOnlyProjects.Contains(proj))
|
||||
continue;
|
||||
|
||||
if (target.Interface == InterfaceType.Cli && guiProjects.Contains(proj))
|
||||
continue;
|
||||
|
||||
// TODO: Creating multiple self-contained binaries really bloats the build size.
|
||||
//
|
||||
// One workaround could be to have a single commandline entry project that
|
||||
// uses the invoked command name to determine the actual command to run
|
||||
// Similar to how busy-box bundles multiple commands into a single binary
|
||||
//
|
||||
// Alternative is to require the .NET runtime to be installed
|
||||
|
||||
var command = new string[] {
|
||||
"dotnet", "publish", proj,
|
||||
"-c", "Release",
|
||||
"-o", tmpfolder,
|
||||
"-r", target.BuildArchString,
|
||||
$"/p:AssemblyVersion={releaseInfo.Version}",
|
||||
$"/p:Version={releaseInfo.Version}-{releaseInfo.Type}-{releaseInfo.Timestamp:yyyyMMdd}",
|
||||
"--self-contained", "true"
|
||||
};
|
||||
await ProcessHelper.ExecuteWithLog(command, workingDirectory: tmpfolder, logFolder: logFolder, logFilename: (pid, isStdOut) => $"{Path.GetFileNameWithoutExtension(proj)}.{target.BuildTargetString}.{pid}.{(isStdOut ? "stdout" : "stderr")}.log");
|
||||
}
|
||||
|
||||
Directory.Move(tmpfolder, outputFolder);
|
||||
}
|
||||
|
||||
// Perform any post-build steps, cleaning and signing as needed
|
||||
await PostCompile.PrepareTargetDirectory(baseDir, outputFolder, target.OS, target.Arch, rtcfg, keepBuilds);
|
||||
|
||||
Console.WriteLine("Completed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace ReleaseBuilder.CliCommand;
|
||||
|
||||
public static partial class Build
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of the git push command
|
||||
/// </summary>
|
||||
private static class GitPush
|
||||
{
|
||||
/// <summary>
|
||||
/// Tags the release and pushes it to the repository
|
||||
/// </summary>
|
||||
/// <param name="baseDir">The base git dir</param>
|
||||
/// <param name="releaseInfo">The release info</param>
|
||||
/// <returns>A task that completes when the push is done</returns>
|
||||
public static async Task TagAndPush(string baseDir, ReleaseInfo releaseInfo)
|
||||
{
|
||||
// Add modified files
|
||||
await ProcessHelper.Execute(new[] {
|
||||
"git", "add",
|
||||
"Updates/build_version.txt",
|
||||
"changelog.txt"
|
||||
}, workingDirectory: baseDir);
|
||||
|
||||
// Make a commit
|
||||
await ProcessHelper.Execute(new[] {
|
||||
"git", "commit",
|
||||
"-m", $"Version bump to v{releaseInfo.Version}-{releaseInfo.ReleaseName}",
|
||||
"-m", "You can download this build from: ",
|
||||
"-m", $"Binaries: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip",
|
||||
"-m", $"Signature file: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip.sig",
|
||||
"-m", $"ASCII signature file: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip.sig.asc",
|
||||
"-m", $"MD5: {releaseInfo.ReleaseName}.zip.md5",
|
||||
"-m", $"SHA1: {releaseInfo.ReleaseName}.zip.sha1",
|
||||
"-m", $"SHA256: {releaseInfo.ReleaseName}.zip.sha256"
|
||||
}, workingDirectory: baseDir);
|
||||
|
||||
// And tag the release
|
||||
await ProcessHelper.Execute(new[] {
|
||||
"git", "tag", $"v{releaseInfo.Version}-{releaseInfo.ReleaseName}",
|
||||
"-m", "You can download this build from: ",
|
||||
"-m", $"Binaries: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip",
|
||||
"-m", $"Signature file: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip.sig",
|
||||
"-m", $"ASCII signature file: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip.sig.asc",
|
||||
"-m", $"MD5: {releaseInfo.ReleaseName}.zip.md5",
|
||||
"-m", $"SHA1: {releaseInfo.ReleaseName}.zip.sha1",
|
||||
"-m", $"SHA256: {releaseInfo.ReleaseName}.zip.sha256"
|
||||
}, workingDirectory: baseDir);
|
||||
|
||||
// The push the release
|
||||
await ProcessHelper.Execute(new[] { "git", "push", "--tags" }, workingDirectory: baseDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.CommandLine;
|
||||
using System.IO.Compression;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ReleaseBuilder.CliCommand;
|
||||
|
||||
/// <summary>
|
||||
/// The build command implementation
|
||||
/// </summary>
|
||||
public static class Build
|
||||
public static partial class Build
|
||||
{
|
||||
/// <summary>
|
||||
/// The primary project to build for GUI builds
|
||||
@@ -366,19 +364,15 @@ public static class Build
|
||||
rtcfg.ToggleAuthenticodeSigning();
|
||||
rtcfg.ToggleSignCodeSigning();
|
||||
|
||||
if (!keepBuilds)
|
||||
if (!keepBuilds && Directory.Exists(buildTemp.FullName))
|
||||
{
|
||||
if (Directory.Exists(buildTemp.FullName))
|
||||
{
|
||||
Console.WriteLine($"Deleting build folder: {buildTemp.FullName}");
|
||||
Directory.Delete(buildTemp.FullName, true);
|
||||
}
|
||||
Console.WriteLine($"Deleting build folder: {buildTemp.FullName}");
|
||||
Directory.Delete(buildTemp.FullName, true);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(buildTemp.FullName))
|
||||
Directory.CreateDirectory(buildTemp.FullName);
|
||||
|
||||
|
||||
// Generally, the builds should happen with a clean source tree,
|
||||
// but this can be disabled for debugging
|
||||
if (gitStashPush)
|
||||
@@ -387,72 +381,11 @@ public static class Build
|
||||
// Inject various files that will be embedded into the build artifacts
|
||||
await PrepareSourceDirectory(baseDir, releaseInfo, updateUrls);
|
||||
|
||||
// For tracing, create a log folder and store all logs there
|
||||
var logFolder = Path.Combine(buildTemp.FullName, "logs");
|
||||
Directory.CreateDirectory(logFolder);
|
||||
// Perform the main compilations
|
||||
await Compile.BuildProjects(baseDir, buildTemp.FullName, sourceProjects, windowsOnly, GUIProjects, buildTargets, releaseInfo, keepBuilds, rtcfg);
|
||||
|
||||
// Get the unique build targets (ignoring the package type)
|
||||
var buildArchTargets = buildTargets.DistinctBy(x => (x.OS, x.Arch, x.Interface)).ToArray();
|
||||
|
||||
if (buildArchTargets.Length == 1)
|
||||
Console.WriteLine($"Building single release: {buildArchTargets.First().BuildTargetString}");
|
||||
else
|
||||
Console.WriteLine($"Building {buildArchTargets.Length} versions");
|
||||
|
||||
foreach (var target in buildArchTargets)
|
||||
{
|
||||
var outputFolder = Path.Combine(buildTemp.FullName, target.BuildTargetString);
|
||||
|
||||
// Faster iteration for debugging is to keep the build folder
|
||||
if (keepBuilds && Directory.Exists(outputFolder))
|
||||
{
|
||||
Console.WriteLine($"Skipping build as output exists for {target.BuildTargetString}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var tmpfolder = Path.Combine(buildTemp.FullName, target.BuildTargetString + "-tmp");
|
||||
Console.WriteLine($"Building {target.BuildTargetString} ...");
|
||||
|
||||
foreach (var proj in sourceProjects)
|
||||
{
|
||||
if (target.OS != OSType.Windows && windowsOnly.Contains(proj))
|
||||
continue;
|
||||
|
||||
if (target.Interface == InterfaceType.Cli && GUIProjects.Contains(proj))
|
||||
continue;
|
||||
|
||||
var command = new string[] {
|
||||
"dotnet", "publish", proj,
|
||||
"-c", "Release",
|
||||
"-o", tmpfolder,
|
||||
"-r", target.BuildArchString,
|
||||
$"/p:AssemblyVersion={releaseInfo.Version}",
|
||||
$"/p:Version={releaseInfo.Version}-{releaseInfo.Type}-{releaseInfo.Timestamp:yyyyMMdd}",
|
||||
"--self-contained", "false"
|
||||
};
|
||||
await ProcessHelper.ExecuteWithLog(command, workingDirectory: tmpfolder, logFolder: logFolder, logFilename: (pid, isStdOut) => $"{Path.GetFileNameWithoutExtension(proj)}.{target.BuildTargetString}.{pid}.{(isStdOut ? "stdout" : "stderr")}.log");
|
||||
}
|
||||
|
||||
Directory.Move(tmpfolder, outputFolder);
|
||||
}
|
||||
|
||||
await PrepareTargetDirectory(baseDir, outputFolder, target.OS, target.Arch, rtcfg);
|
||||
|
||||
Console.WriteLine("Completed!");
|
||||
}
|
||||
|
||||
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, buildTemp.FullName, target, rtcfg);
|
||||
Console.WriteLine("Completed!");
|
||||
}
|
||||
// Create the packages
|
||||
await CreatePackage.BuildPackages(baseDir, buildTemp.FullName, buildTargets, keepBuilds, rtcfg);
|
||||
|
||||
Console.WriteLine("Build completed, uploading packages ...");
|
||||
|
||||
@@ -460,10 +393,8 @@ public static class Build
|
||||
|
||||
Console.WriteLine("Release completed, posting release notes ...");
|
||||
|
||||
if (gitStashPush)
|
||||
{
|
||||
// Clean up the source tree
|
||||
await ProcessHelper.Execute(new[] {
|
||||
// Clean up the source tree
|
||||
await ProcessHelper.Execute(new[] {
|
||||
"git", "checkout",
|
||||
"Duplicati/License/VersionTag.txt",
|
||||
"Duplicati/Library/AutoUpdater/AutoUpdateURL.txt",
|
||||
@@ -471,41 +402,8 @@ public static class Build
|
||||
"Duplicati/Library/AutoUpdater/AutoUpdateBuildChannel.txt"
|
||||
}, workingDirectory: baseDir);
|
||||
|
||||
// Add modified files
|
||||
await ProcessHelper.Execute(new[] {
|
||||
"git", "add",
|
||||
"Updates/build_version.txt",
|
||||
"changelog.txt"
|
||||
}, workingDirectory: baseDir);
|
||||
|
||||
// Make a commit
|
||||
await ProcessHelper.Execute(new[] {
|
||||
"git", "commit",
|
||||
"-m", $"Version bump to v{releaseInfo.Version}-{releaseInfo.ReleaseName}",
|
||||
"-m", "You can download this build from: ",
|
||||
"-m", $"Binaries: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip",
|
||||
"-m", $"Signature file: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip.sig",
|
||||
"-m", $"ASCII signature file: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip.sig.asc",
|
||||
"-m", $"MD5: {releaseInfo.ReleaseName}.zip.md5",
|
||||
"-m", $"SHA1: {releaseInfo.ReleaseName}.zip.sha1",
|
||||
"-m", $"SHA256: {releaseInfo.ReleaseName}.zip.sha256"
|
||||
}, workingDirectory: baseDir);
|
||||
|
||||
// And tag the release
|
||||
await ProcessHelper.Execute(new[] {
|
||||
"git", "tag", $"v{releaseInfo.Version}-{releaseInfo.ReleaseName}",
|
||||
"-m", "You can download this build from: ",
|
||||
"-m", $"Binaries: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip",
|
||||
"-m", $"Signature file: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip.sig",
|
||||
"-m", $"ASCII signature file: https://updates.duplicati.com/{releaseInfo.Type}/{releaseInfo.ReleaseName}.zip.sig.asc",
|
||||
"-m", $"MD5: {releaseInfo.ReleaseName}.zip.md5",
|
||||
"-m", $"SHA1: {releaseInfo.ReleaseName}.zip.sha1",
|
||||
"-m", $"SHA256: {releaseInfo.ReleaseName}.zip.sha256"
|
||||
}, workingDirectory: baseDir);
|
||||
|
||||
// The push the release
|
||||
await ProcessHelper.Execute(new[] { "git", "push", "--tags" }, workingDirectory: baseDir);
|
||||
}
|
||||
if (gitStashPush)
|
||||
await GitPush.TagAndPush(baseDir, releaseInfo);
|
||||
|
||||
Console.WriteLine("All done");
|
||||
|
||||
@@ -514,275 +412,6 @@ public static class Build
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
private static async Task BuildPackage(string baseDir, string buildRoot, PackageTarget target, RuntimeConfig rtcfg)
|
||||
{
|
||||
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))
|
||||
{
|
||||
Console.WriteLine($"Package file already exists, skipping package build for {target.PackageTargetString}");
|
||||
return;
|
||||
}
|
||||
|
||||
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.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>
|
||||
private 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>
|
||||
private 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>
|
||||
private 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>
|
||||
private 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>
|
||||
/// Updates the source directory prior to building
|
||||
/// </summary>
|
||||
@@ -808,265 +437,4 @@ public static class Build
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a target directory with fixes that are done post-build, but before making the individual packages
|
||||
/// </summary>
|
||||
/// <param name="baseDir">The source directory</param>
|
||||
/// <param name="buildDir">The output build directory to modify</param>
|
||||
/// <param name="os">The target operating system</param>
|
||||
/// <param name="arch">The target architecture</param>
|
||||
/// <param name="rtcfg">The runtime config</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static async Task PrepareTargetDirectory(string baseDir, string buildDir, OSType os, ArchType arch, RuntimeConfig rtcfg)
|
||||
{
|
||||
await RemoveUnwantedFiles(os, buildDir);
|
||||
|
||||
switch (os)
|
||||
{
|
||||
case OSType.Windows:
|
||||
await SignWindowsExecutables(buildDir, rtcfg);
|
||||
break;
|
||||
|
||||
case OSType.MacOS:
|
||||
await MakeSymlinks(buildDir);
|
||||
await BundleMacOSApplication(baseDir, buildDir, rtcfg);
|
||||
break;
|
||||
|
||||
case OSType.Linux:
|
||||
await MakeSymlinks(buildDir);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list of folders that are unwanted for a given OS target
|
||||
/// </summary>
|
||||
/// <param name="os">The OS to get unwanted folders for</param>
|
||||
/// <returns>The unwanted folders</returns>
|
||||
static string[] UnwantedFolders(OSType os)
|
||||
=> os switch
|
||||
{
|
||||
OSType.Windows => ["lvm-scripts"],
|
||||
OSType.MacOS => ["lvm-scripts", "win-tools"],
|
||||
OSType.Linux => ["win-tools"],
|
||||
_ => throw new Exception($"Not supported os: {os}")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A list of files that are unwanted for a given OS target
|
||||
/// </summary>
|
||||
/// <param name="os">The OS to get unwanted files for</param>
|
||||
/// <returns>The files that are unwanted</returns>
|
||||
static string[] UnwantedFiles(OSType os)
|
||||
=> os switch
|
||||
{
|
||||
OSType.Windows => [],
|
||||
OSType.MacOS => [Path.Combine("utility-scripts", "DuplicatiVerify.ps1")],
|
||||
OSType.Linux => [Path.Combine("utility-scripts", "DuplicatiVerify.ps1")],
|
||||
_ => throw new Exception($"Not supported os: {os}")
|
||||
};
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The unwanted filenames
|
||||
/// </summary>
|
||||
/// <param name="os">The operating system to get the unwanted filenames for</param>
|
||||
/// <returns>The list of unwanted filenames</returns>
|
||||
static IEnumerable<string> UnwantedFileGlobExps(OSType os)
|
||||
=> new[] {
|
||||
"Thumbs.db",
|
||||
"desktop.ini",
|
||||
".DS_Store",
|
||||
"*.bak",
|
||||
"*.pdb",
|
||||
"*.mdb",
|
||||
"._*",
|
||||
os == OSType.Windows ? "*.sh" : "*.bat"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns a regular expression mapping files that are not wanted in the build folders
|
||||
/// </summary>
|
||||
/// <param name="os">The operating system to get the unwanted filenames for</param>
|
||||
/// <returns>A regular expression for matching unwanted filenames</returns>
|
||||
static Regex UnwantedFilePatterns(OSType os)
|
||||
=> new Regex(@$"^({string.Join("|", UnwantedFileGlobExps(os).Select(x => x.Replace(".", "\\.").Replace("*", ".*")))})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Removes unwanted contents from the build folders
|
||||
/// </summary>
|
||||
/// <param name="os">The operating system the folder is targeting</param>
|
||||
/// <param name="buildDir">The directory where the build output is placed</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static Task RemoveUnwantedFiles(OSType os, string buildDir)
|
||||
{
|
||||
foreach (var d in UnwantedFolders(os).Select(x => Path.Combine(buildDir, x)))
|
||||
if (Directory.Exists(d))
|
||||
Directory.Delete(d, true);
|
||||
|
||||
foreach (var f in UnwantedFiles(os).Select(x => Path.Combine(buildDir, x)))
|
||||
if (File.Exists(f))
|
||||
File.Delete(f);
|
||||
|
||||
var patterns = UnwantedFilePatterns(os);
|
||||
foreach (var f in Directory.EnumerateFiles(buildDir, "*", SearchOption.AllDirectories).Where(x => patterns.IsMatch(Path.GetFileName(x))))
|
||||
if (File.Exists(f))
|
||||
File.Delete(f);
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Introduces symbolic links for executables that have a different name
|
||||
/// </summary>
|
||||
/// <param name="buildDir">The build path to use</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static Task MakeSymlinks(string buildDir)
|
||||
{
|
||||
foreach (var k in ExecutableRenames)
|
||||
if (File.Exists(Path.Combine(buildDir, k.Key)) && !File.Exists(Path.Combine(buildDir, k.Value)))
|
||||
File.CreateSymbolicLink(Path.Combine(buildDir, k.Value), Path.Combine(".", k.Key));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the executable flags for the build output
|
||||
/// </summary>
|
||||
/// <param name="buildDir">The build directory</param>
|
||||
/// <param name="rtcfg">The runtime configuration</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static Task SetExecutableFlags(string buildDir, RuntimeConfig rtcfg)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
// Mark executables with the execute flag
|
||||
var executables = rtcfg.ExecutableBinaries.Select(x => Path.Combine(buildDir, x))
|
||||
.Concat(Directory.EnumerateFiles(buildDir, "*.sh", SearchOption.AllDirectories));
|
||||
var filemode = EnvHelper.GetUnixFileMode("+x");
|
||||
foreach (var x in executables)
|
||||
if (File.Exists(x))
|
||||
EnvHelper.AddFilemode(x, filemode);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the MacOS folder structure by moving all files into a .app folder
|
||||
/// </summary>
|
||||
/// <param name="baseDir">The source folder</param>
|
||||
/// <param name="buildDir">The MacOS build output</param>
|
||||
/// <param name="rtcfg">The runtime configuration</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static async Task BundleMacOSApplication(string baseDir, string buildDir, RuntimeConfig rtcfg)
|
||||
{
|
||||
var buildroot = Path.GetDirectoryName(buildDir) ?? throw new Exception("Bad build dir");
|
||||
// Create target .app folder
|
||||
var appDir = Path.Combine(
|
||||
buildroot,
|
||||
$"{Path.GetFileName(buildDir)}-{MacOSAppName}"
|
||||
);
|
||||
|
||||
if (Directory.Exists(appDir))
|
||||
{
|
||||
Console.WriteLine("App folder already exsists, skipping MacOS application build");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the .app folder structure
|
||||
var tmpApp = Path.Combine(buildroot, "tmpapp", MacOSAppName);
|
||||
|
||||
var folders = new[] {
|
||||
Path.Combine("Contents"),
|
||||
Path.Combine("Contents", "MacOS"),
|
||||
Path.Combine("Contents", "Resources"),
|
||||
};
|
||||
|
||||
if (Directory.Exists(tmpApp))
|
||||
Directory.Delete(tmpApp, true);
|
||||
|
||||
Directory.CreateDirectory(tmpApp);
|
||||
foreach (var f in folders)
|
||||
Directory.CreateDirectory(Path.Combine(tmpApp, f));
|
||||
|
||||
// Copy the primary contents into the binary folder
|
||||
var binDir = Path.Combine(tmpApp, "Contents", "MacOS");
|
||||
EnvHelper.CopyDirectory(buildDir, binDir, recursive: true);
|
||||
|
||||
// Patch the plist and place the icon from the resources
|
||||
var installerDir = Path.Combine(baseDir, "Installer", "MacOS");
|
||||
|
||||
var plist = File.ReadAllText(Path.Combine(installerDir, "app-resources", "Info.plist"))
|
||||
.Replace("!LONG_VERSION!", rtcfg.ReleaseInfo.ReleaseName)
|
||||
.Replace("!SHORT_VERSION!", rtcfg.ReleaseInfo.Version.ToString());
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(tmpApp, "Contents", "Info.plist"),
|
||||
plist
|
||||
);
|
||||
|
||||
File.Copy(
|
||||
Path.Combine(installerDir, "app-resources", "Duplicati.icns"),
|
||||
Path.Combine(tmpApp, "Contents", "Resources", "Duplicati.icns"),
|
||||
overwrite: true
|
||||
);
|
||||
|
||||
// Inject the launch agent
|
||||
EnvHelper.CopyDirectory(
|
||||
Path.Combine(installerDir, "daemon"),
|
||||
Path.Combine(tmpApp, "Contents", "Resources"),
|
||||
recursive: true
|
||||
);
|
||||
|
||||
// Inject the uninstall.sh script
|
||||
File.Copy(
|
||||
Path.Combine(installerDir, "uninstall.sh"),
|
||||
Path.Combine(tmpApp, "Contents", "MacOS", "uninstall.sh"),
|
||||
overwrite: true
|
||||
);
|
||||
|
||||
if (rtcfg.UseCodeSignSigning)
|
||||
{
|
||||
var entitlementFile = Path.Combine(installerDir, "Entitlements.plist");
|
||||
foreach (var f in Directory.EnumerateFiles(binDir, "*", SearchOption.AllDirectories))
|
||||
await rtcfg.Codesign(f, entitlementFile);
|
||||
|
||||
await rtcfg.Codesign(Path.Combine(tmpApp), entitlementFile);
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
foreach (var f in Directory.EnumerateFiles(binDir, "*.launchagent.plist", SearchOption.TopDirectoryOnly))
|
||||
File.SetUnixFileMode(f, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead);
|
||||
|
||||
Directory.Move(tmpApp, appDir);
|
||||
Directory.Delete(Path.GetDirectoryName(tmpApp) ?? throw new Exception("Unexpected empty path"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs all .exe and .dll files with Authenticode
|
||||
/// </summary>
|
||||
/// <param name="buildDir">The folder to sign files in</param>
|
||||
/// <param name="rtcfg">The runtime config</param>
|
||||
/// <returns>An awaitable task</returns>
|
||||
static async Task SignWindowsExecutables(string buildDir, RuntimeConfig rtcfg)
|
||||
{
|
||||
var cfg = Program.Configuration;
|
||||
if (!rtcfg.UseAuthenticodeSigning)
|
||||
return;
|
||||
|
||||
var filenames = Directory.EnumerateFiles(buildDir, "Duplicati.*", SearchOption.TopDirectoryOnly)
|
||||
.Where(x => x.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || x.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
Console.WriteLine($"Performing Authenticode signing of {filenames.Count} files");
|
||||
|
||||
foreach (var file in filenames)
|
||||
await rtcfg.AuthenticodeSign(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,18 @@ class Program
|
||||
"linux-x64-gui.zip",
|
||||
"linux-x64-gui.deb",
|
||||
"linux-x64-gui.rpm",
|
||||
"linux-x64-cli.zip",
|
||||
"linux-x64-cli.deb",
|
||||
"linux-x64-cli.rpm",
|
||||
"linux-x64-cli.docker",
|
||||
// "linux-x64-cli.spk",
|
||||
"linux-arm64-cli.docker",
|
||||
"linux-arm64-gui.zip",
|
||||
"linux-arm64-gui.deb",
|
||||
"linux-arm64-gui.rpm",
|
||||
"linux-arm64-cli.zip",
|
||||
"linux-arm64-cli.deb",
|
||||
"linux-arm64-cli.rpm",
|
||||
// "linux-arm64-cli.spk",
|
||||
"osx-x64-gui.dmg",
|
||||
"osx-x64-gui.pkg",
|
||||
|
||||
Reference in New Issue
Block a user