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)
);
}