2024-03-20 16:17:17 +01:00
using System.CommandLine ;
2024-03-22 15:20:45 +01:00
using System.CommandLine.NamingConventionBinder ;
2024-03-27 19:52:40 +01:00
using System.Security.Cryptography ;
using System.Text.RegularExpressions ;
using Duplicati.Library.AutoUpdater ;
2024-03-20 16:17:17 +01:00
2024-03-27 19:52:40 +01:00
namespace ReleaseBuilder.Build ;
2024-03-20 16:17:17 +01:00
/// <summary>
/// The build command implementation
/// </summary>
2024-03-27 19:52:40 +01:00
public static partial class Command
2024-03-20 16:17:17 +01:00
{
/// <summary>
2024-03-22 12:15:53 +01:00
/// The primary project to build for GUI builds
2024-03-20 16:17:17 +01:00
/// </summary>
2024-03-22 12:15:53 +01:00
private const string PrimaryGUIProject = "Duplicati.GUI.TrayIcon.csproj" ;
/// <summary>
/// The secondary project to build for CLI builds
/// </summary>
private const string PrimaryCLIProject = "Duplicati.CommandLine.csproj" ;
2024-03-20 16:17:17 +01:00
/// <summary>
/// Projects that only makes sense for Windows
/// </summary>
private static readonly IReadOnlySet < string > WindowsOnlyProjects = new HashSet < string >( StringComparer . InvariantCultureIgnoreCase ) { "Duplicati.WindowsService.csproj" };
2024-03-22 12:15:53 +01:00
/// <summary>
/// Projects the pull in GUI dependencies
/// </summary>
private static readonly IReadOnlySet < string > GUIProjects = new HashSet < string >( StringComparer . InvariantCultureIgnoreCase ) { "Duplicati.GUI.TrayIcon.csproj" };
/// <summary>
/// Some executables have shorter names that follow the Linux convention of all-lowercase
/// </summary>
2024-04-15 13:17:49 +02:00
/// <remarks>Note that the values here mirror the values in the AutoUpdater.PackageHelper, so changes should be coordinated between the two</remarks>
2024-03-22 12:15:53 +01:00
private static readonly IDictionary < string , string > ExecutableRenames = new Dictionary < string , string >( StringComparer . InvariantCultureIgnoreCase )
{
{ "Duplicati.CommandLine.BackendTester" , "duplicati-backend-tester" },
{ "Duplicati.CommandLine.BackendTool" , "duplicati-backend-tool" },
{ "Duplicati.CommandLine.RecoveryTool" , "duplicati-recovery-tool" },
2024-04-08 14:29:09 +02:00
{ "Duplicati.CommandLine.AutoUpdater" , "duplicati-autoupdater" },
{ "Duplicati.CommandLine.ConfigurationImporter" , "duplicati-configuration-importer" },
{ "Duplicati.CommandLine" , "duplicati-cli" },
{ "Duplicati.Server" , "duplicati-server" },
2024-03-22 12:15:53 +01:00
{ "Duplicati.GUI.TrayIcon" , "duplicati" }
};
2024-04-11 19:35:25 +02:00
/// <summary>
/// The supported versions of libicu for Debian
/// </summary>
private static string DebianLibIcuVersions => "libicu | " + string . Join ( " | " ,
"72;71;70;69;68;67;66;65;63;60;57;55;52"
. Split ( ";" , StringSplitOptions . RemoveEmptyEntries )
. Select ( x => $"libicu{x}" ));
/// <summary>
/// The supported versions of libssl for Debian
/// </summary>
private static string DebianLibSslVersions => "libssl3 | libssl1.1" ;
2024-03-24 17:34:20 +01:00
/// <summary>
/// The packages that are required for GUI builds
/// </summary>
2024-04-11 19:35:25 +02:00
private static readonly IReadOnlyList < string > DebianGUIDepends = [ "libice6" , "libsm6" , "libfontconfig1" , DebianLibIcuVersions , DebianLibSslVersions ];
2024-03-24 17:34:20 +01:00
/// <summary>
/// The packages that are required for CLI builds
/// </summary>
2024-04-11 19:35:25 +02:00
private static readonly IReadOnlyList < string > DebianCLIDepends = [ DebianLibIcuVersions , DebianLibSslVersions ];
2024-03-24 17:34:20 +01:00
2024-03-26 11:56:07 +01:00
/// <summary>
/// The packages that are required for GUI builds
/// </summary>
2024-04-09 13:02:13 +02:00
private static readonly IReadOnlyList < string > FedoraGUIDepends = [ "libICE" , "libSM" , "fontconfig" , "libicu" ];
2024-03-26 11:56:07 +01:00
/// <summary>
/// The packages that are required for CLI builds
/// </summary>
2024-04-08 14:29:09 +02:00
private static readonly IReadOnlyList < string > FedoraCLIDepends = [ "libicu" ];
2024-03-26 11:56:07 +01:00
2024-03-20 16:17:17 +01:00
/// <summary>
/// Structure for keeping all variables for a single release
/// </summary>
/// <param name="Version">The version to use</param>
2024-03-25 11:53:52 +01:00
/// <param name="Channel">The release channel</param>
2024-03-20 16:17:17 +01:00
/// <param name="Timestamp">The release timestamp</param>
2024-03-25 11:53:52 +01:00
private record ReleaseInfo ( Version Version , ReleaseChannel Channel , DateTime Timestamp )
2024-03-20 16:17:17 +01:00
{
/// <summary>
/// Gets the string name for the release
/// </summary>
2024-03-25 11:53:52 +01:00
public string ReleaseName => $"{Version}_{Channel.ToString().ToLowerInvariant()}_{Timestamp:yyy-MM-dd}" ;
2024-03-20 16:17:17 +01:00
/// <summary>
/// Create a new release info
/// </summary>
/// <param name="type">The release type</param>
/// <param name="incVersion">The incremental version</param>
/// <returns>The release info</returns>
2024-03-22 15:20:45 +01:00
public static ReleaseInfo Create ( ReleaseChannel type , int incVersion )
2024-03-20 16:17:17 +01:00
=> new ReleaseInfo ( new Version ( 2 , 0 , 0 , incVersion ), type , DateTime . Today );
2024-04-04 16:08:45 +02:00
/// <summary>
/// Create a new release info
/// </summary>
/// <param name="type">The release type</param>
/// <param name="version">The version</param>
/// <returns>The release info</returns>
public static ReleaseInfo Create ( ReleaseChannel type , Version version )
=> new ReleaseInfo ( version , type , DateTime . Today );
2024-03-20 16:17:17 +01:00
}
/// <summary>
/// Creates the build command
/// </summary>
/// <returns>The command</returns>
2024-03-27 19:52:40 +01:00
public static System . CommandLine . Command Create ()
2024-03-20 16:17:17 +01:00
{
2024-04-04 16:08:45 +02:00
var releaseChannelArgument = new Argument < ReleaseChannel >(
name : "channel" ,
description : "The release channel" ,
getDefaultValue : () => ReleaseChannel . Canary
);
2024-03-20 16:17:17 +01:00
var buildTargetOption = new Option < PackageTarget []>(
name : "--targets" ,
description : "The possible build targets, multiple arguments supported. Use the format os-arch.package, example: x64-win.msi." ,
parseArgument : arg =>
{
if (! arg . Tokens . Any ())
return Program . SupportedPackageTargets . ToArray ();
var requested = arg . Tokens . Select ( x => PackageTarget . ParsePackageId ( x . Value )). Distinct (). ToArray ();
var invalid = requested . Where ( x => ! Program . SupportedPackageTargets . Contains ( x )). ToList ();
if ( invalid . Any ())
throw new Exception ( $"Following targets are not supported: {string.Join(" , ", invalid.Select(x => x.PackageTargetString))}" );
return requested ;
});
var gitStashPushOption = new Option < bool >(
2024-03-22 15:20:45 +01:00
name : "--git-stash-push" ,
2024-03-20 16:17:17 +01:00
description : "Performs a git stash command before running the build, and a git commit after updating files" ,
getDefaultValue : () => true
);
var keepBuildsOption = new Option < bool >(
2024-03-22 15:20:45 +01:00
name : "--keep-builds" ,
2024-03-20 16:17:17 +01:00
description : "Do not delete the build folders if they already exist (re-use build)" ,
getDefaultValue : () => false
);
var buildTempOption = new Option < DirectoryInfo >(
name : "--build-path" ,
description : "The path to the temporary folder used for builds; will be deleted on startup" ,
getDefaultValue : () => new DirectoryInfo ( Path . GetFullPath ( "build-temp" ))
);
var solutionFileOption = new Option < FileInfo >(
2024-03-22 15:20:45 +01:00
name : "--solution-file" ,
2024-03-20 16:17:17 +01:00
description : "Path to the Duplicati.sln file" ,
getDefaultValue : () => new FileInfo ( Path . GetFullPath ( Path . Combine ( ".." , "Duplicati.sln" )))
);
2024-03-22 15:20:45 +01:00
var disableAuthenticodeOption = new Option < bool >(
name : "--disable-authenticode" ,
description : "Disables authenticode signing" ,
getDefaultValue : () => false
);
var disableCodeSignOption = new Option < bool >(
name : "--disable-signcode" ,
description : "Disables Apple signcode signing" ,
getDefaultValue : () => false
);
2024-03-27 19:52:40 +01:00
var passwordOption = SharedOptions . passwordOption ;
2024-03-22 15:20:45 +01:00
2024-03-25 11:53:52 +01:00
var disableDockerPushOption = new Option < bool >(
name : "--disable-docker-push" ,
description : "Disables pushing the docker image to the repository" ,
getDefaultValue : () => false
);
var macOsAppNameOption = new Option < string >(
name : "--macos-app-name" ,
description : "The name of the MacOS app bundle" ,
getDefaultValue : () => "Duplicati.app"
);
var dockerRepoOption = new Option < string >(
name : "--docker-repo" ,
description : "The docker repository to push to" ,
getDefaultValue : () => "duplicati/duplicati"
);
2024-03-27 19:52:40 +01:00
var changelogFileOption = new Option < FileInfo >(
name : "--changelog-file" ,
description : "The path to the changelog news file. Contents from this file are prepended to the changelog." ,
getDefaultValue : () => new FileInfo ( Path . GetFullPath ( "changelog-news.txt" ))
);
var disableNotarizeSigningOption = new Option < bool >(
name : "--disable-notarize-signing" ,
description : "Disables notarize signing for MacOS packages" ,
getDefaultValue : () => false
);
2024-04-04 15:53:11 +02:00
var disableGpgSigningOption = new Option < bool >(
name : "--disable-gpg-signing" ,
description : "Disables GPG signing of packages" ,
getDefaultValue : () => false
);
2024-04-04 16:08:45 +02:00
var versionOverrideOption = new Option < string? >(
name : "--version" ,
description : "Sets a custom version to use" ,
getDefaultValue : () => null
);
2024-04-11 12:44:12 +02:00
var disableS3UploadOption = new Option < bool >(
name : "--disable-s3-upload" ,
description : "Disables uploading to S3" ,
getDefaultValue : () => false
);
var disableGithubUploadOption = new Option < bool >(
name : "--disable-github-upload" ,
description : "Disables uploading to Github" ,
getDefaultValue : () => false
);
var disableUpdateServerReloadOption = new Option < bool >(
name : "--disable-update-server-reload" ,
description : "Disables reloading the update server" ,
getDefaultValue : () => false
);
var disableDiscordAnnounceOption = new Option < bool >(
name : "--disable-discourse-announce" ,
description : "Disables posting to the forum" ,
getDefaultValue : () => false
);
2024-04-04 16:08:45 +02:00
2024-03-27 19:52:40 +01:00
var command = new System . CommandLine . Command ( "build" , "Builds the packages for a release" ) {
2024-03-20 16:17:17 +01:00
gitStashPushOption ,
2024-04-04 16:08:45 +02:00
releaseChannelArgument ,
2024-03-20 16:17:17 +01:00
buildTempOption ,
buildTargetOption ,
solutionFileOption ,
2024-03-22 15:20:45 +01:00
keepBuildsOption ,
disableAuthenticodeOption ,
disableCodeSignOption ,
2024-03-25 11:53:52 +01:00
passwordOption ,
macOsAppNameOption ,
disableDockerPushOption ,
2024-03-27 19:52:40 +01:00
dockerRepoOption ,
changelogFileOption ,
2024-04-04 15:53:11 +02:00
disableNotarizeSigningOption ,
2024-04-04 16:08:45 +02:00
disableGpgSigningOption ,
2024-04-11 12:44:12 +02:00
versionOverrideOption ,
disableS3UploadOption ,
disableGithubUploadOption ,
disableUpdateServerReloadOption ,
disableDiscordAnnounceOption
2024-03-20 16:17:17 +01:00
};
2024-03-22 15:20:45 +01:00
command . Handler = CommandHandler . Create < CommandInput >( DoBuild );
return command ;
}
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
/// <summary>
/// The input for the build command
/// </summary>
/// <param name="Targets">The build targets</param>
/// <param name="BuildPath">The build path</param>
/// <param name="SolutionFile">The solution path</param>
2024-03-25 11:53:52 +01:00
/// <param name="GitStashPush">If the git stash should be performed</param>
/// <param name="Channel">The release channel</param>
2024-04-04 16:08:45 +02:00
/// <param name="Version">The version override to use</param>
2024-03-22 15:20:45 +01:00
/// <param name="KeepBuilds">If the builds should be kept</param>
/// <param name="DisableAuthenticode">If authenticode signing should be disabled</param>
/// <param name="DisableSignCode">If signcode should be disabled</param>
/// <param name="Password">The password to use for the keyfile</param>
2024-03-25 11:53:52 +01:00
/// <param name="DisableDockerPush">If the docker push should be disabled</param>
/// <param name="MacOSAppName">The name of the MacOS app bundle</param>
/// <param name="DockerRepo">The docker repository to push to</param>
2024-03-27 19:52:40 +01:00
/// <param name="ChangelogFile">The path to the changelog file</param>
/// <param name="DisableNotarizeSigning">If notarize signing should be disabled</param>
2024-04-04 15:53:11 +02:00
/// <param name="DisableGpgSigning">If GPG signing should be disabled</param>
2024-04-11 12:44:12 +02:00
/// <param name="DisableS3Upload">If S3 upload should be disabled</param>
/// <param name="DisableGithubUpload">If Github upload should be disabled</param>
/// <param name="DisableUpdateServerReload">If the update server should not be reloaded</param>
/// <param name="DisableDiscordAnnounce">If forum posting should be disabled</param>
2024-03-22 15:20:45 +01:00
record CommandInput (
PackageTarget [] Targets ,
DirectoryInfo BuildPath ,
FileInfo SolutionFile ,
bool GitStashPush ,
ReleaseChannel Channel ,
2024-04-04 16:08:45 +02:00
string? Version ,
2024-03-22 15:20:45 +01:00
bool KeepBuilds ,
bool DisableAuthenticode ,
bool DisableSignCode ,
2024-03-25 11:53:52 +01:00
string Password ,
bool DisableDockerPush ,
string MacOSAppName ,
2024-03-27 19:52:40 +01:00
string DockerRepo ,
FileInfo ChangelogFile ,
2024-04-04 15:53:11 +02:00
bool DisableNotarizeSigning ,
2024-04-11 12:44:12 +02:00
bool DisableGpgSigning ,
bool DisableS3Upload ,
bool DisableGithubUpload ,
bool DisableUpdateServerReload ,
bool DisableDiscordAnnounce
2024-03-22 15:20:45 +01:00
);
static async Task DoBuild ( CommandInput input )
{
Console . WriteLine ( $"Building {input.Channel} release ..." );
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
var buildTargets = input . Targets ;
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
if (! buildTargets . Any ())
buildTargets = Program . SupportedPackageTargets . ToArray ();
2024-03-22 12:15:53 +01:00
2024-03-22 15:20:45 +01:00
if (! input . SolutionFile . Exists )
throw new FileNotFoundException ( $"Solution file not found: {input.SolutionFile.FullName}" );
2024-03-22 12:15:53 +01:00
2024-03-22 15:20:45 +01:00
// This could be fixed, so we will throw an exception if the build is not possible
if ( buildTargets . Any ( x => x . Package == PackageType . MSI ) && ! Program . Configuration . IsMSIBuildPossible ())
throw new Exception ( "WiX toolset not configured, cannot build MSI files" );
2024-03-22 12:15:53 +01:00
2024-03-22 15:20:45 +01:00
// This will be fixed in the future, but requires a new http-interface for Synology DSM
if ( buildTargets . Any ( x => x . Package == PackageType . SynologySpk ) && ! Program . Configuration . IsSynologyPkgPossible ())
throw new Exception ( "Synology SPK files are currently not supported" );
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
// This will not work, so to make it easier for non-MacOS developers, we will remove the MacOS packages
if ( buildTargets . Any ( x => x . Package == PackageType . MacPkg || x . Package == PackageType . DMG ) && ! Program . Configuration . IsMacPkgBuildPossible ())
{
Console . WriteLine ( "MacOS packages requested but not running on MacOS, removing from build targets" );
buildTargets = buildTargets . Where ( x => x . Package != PackageType . MacPkg && x . Package != PackageType . DMG ). ToArray ();
}
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
var baseDir = Path . GetDirectoryName ( input . SolutionFile . FullName ) ?? throw new Exception ( "Path to solution file was invalid" );
2024-04-23 14:24:38 +02:00
var versionFilePath = Path . Combine ( baseDir , "ReleaseBuilder" , "build_version.txt" );
2024-03-22 15:20:45 +01:00
if (! File . Exists ( versionFilePath ))
throw new FileNotFoundException ( $"Version file not found: {versionFilePath}" );
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
var sourceProjects = Directory . EnumerateDirectories ( Path . Combine ( baseDir , "Executables" , "net8" ), "*" , SearchOption . TopDirectoryOnly )
. SelectMany ( x => Directory . EnumerateFiles ( x , "*.csproj" , SearchOption . TopDirectoryOnly ))
. ToList ();
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
var primaryGUI = sourceProjects . FirstOrDefault ( x => string . Equals ( Path . GetFileName ( x ), PrimaryGUIProject , StringComparison . OrdinalIgnoreCase )) ?? throw new Exception ( "Failed to find tray icon executable" );
var primaryCLI = sourceProjects . FirstOrDefault ( x => string . Equals ( Path . GetFileName ( x ), PrimaryCLIProject , StringComparison . OrdinalIgnoreCase )) ?? throw new Exception ( "Failed to find cli executable" );
var windowsOnly = sourceProjects . Where ( x => WindowsOnlyProjects . Contains ( Path . GetFileName ( x ))). ToHashSet ( StringComparer . OrdinalIgnoreCase );
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
// Put primary at the end
sourceProjects . Remove ( primaryGUI );
sourceProjects . Remove ( primaryCLI );
sourceProjects . Add ( primaryCLI );
sourceProjects . Add ( primaryGUI );
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
if (! File . Exists ( primaryGUI ))
throw new Exception ( $"Failed to locate project file: {primaryGUI}" );
if (! File . Exists ( primaryCLI ))
throw new Exception ( $"Failed to locate project file: {primaryCLI}" );
2024-03-20 16:17:17 +01:00
2024-03-27 19:52:40 +01:00
if (! input . ChangelogFile . Exists )
{
Console . WriteLine ( $"Changelog news file not found: {input.ChangelogFile.FullName}" );
Console . WriteLine ( $"Create an empty file if you want a release without changes" );
if ( OperatingSystem . IsWindows ())
Console . WriteLine ( $"> type nul > {input.ChangelogFile.FullName}" );
else
Console . WriteLine ( $"> touch {input.ChangelogFile.FullName}" );
Program . ReturnCode = 1 ;
return ;
}
2024-04-11 12:44:12 +02:00
if ( input . ChangelogFile . LastAccessTimeUtc < DateTime . UtcNow . AddDays (- 1 ))
{
Console . WriteLine ( $"Changelog news file was last modified {input.ChangelogFile.LastAccessTimeUtc:yyyy-MM-dd}, indicating a stale file" );
Console . WriteLine ( $"Please update the file with the changelog news for the release" );
Program . ReturnCode = 1 ;
return ;
}
2024-03-27 19:52:40 +01:00
var changelogNews = File . ReadAllText ( input . ChangelogFile . FullName );
2024-04-04 16:08:45 +02:00
var releaseInfo = string . IsNullOrWhiteSpace ( input . Version )
? ReleaseInfo . Create ( input . Channel , int . Parse ( File . ReadAllText ( versionFilePath )) + 1 )
: ReleaseInfo . Create ( input . Channel , Version . Parse ( input . Version ));
2024-03-22 15:20:45 +01:00
Console . WriteLine ( $"Building {releaseInfo.ReleaseName} ..." );
2024-03-20 16:17:17 +01:00
2024-04-11 12:44:12 +02:00
var keyfilePassword = input . Password ;
if ( string . IsNullOrWhiteSpace ( keyfilePassword ))
keyfilePassword = EnvHelper . GetEnvKey ( "KEYFILE_PASSWORD" , "" );
if ( string . IsNullOrWhiteSpace ( keyfilePassword ))
keyfilePassword = ConsoleHelper . ReadPassword ( "Enter keyfile password" );
2024-03-22 15:20:45 +01:00
2024-03-27 19:52:40 +01:00
var primarySignKey = LoadKeyFile ( Program . Configuration . ConfigFiles . UpdaterKeyfile . FirstOrDefault (), keyfilePassword , false );
var additionalKeys = Program . Configuration . ConfigFiles . UpdaterKeyfile
. Skip ( 1 )
. Select ( x => LoadKeyFile ( x , keyfilePassword , true ));
2024-03-22 15:20:45 +01:00
// Configure runtime environment
2024-03-25 11:53:52 +01:00
var rtcfg = new RuntimeConfig (
releaseInfo ,
2024-03-27 19:52:40 +01:00
additionalKeys . Prepend ( primarySignKey ). ToList (),
2024-03-25 11:53:52 +01:00
keyfilePassword ,
2024-03-27 19:52:40 +01:00
changelogNews ,
2024-03-25 11:53:52 +01:00
input );
rtcfg . ToggleAuthenticodeSigning ();
rtcfg . ToggleSignCodeSigning ();
2024-03-27 19:52:40 +01:00
rtcfg . ToggleNotarizeSigning ();
2024-04-04 15:53:11 +02:00
rtcfg . ToggleGpgSigning ();
2024-04-11 12:44:12 +02:00
rtcfg . ToggleS3Upload ();
rtcfg . ToggleGithubUpload ( rtcfg . ReleaseInfo . Channel );
rtcfg . ToogleDiscourseAnnounce ( rtcfg . ReleaseInfo . Channel );
rtcfg . ToggleUpdateServerReload ();
2024-03-22 15:20:45 +01:00
await rtcfg . ToggleDockerBuild ();
if (! rtcfg . UseDockerBuild )
{
var unsupportedBuilds = buildTargets . Where ( x => x . Package == PackageType . Docker || x . Package == PackageType . Deb || x . Package == PackageType . RPM ). ToList ();
if ( unsupportedBuilds . Any ())
2024-04-04 16:08:45 +02:00
throw new Exception ( $"The following packages cannot be built without Docker: {string.Join(" , ", unsupportedBuilds.Select(x => x.PackageTargetString))}" );
2024-03-22 15:20:45 +01:00
}
if (! input . KeepBuilds && Directory . Exists ( input . BuildPath . FullName ))
{
Console . WriteLine ( $"Deleting build folder: {input.BuildPath.FullName}" );
Directory . Delete ( input . BuildPath . FullName , true );
}
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
if (! Directory . Exists ( input . BuildPath . FullName ))
Directory . CreateDirectory ( input . BuildPath . FullName );
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
// Generally, the builds should happen with a clean source tree,
// but this can be disabled for debugging
if ( input . GitStashPush )
2024-03-27 19:52:40 +01:00
await ProcessHelper . Execute ([ "git" , "stash" , "save" , $"auto-build-{releaseInfo.Timestamp:yyyy-MM-dd}" ], workingDirectory : baseDir );
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
// Inject various files that will be embedded into the build artifacts
2024-03-27 19:52:40 +01:00
await PrepareSourceDirectory ( baseDir , releaseInfo , rtcfg );
// Inject a version tag into the html files
var revertableFiles = InjectVersionIntoFiles ( baseDir , releaseInfo );
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
// Perform the main compilations
await Compile . BuildProjects ( baseDir , input . BuildPath . FullName , sourceProjects , windowsOnly , GUIProjects , buildTargets , releaseInfo , input . KeepBuilds , rtcfg );
2024-03-20 16:17:17 +01:00
2024-03-22 15:20:45 +01:00
// Create the packages
2024-03-27 19:52:40 +01:00
var builtPackages = await CreatePackage . BuildPackages ( baseDir , input . BuildPath . FullName , buildTargets , input . KeepBuilds , rtcfg );
2024-04-11 12:44:12 +02:00
var files = builtPackages . Select ( x => x . CreatedFile ). ToList ();
2024-03-27 19:52:40 +01:00
// Build the signed manifest to be uploaded to remote storage
var manifestfile = Path . Combine ( input . BuildPath . FullName , "packages" , "autoupdate.manifest" );
2024-04-11 12:44:12 +02:00
if ( File . Exists ( manifestfile ) && new FileInfo ( manifestfile ). LastWriteTimeUtc > files . Max ( x => new FileInfo ( x ). LastWriteTimeUtc ))
{
Console . WriteLine ( "Manifest file already exists, skipping ..." );
}
else
{
Console . WriteLine ( "Build completed, creating signed manifest ..." );
if ( File . Exists ( manifestfile ))
File . Delete ( manifestfile );
UpdaterManager . CreateSignedManifest (
rtcfg . SignKeys . First (),
null ,
Path . Combine ( input . BuildPath . FullName , "packages" ),
version : releaseInfo . Version . ToString (),
updateFromV1Url : Program . Configuration . ExtraSettings . UpdateFromV1Url ,
genericUpdatePageUrl : Program . Configuration . ExtraSettings . GenericUpdatePageUrl ,
releaseType : releaseInfo . Channel . ToString (). ToLowerInvariant (),
packages : builtPackages . Select ( x => new PackageEntry ()
{
RemoteUrls = Program . Configuration . ExtraSettings . PackageUrls
. Select ( u =>
u . Replace ( "${RELEASE_TYPE}" , releaseInfo . Channel . ToString (). ToLowerInvariant ())
. Replace ( "${RELEASE_VERSION}" , releaseInfo . Version . ToString ())
. Replace ( "${RELEASE_TIMESTAMP}" , releaseInfo . Timestamp . ToString ( "yyyy-MM-dd" ))
. Replace ( "${FILENAME}" , x . CreatedFile )
). ToArray (),
PackageTypeId = x . Target . PackageTargetString ,
Length = new FileInfo ( Path . Combine ( input . BuildPath . FullName , x . CreatedFile )). Length ,
MD5 = CalculateHash ( Path . Combine ( input . BuildPath . FullName , x . CreatedFile ), "md5" ),
SHA256 = CalculateHash ( Path . Combine ( input . BuildPath . FullName , x . CreatedFile ), "sha256" )
})
);
}
2024-04-04 15:53:11 +02:00
// Create the GPG signatures for the files
if ( rtcfg . UseGPGSigning )
{
2024-04-11 12:44:12 +02:00
var sigfile = Path . Combine ( input . BuildPath . FullName , "packages" , $"duplicati-{releaseInfo.ReleaseName}.signatures.zip" );
if ( File . Exists ( sigfile ) && new FileInfo ( sigfile ). LastWriteTimeUtc > files . Max ( x => new FileInfo ( x ). LastWriteTimeUtc ))
{
Console . WriteLine ( "Signature file already exists, skipping ..." );
}
else
{
Console . WriteLine ( "Creating GPG signatures ..." );
await GpgSign . SignReleaseFiles ( files , sigfile , rtcfg );
}
files . Add ( sigfile );
2024-04-04 15:53:11 +02:00
}
2024-03-22 12:15:53 +01:00
2024-04-11 12:44:12 +02:00
if ( rtcfg . UseS3Upload || rtcfg . UseGithubUpload )
{
Console . WriteLine ( "Build completed, uploading packages ..." );
if ( rtcfg . UseS3Upload )
{
var manifestNames = new [] { $"duplicati-{releaseInfo.ReleaseName}.manifest" , "latest-v2.manifest" };
2024-04-04 15:53:11 +02:00
2024-04-11 13:40:58 +02:00
var packageJson = Path . Combine ( input . BuildPath . FullName , "packages" , "latest-v2.json" );
var packageJs = Path . Combine ( input . BuildPath . FullName , "packages" , "latest-v2.js" );
var content = Upload . CreatePackageJson ( builtPackages , rtcfg );
File . WriteAllText ( packageJson , content );
File . WriteAllText ( packageJs , $"duplicati_installers = {content};" );
2024-04-11 12:44:12 +02:00
var uploads = files . Select ( x => new Upload . UploadFile ( x , Path . GetFileName ( x )))
2024-04-11 13:40:58 +02:00
. Concat ( manifestNames . Select ( x => new Upload . UploadFile ( manifestfile , x )))
. Append ( new Upload . UploadFile ( packageJson , Path . GetFileName ( packageJson )))
. Append ( new Upload . UploadFile ( packageJs , Path . GetFileName ( packageJs )));
2024-03-22 12:15:53 +01:00
2024-04-11 12:44:12 +02:00
await Upload . UploadToS3 ( uploads , rtcfg );
}
2024-03-22 12:15:53 +01:00
2024-04-11 12:44:12 +02:00
if ( rtcfg . UseGithubUpload )
await Upload . UploadToGithub (
files . Select ( x => new Upload . UploadFile ( x , Path . GetFileName ( x ))),
rtcfg
);
}
if ( rtcfg . UseUpdateServerReload )
{
Console . WriteLine ( "Release completed, reloading update server ..." );
await Upload . ReloadUpdateServer ( rtcfg );
}
if ( rtcfg . UseForumPosting )
{
Console . WriteLine ( "Release completed, posting release notes ..." );
await Upload . PostToForum ( rtcfg );
}
2024-03-22 12:15:53 +01:00
2024-03-22 15:20:45 +01:00
// Clean up the source tree
await ProcessHelper . Execute ( new [] {
2024-03-27 19:52:40 +01:00
"git" , "checkout" ,
"Duplicati/License/VersionTag.txt" ,
"Duplicati/Library/AutoUpdater/AutoUpdateURL.txt" ,
"Duplicati/Library/AutoUpdater/AutoUpdateBuildChannel.txt" ,
"Duplicati/Library/AutoUpdater/AutoUpdateSignKeys.txt" ,
}. Concat ( revertableFiles . Select ( x => Path . GetRelativePath ( baseDir , x )))
, workingDirectory : baseDir );
2024-03-22 12:15:53 +01:00
2024-03-22 15:20:45 +01:00
if ( input . GitStashPush )
2024-04-11 12:44:12 +02:00
{
2024-03-22 15:20:45 +01:00
await GitPush . TagAndPush ( baseDir , releaseInfo );
2024-04-11 13:43:09 +02:00
// Contents are added to changelog, so we can remove the file
2024-04-11 12:44:12 +02:00
input . ChangelogFile . Delete ();
}
2024-03-22 12:15:53 +01:00
2024-04-11 12:44:12 +02:00
Console . WriteLine ( "All done!" );
2024-03-20 16:17:17 +01:00
}
/// <summary>
2024-03-27 19:52:40 +01:00
/// Injects the version number into some html files
/// </summary>
/// <param name="baseDir">The base directory</param>
/// <param name="releaseInfo">The release info</param>
/// <returns>The paths that were modified</returns>
2024-04-17 16:14:55 +02:00
private static IEnumerable < string > InjectVersionIntoFiles ( string baseDir , ReleaseInfo releaseInfo )
2024-03-27 19:52:40 +01:00
{
var targetfiles = Directory . EnumerateFiles ( Path . Combine ( baseDir , "Duplicati" , "Server" , "webroot" ), "*" , SearchOption . AllDirectories )
. Where ( x => x . EndsWith ( ".html" ) || x . EndsWith ( ".js" ))
2024-04-17 16:14:55 +02:00
. ToList ();
2024-03-27 19:52:40 +01:00
var versionre = @"(?<version>\d+\.\d+\.(\*|(\d+(\.(\*|\d+)))?))" ;
var regex = new Regex ( @"\?v\=" + versionre );
foreach ( var file in targetfiles )
File . WriteAllText (
file ,
regex . Replace ( File . ReadAllText ( file ), $"?v={releaseInfo.Version}" )
);
2024-04-23 14:24:38 +02:00
var wixFile = Path . Combine ( baseDir , "ReleaseBuilder" , "Resources" , "Windows" , "UpgradeData.wxi" );
2024-04-17 16:14:55 +02:00
File . WriteAllText (
wixFile ,
Regex . Replace (
File . ReadAllText ( wixFile ),
@"\<\?define ProductVersion\=\""" + versionre + @"\"" \?\>" ,
$"<?define ProductVersion=\" { releaseInfo . Version } \ " ?>"
)
);
targetfiles . Add ( wixFile );
2024-03-27 19:52:40 +01:00
return targetfiles ;
}
/// <summary>
/// Updates the source directory prior to building.
/// This writes a stub package manifest inside the source folder,
/// which will be embedded in the excutable to indicate which version it was built from.
2024-03-20 16:17:17 +01:00
/// </summary>
/// <param name="baseDir">The source folder base</param>
/// <param name="releaseInfo">The release info to use</param>
2024-03-27 19:52:40 +01:00
/// <param name="rtcfg">The runtime configuration</param>
2024-03-20 16:17:17 +01:00
/// <returns>An awaitable task</returns>
2024-03-27 19:52:40 +01:00
static Task PrepareSourceDirectory ( string baseDir , ReleaseInfo releaseInfo , RuntimeConfig rtcfg )
2024-03-20 16:17:17 +01:00
{
2024-03-27 19:52:40 +01:00
var urlstring = string . Join ( ";" , Program . Configuration . ExtraSettings . UpdaterUrls . Select ( x =>
x . Replace ( "${RELEASE_TYPE}" , releaseInfo . Channel . ToString (). ToLowerInvariant ())
2024-03-20 16:17:17 +01:00
. Replace ( "${RELEASE_VERSION}" , releaseInfo . Version . ToString ())
2024-03-27 19:52:40 +01:00
. Replace ( "${RELEASE_TIMESTAMP}" , releaseInfo . Timestamp . ToString ( "yyyy-MM-dd" ))
. Replace ( "${FILENAME}" , "latest-v2.manifest" )
));
2024-03-20 16:17:17 +01:00
File . WriteAllText ( Path . Combine ( baseDir , "Duplicati" , "License" , "VersionTag.txt" ), releaseInfo . Version . ToString ());
2024-03-25 11:53:52 +01:00
File . WriteAllText ( Path . Combine ( baseDir , "Duplicati" , "Library" , "AutoUpdater" , "AutoUpdateBuildChannel.txt" ), releaseInfo . Channel . ToString (). ToLowerInvariant ());
2024-03-27 19:52:40 +01:00
File . WriteAllText ( Path . Combine ( baseDir , "Duplicati" , "Library" , "AutoUpdater" , "AutoUpdateURL.txt" ), urlstring );
File . WriteAllLines ( Path . Combine ( baseDir , "Duplicati" , "Library" , "AutoUpdater" , "AutoUpdateSignKeys.txt" ), rtcfg . SignKeys . Select ( x => x . ToXmlString ( false )));
if (! string . IsNullOrWhiteSpace ( rtcfg . ChangelogNews ))
2024-04-11 12:44:12 +02:00
{
var current = File . ReadAllText ( Path . Combine ( baseDir , "changelog.txt" ));
var previousEntry = current . IndexOf ( rtcfg . ChangelogNews );
if ( previousEntry >= 0 && previousEntry < 200 )
{
Console . WriteLine ( "Note: release already in changelog, skipping ..." );
}
else
{
File . WriteAllText (
Path . Combine ( baseDir , "changelog.txt" ),
string . Join ( Environment . NewLine , [
$"{rtcfg.ReleaseInfo.Timestamp:yyy-MM-dd} - {rtcfg.ReleaseInfo.ReleaseName}" ,
"==========" ,
rtcfg . ChangelogNews . Trim (),
"" ,
current
])
);
}
}
2024-03-27 19:52:40 +01:00
// Previous versions used to install the assembly redirects after the build
// but it looks like .Net now handles this automatically
// If not, we need to adjust the .csproj files before building
// find "${UPDATE_SOURCE}" - type f - name Duplicati.*.exe - maxdepth 1 - exec cp Installer/ AssemblyRedirects.xml { }.config \;
var manifestFile = Path . Combine ( baseDir , "Duplicati" , "Library" , "AutoUpdater" , "autoupdate.manifest" );
if ( File . Exists ( manifestFile ))
File . Delete ( manifestFile );
UpdaterManager . CreateSignedManifest (
rtcfg . SignKeys . First (),
null ,
Path . Combine ( baseDir , "Duplicati" , "Library" , "AutoUpdater" ),
version : releaseInfo . Version . ToString (),
updateFromV1Url : Program . Configuration . ExtraSettings . UpdateFromV1Url ,
genericUpdatePageUrl : Program . Configuration . ExtraSettings . GenericUpdatePageUrl ,
releaseType : releaseInfo . Channel . ToString (). ToLowerInvariant ()
2024-03-20 16:17:17 +01:00
);
2024-03-27 19:52:40 +01:00
2024-03-20 16:17:17 +01:00
return Task . CompletedTask ;
}
2024-03-27 19:52:40 +01:00
/// <summary>
/// Loads a keyfile and decrypts the RSA key inside
/// </summary>
/// <param name="keyfile">The keyfile to decrypt</param>
/// <param name="password">The keyfile password</param>
/// <param name="askForNewPassword">Allow asking for a new password if the password did not match</param>
/// <returns>The matching key</returns>
static RSA LoadKeyFile ( string? keyfile , string password , bool askForNewPassword )
{
if ( string . IsNullOrWhiteSpace ( keyfile ))
throw new Exception ( "Unable to load keyfile, no keyfile specified" );
if (! File . Exists ( keyfile ))
throw new FileNotFoundException ( $"Keyfile not found: {keyfile}" );
try
{
using var ms = new MemoryStream ();
using var fs = File . OpenRead ( keyfile );
SharpAESCrypt . SharpAESCrypt . Decrypt ( password , fs , ms );
var rsa = RSA . Create ();
rsa . FromXmlString ( System . Text . Encoding . UTF8 . GetString ( ms . ToArray ()));
return rsa ;
}
catch ( SharpAESCrypt . SharpAESCrypt . WrongPasswordException )
{
if (! askForNewPassword )
throw ;
}
password = ConsoleHelper . ReadPassword ( $"Enter password for {keyfile}" );
return LoadKeyFile ( keyfile , password , false );
}
/// <summary>
/// Calculates the hash of a file and returns the hash as a base64 string
/// </summary>
/// <param name="file">The file to calculate the has for</param>
/// <param name="algorithm">The hash algorithm</param>
/// <returns>The base64 encoded hash</returns>
static string CalculateHash ( string file , string algorithm )
{
using var fs = File . OpenRead ( file );
using var hash = algorithm . ToLowerInvariant () switch
{
"md5" => ( HashAlgorithm ) MD5 . Create (),
"sha256" => SHA256 . Create (),
_ => throw new Exception ( $"Unknown hash algorithm: {algorithm}" )
};
return Convert . ToBase64String ( hash . ComputeHash ( fs ));
}
2024-03-20 16:17:17 +01:00
}