From 21a6c9ef4ddbefe9a6e6c5abf287f2ad993edffb Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Fri, 1 Dec 2023 01:30:05 +0200 Subject: [PATCH] refactor: mark types as `non_exhaustive` (#84) * refactor: mark types as `non_exhaustive` * fix macos --- .changes/non-exhuastive.md | 5 + bindings/updater/nodejs/src/from_impls.rs | 12 +- bindings/updater/nodejs/src/lib.rs | 4 +- crates/packager/schema.json | 490 ++++++------ crates/packager/src/cli/config.rs | 4 +- crates/packager/src/config/builder.rs | 209 +++++ crates/packager/src/config/mod.rs | 886 +++++++++++++++++++--- crates/packager/src/package/app/mod.rs | 4 +- crates/packager/src/package/deb/mod.rs | 7 +- crates/packager/src/package/mod.rs | 3 +- crates/packager/src/package/nsis/mod.rs | 3 +- crates/packager/src/package/wix/mod.rs | 17 +- crates/packager/src/sign.rs | 23 +- crates/packager/src/util.rs | 2 +- crates/updater/src/lib.rs | 40 +- 15 files changed, 1309 insertions(+), 400 deletions(-) create mode 100644 .changes/non-exhuastive.md create mode 100644 crates/packager/src/config/builder.rs diff --git a/.changes/non-exhuastive.md b/.changes/non-exhuastive.md new file mode 100644 index 00000000..d0836685 --- /dev/null +++ b/.changes/non-exhuastive.md @@ -0,0 +1,5 @@ +--- +"cargo-packager": minor +--- + +Mark most of the types as `non_exhaustive` to allow adding more field later on without having to break downstream users use the newly added helper methods on these types to modify the corresponding fields in-place. diff --git a/bindings/updater/nodejs/src/from_impls.rs b/bindings/updater/nodejs/src/from_impls.rs index f3b3cbe4..2e93744d 100644 --- a/bindings/updater/nodejs/src/from_impls.rs +++ b/bindings/updater/nodejs/src/from_impls.rs @@ -23,16 +23,16 @@ impl From for WindowsUpdateIns impl From for UpdaterWindowsOptions { fn from(value: cargo_packager_updater::UpdaterWindowsConfig) -> Self { Self { - installer_args: Some(value.installer_args), - install_mode: Some(value.install_mode.into()), + installer_args: value.installer_args, + install_mode: value.install_mode.map(Into::into), } } } impl From for cargo_packager_updater::UpdaterWindowsConfig { fn from(value: UpdaterWindowsOptions) -> Self { Self { - installer_args: value.installer_args.unwrap_or_default(), - install_mode: value.install_mode.map(Into::into).unwrap_or_default(), + installer_args: value.installer_args, + install_mode: value.install_mode.map(Into::into), } } } @@ -46,7 +46,7 @@ impl From for cargo_packager_updater::Config { .filter_map(|e| e.parse().ok()) .collect(), pubkey: value.pubkey, - windows: value.windows.map(Into::into).unwrap_or_default(), + windows: value.windows.map(Into::into), } } } @@ -99,7 +99,7 @@ impl From for Update { }) .collect(), format: value.format.into(), - windows: value.config.windows.into(), + windows: value.config.windows.map(Into::into), } } } diff --git a/bindings/updater/nodejs/src/lib.rs b/bindings/updater/nodejs/src/lib.rs index dd0c599b..d7534dc8 100644 --- a/bindings/updater/nodejs/src/lib.rs +++ b/bindings/updater/nodejs/src/lib.rs @@ -124,7 +124,7 @@ pub struct Update { /// Update format pub format: UpdateFormat, /// The Windows options for the updater. - pub windows: UpdaterWindowsOptions, + pub windows: Option, /// Update description pub body: Option, /// Update publish date @@ -138,7 +138,7 @@ impl Update { Ok(cargo_packager_updater::Update { config: cargo_packager_updater::Config { pubkey: self.pubkey.clone(), - windows: self.windows.clone().into(), + windows: self.windows.clone().map(Into::into), ..Default::default() }, body: self.body.clone(), diff --git a/crates/packager/schema.json b/crates/packager/schema.json index aaeb5f08..aa83e3ca 100644 --- a/crates/packager/schema.json +++ b/crates/packager/schema.json @@ -4,28 +4,53 @@ "description": "The packaging config.", "type": "object", "properties": { - "enabled": { - "description": "Whether this config is enabled or not. Defaults to `true`.", - "default": true, - "type": "boolean" - }, "$schema": { - "description": "The JSON schema for the config.\n\nSetting this field has no effect, this just exists so we can parse the JSON correct when it has `$schema` field set.", + "description": "The JSON schema for the config.\n\nSetting this field has no effect, this just exists so we can parse the JSON correctly when it has `$schema` field set.", "type": [ "string", "null" ] }, "name": { - "description": "The app name, this is just an identifier that could be used to filter which app to package using `--packages` cli arg when there is multiple apps in the workspace or in the same config.\n\nThis field resembles, the `name` field in `Cargo.toml` and `package.json`\n\nIf `unset`, the CLI will try to auto-detect it from `Cargo.toml` or `package.json` otherwise, it will keep it as null.", + "description": "The app name, this is just an identifier that could be used to filter which app to package using `--packages` cli arg when there is multiple apps in the workspace or in the same config.\n\nThis field resembles, the `name` field in `Cargo.toml` or `package.json`\n\nIf `unset`, the CLI will try to auto-detect it from `Cargo.toml` or `package.json` otherwise, it will keep it unset.", "type": [ "string", "null" ] }, + "enabled": { + "description": "Whether this config is enabled or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "productName": { + "description": "The package's product name, for example \"My Awesome App\".", + "default": "", + "type": "string" + }, + "version": { + "description": "The package's version.", + "default": "", + "type": "string" + }, + "binaries": { + "description": "The binaries to package.", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Binary" + } + }, + "identifier": { + "description": "The application identifier in reverse domain name notation (e.g. `com.packager.example`). This string must be unique across applications since it is used in some system configurations. This string must contain only alphanumeric characters (A–Z, a–z, and 0–9), hyphens (-), and periods (.).", + "type": [ + "string", + "null" + ], + "pattern": "^[a-zA-Z0-9-\\.]*$" + }, "beforePackagingCommand": { - "description": "Specify a command to run before starting to package an application.\n\nThis runs only once.", - "default": null, + "description": "The command to run before starting to package an application.\n\nThis runs only once.", "anyOf": [ { "$ref": "#/definitions/HookCommand" @@ -36,8 +61,7 @@ ] }, "beforeEachPackageCommand": { - "description": "Specify a command to run before packaging each format for an application.\n\nThis will run multiple times depending on the formats specifed.", - "default": null, + "description": "The command to run before packaging each format for an application.\n\nThis will run multiple times depending on the formats specifed.", "anyOf": [ { "$ref": "#/definitions/HookCommand" @@ -48,7 +72,7 @@ ] }, "logLevel": { - "description": "The log level.", + "description": "The logging level.", "anyOf": [ { "$ref": "#/definitions/LogLevel" @@ -59,7 +83,7 @@ ] }, "formats": { - "description": "The package types we're creating.\n\nif not present, we'll use the PackageType list for the target OS.", + "description": "The packaging formats to create, if not present, [`PackageFormat::platform_default`] is used.", "type": [ "array", "null" @@ -69,66 +93,51 @@ } }, "outDir": { - "description": "The directory where the `binaries` exist and where the packages will be placed.", + "description": "The directory where the [`Config::binaries`] exist and where the generated packages will be placed.", "default": "", "type": "string" }, "targetTriple": { - "description": "The target triple. Defaults to the current OS target triple.", + "description": "The target triple we are packaging for. This mainly affects [`Config::external_binaries`].\n\nDefaults to the current OS target triple.", "type": [ "string", "null" ] }, - "productName": { - "description": "the package's product name, for example \"My Awesome App\".", - "default": "", - "type": "string" - }, - "version": { - "description": "the package's version.", - "default": "", - "type": "string" - }, "description": { - "description": "the package's description.", + "description": "The package's description.", "type": [ "string", "null" ] }, "longDescription": { - "description": "the app's long description.", + "description": "The app's long description.", "type": [ "string", "null" ] }, "homepage": { - "description": "the package's homepage.", + "description": "The package's homepage.", "type": [ "string", "null" ] }, "authors": { - "description": "the package's authors.", - "default": [], - "type": "array", - "items": { - "type": "string" - } - }, - "identifier": { - "description": "the application identifier in reverse domain name notation (e.g. `com.packager.example`). This string must be unique across applications since it is used in some system configurations. This string must contain only alphanumeric characters (A–Z, a–z, and 0–9), hyphens (-), and periods (.).", + "description": "The package's authors.", + "default": null, "type": [ - "string", + "array", "null" ], - "pattern": "^[a-zA-Z0-9-\\.]*$" + "items": { + "type": "string" + } }, "publisher": { - "description": "The app's publisher. Defaults to the second element in the identifier string. Currently maps to the Manufacturer property of the Windows Installer.", + "description": "The app's publisher. Defaults to the second element in [`Config::identifier`](Config::identifier) string. Currently maps to the Manufacturer property of the Windows Installer.", "type": [ "string", "null" @@ -142,14 +151,14 @@ ] }, "copyright": { - "description": "the app's copyright.", + "description": "The app's copyright.", "type": [ "string", "null" ] }, "category": { - "description": "the app's category.", + "description": "The app's category.", "anyOf": [ { "$ref": "#/definitions/AppCategory" @@ -160,7 +169,7 @@ ] }, "icons": { - "description": "the app's icon list.", + "description": "The app's icon list.", "type": [ "array", "null" @@ -169,16 +178,8 @@ "type": "string" } }, - "binaries": { - "description": "the binaries to package.", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Binary" - } - }, "fileAssociations": { - "description": "the file associations", + "description": "The file associations", "type": [ "array", "null" @@ -198,7 +199,7 @@ } }, "externalBinaries": { - "description": "External binaries to add to the package.\n\nNote that each binary name should have the target platform's target triple appended, as well as `.exe` for Windows. For example, if you're packaging a sidecar called `sqlite3`, the packager expects a binary named `sqlite3-x86_64-unknown-linux-gnu` on linux, and `sqlite3-x86_64-pc-windows-gnu.exe` on windows.\n\nIf you are building a universal binary for MacOS, the packager expects your external binary to also be universal, and named after the target triple, e.g. `sqlite3-universal-apple-darwin`. See ", + "description": "Paths to external binaries to add to the package.\n\nThe path specified should not include `-<.exe>` suffix, it will be auto-added when by the packager when reading these paths, so the actual binary name should have the target platform's target triple appended, as well as `.exe` for Windows.\n\nFor example, if you're packaging an external binary called `sqlite3`, the packager expects a binary named `sqlite3-x86_64-unknown-linux-gnu` on linux, and `sqlite3-x86_64-pc-windows-gnu.exe` on windows.\n\nIf you are building a universal binary for MacOS, the packager expects your external binary to also be universal, and named after the target triple, e.g. `sqlite3-universal-apple-darwin`. See ", "type": [ "array", "null" @@ -207,66 +208,66 @@ "type": "string" } }, - "deb": { - "description": "Debian-specific settings.", + "windows": { + "description": "Windows-specific configuration.", "anyOf": [ { - "$ref": "#/definitions/DebianConfig" + "$ref": "#/definitions/WindowsConfig" }, { "type": "null" } ] }, - "appimage": { - "description": "Debian-specific settings.", + "macos": { + "description": "MacOS-specific configuration.", "anyOf": [ { - "$ref": "#/definitions/AppImageConfig" + "$ref": "#/definitions/MacOsConfig" }, { "type": "null" } ] }, - "wix": { - "description": "WiX configuration.", + "deb": { + "description": "Debian-specific configuration.", "anyOf": [ { - "$ref": "#/definitions/WixConfig" + "$ref": "#/definitions/DebianConfig" }, { "type": "null" } ] }, - "nsis": { - "description": "Nsis configuration.", + "appimage": { + "description": "AppImage configuration.", "anyOf": [ { - "$ref": "#/definitions/NsisConfig" + "$ref": "#/definitions/AppImageConfig" }, { "type": "null" } ] }, - "macos": { - "description": "MacOS-specific settings.", + "wix": { + "description": "WiX configuration.", "anyOf": [ { - "$ref": "#/definitions/MacOsConfig" + "$ref": "#/definitions/WixConfig" }, { "type": "null" } ] }, - "windows": { - "description": "Windows-specific settings.", + "nsis": { + "description": "Nsis configuration.", "anyOf": [ { - "$ref": "#/definitions/WindowsConfig" + "$ref": "#/definitions/NsisConfig" }, { "type": "null" @@ -276,6 +277,25 @@ }, "additionalProperties": false, "definitions": { + "Binary": { + "description": "A binary to package within the final package.", + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "description": "Path to the binary (without `.exe` on Windows). If it's relative, it will be resolved from [`Config::out_dir`].", + "type": "string" + }, + "main": { + "description": "Whether this is the main binary or not", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, "HookCommand": { "description": "Describes a shell command to be executed when a CLI hook is triggered.", "anyOf": [ @@ -452,41 +472,22 @@ "Weather" ] }, - "Binary": { - "description": "A binary to package within the final package.", - "type": "object", - "required": [ - "path" - ], - "properties": { - "path": { - "description": "Path to the binary (without `.exe` on Windows). If it's relative, it will be resolved from [`Config::out_dir`].", - "type": "string" - }, - "main": { - "description": "Whether this is the main binary or not", - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - }, "FileAssociation": { "description": "A file association configuration.", "type": "object", "required": [ - "ext" + "extensions" ], "properties": { - "ext": { + "extensions": { "description": "File extensions to associate with this app. e.g. 'png'", "type": "array", "items": { "type": "string" } }, - "name": { - "description": "The name. Maps to `CFBundleTypeName` on macOS. Default to the first item in `ext`", + "mimeType": { + "description": "The mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**.", "type": [ "string", "null" @@ -499,21 +500,21 @@ "null" ] }, + "name": { + "description": "The name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext`", + "type": [ + "string", + "null" + ] + }, "role": { - "description": "The app’s role with respect to the type. Maps to `CFBundleTypeRole` on macOS.", + "description": "The app’s role with respect to the type. Maps to `CFBundleTypeRole` on macOS. Defaults to [`BundleTypeRole::Editor`]", "default": "editor", "allOf": [ { "$ref": "#/definitions/BundleTypeRole" } ] - }, - "mimeType": { - "description": "The mime-type e.g. 'image/png' or 'text/plain'. Linux-only.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -585,12 +586,109 @@ } ] }, + "WindowsConfig": { + "description": "The Windows configuration.", + "type": "object", + "properties": { + "digestAlgorithm": { + "description": "The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended.", + "type": [ + "string", + "null" + ] + }, + "certificateThumbprint": { + "description": "The SHA1 hash of the signing certificate.", + "type": [ + "string", + "null" + ] + }, + "tsp": { + "description": "Whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true.", + "default": false, + "type": "boolean" + }, + "timestampUrl": { + "description": "Server to use during timestamping.", + "type": [ + "string", + "null" + ] + }, + "allowDowngrades": { + "description": "Whether to validate a second app installation, blocking the user from installing an older version if set to `false`.\n\nFor instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`.\n\nThe default value of this flag is `true`.", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "MacOsConfig": { + "description": "The macOS configuration.", + "type": "object", + "properties": { + "frameworks": { + "description": "MacOS frameworks that need to be packaged with the app.\n\nEach string can either be the name of a framework (without the `.framework` extension, e.g. `\"SDL2\"`), in which case we will search for that framework in the standard install locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and `/Network/Library/Frameworks/`), or a path to a specific framework bundle (e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes cargo-packager copy the specified frameworks into the OS X app bundle (under `Foobar.app/Contents/Frameworks/`); you are still responsible for:\n\n- arranging for the compiled binary to link against those frameworks (e.g. by emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your `build.rs` script)\n\n- embedding the correct rpath in your binary (e.g. by running `install_name_tool -add_rpath \"@executable_path/../Frameworks\" path/to/binary` after compiling)", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "minimumSystemVersion": { + "description": "A version string indicating the minimum MacOS version that the packaged app supports (e.g. `\"10.11\"`). If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`.", + "type": [ + "string", + "null" + ] + }, + "exceptionDomain": { + "description": "The exception domain to use on the macOS .app package.\n\nThis allows communication to the outside world e.g. a web server you're shipping.", + "type": [ + "string", + "null" + ] + }, + "signingIdentity": { + "description": "Code signing identity.", + "type": [ + "string", + "null" + ] + }, + "providerShortName": { + "description": "Provider short name for notarization.", + "type": [ + "string", + "null" + ] + }, + "entitlements": { + "description": "Path to the entitlements.plist file.", + "type": [ + "string", + "null" + ] + }, + "infoPlistPath": { + "description": "Path to the Info.plist file for the package.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "DebianConfig": { "description": "The Linux debian configuration.", "type": "object", "properties": { "depends": { - "description": "the list of debian dependencies.", + "description": "The list of debian dependencies.", "type": [ "array", "null" @@ -624,7 +722,7 @@ "type": "object", "properties": { "libs": { - "description": "List of libs that exist in `/usr/lib*` to be include in the final AppImage. The libs will be searched for using the command `find -L /usr/lib* -name `", + "description": "List of libs that exist in `/usr/lib*` to be include in the final AppImage. The libs will be searched for, using the command `find -L /usr/lib* -name `", "type": [ "array", "null" @@ -643,8 +741,8 @@ "type": "string" } }, - "linuxdeployPlugins": { - "description": "Hashmap of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) plugin name and its URL to be downloaded and executed while packaing the appimage. For example, if you want to use the [`gtk`](https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh) plugin, you'd specify `gtk` as the key and its url as the value.", + "files": { + "description": "List of custom files to add to the appimage package. Maps a dir/file to a dir/file inside the appimage package.", "type": [ "object", "null" @@ -653,8 +751,8 @@ "type": "string" } }, - "files": { - "description": "List of custom files to add to the appimage package. Maps a dir/file to a dir/file inside the appimage package.", + "linuxdeployPlugins": { + "description": "A map of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) plugin name and its URL to be downloaded and executed while packaing the appimage. For example, if you want to use the [`gtk`](https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh) plugin, you'd specify `gtk` as the key and its url as the value.", "type": [ "object", "null" @@ -672,19 +770,13 @@ "properties": { "languages": { "description": "The app languages to build. See .", - "default": [ - [ - "en-US", - { - "localePath": null - } - ] + "type": [ + "array", + "null" ], - "allOf": [ - { - "$ref": "#/definitions/WixLanguages" - } - ] + "items": { + "$ref": "#/definitions/WixLanguage" + } }, "template": { "description": "By default, the packager uses an internal template. This option allows you to define your own wix file.", @@ -805,36 +897,34 @@ }, "additionalProperties": false }, - "WixLanguages": { - "description": "The languages to build using WiX.", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/WixLanguageConfig" + "WixLanguage": { + "description": "A wix language.", + "anyOf": [ + { + "description": "Built-in wix language identifier.", + "type": "string" + }, + { + "description": "Custom wix language.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "identifier": { + "description": "Idenitifier of this language, for example `en-US`", + "type": "string" + }, + "path": { + "description": "The path to a locale (`.wxl`) file. See .", + "type": [ + "string", + "null" + ] + } } - ], - "maxItems": 2, - "minItems": 2 - } - }, - "WixLanguageConfig": { - "description": "Configuration for a target language for the WiX build.", - "type": "object", - "properties": { - "localePath": { - "description": "The path to a locale (`.wxl`) file. See .", - "type": [ - "string", - "null" - ] } - }, - "additionalProperties": false + ] }, "NsisConfig": { "description": "The NSIS format configuration.", @@ -957,6 +1047,13 @@ "enum": [ "lzma" ] + }, + { + "description": "Disable compression.", + "type": "string", + "enum": [ + "off" + ] } ] }, @@ -985,103 +1082,6 @@ ] } ] - }, - "MacOsConfig": { - "description": "The macOS configuration.", - "type": "object", - "properties": { - "frameworks": { - "description": "MacOS frameworks that need to be packaged with the app.\n\nEach string can either be the name of a framework (without the `.framework` extension, e.g. `\"SDL2\"`), in which case we will search for that framework in the standard install locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and `/Network/Library/Frameworks/`), or a path to a specific framework bundle (e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes cargo-packager copy the specified frameworks into the OS X app bundle (under `Foobar.app/Contents/Frameworks/`); you are still responsible for:\n\n- arranging for the compiled binary to link against those frameworks (e.g. by emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your `build.rs` script)\n\n- embedding the correct rpath in your binary (e.g. by running `install_name_tool -add_rpath \"@executable_path/../Frameworks\" path/to/binary` after compiling)", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "minimumSystemVersion": { - "description": "A version string indicating the minimum MacOS version that the packaged app supports (e.g. `\"10.11\"`). If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`.", - "type": [ - "string", - "null" - ] - }, - "exceptionDomain": { - "description": "The exception domain to use on the macOS .app package.\n\nThis allows communication to the outside world e.g. a web server you're shipping.", - "type": [ - "string", - "null" - ] - }, - "signingIdentity": { - "description": "Code signing identity.", - "type": [ - "string", - "null" - ] - }, - "providerShortName": { - "description": "Provider short name for notarization.", - "type": [ - "string", - "null" - ] - }, - "entitlements": { - "description": "Path to the entitlements.plist file.", - "type": [ - "string", - "null" - ] - }, - "infoPlistPath": { - "description": "Path to the Info.plist file for the package.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "WindowsConfig": { - "description": "The Windows configuration.", - "type": "object", - "properties": { - "digestAlgorithm": { - "description": "The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended.", - "type": [ - "string", - "null" - ] - }, - "certificateThumbprint": { - "description": "The SHA1 hash of the signing certificate.", - "type": [ - "string", - "null" - ] - }, - "timestampUrl": { - "description": "Server to use during timestamping.", - "type": [ - "string", - "null" - ] - }, - "tsp": { - "description": "Whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true.", - "default": false, - "type": "boolean" - }, - "allowDowngrades": { - "description": "Validates a second app installation, blocking the user from installing an older version if set to `false`.\n\nFor instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`.\n\nThe default value of this flag is `true`.", - "default": true, - "type": "boolean" - } - }, - "additionalProperties": false } } } \ No newline at end of file diff --git a/crates/packager/src/cli/config.rs b/crates/packager/src/cli/config.rs index 192b6b3d..3933b821 100644 --- a/crates/packager/src/cli/config.rs +++ b/crates/packager/src/cli/config.rs @@ -146,8 +146,8 @@ pub fn load_configs_from_cargo_workspace( if config.description.is_none() { config.description = package.description.clone(); } - if config.authors.is_empty() { - config.authors = package.authors.clone(); + if config.authors.is_none() { + config.authors = Some(package.authors.clone()); } if config.license_file.is_none() { config.license_file = package diff --git a/crates/packager/src/config/builder.rs b/crates/packager/src/config/builder.rs new file mode 100644 index 00000000..2ebfb5e2 --- /dev/null +++ b/crates/packager/src/config/builder.rs @@ -0,0 +1,209 @@ +use std::path::PathBuf; + +use crate::{Config, PackageFormat}; + +use super::{ + AppImageConfig, Binary, DebianConfig, FileAssociation, HookCommand, LogLevel, MacOsConfig, + NsisConfig, Resource, WindowsConfig, WixConfig, +}; + +/// A builder type for [`Config`]. +#[derive(Default)] +pub struct ConfigBuilder(Config); + +impl ConfigBuilder { + /// Creates a new config builder. + pub fn new() -> Self { + Self::default() + } + + /// Returns a reference to the config used by this builder. + pub fn config(&self) -> &Config { + &self.0 + } + + /// Sets [`Config::product_name`]. + pub fn product_name>(mut self, product_name: S) -> Self { + self.0.product_name = product_name.into(); + self + } + + /// Sets [`Config::version`]. + pub fn version>(mut self, version: S) -> Self { + self.0.version = version.into(); + self + } + + /// Sets [`Config::binaries`]. + pub fn binaries>(mut self, binaries: I) -> Self { + self.0.binaries = binaries.into_iter().collect(); + self + } + + /// Sets [`Config::identifier`]. + pub fn identifier>(mut self, identifier: S) -> Self { + self.0.identifier.replace(identifier.into()); + self + } + + /// Sets [`Config::before_packaging_command`]. + pub fn before_packaging_command(mut self, command: HookCommand) -> Self { + self.0.before_packaging_command.replace(command); + self + } + + /// Sets [`Config::before_each_package_command`]. + pub fn before_each_package_command(mut self, command: HookCommand) -> Self { + self.0.before_each_package_command.replace(command); + self + } + + /// Sets [`Config::log_level`]. + pub fn log_level(mut self, level: LogLevel) -> Self { + self.0.log_level.replace(level); + self + } + + /// Sets [`Config::formats`]. + pub fn formats>(mut self, formats: I) -> Self { + self.0.formats = Some(formats.into_iter().collect()); + self + } + + /// Sets [`Config::out_dir`]. + pub fn out_dir>(mut self, path: P) -> Self { + self.0.out_dir = path.into(); + self + } + + /// Sets [`Config::target_triple`]. + pub fn target_triple>(mut self, target_triple: S) -> Self { + self.0.target_triple.replace(target_triple.into()); + self + } + + /// Sets [`Config::description`]. + pub fn description>(mut self, description: S) -> Self { + self.0.description.replace(description.into()); + self + } + + /// Sets [`Config::long_description`]. + pub fn long_description>(mut self, long_description: S) -> Self { + self.0.long_description.replace(long_description.into()); + self + } + + /// Sets [`Config::homepage`]. + pub fn homepage>(mut self, homepage: S) -> Self { + self.0.homepage.replace(homepage.into()); + self + } + + /// Sets [`Config::authors`]. + pub fn authors(mut self, authors: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.0 + .authors + .replace(authors.into_iter().map(Into::into).collect()); + self + } + + /// Sets [`Config::publisher`]. + pub fn publisher>(mut self, publisher: S) -> Self { + self.0.publisher.replace(publisher.into()); + self + } + + /// Sets [`Config::license_file`]. + pub fn license_file>(mut self, license_file: P) -> Self { + self.0.license_file.replace(license_file.into()); + self + } + + /// Sets [`Config::copyright`]. + pub fn copyright>(mut self, copyright: S) -> Self { + self.0.copyright.replace(copyright.into()); + self + } + + /// Sets [`Config::icons`]. + pub fn icons(mut self, icons: I) -> Self + where + I: IntoIterator, + P: Into, + { + self.0 + .icons + .replace(icons.into_iter().map(Into::into).collect()); + self + } + + /// Sets [`Config::file_associations`]. + pub fn file_associations>( + mut self, + file_associations: I, + ) -> Self { + self.0 + .file_associations + .replace(file_associations.into_iter().collect()); + self + } + + /// Sets [`Config::resources`]. + pub fn resources>(mut self, resources: I) -> Self { + self.0.resources.replace(resources.into_iter().collect()); + self + } + + /// Sets [`Config::external_binaries`]. + pub fn external_binaries(mut self, external_binaries: I) -> Self + where + I: IntoIterator, + P: Into, + { + self.0 + .external_binaries + .replace(external_binaries.into_iter().map(Into::into).collect()); + self + } + + /// Set the [Windows](Config::windows) specific configuration. + pub fn windows(mut self, windows: WindowsConfig) -> Self { + self.0.windows.replace(windows); + self + } + + /// Set the [MacOS](Config::macos) specific configuration. + pub fn macos(mut self, macos: MacOsConfig) -> Self { + self.0.macos.replace(macos); + self + } + + /// Set the [WiX](Config::wix) specific configuration. + pub fn wix(mut self, wix: WixConfig) -> Self { + self.0.wix.replace(wix); + self + } + + /// Set the [Nsis](Config::nsis) specific configuration. + pub fn nsis(mut self, nsis: NsisConfig) -> Self { + self.0.nsis.replace(nsis); + self + } + + /// Set the [Debian](Config::deb) specific configuration. + pub fn deb(mut self, deb: DebianConfig) -> Self { + self.0.deb.replace(deb); + self + } + + /// Set the [Appimage](Config::appimage) specific configuration. + pub fn appimage(mut self, appimage: AppImageConfig) -> Self { + self.0.appimage.replace(appimage); + self + } +} diff --git a/crates/packager/src/config/mod.rs b/crates/packager/src/config/mod.rs index e0fb43d0..343231fd 100644 --- a/crates/packager/src/config/mod.rs +++ b/crates/packager/src/config/mod.rs @@ -15,16 +15,19 @@ use serde::{Deserialize, Serialize}; use crate::util; +mod builder; mod category; + +pub use builder::*; pub use category::AppCategory; /// The type of the package we're packaging. -#[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "clap", value(rename_all = "lowercase"))] #[serde(rename_all = "lowercase")] +#[non_exhaustive] pub enum PackageFormat { /// All available package formats for the current platform. /// @@ -209,27 +212,82 @@ impl Display for BundleTypeRole { #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[non_exhaustive] pub struct FileAssociation { /// File extensions to associate with this app. e.g. 'png' - pub ext: Vec, - /// The name. Maps to `CFBundleTypeName` on macOS. Default to the first item in `ext` - pub name: Option, + pub extensions: Vec, + /// The mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**. + #[serde(alias = "mime-type", alias = "mime_type")] + pub mime_type: Option, /// The association description. **Windows-only**. It is displayed on the `Type` column on Windows Explorer. pub description: Option, + /// The name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext` + pub name: Option, /// The app’s role with respect to the type. Maps to `CFBundleTypeRole` on macOS. + /// Defaults to [`BundleTypeRole::Editor`] #[serde(default)] pub role: BundleTypeRole, - /// The mime-type e.g. 'image/png' or 'text/plain'. Linux-only. - #[serde(alias = "mime-type", alias = "mime_type")] - pub mime_type: Option, +} + +impl FileAssociation { + /// Creates a new [`FileAssociation``] using provided extensions. + pub fn new(extensions: I) -> Self + where + I: IntoIterator, + S: Into, + { + Self { + extensions: extensions.into_iter().map(Into::into).collect(), + mime_type: None, + description: None, + name: None, + role: BundleTypeRole::default(), + } + } + + /// Set the extenstions to associate with this app. e.g. 'png'. + pub fn extensions(mut self, extensions: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.extensions = extensions.into_iter().map(Into::into).collect(); + self + } + + /// Set the mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**. + pub fn mime_type>(mut self, mime_type: S) -> Self { + self.mime_type.replace(mime_type.into()); + self + } + + /// Se the association description. **Windows-only**. It is displayed on the `Type` column on Windows Explorer. + pub fn description>(mut self, description: S) -> Self { + self.description.replace(description.into()); + self + } + + /// Set he name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext` + pub fn name>(mut self, name: S) -> Self { + self.name.replace(name.into()); + self + } + + /// Set he app’s role with respect to the type. Maps to `CFBundleTypeRole` on macOS. + /// Defaults to [`BundleTypeRole::Editor`] + pub fn role(mut self, role: BundleTypeRole) -> Self { + self.role = role; + self + } } /// The Linux debian configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[non_exhaustive] pub struct DebianConfig { - /// the list of debian dependencies. + /// The list of debian dependencies. pub depends: Option>, /// Path to a custom desktop file Handlebars template. /// @@ -258,28 +316,158 @@ pub struct DebianConfig { pub files: Option>, } +impl DebianConfig { + /// Creates a new [`DebianConfig`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the list of debian dependencies. + pub fn depends(mut self, depends: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.depends + .replace(depends.into_iter().map(Into::into).collect()); + self + } + + /// Set the path to a custom desktop file Handlebars template. + /// + /// Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`. + /// + /// Default file contents: + /// ```text + /// [Desktop Entry] + /// Categories={{categories}} + /// {{#if comment}} + /// Comment={{comment}} + /// {{/if}} + /// Exec={{exec}} + /// Icon={{icon}} + /// Name={{name}} + /// Terminal=false + /// Type=Application + /// {{#if mime_type}} + /// MimeType={{mime_type}} + /// {{/if}} + /// ``` + pub fn desktop_template>(mut self, desktop_template: P) -> Self { + self.desktop_template.replace(desktop_template.into()); + self + } + + /// Set the list of custom files to add to the deb package. + /// Maps a dir/file to a dir/file inside the debian package. + pub fn files(mut self, files: I) -> Self + where + I: IntoIterator, + S: Into, + T: Into, + { + self.files.replace( + files + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(), + ); + self + } +} + /// The Linux AppImage configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[non_exhaustive] pub struct AppImageConfig { /// List of libs that exist in `/usr/lib*` to be include in the final AppImage. - /// The libs will be searched for using the command + /// The libs will be searched for, using the command /// `find -L /usr/lib* -name ` pub libs: Option>, /// List of binary paths to include in the final AppImage. /// For example, if you want `xdg-open`, you'd specify `/usr/bin/xdg-open` pub bins: Option>, - /// Hashmap of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) + /// List of custom files to add to the appimage package. + /// Maps a dir/file to a dir/file inside the appimage package. + pub files: Option>, + /// A map of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) /// plugin name and its URL to be downloaded and executed while packaing the appimage. /// For example, if you want to use the /// [`gtk`](https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh) plugin, /// you'd specify `gtk` as the key and its url as the value. #[serde(alias = "linuxdeploy-plugins", alias = "linuxdeploy_plugins")] pub linuxdeploy_plugins: Option>, - /// List of custom files to add to the appimage package. +} + +impl AppImageConfig { + /// Creates a new [`DebianConfig`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the list of libs that exist in `/usr/lib*` to be include in the final AppImage. + /// The libs will be searched for using, the command + /// `find -L /usr/lib* -name ` + pub fn libs(mut self, libs: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.libs + .replace(libs.into_iter().map(Into::into).collect()); + self + } + + /// Set the list of binary paths to include in the final AppImage. + /// For example, if you want `xdg-open`, you'd specify `/usr/bin/xdg-open` + pub fn bins(mut self, bins: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.bins + .replace(bins.into_iter().map(Into::into).collect()); + self + } + + /// Set the list of custom files to add to the appimage package. /// Maps a dir/file to a dir/file inside the appimage package. - pub files: Option>, + pub fn files(mut self, files: I) -> Self + where + I: IntoIterator, + S: Into, + T: Into, + { + self.files.replace( + files + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(), + ); + self + } + + /// Set the map of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) + /// plugin name and its URL to be downloaded and executed while packaing the appimage. + /// For example, if you want to use the + /// [`gtk`](https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh) plugin, + /// you'd specify `gtk` as the key and its url as the value. + pub fn linuxdeploy_plugins(mut self, linuxdeploy_plugins: I) -> Self + where + I: IntoIterator, + S: Into, + T: Into, + { + self.linuxdeploy_plugins.replace( + linuxdeploy_plugins + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(), + ); + self + } } /// The macOS configuration. @@ -320,24 +508,93 @@ pub struct MacOsConfig { pub info_plist_path: Option, } -/// Configuration for a target language for the WiX build. -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct WixLanguageConfig { - /// The path to a locale (`.wxl`) file. See . - #[serde(alias = "locale-Path", alias = "locale_Path")] - pub locale_path: Option, +impl MacOsConfig { + /// Creates a new [`MacOsConfig`]. + pub fn new() -> Self { + Self::default() + } + + /// MacOS frameworks that need to be packaged with the app. + /// + /// Each string can either be the name of a framework (without the `.framework` extension, e.g. `"SDL2"`), + /// in which case we will search for that framework in the standard install locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and `/Network/Library/Frameworks/`), + /// or a path to a specific framework bundle (e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes cargo-packager copy the specified frameworks into the OS X app bundle + /// (under `Foobar.app/Contents/Frameworks/`); you are still responsible for: + /// + /// - arranging for the compiled binary to link against those frameworks (e.g. by emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your `build.rs` script) + /// + /// - embedding the correct rpath in your binary (e.g. by running `install_name_tool -add_rpath "@executable_path/../Frameworks" path/to/binary` after compiling) + pub fn frameworks(mut self, frameworks: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.frameworks + .replace(frameworks.into_iter().map(Into::into).collect()); + self + } + + /// A version string indicating the minimum MacOS version that the packaged app supports (e.g. `"10.11"`). + /// If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`. + pub fn minimum_system_version>(mut self, minimum_system_version: S) -> Self { + self.minimum_system_version + .replace(minimum_system_version.into()); + self + } + + /// The exception domain to use on the macOS .app package. + /// + /// This allows communication to the outside world e.g. a web server you're shipping. + pub fn exception_domain>(mut self, exception_domain: S) -> Self { + self.exception_domain.replace(exception_domain.into()); + self + } + + /// Code signing identity. + pub fn signing_identity>(mut self, signing_identity: S) -> Self { + self.signing_identity.replace(signing_identity.into()); + self + } + + /// Provider short name for notarization. + pub fn provider_short_name>(mut self, provider_short_name: S) -> Self { + self.provider_short_name.replace(provider_short_name.into()); + self + } + + /// Path to the entitlements.plist file. + pub fn entitlements>(mut self, entitlements: S) -> Self { + self.entitlements.replace(entitlements.into()); + self + } + + /// Path to the Info.plist file for the package. + pub fn info_plist_path>(mut self, info_plist_path: S) -> Self { + self.info_plist_path.replace(info_plist_path.into()); + self + } } -/// The languages to build using WiX. +/// A wix language. #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -pub struct WixLanguages(pub Vec<(String, WixLanguageConfig)>); +#[serde(untagged)] +#[non_exhaustive] +pub enum WixLanguage { + /// Built-in wix language identifier. + Identifier(String), + /// Custom wix language. + Custom { + /// Idenitifier of this language, for example `en-US` + identifier: String, + /// The path to a locale (`.wxl`) file. See . + path: Option, + }, +} -impl Default for WixLanguages { +impl Default for WixLanguage { fn default() -> Self { - Self(vec![("en-US".into(), Default::default())]) + Self::Identifier("en-US".into()) } } @@ -345,10 +602,10 @@ impl Default for WixLanguages { #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[non_exhaustive] pub struct WixConfig { /// The app languages to build. See . - #[serde(default)] - pub languages: WixLanguages, + pub languages: Option>, /// By default, the packager uses an internal template. /// This option allows you to define your own wix file. pub template: Option, @@ -410,10 +667,169 @@ pub struct WixConfig { pub fips_compliant: bool, } +impl WixConfig { + /// Creates a new [`WixConfig`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the app languages to build. See . + pub fn languages>(mut self, languages: I) -> Self { + self.languages.replace(languages.into_iter().collect()); + self + } + + /// By default, the packager uses an internal template. + /// This option allows you to define your own wix file. + pub fn template>(mut self, template: P) -> Self { + self.template.replace(template.into()); + self + } + + /// Set a list of merge modules to include in your installer. + /// For example, if you want to include [C++ Redis merge modules] + /// + /// [C++ Redis merge modules]: https://wixtoolset.org/docs/v3/howtos/redistributables_and_install_checks/install_vcredist/ + pub fn merge_modules(mut self, merge_modules: I) -> Self + where + I: IntoIterator, + P: Into, + { + self.merge_modules + .replace(merge_modules.into_iter().map(Into::into).collect()); + self + } + + /// Set a list of paths to .wxs files with WiX fragments to use. + pub fn fragment_paths(mut self, fragment_paths: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.fragment_paths + .replace(fragment_paths.into_iter().map(Into::into).collect()); + self + } + + /// Set a list of WiX fragments as strings. This is similar to [`WixConfig::fragment_paths`] but + /// is a string so you can define it inline in your config. + /// + /// ```text + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ``` + pub fn fragments(mut self, fragments: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.fragments + .replace(fragments.into_iter().map(Into::into).collect()); + self + } + + /// Set the ComponentGroup element ids you want to reference from the fragments. + pub fn component_group_refs(mut self, component_group_refs: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.component_group_refs + .replace(component_group_refs.into_iter().map(Into::into).collect()); + self + } + + /// Set the Component element ids you want to reference from the fragments. + pub fn component_refs(mut self, component_refs: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.component_refs + .replace(component_refs.into_iter().map(Into::into).collect()); + self + } + + /// Set the CustomAction element ids you want to reference from the fragments. + pub fn custom_action_refs(mut self, custom_action_refs: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.custom_action_refs + .replace(custom_action_refs.into_iter().map(Into::into).collect()); + self + } + + /// Set he FeatureGroup element ids you want to reference from the fragments. + pub fn feature_group_refs(mut self, feature_group_refs: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.feature_group_refs + .replace(feature_group_refs.into_iter().map(Into::into).collect()); + self + } + + /// Set the Feature element ids you want to reference from the fragments. + pub fn feature_refs(mut self, feature_refs: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.feature_refs + .replace(feature_refs.into_iter().map(Into::into).collect()); + self + } + + /// Set he Merge element ids you want to reference from the fragments. + pub fn merge_refs(mut self, merge_refs: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.merge_refs + .replace(merge_refs.into_iter().map(Into::into).collect()); + self + } + + /// Set the path to a bitmap file to use as the installation user interface banner. + /// This bitmap will appear at the top of all but the first page of the installer. + /// + /// The required dimensions are 493px × 58px. + pub fn banner_path>(mut self, path: P) -> Self { + self.banner_path.replace(path.into()); + self + } + + /// Set the path to a bitmap file to use on the installation user interface dialogs. + /// It is used on the welcome and completion dialogs. + /// The required dimensions are 493px × 312px. + pub fn dialog_image_path>(mut self, path: P) -> Self { + self.dialog_image_path.replace(path.into()); + self + } + + /// Set whether to enable or disable FIPS compliant algorithms. + pub fn fips_compliant(mut self, fips_compliant: bool) -> Self { + self.fips_compliant = fips_compliant; + self + } +} + /// Install Modes for the NSIS installer. #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[non_exhaustive] pub enum NSISInstallerMode { /// Default mode for the installer. /// @@ -446,6 +862,7 @@ impl Default for NSISInstallerMode { #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub enum NsisCompression { /// ZLIB uses the deflate algorithm, it is a quick and simple method. With the default compression level it uses about 300 KB of memory. Zlib, @@ -453,12 +870,15 @@ pub enum NsisCompression { Bzip2, /// LZMA (default) is a new compression method that gives very good compression ratios. The decompression speed is high (10-20 MB/s on a 2 GHz CPU), the compression speed is lower. The memory size that will be used for decompression is the dictionary size plus a few KBs, the default is 8 MB. Lzma, + /// Disable compression. + Off, } /// The NSIS format configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[non_exhaustive] pub struct NsisConfig { /// Set the compression algorithm used to compress files in the installer. /// @@ -547,10 +967,155 @@ pub struct NsisConfig { pub appdata_paths: Option>, } +impl NsisConfig { + /// Creates a new [`NsisConfig`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the compression algorithm used to compress files in the installer. + /// + /// See + pub fn compression(mut self, compression: NsisCompression) -> Self { + self.compression.replace(compression); + self + } + + /// Set a custom `.nsi` template to use. + /// + /// See the default template here + /// + pub fn template>(mut self, template: P) -> Self { + self.template.replace(template.into()); + self + } + + /// Set the logic of an NSIS section that will be ran before the install section. + /// + /// See the available libraries, dlls and global variables here + /// + /// + /// ### Example + /// ```toml + /// [package.metadata.packager.nsis] + /// preinstall-section = """ + /// ; Setup custom messages + /// LangString webview2AbortError ${LANG_ENGLISH} "Failed to install WebView2! The app can't run without it. Try restarting the installer." + /// LangString webview2DownloadError ${LANG_ARABIC} "خطأ: فشل تنزيل WebView2 - $0" + /// + /// Section PreInstall + /// ;
+ /// SectionEnd + /// + /// Section AnotherPreInstall + /// ;
+ /// SectionEnd + /// """ + /// ``` + pub fn preinstall_section>(mut self, preinstall_section: S) -> Self { + self.preinstall_section.replace(preinstall_section.into()); + self + } + + /// Set the path to a bitmap file to display on the header of installers pages. + /// + /// The recommended dimensions are 150px x 57px. + pub fn header_image>(mut self, header_image: P) -> Self { + self.header_image.replace(header_image.into()); + self + } + + /// Set the path to a bitmap file for the Welcome page and the Finish page. + /// + /// The recommended dimensions are 164px x 314px. + pub fn sidebar_image>(mut self, sidebar_image: P) -> Self { + self.sidebar_image.replace(sidebar_image.into()); + self + } + + /// Set the path to an icon file used as the installer icon. + pub fn installer_icon>(mut self, installer_icon: P) -> Self { + self.installer_icon.replace(installer_icon.into()); + self + } + + /// Set whether the installation will be for all users or just the current user. + pub fn install_mode(mut self, install_mode: NSISInstallerMode) -> Self { + self.install_mode = install_mode; + self + } + + /// Set a list of installer languages. + /// By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. + /// To allow the user to select the language, set `display_language_selector` to `true`. + /// + /// See for the complete list of languages. + pub fn languages(mut self, languages: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.languages + .replace(languages.into_iter().map(Into::into).collect()); + self + } + + /// Set a map of key-value pair where the key is the language and the + /// value is the path to a custom `.nsi` file that holds the translated text for cargo-packager's custom messages. + /// + /// See for an example `.nsi` file. + /// + /// **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array, + pub fn custom_language_files(mut self, custom_language_files: I) -> Self + where + I: IntoIterator, + S: Into, + P: Into, + { + self.custom_language_files.replace( + custom_language_files + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(), + ); + self + } + + /// Set wether to display a language selector dialog before the installer and uninstaller windows are rendered or not. + /// By default the OS language is selected, with a fallback to the first language in the `languages` array. + pub fn display_language_selector(mut self, display: bool) -> Self { + self.display_language_selector = display; + self + } + + /// Set a list of paths where your app stores data. + /// This options tells the uninstaller to provide the user with an option + /// (disabled by default) whether they want to rmeove your app data or keep it. + /// + /// The path should use a constant from + /// in addition to `$IDENTIFIER`, `$PUBLISHER` and `$PRODUCTNAME`, for example, if you store your + /// app data in `C:\\Users\\\\AppData\\Local\\\\` + /// you'd need to specify + /// ```toml + /// [package.metadata.packager.nsis] + /// appdata-paths = ["$LOCALAPPDATA/$PUBLISHER/$PRODUCTNAME"] + /// ``` + pub fn appdata_paths(mut self, appdata_paths: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.appdata_paths + .replace(appdata_paths.into_iter().map(Into::into).collect()); + self + } +} + /// The Windows configuration. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[non_exhaustive] pub struct WindowsConfig { /// The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended. #[serde(alias = "digest-algorithim", alias = "digest_algorithim")] @@ -558,14 +1123,14 @@ pub struct WindowsConfig { /// The SHA1 hash of the signing certificate. #[serde(alias = "certificate-thumbprint", alias = "certificate_thumbprint")] pub certificate_thumbprint: Option, - /// Server to use during timestamping. - #[serde(alias = "timestamp-url", alias = "timestamp_url")] - pub timestamp_url: Option, /// Whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may /// use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true. #[serde(default)] pub tsp: bool, - /// Validates a second app installation, blocking the user from installing an older version if set to `false`. + /// Server to use during timestamping. + #[serde(alias = "timestamp-url", alias = "timestamp_url")] + pub timestamp_url: Option, + /// Whether to validate a second app installation, blocking the user from installing an older version if set to `false`. /// /// For instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`. /// @@ -578,10 +1143,6 @@ pub struct WindowsConfig { pub allow_downgrades: bool, } -fn default_true() -> bool { - true -} - impl Default for WindowsConfig { fn default() -> Self { Self { @@ -594,6 +1155,49 @@ impl Default for WindowsConfig { } } +impl WindowsConfig { + /// Creates a new [`WindowsConfig`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended. + pub fn digest_algorithm>(mut self, digest_algorithm: S) -> Self { + self.digest_algorithm.replace(digest_algorithm.into()); + self + } + + /// Set the SHA1 hash of the signing certificate. + pub fn certificate_thumbprint>(mut self, certificate_thumbprint: S) -> Self { + self.certificate_thumbprint + .replace(certificate_thumbprint.into()); + self + } + + /// Set whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may + /// use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true. + pub fn tsp(mut self, tsp: bool) -> Self { + self.tsp = tsp; + self + } + + /// Set server url to use during timestamping. + pub fn timestamp_url>(mut self, timestamp_url: S) -> Self { + self.timestamp_url.replace(timestamp_url.into()); + self + } + + /// Set whether to validate a second app installation, blocking the user from installing an older version if set to `false`. + /// + /// For instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`. + /// + /// The default value of this flag is `true`. + pub fn allow_downgrades(mut self, allow: bool) -> Self { + self.allow_downgrades = allow; + self + } +} + /// An enum representing the available verbosity levels of the logger. #[derive(Deserialize, Serialize)] #[repr(usize)] @@ -633,19 +1237,45 @@ impl Default for LogLevel { #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[non_exhaustive] pub struct Binary { - /// Path to the binary (without `.exe` on Windows). If it's relative, it will be resolved from [`Config::out_dir`]. + /// Path to the binary (without `.exe` on Windows). + /// If it's relative, it will be resolved from [`Config::out_dir`]. pub path: PathBuf, /// Whether this is the main binary or not #[serde(default)] pub main: bool, } +impl Binary { + /// Creates a new [`Binary`] from a path to the binary (without `.exe` on Windows). + /// If it's relative, it will be resolved from [`Config::out_dir`]. + pub fn new>(path: P) -> Self { + Self { + path: path.into(), + main: false, + } + } + + /// Set the path of the binary. + pub fn path>(mut self, path: P) -> Self { + self.path = path.into(); + self + } + + /// Set the binary as main binary. + pub fn main(mut self, main: bool) -> Self { + self.main = main; + self + } +} + /// A path to a resource (with optional glob pattern) /// or an object of `src` and `target` paths. #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(untagged)] +#[non_exhaustive] pub enum Resource { /// Supports glob patterns Single(String), @@ -666,6 +1296,7 @@ pub enum Resource { #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(untagged)] +#[non_exhaustive] pub enum HookCommand { /// Run the given script with the default options. Script(String), @@ -683,93 +1314,88 @@ pub enum HookCommand { #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Config { - /// Whether this config is enabled or not. Defaults to `true`. - #[serde(default = "default_true")] - pub enabled: bool, /// The JSON schema for the config. /// /// Setting this field has no effect, this just exists so - /// we can parse the JSON correct when it has `$schema` field set. + /// we can parse the JSON correctly when it has `$schema` field set. #[serde(rename = "$schema")] - pub schema: Option, + schema: Option, /// The app name, this is just an identifier that could be used /// to filter which app to package using `--packages` cli arg when there is multiple apps in the /// workspace or in the same config. /// - /// This field resembles, the `name` field in `Cargo.toml` and `package.json` + /// This field resembles, the `name` field in `Cargo.toml` or `package.json` /// /// If `unset`, the CLI will try to auto-detect it from `Cargo.toml` or - /// `package.json` otherwise, it will keep it as null. - pub name: Option, - /// Specify a command to run before starting to package an application. + /// `package.json` otherwise, it will keep it unset. + pub(crate) name: Option, + /// Whether this config is enabled or not. Defaults to `true`. + #[serde(default = "default_true")] + pub(crate) enabled: bool, + /// The package's product name, for example "My Awesome App". + #[serde(default, alias = "product-name", alias = "product_name")] + pub product_name: String, + /// The package's version. + #[serde(default)] + pub version: String, + /// The binaries to package. + #[serde(default)] + pub binaries: Vec, + /// The application identifier in reverse domain name notation (e.g. `com.packager.example`). + /// This string must be unique across applications since it is used in some system configurations. + /// This string must contain only alphanumeric characters (A–Z, a–z, and 0–9), hyphens (-), + /// and periods (.). + #[cfg_attr(feature = "schema", schemars(regex(pattern = r"^[a-zA-Z0-9-\.]*$")))] + pub identifier: Option, + /// The command to run before starting to package an application. /// /// This runs only once. - #[serde( - default, - alias = "before-packaging-command", - alias = "before_packaging_command" - )] + #[serde(alias = "before-packaging-command", alias = "before_packaging_command")] pub before_packaging_command: Option, - /// Specify a command to run before packaging each format for an application. + /// The command to run before packaging each format for an application. /// /// This will run multiple times depending on the formats specifed. #[serde( - default, alias = "before-each-package-command", alias = "before_each_package_command" )] pub before_each_package_command: Option, - /// The log level. + /// The logging level. #[serde(alias = "log-level", alias = "log_level")] pub log_level: Option, - /// The package types we're creating. - /// - /// if not present, we'll use the PackageType list for the target OS. + /// The packaging formats to create, if not present, [`PackageFormat::platform_default`] is used. pub formats: Option>, - /// The directory where the `binaries` exist and where the packages will be placed. + /// The directory where the [`Config::binaries`] exist and where the generated packages will be placed. #[serde(default, alias = "out-dir", alias = "out_dir")] pub out_dir: PathBuf, - /// The target triple. Defaults to the current OS target triple. + /// The target triple we are packaging for. This mainly affects [`Config::external_binaries`]. + /// + /// Defaults to the current OS target triple. #[serde(alias = "target-triple", alias = "target_triple")] pub target_triple: Option, - /// the package's product name, for example "My Awesome App". - #[serde(default, alias = "product-name", alias = "product_name")] - pub product_name: String, - /// the package's version. - #[serde(default)] - pub version: String, - /// the package's description. + /// The package's description. pub description: Option, - /// the app's long description. + /// The app's long description. #[serde(alias = "long-description", alias = "long_description")] pub long_description: Option, - /// the package's homepage. + /// The package's homepage. pub homepage: Option, - /// the package's authors. + /// The package's authors. #[serde(default)] - pub authors: Vec, - /// the application identifier in reverse domain name notation (e.g. `com.packager.example`). - /// This string must be unique across applications since it is used in some system configurations. - /// This string must contain only alphanumeric characters (A–Z, a–z, and 0–9), hyphens (-), - /// and periods (.). - #[cfg_attr(feature = "schema", schemars(regex(pattern = r"^[a-zA-Z0-9-\.]*$")))] - pub identifier: Option, - /// The app's publisher. Defaults to the second element in the identifier string. + pub authors: Option>, + /// The app's publisher. Defaults to the second element in [`Config::identifier`](Config::identifier) string. /// Currently maps to the Manufacturer property of the Windows Installer. pub publisher: Option, /// A path to the license file. #[serde(alias = "license-file", alias = "license_file")] pub license_file: Option, - /// the app's copyright. + /// The app's copyright. pub copyright: Option, - /// the app's category. + /// The app's category. pub category: Option, - /// the app's icon list. - pub icons: Option>, - /// the binaries to package. - #[serde(default)] - pub binaries: Vec, - /// the file associations + /// The app's icon list. + pub icons: Option>, + /// The file associations #[serde(alias = "file-associations", alias = "file_associations")] pub file_associations: Option>, /// The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory @@ -782,11 +1408,14 @@ pub struct Config { /// - **[PackageFormat::Nsis] / [PackageFormat::Wix]**: The resources are placed next to the executable in the root of the packager. /// - **[PackageFormat::Deb]**: The resources are placed in `usr/lib` of the package. pub resources: Option>, - /// External binaries to add to the package. + /// Paths to external binaries to add to the package. /// - /// Note that each binary name should have the target platform's target triple appended, + /// The path specified should not include `-<.exe>` suffix, + /// it will be auto-added when by the packager when reading these paths, + /// so the actual binary name should have the target platform's target triple appended, /// as well as `.exe` for Windows. - /// For example, if you're packaging a sidecar called `sqlite3`, the packager expects + /// + /// For example, if you're packaging an external binary called `sqlite3`, the packager expects /// a binary named `sqlite3-x86_64-unknown-linux-gnu` on linux, /// and `sqlite3-x86_64-pc-windows-gnu.exe` on windows. /// @@ -795,59 +1424,58 @@ pub struct Config { /// e.g. `sqlite3-universal-apple-darwin`. See /// #[serde(alias = "external-binaries", alias = "external_binaries")] - pub external_binaries: Option>, - /// Debian-specific settings. + pub external_binaries: Option>, + /// Windows-specific configuration. + pub windows: Option, + /// MacOS-specific configuration. + pub macos: Option, + /// Debian-specific configuration. pub deb: Option, - /// Debian-specific settings. + /// AppImage configuration. pub appimage: Option, /// WiX configuration. pub wix: Option, /// Nsis configuration. pub nsis: Option, - /// MacOS-specific settings. - pub macos: Option, - /// Windows-specific settings. - pub windows: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct IResource { - pub src: PathBuf, - pub target: PathBuf, } impl Config { - /// Returns the windows specific configuration + /// Creates a new [`ConfigBuilder`]. + pub fn builder() -> ConfigBuilder { + ConfigBuilder::default() + } + + /// Returns the [windows](Config::windows) specific configuration. pub fn windows(&self) -> Option<&WindowsConfig> { self.windows.as_ref() } - /// Returns the macos specific configuration + /// Returns the [macos](Config::macos) specific configuration. pub fn macos(&self) -> Option<&MacOsConfig> { self.macos.as_ref() } - /// Returns the nsis specific configuration + /// Returns the [nsis](Config::nsis) specific configuration. pub fn nsis(&self) -> Option<&NsisConfig> { self.nsis.as_ref() } - /// Returns the wix specific configuration + /// Returns the [wix](Config::wix) specific configuration. pub fn wix(&self) -> Option<&WixConfig> { self.wix.as_ref() } - /// Returns the debian specific configuration + /// Returns the [debian](Config::deb) specific configuration. pub fn deb(&self) -> Option<&DebianConfig> { self.deb.as_ref() } - /// Returns the appimage specific configuration + /// Returns the [appimage](Config::appimage) specific configuration. pub fn appimage(&self) -> Option<&AppImageConfig> { self.appimage.as_ref() } - /// Returns the target triple for the package to be built (e.g. "aarch64-unknown-linux-gnu"). + /// Returns the target triple of this config, if not set, fallsback to the current OS target triple. pub fn target_triple(&self) -> String { self.target_triple.clone().unwrap_or_else(|| { util::target_triple().expect("Failed to detect current target triple") @@ -881,20 +1509,24 @@ impl Config { } } - /// Returns the package identifier + /// Returns the package identifier. Defaults an empty string. pub fn identifier(&self) -> &str { self.identifier.as_deref().unwrap_or("") } - /// Returns the package publisher + /// Returns the package publisher. + /// Defaults to the second element in [`Config::identifier`](Config::identifier()). pub fn publisher(&self) -> String { - let identifier = self.identifier(); - self.publisher - .clone() - .unwrap_or_else(|| identifier.split('.').nth(1).unwrap_or(identifier).into()) + self.publisher.clone().unwrap_or_else(|| { + self.identifier() + .split('.') + .nth(1) + .unwrap_or(self.identifier()) + .into() + }) } - /// Returns the out dir + /// Returns the out dir. Defaults to the current directory. pub fn out_dir(&self) -> PathBuf { if self.out_dir.as_os_str().is_empty() { std::env::current_dir().expect("failed to resolve cwd") @@ -903,7 +1535,7 @@ impl Config { } } - /// Returns the main binary + /// Returns the main binary. pub fn main_binary(&self) -> crate::Result<&Binary> { self.binaries .iter() @@ -911,7 +1543,7 @@ impl Config { .ok_or_else(|| crate::Error::MainBinaryNotFound) } - /// Returns the main binary name + /// Returns the main binary name. pub fn main_binary_name(&self) -> crate::Result { self.binaries .iter() @@ -921,19 +1553,25 @@ impl Config { } } +#[derive(Debug, Clone)] +pub(crate) struct ResolvedResource { + pub src: PathBuf, + pub target: PathBuf, +} + impl Config { #[inline] pub(crate) fn resources_from_dir( src_dir: &Path, target_dir: &Path, - ) -> crate::Result> { + ) -> crate::Result> { let mut out = Vec::new(); for entry in walkdir::WalkDir::new(src_dir) { let entry = entry?; let path = entry.path(); if path.is_file() { let relative = path.relative_to(src_dir)?.to_path(""); - let resource = IResource { + let resource = ResolvedResource { src: dunce::canonicalize(path)?, target: target_dir.join(relative), }; @@ -944,17 +1582,17 @@ impl Config { } #[inline] - pub(crate) fn resources_from_glob(glob: &str) -> crate::Result> { + pub(crate) fn resources_from_glob(glob: &str) -> crate::Result> { let mut out = Vec::new(); for src in glob::glob(glob)? { let src = dunce::canonicalize(src?)?; let target = PathBuf::from(src.file_name().unwrap_or_default()); - out.push(IResource { src, target }) + out.push(ResolvedResource { src, target }) } Ok(out) } - pub(crate) fn resources(&self) -> crate::Result> { + pub(crate) fn resources(&self) -> crate::Result> { if let Some(resources) = &self.resources { let mut out = Vec::new(); for r in resources { @@ -974,7 +1612,7 @@ impl Config { if src_path.is_dir() { out.extend(Self::resources_from_dir(&src_path, &target_dir)?); } else if src_path.is_file() { - out.push(IResource { + out.push(ResolvedResource { src: dunce::canonicalize(src_path)?, target: sanitize_path(target), }); @@ -1031,7 +1669,7 @@ impl Config { let mut paths = Vec::new(); if let Some(external_binaries) = &self.external_binaries { for src in external_binaries { - let src = dunce::canonicalize(PathBuf::from(src))?; + let src = dunce::canonicalize(src)?; let file_name_no_triple = src .file_name() .ok_or_else(|| crate::Error::FailedToExtractFilename(src.clone()))? @@ -1056,3 +1694,7 @@ fn sanitize_path>(path: P) -> PathBuf { } dest } + +fn default_true() -> bool { + true +} diff --git a/crates/packager/src/package/app/mod.rs b/crates/packager/src/package/app/mod.rs index 7071afc2..73891bba 100644 --- a/crates/packager/src/package/app/mod.rs +++ b/crates/packager/src/package/app/mod.rs @@ -188,7 +188,7 @@ fn create_info_plist( "CFBundleTypeExtensions".into(), plist::Value::Array( association - .ext + .extensions .iter() .map(|ext| ext.to_string().into()) .collect(), @@ -199,7 +199,7 @@ fn create_info_plist( association .name .as_ref() - .unwrap_or(&association.ext[0]) + .unwrap_or(&association.extensions[0]) .to_string() .into(), ); diff --git a/crates/packager/src/package/deb/mod.rs b/crates/packager/src/package/deb/mod.rs index cde9dcad..84a24788 100644 --- a/crates/packager/src/package/deb/mod.rs +++ b/crates/packager/src/package/deb/mod.rs @@ -247,8 +247,9 @@ fn generate_control_file( writeln!(file, "Architecture: {}", arch)?; // Installed-Size must be divided by 1024, see https://www.debian.org/doc/debian-policy/ch-controlfields.html#installed-size writeln!(file, "Installed-Size: {}", get_size(data_dir)? / 1024)?; - let authors = config.authors.join(", "); - writeln!(file, "Maintainer: {}", authors)?; + if let Some(authors) = &config.authors { + writeln!(file, "Maintainer: {}", authors.join(", "))?; + } if let Some(homepage) = &config.homepage { writeln!(file, "Homepage: {}", homepage)?; } @@ -284,7 +285,7 @@ fn generate_control_file( Ok(()) } -/// Create an `md5sums` file in the `control_dir` containing the MD5 checksums +/// Creates an `md5sums` file in the `control_dir` containing the MD5 checksums /// for each file within the `data_dir`. #[tracing::instrument(level = "trace")] fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> { diff --git a/crates/packager/src/package/mod.rs b/crates/packager/src/package/mod.rs index 75335141..3551e15b 100644 --- a/crates/packager/src/package/mod.rs +++ b/crates/packager/src/package/mod.rs @@ -35,7 +35,8 @@ mod wix; mod context; /// Generated Package metadata. -#[derive(Debug)] +#[derive(Debug, Clone)] +#[non_exhaustive] pub struct PackageOuput { /// The package type. pub format: PackageFormat, diff --git a/crates/packager/src/package/nsis/mod.rs b/crates/packager/src/package/nsis/mod.rs index 10f5005f..cef7a396 100644 --- a/crates/packager/src/package/nsis/mod.rs +++ b/crates/packager/src/package/nsis/mod.rs @@ -80,7 +80,7 @@ fn generate_binaries_data(config: &Config) -> crate::Result { if let Some(external_binaries) = &config.external_binaries { for src in external_binaries { - let src = PathBuf::from(src).with_extension("exe"); + let src = src.with_extension("exe"); let bin_path = dunce::canonicalize(cwd.join(src))?; let dest_filename = bin_path .file_name() @@ -383,6 +383,7 @@ fn build_nsis_app_installer(ctx: &Context, nsis_path: &Path) -> crate::Result "zlib", NsisCompression::Bzip2 => "bzip2", NsisCompression::Lzma => "lzma", + NsisCompression::Off => "off", }), ); } diff --git a/crates/packager/src/package/wix/mod.rs b/crates/packager/src/package/wix/mod.rs index 306bbaee..583b2da2 100644 --- a/crates/packager/src/package/wix/mod.rs +++ b/crates/packager/src/package/wix/mod.rs @@ -19,7 +19,7 @@ use uuid::Uuid; use super::Context; use crate::{ codesign, - config::{Config, LogLevel}, + config::{Config, LogLevel, WixLanguage}, shell::CommandExt, util::{self, download_and_verify, extract_zip, HashAlgorithm}, }; @@ -139,7 +139,7 @@ fn generate_binaries_data(config: &Config) -> crate::Result> { if let Some(external_binaries) = &config.external_binaries { for src in external_binaries { - let src = PathBuf::from(src).with_extension("exe"); + let src = src.with_extension("exe"); let bin_path = dunce::canonicalize(cwd.join(src))?; let dest_filename = bin_path .file_name() @@ -655,9 +655,14 @@ fn build_wix_app_installer(ctx: &Context, wix_path: &Path) -> crate::Result (identifier, None), + WixLanguage::Custom { identifier, path } => (identifier, path), + }; + let language_metadata = language_map.get(&language).ok_or_else(|| { crate::Error::UnsupportedWixLanguage( language.clone(), @@ -669,7 +674,7 @@ fn build_wix_app_installer(ctx: &Context, wix_path: &Path) -> crate::Result std::fs::read_to_string(p)?, None => format!( r#""#, diff --git a/crates/packager/src/sign.rs b/crates/packager/src/sign.rs index 0e33b5bd..ba1aecbb 100644 --- a/crates/packager/src/sign.rs +++ b/crates/packager/src/sign.rs @@ -98,8 +98,9 @@ pub fn save_keypair + Debug>( } /// Signing configuration. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[non_exhaustive] pub struct SigningConfig { /// The private key to use for signing. pub private_key: String, @@ -110,6 +111,26 @@ pub struct SigningConfig { pub password: Option, } +impl SigningConfig { + /// Creates a new [`SigningConfig`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the private key to use for signing. + pub fn private_key>(mut self, private_key: S) -> Self { + self.private_key = private_key.into(); + self + } + + /// Set the private key password. + pub fn password>(mut self, password: S) -> Self { + self.password.replace(password.into()); + + self + } +} + /// Signs a specified file using the specified signing configuration. #[tracing::instrument(level = "trace")] pub fn sign_file + Debug>( diff --git a/crates/packager/src/util.rs b/crates/packager/src/util.rs index d2efbb18..c2a6ef16 100644 --- a/crates/packager/src/util.rs +++ b/crates/packager/src/util.rs @@ -80,7 +80,7 @@ pub fn target_triple() -> crate::Result { .target_arch .expect("could not find `target_arch` when running `rustc --print cfg`."), Err(err) => { - tracing:: warn!( + tracing:: debug!( "Failed to determine target arch using rustc, error: `{err}`. Falling back to the architecture of the machine that compiled this crate.", ); if cfg!(target_arch = "x86") { diff --git a/crates/updater/src/lib.rs b/crates/updater/src/lib.rs index 256707d8..4d6f2315 100644 --- a/crates/updater/src/lib.rs +++ b/crates/updater/src/lib.rs @@ -72,9 +72,9 @@ impl WindowsUpdateInstallMode { #[serde(rename_all = "camelCase")] pub struct UpdaterWindowsConfig { /// Additional arguments given to the NSIS or WiX installer. - pub installer_args: Vec, + pub installer_args: Option>, /// The installation mode for the update on Windows. Defaults to `passive`. - pub install_mode: WindowsUpdateInstallMode, + pub install_mode: Option, } /// Updater configuration. @@ -86,7 +86,7 @@ pub struct Config { /// Signature public key. pub pubkey: String, /// The Windows configuration for the updater. - pub windows: UpdaterWindowsConfig, + pub windows: Option, } /// Supported update format @@ -265,7 +265,15 @@ impl UpdaterBuilder { I: IntoIterator, S: Into, { - self.config.windows.installer_args = args.into_iter().map(Into::into).collect(); + if self.config.windows.is_none() { + self.config.windows.replace(Default::default()); + } + self.config + .windows + .as_mut() + .unwrap() + .installer_args + .replace(args.into_iter().map(Into::into).collect()); self } @@ -596,10 +604,17 @@ impl Update { .arg("-ArgumentList") .arg( [ - self.config.windows.install_mode.nsis_args(), self.config .windows - .installer_args + .as_ref() + .and_then(|w| w.install_mode.clone()) + .unwrap_or_default() + .nsis_args(), + self.config + .windows + .as_ref() + .and_then(|w| w.installer_args.clone()) + .unwrap_or_default() .iter() .map(AsRef::as_ref) .collect::>() @@ -629,7 +644,9 @@ impl Update { let msiexec_args = self .config .windows - .install_mode + .as_ref() + .and_then(|w| w.install_mode.clone()) + .unwrap_or_default() .msiexec_args() .iter() .map(|p| p.to_string()) @@ -795,7 +812,14 @@ impl Update { } } -/// Gets the target string used on the updater. +/// Check for an update using the provided +pub fn check_update(current_version: Version, config: crate::Config) -> Result> { + UpdaterBuilder::new(current_version, config) + .build()? + .check() +} + +/// Get the updater target for the current platform. pub fn target() -> Option { if let (Some(target), Some(arch)) = (get_updater_target(), get_updater_arch()) { Some(format!("{target}-{arch}"))