namespace ReleaseBuilder; using static EnvHelper; /// /// The release channels /// public enum ReleaseChannel { /// /// The primary release form /// Stable, /// /// Beta releases /// Beta, /// /// Experimental are slightly less unstable than canary /// Experimental, /// /// The regular releases, may have breaking changes /// Canary, /// /// Nightly, unmonitored builds /// Nightly, /// /// Debug builds /// Debug } /// /// Represents the environment configuration /// /// The configuration files /// The commands /// Extra settings public record Configuration( ConfigFiles ConfigFiles, Commands Commands, ExtraSettings ExtraSettings ) { /// /// Creates a new /// /// The new configuration public static Configuration Create() => new( ConfigFiles.Create(), Commands.Create(), ExtraSettings.Create() ); /// /// Checks if signing with authenticode is possible given the current configuration /// /// A boolean indicating if signing is possible public bool IsAuthenticodePossible() { if (string.IsNullOrWhiteSpace(ConfigFiles.AuthenticodePasswordFile) || string.IsNullOrWhiteSpace(ConfigFiles.AuthenticodePfxFile) || string.IsNullOrWhiteSpace(Commands.OsslSignCode)) return false; if (!File.Exists(ConfigFiles.AuthenticodePasswordFile) || !File.Exists(ConfigFiles.AuthenticodePfxFile)) return false; return true; } /// /// Checks if signing with MacOS codesign is possible given the current configuration /// /// A boolean indicating if codesign is possible public bool IsCodeSignPossible() { if (!OperatingSystem.IsMacOS()) return false; if (string.IsNullOrWhiteSpace(ConfigFiles.CodesignIdentity) || string.IsNullOrWhiteSpace(Commands.Codesign) || string.IsNullOrWhiteSpace(Commands.Productsign)) return false; return true; } /// /// Checks if signing with notarize is possible given the current configuration /// /// public bool IsNotarizePossible() { if (!OperatingSystem.IsMacOS()) return false; if (string.IsNullOrWhiteSpace(ConfigFiles.NotarizeProfile)) return false; return true; } /// /// Checks if building MSI files is possible given the current configuration /// /// A boolean indicating if MSI building is possible public bool IsMSIBuildPossible() { if (string.IsNullOrWhiteSpace(Commands.Wix)) return false; return true; } /// /// Checks if building MacOS packages is possible given the current configuration /// /// A boolean indicating if MacOS package building is possible public bool IsMacPkgBuildPossible() { if (!OperatingSystem.IsMacOS()) return false; return true; } /// /// Checks if building Docker images is possible given the current configuration /// /// A boolean indicating if Docker image building is possible public bool IsDockerBuildPossible() { if (string.IsNullOrWhiteSpace(Commands.Docker)) return false; return true; } /// /// Checks if AWS uploads are possible /// /// A boolean indicating if AWS uploading is possible public bool IsAwsUploadPossible() { if (string.IsNullOrWhiteSpace(ConfigFiles.AwsUploadProfile) || string.IsNullOrWhiteSpace(ConfigFiles.AwsUploadBucket)) return false; return true; } /// /// Checks if Github uploads are possible /// public bool IsGithubUploadPossible() { if (string.IsNullOrWhiteSpace(ConfigFiles.GithubTokenFile)) return false; if (!File.Exists(ConfigFiles.GithubTokenFile)) return false; return true; } /// /// Checks if update server reloads are possible /// /// A boolean indicating if update server reloads are possible public bool IsUpdateServerReloadPossible() { if (string.IsNullOrWhiteSpace(ConfigFiles.ReloadUpdatesApiKey)) return false; return true; } /// /// Checks if Discourse announcements are possible /// /// A boolean indicating if Discourse announcements are possible public bool IsDiscourseAnnouncePossible() { if (string.IsNullOrWhiteSpace(ConfigFiles.DiscourseTokenFile)) return false; if (!File.Exists(ConfigFiles.DiscourseTokenFile)) return false; return true; } /// /// Checks if GPG signing is possible /// /// A boolean indicating if GPG signing is possible public bool IsGpgPossible() { if (string.IsNullOrWhiteSpace(ConfigFiles.GpgKeyfile) || string.IsNullOrWhiteSpace(Commands.Gpg)) return false; if (!File.Exists(ConfigFiles.GpgKeyfile)) return false; return true; } /// /// Determines if creating a Synology package is possible. /// /// true if creating a Synology package is possible; otherwise, false. public bool IsSynologyPkgPossible() => false; } /// /// Configuration files used by the build script /// /// The key file used to sign manifests /// The GPG key used to build signed hash files /// The PFX file used to sign binaries /// The encrypted file containing the password used to unlock the PFX file /// The token used for Github uploads /// The token used for Discourse forum announce /// The identity to use for MacOS signing /// The profile to use for MacOS notarization /// The profile used by the aws-cli tool for uploads /// The S3 bucket to upload files to public record ConfigFiles( string[] UpdaterKeyfile, string GpgKeyfile, string AuthenticodePfxFile, string AuthenticodePasswordFile, string GithubTokenFile, string DiscourseTokenFile, string CodesignIdentity, string NotarizeProfile, string AwsUploadProfile, string AwsUploadBucket, string ReloadUpdatesApiKey ) { /// /// Parses an environment file and sets the environment variables, /// similar to the `source` command in bash /// /// The path to the file private static void ParseEnvironmentFile(string path) { if (File.Exists(path)) { var kvp = File.ReadAllLines(path) .Where(x => !string.IsNullOrWhiteSpace(x) && x.IndexOf('=') > 0) .Select(x => x.StartsWith("export ") ? x.Substring("export ".Length).Trim() : x) .Select(x => x.Trim().Split("=", 2)) .Where(x => x.Length == 2) .Select(x => new { Key = x[0], Value = x[1] }); foreach (var k in kvp) Environment.SetEnvironmentVariable(k.Key, k.Value); } } /// /// Generates a new config files instance /// /// The config files instance public static ConfigFiles Create() { ParseEnvironmentFile(ExpandEnv("BUILD_SETTINGS_FILE", "${HOME}/.config/build-settings")); ParseEnvironmentFile(ExpandEnv("GATEKEEPER_SETTINGS_FILE", "${HOME}/.config/signkeys/Duplicati/macos-gatekeeper")); return new( ExpandEnv("UPDATER_KEYFILE", "${HOME}/.config/signkeys/Duplicati/updater-release.key").Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), ExpandEnv("GPG_KEYFILE", "${HOME}/.config/signkeys/Duplicati/updater-gpgkey.key"), ExpandEnv("AUTHENTICODE_PFXFILE", "${HOME}/.config/signkeys/Duplicati/authenticode.pfx"), ExpandEnv("AUTHENTICODE_PASSWORD", "${HOME}/.config/signkeys/Duplicati/authenticode.key"), ExpandEnv("GITHUB_TOKEN_FILE", "${HOME}/.config/github-api-token"), ExpandEnv("DISCOURSE_TOKEN_FILE", "${HOME}/.config/discourse-api-token"), ExpandEnv("CODESIGN_IDENTITY", ""), ExpandEnv("NOTARIZE_PROFILE", "duplicati-notarize"), ExpandEnv("AWS_UPLOAD_PROFILE", "duplicati-upload"), ExpandEnv("AWS_UPLOAD_BUCKET", "updates.duplicati.com"), ExpandEnv("RELOAD_UPDATES_API_KEY", "") ); } } /// /// Configuration of commands used by the build script /// /// The "build" command /// The "gpg" command /// The "aws" command /// The "osslsigncode" command /// The "codesign" command /// The "productsign" command /// The "wix" command /// The "docker" command public record Commands( string Dotnet, string? Gpg, string? OsslSignCode, string? Codesign, string? Productsign, string? Wix, string? Docker ) { /// /// Generates a new command instance /// /// The command instance public static Commands Create() => new( FindCommand("dotnet", "DOTNET") ?? throw new Exception("Failed to find the \"dotnet\" command"), FindCommand("gpg2", "GPG", FindCommand("gpg", "GPG")), FindCommand(OperatingSystem.IsWindows() ? "signtool.exe" : "osslsigncode", "SIGNTOOL"), OperatingSystem.IsMacOS() ? FindCommand("codesign", "CODESIGN") : null, OperatingSystem.IsMacOS() ? FindCommand("productsign", "PRODUCTSIGN") : null, FindCommand(OperatingSystem.IsWindows() ? "wix" : "wixl", "WIX"), FindCommand("docker", "DOCKER") ); } /// /// Extra settings used by the build script, that are not expected to be changed often /// /// The URL to use for clients upgrading from earlier versions /// The URL to redirect to when the update has no specific package /// The urls where packages are stored /// The urls where manifest files are stored public record ExtraSettings( string UpdateFromIncompatibleVersionUrl, string GenericUpdatePageUrl, string[] PackageUrls, string[] UpdaterUrls ) { /// /// Generates a new extra settings instance /// /// The extra settings instance public static ExtraSettings Create() => new( GetEnvKey("UPDATE_FROM_INCOMPATIBLE_URL", "https://duplicati.com/download-dynamic?channel=${RELEASE_CHANNEL}&update_from=${RELEASE_VERSION}"), GetEnvKey("GENERIC_UPDATE_PAGE_URL", "https://duplicati.com/download-dynamic?channel=${RELEASE_CHANNEL}&from=${RELEASE_VERSION}"), GetEnvKey("PACKAGE_URLS", "https://updates.duplicati.com/${RELEASE_CHANNEL}/${FILENAME};https://alt.updates.duplicati.com/${RELEASE_CHANNEL}/${FILENAME}").Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), GetEnvKey("UPDATER_URLS", "https://updates.duplicati.com/${RELEASE_CHANNEL}/${FILENAME};https://alt.updates.duplicati.com/${RELEASE_CHANNEL}/${FILENAME}").Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ); }