2025-01-07 09:40:39 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
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 ;
2024-04-23 13:40:57 +02:00
using System.Text.Json ;
2024-03-27 19:52:40 +01:00
using System.Text.RegularExpressions ;
2024-04-23 13:40:57 +02:00
using System.Web ;
2024-03-27 19:52:40 +01:00
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>
2024-09-18 15:29:52 +02:00
/// The primary project to build for CLI builds
2024-03-22 12:15:53 +01:00
/// </summary>
private const string PrimaryCLIProject = "Duplicati.CommandLine.csproj" ;
2024-09-18 15:29:52 +02:00
/// <summary>
/// The primary project to build for Agent builds
/// </summary>
private const string PrimaryAgentProject = "Duplicati.Agent.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>
2024-09-18 15:29:52 +02:00
private static readonly IReadOnlySet < string > GUIOnlyProjects = new HashSet < string >( StringComparer . InvariantCultureIgnoreCase ) { "Duplicati.GUI.TrayIcon.csproj" };
/// <summary>
/// Projects that are Agent only
/// </summary>
private static readonly IReadOnlySet < string > AgentOnlyProjects = new HashSet < string >( StringComparer . InvariantCultureIgnoreCase ) { "Duplicati.Agent.csproj" };
/// <summary>
/// The projects that are part of the agent builds
/// </summary>
private static readonly IReadOnlySet < string > AgentProjects = new HashSet < string >( StringComparer . InvariantCultureIgnoreCase ) {
"Duplicati.Agent.csproj" ,
"Duplicati.Service.csproj" ,
"Duplicati.WindowsService.csproj"
};
2024-03-22 12:15:53 +01:00
/// <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" },
2024-05-21 09:54:01 +02:00
{ "Duplicati.CommandLine.SharpAESCrypt" , "duplicati-aescrypt" },
{ "Duplicati.CommandLine.Snapshots" , "duplicati-snapshots" },
2024-08-26 20:00:13 +02:00
{ "Duplicati.CommandLine.ServerUtil" , "duplicati-server-util" },
2024-10-24 15:56:39 +02:00
{ "Duplicati.CommandLine.SecretTool" , "duplicati-secret-tool" },
2025-02-10 21:37:52 +01:00
{ "Duplicati.CommandLine.SyncTool" , "duplicati-sync-tool" },
2025-02-21 11:51:45 +01:00
{ "Duplicati.CommandLine.SourceTool" , "duplicati-source-tool" },
2024-08-28 00:15:53 +02:00
{ "Duplicati.Service" , "duplicati-service" },
2024-09-18 11:07:59 +02:00
{ "Duplicati.Agent" , "duplicati-agent" },
2024-04-08 14:29:09 +02:00
{ "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 ( " | " ,
2024-05-03 10:49:15 +02:00
"74;72;71;70;69;68;67;66;65;63;60;57;55;52"
2024-04-11 19:35:25 +02:00
. 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-05-22 13:00:43 +02:00
private static readonly IReadOnlyList < string > FedoraGUIDepends = [ "libICE" , "libSM" , "fontconfig" , "libicu" , "desktop-file-utils" ];
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
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>
2024-05-30 14:10:12 +02:00
/// <param name="increment">The build version increment</param>
2024-04-04 16:08:45 +02:00
/// <returns>The release info</returns>
2024-05-30 14:10:12 +02:00
public static ReleaseInfo Create ( ReleaseChannel type , Version version , int increment )
=> new ReleaseInfo ( new Version ( version . Major , version . Minor , version . Build , version . Revision + increment ), 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
);
2024-04-23 13:44:22 +02:00
var buildOnlyOption = new Option < bool >(
name : "--build-only" ,
description : "Only build the binaries, do not create packages" ,
getDefaultValue : () => false
);
2024-03-20 16:17:17 +01:00
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
2025-02-26 09:20:20 +01:00
var signkeyPinOption = new Option < string >(
name : "--signkey-pin" ,
description : "The pin to use for the signing key" ,
getDefaultValue : () => string . Empty
);
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 >(
2024-12-06 09:19:01 +01:00
name : "--changelog-news-file" ,
2024-03-27 19:52:40 +01:00
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-05-27 12:21:19 +02:00
var useHostedBuildsOption = new Option < bool >(
name : "--use-hosted-builds" ,
description : "Create hosted builds that require .NET installed, instead of self-contained builds with no .NET dependency" ,
getDefaultValue : () => false
);
2025-01-07 17:55:07 +01:00
var resumeFromUploadOption = new Option < bool >(
name : "--resume-from-upload" ,
description : "Resumes the build process from the upload step" ,
getDefaultValue : () => false
);
2025-02-06 22:54:30 +01:00
var propagateReleaseOption = new Option < bool >(
name : "--propagate-release" ,
description : "Propagate the release to the next channel" ,
getDefaultValue : () => false
);
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 ,
2024-04-23 13:44:22 +02:00
buildOnlyOption ,
2024-03-22 15:20:45 +01:00
disableAuthenticodeOption ,
disableCodeSignOption ,
2024-03-25 11:53:52 +01:00
passwordOption ,
2025-02-26 09:20:20 +01:00
signkeyPinOption ,
2024-03-25 11:53:52 +01:00
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 ,
2024-05-27 12:21:19 +02:00
disableDiscordAnnounceOption ,
2025-01-07 17:55:07 +01:00
useHostedBuildsOption ,
2025-02-06 22:54:30 +01:00
resumeFromUploadOption ,
propagateReleaseOption
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>
2024-04-23 13:44:22 +02:00
/// <param name="BuildOnly">If only the build should be performed</param>
2024-03-22 15:20:45 +01:00
/// <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>
2025-02-26 09:20:20 +01:00
/// <param name="SignkeyPin">The pin to use for the signing key</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-12-06 09:19:01 +01:00
/// <param name="ChangelogNewsFile">The path to the changelog news file</param>
2024-03-27 19:52:40 +01:00
/// <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-05-27 12:21:19 +02:00
/// <param name="UseHostedBuilds">If hosted builds should be used</param>
2025-01-07 17:55:07 +01:00
/// <param name="ResumeFromUpload">If the process should resume from the upload step</param>
2025-02-06 22:54:30 +01:00
/// <param name="PropagateRelease">If the release should be propagated to the next channel</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 ,
2024-04-23 13:44:22 +02:00
bool BuildOnly ,
2024-03-22 15:20:45 +01:00
bool DisableAuthenticode ,
bool DisableSignCode ,
2024-03-25 11:53:52 +01:00
string Password ,
2025-02-26 09:20:20 +01:00
string SignkeyPin ,
2024-03-25 11:53:52 +01:00
bool DisableDockerPush ,
string MacOSAppName ,
2024-03-27 19:52:40 +01:00
string DockerRepo ,
2024-12-06 09:19:01 +01:00
FileInfo ChangelogNewsFile ,
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 ,
2024-05-27 12:21:19 +02:00
bool DisableDiscordAnnounce ,
2025-01-07 17:55:07 +01:00
bool UseHostedBuilds ,
2025-02-06 22:54:30 +01:00
bool ResumeFromUpload ,
bool PropagateRelease
2024-03-22 15:20:45 +01:00
);
static async Task DoBuild ( CommandInput input )
{
Console . WriteLine ( $"Building {input.Channel} release ..." );
2025-02-25 15:07:17 +01:00
var configuration = Configuration . Create ( input . Channel );
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
2025-02-25 15:07:17 +01:00
if ( buildTargets . Any ( x => x . Package == PackageType . MSI ) && ! configuration . IsMSIBuildPossible ())
2024-03-22 15:20:45 +01:00
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
2025-02-25 15:07:17 +01:00
if ( buildTargets . Any ( x => x . Package == PackageType . SynologySpk ) && ! configuration . IsSynologyPkgPossible ())
2024-03-22 15:20:45 +01:00
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
2025-02-25 15:07:17 +01:00
if ( buildTargets . Any ( x => x . Package == PackageType . MacPkg || x . Package == PackageType . DMG ) && ! configuration . IsMacPkgBuildPossible ())
2024-03-22 15:20:45 +01:00
{
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" );
2024-09-18 15:29:52 +02:00
var primaryAgent = sourceProjects . FirstOrDefault ( x => string . Equals ( Path . GetFileName ( x ), PrimaryAgentProject , StringComparison . OrdinalIgnoreCase )) ?? throw new Exception ( "Failed to find agent executable" );
2024-03-22 15:20:45 +01:00
var windowsOnly = sourceProjects . Where ( x => WindowsOnlyProjects . Contains ( Path . GetFileName ( x ))). ToHashSet ( StringComparer . OrdinalIgnoreCase );
2024-09-18 15:29:52 +02:00
var guiOnlyProjects = sourceProjects . Where ( x => GUIOnlyProjects . Contains ( Path . GetFileName ( x ))). ToHashSet ( StringComparer . OrdinalIgnoreCase );
var agentOnlyProjects = sourceProjects . Where ( x => AgentOnlyProjects . 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 );
2024-09-18 15:29:52 +02:00
sourceProjects . Remove ( primaryAgent );
sourceProjects . Add ( primaryAgent );
2024-03-22 15:20:45 +01:00
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-12-06 09:19:01 +01:00
if (! input . ChangelogNewsFile . Exists )
2024-03-27 19:52:40 +01:00
{
2024-12-06 09:19:01 +01:00
Console . WriteLine ( $"Changelog news file not found: {input.ChangelogNewsFile.FullName}" );
2024-03-27 19:52:40 +01:00
Console . WriteLine ( $"Create an empty file if you want a release without changes" );
if ( OperatingSystem . IsWindows ())
2024-12-06 09:19:01 +01:00
Console . WriteLine ( $"> type nul > {input.ChangelogNewsFile.FullName}" );
2024-03-27 19:52:40 +01:00
else
2024-12-06 09:19:01 +01:00
Console . WriteLine ( $"> touch {input.ChangelogNewsFile.FullName}" );
2024-03-27 19:52:40 +01:00
Program . ReturnCode = 1 ;
return ;
}
2024-12-06 09:19:01 +01:00
if ( input . ChangelogNewsFile . LastAccessTimeUtc < DateTime . UtcNow . AddDays (- 1 ))
2024-04-11 12:44:12 +02:00
{
2024-12-06 09:19:01 +01:00
Console . WriteLine ( $"Changelog news file was last modified {input.ChangelogNewsFile.LastAccessTimeUtc:yyyy-MM-dd}, indicating a stale file" );
2024-04-11 12:44:12 +02:00
Console . WriteLine ( $"Please update the file with the changelog news for the release" );
Program . ReturnCode = 1 ;
return ;
}
2024-12-06 09:19:01 +01:00
var changelogNews = File . ReadAllText ( input . ChangelogNewsFile . FullName );
2024-03-27 19:52:40 +01:00
2024-04-04 16:08:45 +02:00
var releaseInfo = string . IsNullOrWhiteSpace ( input . Version )
2024-05-30 14:10:12 +02:00
? ReleaseInfo . Create ( input . Channel , Version . Parse ( File . ReadAllText ( versionFilePath )), 1 )
: ReleaseInfo . Create ( input . Channel , Version . Parse ( input . Version ), 0 );
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
2025-02-25 15:07:17 +01:00
var primarySignKey = LoadKeyFile ( configuration . ConfigFiles . UpdaterKeyfile . FirstOrDefault (), keyfilePassword , false );
var additionalKeys = configuration . ConfigFiles . UpdaterKeyfile
2024-03-27 19:52:40 +01:00
. 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 (
2025-02-25 15:07:17 +01:00
configuration ,
2024-03-25 11:53:52 +01:00
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
}
2024-12-12 12:22:55 +01:00
// Prevent the system from going to sleep during the build
using var _ = new KeepAliveAssertion ();
2024-12-06 09:20:46 +01:00
if ( rtcfg . UseGithubUpload && ! input . GitStashPush )
{
Console . WriteLine ( "Github upload requested, but pushing the tag is disabled" );
Program . ReturnCode = 1 ;
}
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
2025-01-07 17:55:07 +01:00
if ( input . GitStashPush && ! input . ResumeFromUpload )
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-05-04 08:13:54 +02:00
var revertableFiles = await PrepareSourceDirectory ( baseDir , releaseInfo , rtcfg );
2024-03-27 19:52:40 +01:00
// Inject a version tag into the html files
2024-05-04 08:13:54 +02:00
revertableFiles . AddRange ( InjectVersionIntoFiles ( baseDir , releaseInfo ));
2024-03-20 16:17:17 +01:00
2024-10-03 08:25:41 +02:00
// Record the files that are replaced by the npm package
var foldersToRemove = new List < string >();
revertableFiles . AddRange ( await ReplaceNpmPackages ( baseDir , input . BuildPath . FullName , foldersToRemove , releaseInfo , rtcfg ));
2024-03-22 15:20:45 +01:00
// Perform the main compilations
2024-09-18 15:29:52 +02:00
var sourceBuildMap = new Dictionary < InterfaceType , IEnumerable < string >> {
{ InterfaceType . GUI , sourceProjects
. Where ( x => ! AgentOnlyProjects . Contains ( Path . GetFileName ( x ))) },
{ InterfaceType . Cli , sourceProjects
. Where ( x => ! GUIOnlyProjects . Contains ( Path . GetFileName ( x )))
. Where ( x => ! AgentOnlyProjects . Contains ( Path . GetFileName ( x ))) },
{ InterfaceType . Agent , sourceProjects
. Where ( x => AgentProjects . Contains ( Path . GetFileName ( x )))
}
};
await Compile . BuildProjects ( baseDir , input . BuildPath . FullName , sourceBuildMap , windowsOnly , buildTargets , releaseInfo , input . KeepBuilds , rtcfg , input . UseHostedBuilds );
2024-03-20 16:17:17 +01:00
2024-04-23 13:44:22 +02:00
if ( input . BuildOnly )
{
Console . WriteLine ( "Build completed, skipping package creation ..." );
return ;
}
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 (
2024-04-23 13:40:57 +02:00
rtcfg . SignKeys ,
JsonSerializer . Serialize ( new
{
Displayname = $"Duplicati v{releaseInfo.Version} - {releaseInfo.Channel}" ,
ChangeInfo = rtcfg . ChangelogNews
}),
2024-04-11 12:44:12 +02:00
Path . Combine ( input . BuildPath . FullName , "packages" ),
version : releaseInfo . Version . ToString (),
2025-02-25 15:07:17 +01:00
incompatibleUpdateUrl : ReplaceVersionPlaceholders ( configuration . ExtraSettings . UpdateFromIncompatibleVersionUrl , releaseInfo ),
genericUpdatePageUrl : ReplaceVersionPlaceholders ( configuration . ExtraSettings . GenericUpdatePageUrl , releaseInfo ),
2024-04-11 12:44:12 +02:00
releaseType : releaseInfo . Channel . ToString (). ToLowerInvariant (),
2024-04-23 13:40:57 +02:00
packages : builtPackages . Select ( x => new PackageEntry (
2025-02-25 15:07:17 +01:00
RemoteUrls : configuration . ExtraSettings . PackageUrls
2024-04-11 12:44:12 +02:00
. Select ( u =>
2024-04-23 13:40:57 +02:00
ReplaceVersionPlaceholders ( u , releaseInfo )
. Replace ( "${FILENAME}" , HttpUtility . UrlEncode ( Path . GetFileName ( x . CreatedFile )))
2024-04-11 12:44:12 +02:00
). ToArray (),
2024-04-23 13:40:57 +02:00
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-11 12:44:12 +02:00
);
}
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-12-06 09:20:46 +01:00
// Ensure the tag is pushed before uploading, so the uploaded files
// are associated with the release tag
2025-01-07 17:55:07 +01:00
if ( input . GitStashPush && ! input . ResumeFromUpload )
2024-12-06 09:20:46 +01:00
await GitPush . TagAndPush ( baseDir , releaseInfo );
2025-02-06 22:54:30 +01:00
// Propagate the release to the next channel, if selected
var nextChannels = new [] { ReleaseChannel . Experimental , ReleaseChannel . Beta , ReleaseChannel . Stable }
. Where ( x => x > releaseInfo . Channel )
. Where ( x => rtcfg . ReleaseInfo . Channel != ReleaseChannel . Canary )
. Where ( x => input . PropagateRelease )
. ToArray ();
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 content = Upload . CreatePackageJson ( builtPackages , rtcfg );
File . WriteAllText ( packageJson , 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 )))
2024-11-20 17:25:03 +01:00
. Append ( new Upload . UploadFile ( packageJson , $"latest-v2-{releaseInfo.Version}.json" ));
2024-03-22 12:15:53 +01:00
2025-02-06 22:54:30 +01:00
await Upload . UploadToS3 ( uploads , rtcfg , nextChannels );
2024-04-11 12:44:12 +02:00
}
2024-03-22 12:15:53 +01:00
2024-04-11 12:44:12 +02:00
if ( rtcfg . UseGithubUpload )
2024-12-06 09:20:46 +01:00
{
2025-01-07 17:55:07 +01:00
if (! rtcfg . UseS3Upload && input . GitStashPush && ! input . ResumeFromUpload )
2024-12-06 09:20:46 +01:00
{
Console . WriteLine ( "Waiting for Github to create the release tag ..." );
await Task . Delay ( TimeSpan . FromSeconds ( 30 ));
}
2024-04-11 12:44:12 +02:00
await Upload . UploadToGithub (
files . Select ( x => new Upload . UploadFile ( x , Path . GetFileName ( x ))),
rtcfg
);
2024-12-06 09:20:46 +01:00
}
2024-04-11 12:44:12 +02:00
}
if ( rtcfg . UseUpdateServerReload )
{
Console . WriteLine ( "Release completed, reloading update server ..." );
2025-02-06 22:54:30 +01:00
await Upload . ReloadUpdateServer ( rtcfg , nextChannels );
2024-04-11 12:44:12 +02:00
}
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
if ( input . GitStashPush )
2024-04-11 12:44:12 +02:00
{
2024-04-11 13:43:09 +02:00
// Contents are added to changelog, so we can remove the file
2024-12-06 09:19:01 +01:00
input . ChangelogNewsFile . Delete ();
2024-04-11 12:44:12 +02:00
}
2024-03-22 12:15:53 +01:00
2024-06-27 13:42:17 +02:00
// Clean up the source tree
2024-10-03 08:25:41 +02:00
foreach ( var folder in foldersToRemove )
if ( Directory . Exists ( folder ))
Directory . Delete ( folder , true );
2024-12-06 09:19:23 +01:00
// This often fails and should be fixed
try
{
await ProcessHelper . Execute ( new [] {
2024-06-27 13:42:17 +02:00
"git" , "checkout" ,
}. Concat ( revertableFiles . Select ( x => Path . GetRelativePath ( baseDir , x )))
2024-12-06 09:19:23 +01:00
, workingDirectory : baseDir );
}
catch ( Exception ex )
{
Console . WriteLine ( $"Warning: Failed to clean source directory: {ex.Message}" );
}
2024-06-27 13:42:17 +02:00
2024-04-11 12:44:12 +02:00
Console . WriteLine ( "All done!" );
2024-03-20 16:17:17 +01:00
}
2024-04-23 13:40:57 +02:00
/// <summary>
/// Replaces common placeholders with their respective values
/// </summary>
/// <param name="input">The string to update</param>
/// <param name="releaseInfo">The release info to replace with</param>
/// <param name="urlEncode">If the values should be URL encoded</param>
/// <returns>The updated string</returns>
private static string ReplaceVersionPlaceholders ( string input , ReleaseInfo releaseInfo , bool urlEncode = true )
{
return input
. Replace ( "${RELEASE_VERSION}" , urlEncode ? HttpUtility . UrlEncode ( releaseInfo . Version . ToString ()) : releaseInfo . Version . ToString ())
. Replace ( "${RELEASE_TIMESTAMP}" , urlEncode ? HttpUtility . UrlEncode ( releaseInfo . Timestamp . ToString ( "yyyy-MM-dd" )) : releaseInfo . Timestamp . ToString ( "yyyy-MM-dd" ))
. Replace ( "${RELEASE_CHANNEL}" , urlEncode ? HttpUtility . UrlEncode ( releaseInfo . Channel . ToString (). ToLowerInvariant ()) : releaseInfo . Channel . ToString (). ToLowerInvariant ())
. Replace ( "${RELEASE_TYPE}" , urlEncode ? HttpUtility . UrlEncode ( releaseInfo . Channel . ToString (). ToLowerInvariant ()) : releaseInfo . Channel . ToString (). ToLowerInvariant ());
}
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-11-07 23:05:55 +01:00
var wixFileGUI = Path . Combine ( baseDir , "ReleaseBuilder" , "Resources" , "Windows" , "TrayIcon" , "UpgradeData.wxi" );
2024-04-17 16:14:55 +02:00
File . WriteAllText (
2024-11-07 23:05:55 +01:00
wixFileGUI ,
2024-04-17 16:14:55 +02:00
Regex . Replace (
2024-11-07 23:05:55 +01:00
File . ReadAllText ( wixFileGUI ),
2024-04-17 16:14:55 +02:00
@"\<\?define ProductVersion\=\""" + versionre + @"\"" \?\>" ,
$"<?define ProductVersion=\" { releaseInfo . Version } \ " ?>"
)
);
2024-11-07 23:05:55 +01:00
targetfiles . Add ( wixFileGUI );
var wixFileAgent = Path . Combine ( baseDir , "ReleaseBuilder" , "Resources" , "Windows" , "Agent" , "UpgradeData.wxi" );
File . WriteAllText (
wixFileAgent ,
Regex . Replace (
File . ReadAllText ( wixFileAgent ),
@"\<\?define ProductVersion\=\""" + versionre + @"\"" \?\>" ,
$"<?define ProductVersion=\" { releaseInfo . Version } \ " ?>"
)
);
targetfiles . Add ( wixFileAgent );
2024-04-17 16:14:55 +02:00
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-05-04 08:13:54 +02:00
/// <returns>Modified files</returns>
static Task < List < string >> PrepareSourceDirectory ( string baseDir , ReleaseInfo releaseInfo , RuntimeConfig rtcfg )
2024-03-20 16:17:17 +01:00
{
2025-02-25 15:07:17 +01:00
var urlstring = string . Join ( ";" , rtcfg . Configuration . ExtraSettings . UpdaterUrls . Select ( x =>
2024-04-23 13:40:57 +02:00
ReplaceVersionPlaceholders ( x , releaseInfo )
. Replace ( "${FILENAME}" , HttpUtility . UrlEncode ( "latest-v2.manifest" ))
2024-03-27 19:52:40 +01:00
));
2024-03-20 16:17:17 +01:00
2024-05-14 14:42:15 +02:00
File . WriteAllText ( Path . Combine ( baseDir , "Duplicati" , "License" , "VersionTag.txt" ), releaseInfo . ReleaseName );
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 (
2024-04-23 13:40:57 +02:00
rtcfg . SignKeys ,
JsonSerializer . Serialize ( new { Displayname = $"Duplicati v{releaseInfo.Version} - {releaseInfo.Channel}" }),
2024-03-27 19:52:40 +01:00
Path . Combine ( baseDir , "Duplicati" , "Library" , "AutoUpdater" ),
version : releaseInfo . Version . ToString (),
2025-02-25 15:07:17 +01:00
incompatibleUpdateUrl : ReplaceVersionPlaceholders ( rtcfg . Configuration . ExtraSettings . UpdateFromIncompatibleVersionUrl , releaseInfo ),
genericUpdatePageUrl : ReplaceVersionPlaceholders ( rtcfg . Configuration . ExtraSettings . GenericUpdatePageUrl , releaseInfo ),
2024-03-27 19:52:40 +01:00
releaseType : releaseInfo . Channel . ToString (). ToLowerInvariant ()
2024-03-20 16:17:17 +01:00
);
2024-05-04 08:13:54 +02:00
return Task . FromResult ( new List < string > {
manifestFile ,
Path . Combine ( baseDir , "Duplicati" , "License" , "VersionTag.txt" ),
Path . Combine ( baseDir , "Duplicati" , "Library" , "AutoUpdater" , "AutoUpdateBuildChannel.txt" ),
Path . Combine ( baseDir , "Duplicati" , "Library" , "AutoUpdater" , "AutoUpdateURL.txt" ),
Path . Combine ( baseDir , "Duplicati" , "Library" , "AutoUpdater" , "AutoUpdateSignKeys.txt" ),
Path . Combine ( baseDir , "changelog.txt" )
});
2024-03-20 16:17:17 +01:00
}
2024-03-27 19:52:40 +01:00
2024-10-02 22:50:23 +02:00
/// <summary>
/// Uses NPM to replace the node_modules folder in the webroot with a fresh install
/// </summary>
/// <param name="baseDir">The base directory</param>
/// <param name="buildDir">The build directory</param>
2024-10-03 08:25:41 +02:00
/// <param name="foldersToRemove">The folders to remove after the build</param>
2024-10-02 22:50:23 +02:00
/// <param name="releaseInfo">The release info</param>
/// <param name="rtcfg">The runtime configuration</param>
/// <returns>The files that were deleted</returns>
2024-10-03 08:25:41 +02:00
static async Task < List < string >> ReplaceNpmPackages ( string baseDir , string buildDir , List < string > foldersToRemove , ReleaseInfo releaseInfo , RuntimeConfig rtcfg )
2024-10-02 22:50:23 +02:00
{
var deleted = new List < string >();
// Find all webroot folders with a package.json and package-lock.json
2024-10-03 16:54:58 +02:00
var webroot = Path . Combine ( baseDir , "Duplicati" , "Server" , "webroot" );
var targets = Directory . EnumerateDirectories ( webroot , "*" , SearchOption . TopDirectoryOnly )
2024-10-02 22:50:23 +02:00
. Where ( x => File . Exists ( Path . Combine ( x , "package.json" )) && File . Exists ( Path . Combine ( x , "package-lock.json" )))
. ToList ();
foreach ( var target in targets )
{
2025-02-25 15:07:17 +01:00
if ( string . IsNullOrWhiteSpace ( rtcfg . Configuration . Commands . Npm ))
2024-10-02 22:50:23 +02:00
throw new Exception ( "NPM command not found, but required for building" );
// Remove existing node_modules folder
if ( Directory . Exists ( Path . Combine ( target , "node_modules" )))
Directory . Delete ( Path . Combine ( target , "node_modules" ), true );
// These will be deleted after the build
deleted . AddRange ( Directory . EnumerateFiles ( target , "*" , SearchOption . AllDirectories ));
var tmp = Path . Combine ( buildDir , "tmp-npm" );
if ( Directory . Exists ( tmp ))
Directory . Delete ( tmp , true );
Directory . CreateDirectory ( tmp );
EnvHelper . CopyDirectory ( target , tmp , true );
// Run npm install in the temporary folder
2025-02-25 16:26:32 +01:00
await ProcessHelper . Execute ([ rtcfg . Configuration . Commands . Npm , "ci" ], workingDirectory : tmp );
2024-10-02 22:50:23 +02:00
var basefolder = Directory . EnumerateDirectories ( Path . Combine ( tmp , "node_modules" ), "*" , SearchOption . AllDirectories )
. Where ( x => File . Exists ( Path . Combine ( x , "index.html" )))
. OrderBy ( x => x . Length )
. FirstOrDefault ();
if ( string . IsNullOrWhiteSpace ( basefolder ))
throw new Exception ( $"Failed to locate node_modules folder in {target}" );
Directory . Delete ( target , true );
Directory . Move ( basefolder , target );
Directory . Delete ( tmp , true );
2024-10-03 08:25:41 +02:00
foldersToRemove . Add ( target );
2024-10-03 16:54:58 +02:00
var indexfile = Path . Combine ( target , "index.html" );
if ( File . Exists ( indexfile ))
{
var content = File . ReadAllText ( indexfile );
var prefix = target . Substring ( webroot . Length ). Replace ( Path . DirectorySeparatorChar , '/' ). Trim ( '/' );
content = content . Replace ( "<base href=\"/\">" , $"<base href=\" /{ prefix }/ \ ">" );
File . WriteAllText ( indexfile , content );
}
2024-10-02 22:50:23 +02:00
}
2024-10-03 08:25:41 +02:00
return deleted . Where ( x => ! Path . GetFileName ( x ). StartsWith ( "." ))
. ToList ();
2024-10-02 22:50:23 +02:00
}
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
}