From 6967837685ccff0b9085e9dbbd20b2869818f277 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Mon, 11 Dec 2023 20:44:30 +0200 Subject: [PATCH] feat: add `config.windows.sign_command` to override signing command (#94) * feat: add `config.windows.sign_command` to override signing command closes #81 * fix build * clippy --- bindings/packager/nodejs/schema.json | 1202 +++++++++++++++++++++++ crates/packager/schema.json | 7 + crates/packager/src/codesign/macos.rs | 2 +- crates/packager/src/codesign/mod.rs | 10 +- crates/packager/src/codesign/windows.rs | 195 ++-- crates/packager/src/config/mod.rs | 11 + crates/packager/src/error.rs | 5 +- crates/packager/src/package/app/mod.rs | 2 +- crates/packager/src/package/dmg/mod.rs | 2 +- crates/packager/src/package/nsis/mod.rs | 30 +- crates/packager/src/package/wix/mod.rs | 2 +- 11 files changed, 1367 insertions(+), 101 deletions(-) create mode 100644 bindings/packager/nodejs/schema.json diff --git a/bindings/packager/nodejs/schema.json b/bindings/packager/nodejs/schema.json new file mode 100644 index 00000000..39d4c0ce --- /dev/null +++ b/bindings/packager/nodejs/schema.json @@ -0,0 +1,1202 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "description": "The packaging config.", + "type": "object", + "properties": { + "$schema": { + "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` 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": "The command to run before starting to package an application.\n\nThis runs only once.", + "anyOf": [ + { + "$ref": "#/definitions/HookCommand" + }, + { + "type": "null" + } + ] + }, + "beforeEachPackageCommand": { + "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" + }, + { + "type": "null" + } + ] + }, + "logLevel": { + "description": "The logging level.", + "anyOf": [ + { + "$ref": "#/definitions/LogLevel" + }, + { + "type": "null" + } + ] + }, + "formats": { + "description": "The packaging formats to create, if not present, [`PackageFormat::platform_default`] is used.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/PackageFormat" + } + }, + "outDir": { + "description": "The directory where the [`Config::binaries`] exist and where the generated packages will be placed.", + "default": "", + "type": "string" + }, + "targetTriple": { + "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" + ] + }, + "description": { + "description": "The package's description.", + "type": [ + "string", + "null" + ] + }, + "longDescription": { + "description": "The app's long description.", + "type": [ + "string", + "null" + ] + }, + "homepage": { + "description": "The package's homepage.", + "type": [ + "string", + "null" + ] + }, + "authors": { + "description": "The package's authors.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "publisher": { + "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" + ] + }, + "licenseFile": { + "description": "A path to the license file.", + "type": [ + "string", + "null" + ] + }, + "copyright": { + "description": "The app's copyright.", + "type": [ + "string", + "null" + ] + }, + "category": { + "description": "The app's category.", + "anyOf": [ + { + "$ref": "#/definitions/AppCategory" + }, + { + "type": "null" + } + ] + }, + "icons": { + "description": "The app's icon list.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "fileAssociations": { + "description": "The file associations", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/FileAssociation" + } + }, + "resources": { + "description": "The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory or an object of `src` and `target` paths. In the case of using an object, the `src` could be either a glob pattern, path to a file, path to a directory, and the `target` is a path inside the final resources folder in the installed package.\n\n## Format-specific:\n\n- **[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.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Resource" + } + }, + "externalBinaries": { + "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" + ], + "items": { + "type": "string" + } + }, + "windows": { + "description": "Windows-specific configuration.", + "anyOf": [ + { + "$ref": "#/definitions/WindowsConfig" + }, + { + "type": "null" + } + ] + }, + "macos": { + "description": "MacOS-specific configuration.", + "anyOf": [ + { + "$ref": "#/definitions/MacOsConfig" + }, + { + "type": "null" + } + ] + }, + "deb": { + "description": "Debian-specific configuration.", + "anyOf": [ + { + "$ref": "#/definitions/DebianConfig" + }, + { + "type": "null" + } + ] + }, + "appimage": { + "description": "AppImage configuration.", + "anyOf": [ + { + "$ref": "#/definitions/AppImageConfig" + }, + { + "type": "null" + } + ] + }, + "wix": { + "description": "WiX configuration.", + "anyOf": [ + { + "$ref": "#/definitions/WixConfig" + }, + { + "type": "null" + } + ] + }, + "nsis": { + "description": "Nsis configuration.", + "anyOf": [ + { + "$ref": "#/definitions/NsisConfig" + }, + { + "type": "null" + } + ] + }, + "dmg": { + "description": "Dmg configuration.", + "anyOf": [ + { + "$ref": "#/definitions/DmgConfig" + }, + { + "type": "null" + } + ] + } + }, + "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": [ + { + "description": "Run the given script with the default options.", + "type": "string" + }, + { + "description": "Run the given script with custom options.", + "type": "object", + "required": [ + "script" + ], + "properties": { + "script": { + "description": "The script to execute.", + "type": "string" + }, + "dir": { + "description": "The working directory.", + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "LogLevel": { + "description": "An enum representing the available verbosity levels of the logger.", + "oneOf": [ + { + "description": "The \"error\" level.\n\nDesignates very serious errors.", + "type": "string", + "enum": [ + "error" + ] + }, + { + "description": "The \"warn\" level.\n\nDesignates hazardous situations.", + "type": "string", + "enum": [ + "warn" + ] + }, + { + "description": "The \"info\" level.\n\nDesignates useful information.", + "type": "string", + "enum": [ + "info" + ] + }, + { + "description": "The \"debug\" level.\n\nDesignates lower priority information.", + "type": "string", + "enum": [ + "debug" + ] + }, + { + "description": "The \"trace\" level.\n\nDesignates very low priority, often extremely verbose, information.", + "type": "string", + "enum": [ + "trace" + ] + } + ] + }, + "PackageFormat": { + "description": "The type of the package we're packaging.", + "oneOf": [ + { + "description": "All available package formats for the current platform.\n\nSee [`PackageFormat::platform_all`]", + "type": "string", + "enum": [ + "all" + ] + }, + { + "description": "The default list of package formats for the current platform.\n\nSee [`PackageFormat::platform_default`]", + "type": "string", + "enum": [ + "default" + ] + }, + { + "description": "The macOS application bundle (.app).", + "type": "string", + "enum": [ + "app" + ] + }, + { + "description": "The macOS DMG package (.dmg).", + "type": "string", + "enum": [ + "dmg" + ] + }, + { + "description": "The Microsoft Software Installer (.msi) through WiX Toolset.", + "type": "string", + "enum": [ + "wix" + ] + }, + { + "description": "The NSIS installer (.exe).", + "type": "string", + "enum": [ + "nsis" + ] + }, + { + "description": "The Linux Debian package (.deb).", + "type": "string", + "enum": [ + "deb" + ] + }, + { + "description": "The Linux AppImage package (.AppImage).", + "type": "string", + "enum": [ + "appimage" + ] + } + ] + }, + "AppCategory": { + "description": "The possible app categories. Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian.", + "type": "string", + "enum": [ + "Business", + "DeveloperTool", + "Education", + "Entertainment", + "Finance", + "Game", + "ActionGame", + "AdventureGame", + "ArcadeGame", + "BoardGame", + "CardGame", + "CasinoGame", + "DiceGame", + "EducationalGame", + "FamilyGame", + "KidsGame", + "MusicGame", + "PuzzleGame", + "RacingGame", + "RolePlayingGame", + "SimulationGame", + "SportsGame", + "StrategyGame", + "TriviaGame", + "WordGame", + "GraphicsAndDesign", + "HealthcareAndFitness", + "Lifestyle", + "Medical", + "Music", + "News", + "Photography", + "Productivity", + "Reference", + "SocialNetworking", + "Sports", + "Travel", + "Utility", + "Video", + "Weather" + ] + }, + "FileAssociation": { + "description": "A file association configuration.", + "type": "object", + "required": [ + "extensions" + ], + "properties": { + "extensions": { + "description": "File extensions to associate with this app. e.g. 'png'", + "type": "array", + "items": { + "type": "string" + } + }, + "mimeType": { + "description": "The mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**.", + "type": [ + "string", + "null" + ] + }, + "description": { + "description": "The association description. **Windows-only**. It is displayed on the `Type` column on Windows Explorer.", + "type": [ + "string", + "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. Defaults to [`BundleTypeRole::Editor`]", + "default": "editor", + "allOf": [ + { + "$ref": "#/definitions/BundleTypeRole" + } + ] + } + }, + "additionalProperties": false + }, + "BundleTypeRole": { + "description": "*macOS-only**. Corresponds to CFBundleTypeRole", + "oneOf": [ + { + "description": "CFBundleTypeRole.Editor. Files can be read and edited.", + "type": "string", + "enum": [ + "editor" + ] + }, + { + "description": "CFBundleTypeRole.Viewer. Files can be read.", + "type": "string", + "enum": [ + "viewer" + ] + }, + { + "description": "CFBundleTypeRole.Shell", + "type": "string", + "enum": [ + "shell" + ] + }, + { + "description": "CFBundleTypeRole.QLGenerator", + "type": "string", + "enum": [ + "qLGenerator" + ] + }, + { + "description": "CFBundleTypeRole.None", + "type": "string", + "enum": [ + "none" + ] + } + ] + }, + "Resource": { + "description": "A path to a resource (with optional glob pattern) or an object of `src` and `target` paths.", + "anyOf": [ + { + "description": "Supports glob patterns", + "type": "string" + }, + { + "description": "An object descriping the src file or directory and its target location in the final package.", + "type": "object", + "required": [ + "src", + "target" + ], + "properties": { + "src": { + "description": "The src file or directory, supports glob patterns.", + "type": "string" + }, + "target": { + "description": "A relative path from the root of the final package.\n\nIf `src` is a glob, this will always be treated as a directory where all globbed files will be placed under.", + "type": "string" + } + } + } + ] + }, + "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.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "desktopTemplate": { + "description": "Path to a custom desktop file Handlebars template.\n\nAvailable variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.\n\nDefault 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}} ```", + "type": [ + "string", + "null" + ] + }, + "files": { + "description": "List of custom files to add to the deb package. Maps a dir/file to a dir/file inside the debian package.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "AppImageConfig": { + "description": "The Linux AppImage configuration.", + "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 `", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "bins": { + "description": "List of binary paths to include in the final AppImage. For example, if you want `xdg-open`, you'd specify `/usr/bin/xdg-open`", + "type": [ + "array", + "null" + ], + "items": { + "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.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "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" + ], + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "WixConfig": { + "description": "The wix format configuration", + "type": "object", + "properties": { + "languages": { + "description": "The app languages to build. See .", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/WixLanguage" + } + }, + "template": { + "description": "By default, the packager uses an internal template. This option allows you to define your own wix file.", + "type": [ + "string", + "null" + ] + }, + "mergeModules": { + "description": "List of merge modules to include in your installer. For example, if you want to include [C++ Redis merge modules]\n\n[C++ Redis merge modules]: https://wixtoolset.org/docs/v3/howtos/redistributables_and_install_checks/install_vcredist/", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "fragmentPaths": { + "description": "A list of paths to .wxs files with WiX fragments to use.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "fragments": { + "description": "List of WiX fragments as strings. This is similar to `config.wix.fragments_paths` but is a string so you can define it inline in your config.\n\n```text ```", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "componentGroupRefs": { + "description": "The ComponentGroup element ids you want to reference from the fragments.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "componentRefs": { + "description": "The Component element ids you want to reference from the fragments.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "customActionRefs": { + "description": "The CustomAction element ids you want to reference from the fragments.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "featureGroupRefs": { + "description": "The FeatureGroup element ids you want to reference from the fragments.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "featureRefs": { + "description": "The Feature element ids you want to reference from the fragments.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "mergeRefs": { + "description": "The Merge element ids you want to reference from the fragments.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "bannerPath": { + "description": "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.\n\nThe required dimensions are 493px × 58px.", + "type": [ + "string", + "null" + ] + }, + "dialogImagePath": { + "description": "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.", + "type": [ + "string", + "null" + ] + }, + "fipsCompliant": { + "description": "Enables FIPS compliant algorithms.", + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "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" + ] + } + } + } + ] + }, + "NsisConfig": { + "description": "The NSIS format configuration.", + "type": "object", + "properties": { + "compression": { + "description": "Set the compression algorithm used to compress files in the installer.\n\nSee ", + "anyOf": [ + { + "$ref": "#/definitions/NsisCompression" + }, + { + "type": "null" + } + ] + }, + "template": { + "description": "A custom `.nsi` template to use.\n\nSee the default template here ", + "type": [ + "string", + "null" + ] + }, + "preinstallSection": { + "description": "Logic of an NSIS section that will be ran before the install section.\n\nSee the available libraries, dlls and global variables here \n\n### 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\"\n\nSection PreInstall ;
SectionEnd\n\nSection AnotherPreInstall ;
SectionEnd \"\"\" ```", + "type": [ + "string", + "null" + ] + }, + "headerImage": { + "description": "The path to a bitmap file to display on the header of installers pages.\n\nThe recommended dimensions are 150px x 57px.", + "type": [ + "string", + "null" + ] + }, + "sidebarImage": { + "description": "The path to a bitmap file for the Welcome page and the Finish page.\n\nThe recommended dimensions are 164px x 314px.", + "type": [ + "string", + "null" + ] + }, + "installerIcon": { + "description": "The path to an icon file used as the installer icon.", + "type": [ + "string", + "null" + ] + }, + "installMode": { + "description": "Whether the installation will be for all users or just the current user.", + "default": "currentUser", + "allOf": [ + { + "$ref": "#/definitions/NSISInstallerMode" + } + ] + }, + "languages": { + "description": "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`.\n\nSee for the complete list of languages.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "customLanguageFiles": { + "description": "An 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.\n\nSee for an example `.nsi` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array,", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "displayLanguageSelector": { + "description": "Whether 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.", + "default": false, + "type": "boolean" + }, + "appdataPaths": { + "description": "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.\n\nThe 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\"] ```", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "NsisCompression": { + "description": "Compression algorithms used in the NSIS installer.\n\nSee ", + "oneOf": [ + { + "description": "ZLIB uses the deflate algorithm, it is a quick and simple method. With the default compression level it uses about 300 KB of memory.", + "type": "string", + "enum": [ + "zlib" + ] + }, + { + "description": "BZIP2 usually gives better compression ratios than ZLIB, but it is a bit slower and uses more memory. With the default compression level it uses about 4 MB of memory.", + "type": "string", + "enum": [ + "bzip2" + ] + }, + { + "description": "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.", + "type": "string", + "enum": [ + "lzma" + ] + }, + { + "description": "Disable compression.", + "type": "string", + "enum": [ + "off" + ] + } + ] + }, + "NSISInstallerMode": { + "description": "Install Modes for the NSIS installer.", + "oneOf": [ + { + "description": "Default mode for the installer.\n\nInstall the app by default in a directory that doesn't require Administrator access.\n\nInstaller metadata will be saved under the `HKCU` registry path.", + "type": "string", + "enum": [ + "currentUser" + ] + }, + { + "description": "Install the app by default in the `Program Files` folder directory requires Administrator access for the installation.\n\nInstaller metadata will be saved under the `HKLM` registry path.", + "type": "string", + "enum": [ + "perMachine" + ] + }, + { + "description": "Combines both modes and allows the user to choose at install time whether to install for the current user or per machine. Note that this mode will require Administrator access even if the user wants to install it for the current user only.\n\nInstaller metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice.", + "type": "string", + "enum": [ + "both" + ] + } + ] + }, + "DmgConfig": { + "description": "The Apple Disk Image (.dmg) configuration.", + "type": "object", + "properties": { + "background": { + "description": "Image to use as the background in dmg file. Accepted formats: `png`/`jpg`/`gif`.", + "type": [ + "string", + "null" + ] + }, + "windowPosition": { + "description": "Position of volume window on screen.", + "anyOf": [ + { + "$ref": "#/definitions/Position" + }, + { + "type": "null" + } + ] + }, + "windowSize": { + "description": "Size of volume window.", + "anyOf": [ + { + "$ref": "#/definitions/Size" + }, + { + "type": "null" + } + ] + }, + "appPosition": { + "description": "Position of application file on window.", + "anyOf": [ + { + "$ref": "#/definitions/Position" + }, + { + "type": "null" + } + ] + }, + "appFolderPosition": { + "description": "Position of application folder on window.", + "anyOf": [ + { + "$ref": "#/definitions/Position" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Position": { + "description": "Position coordinates struct.", + "type": "object", + "required": [ + "x", + "y" + ], + "properties": { + "x": { + "description": "X coordinate.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "y": { + "description": "Y coordinate.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Size": { + "description": "Size struct.", + "type": "object", + "required": [ + "height", + "width" + ], + "properties": { + "width": { + "description": "Width.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "height": { + "description": "Height.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/crates/packager/schema.json b/crates/packager/schema.json index 39d4c0ce..57170037 100644 --- a/crates/packager/schema.json +++ b/crates/packager/schema.json @@ -631,6 +631,13 @@ "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" + }, + "signCommand": { + "description": "Specify a custom command to sign the binaries. This command needs to have a `%1` in it which is just a placeholder for the binary path, which we will detect and replace before calling the command.\n\nBy Default we use `signtool.exe` which can be found only on Windows so if you are on another platform and want to cross-compile and sign you will need to use another tool like `osslsigncode`.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/packager/src/codesign/macos.rs b/crates/packager/src/codesign/macos.rs index c11572ca..fd893a8e 100644 --- a/crates/packager/src/codesign/macos.rs +++ b/crates/packager/src/codesign/macos.rs @@ -192,7 +192,7 @@ fn sign( pcakger_keychain: bool, ) -> crate::Result<()> { tracing::info!( - "Signing {} with identity \"{}\"", + "Codesigning {} with identity \"{}\"", path_to_sign.display(), identity ); diff --git a/crates/packager/src/codesign/mod.rs b/crates/packager/src/codesign/mod.rs index 679997ff..1e3c254b 100644 --- a/crates/packager/src/codesign/mod.rs +++ b/crates/packager/src/codesign/mod.rs @@ -3,12 +3,6 @@ // SPDX-License-Identifier: MIT #[cfg(target_os = "macos")] -#[path = "macos.rs"] -mod imp; +pub mod macos; -#[cfg(windows)] -#[path = "windows.rs"] -mod imp; - -#[cfg(any(windows, target_os = "macos"))] -pub use imp::*; +pub mod windows; diff --git a/crates/packager/src/codesign/windows.rs b/crates/packager/src/codesign/windows.rs index 33827fe7..c1f23d60 100644 --- a/crates/packager/src/codesign/windows.rs +++ b/crates/packager/src/codesign/windows.rs @@ -3,25 +3,22 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -#![cfg(windows)] - -use std::{ - fmt::Debug, - path::{Path, PathBuf}, - process::Command, -}; +use std::{fmt::Debug, path::Path, process::Command}; +#[cfg(windows)] use once_cell::sync::Lazy; +#[cfg(windows)] +use std::path::PathBuf; +#[cfg(windows)] use winreg::{ enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY}, RegKey, }; -use crate::{ - config::Config, - shell::CommandExt, - util::{self, display_path, Bitness}, -}; +use crate::{config::Config, shell::CommandExt, util}; + +#[cfg(windows)] +use crate::util::Bitness; #[derive(Debug)] pub struct SignParams { @@ -30,8 +27,49 @@ pub struct SignParams { pub certificate_thumbprint: String, pub timestamp_url: Option, pub tsp: bool, + pub sign_command: Option, +} + +pub(crate) trait ConfigSignExt { + fn can_sign(&self) -> bool; + fn custom_sign_command(&self) -> bool; + fn sign_params(&self) -> SignParams; +} + +impl ConfigSignExt for Config { + fn can_sign(&self) -> bool { + self.windows() + .and_then(|w| w.certificate_thumbprint.as_ref()) + .is_some() + || self.custom_sign_command() + } + + fn custom_sign_command(&self) -> bool { + self.windows() + .and_then(|w| w.sign_command.as_ref()) + .is_some() + } + + fn sign_params(&self) -> SignParams { + let windows = self.windows(); + SignParams { + product_name: self.product_name.clone(), + digest_algorithm: windows + .and_then(|w| w.digest_algorithm.as_ref()) + .cloned() + .unwrap_or_else(|| "sha256".to_string()), + certificate_thumbprint: windows + .and_then(|w| w.certificate_thumbprint.as_ref()) + .cloned() + .unwrap_or_default(), + timestamp_url: windows.and_then(|w| w.timestamp_url.as_ref()).cloned(), + tsp: windows.map(|w| w.tsp).unwrap_or_default(), + sign_command: windows.and_then(|w| w.sign_command.as_ref()).cloned(), + } + } } +#[cfg(windows)] static SIGN_TOOL: Lazy> = Lazy::new(|| { let _s = tracing::span!(tracing::Level::TRACE, "locate_signtool"); const INSTALLED_ROOTS_REGKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots"; @@ -94,12 +132,31 @@ static SIGN_TOOL: Lazy> = Lazy::new(|| { Err(crate::Error::SignToolNotFound) }); +#[cfg(windows)] fn signtool() -> Option { (*SIGN_TOOL).as_ref().ok().cloned() } #[tracing::instrument(level = "trace")] -pub fn sign_command + Debug>( +pub fn sign_command_custom + Debug>( + path: P, + command: &str, +) -> crate::Result { + let custom_command = command.replace("%1", &dunce::simplified(path.as_ref()).to_string_lossy()); + + let mut args = custom_command.split(' '); + let bin = args + .next() + .expect("custom signing command doesn't contain a bin?"); + + let mut cmd = Command::new(bin); + cmd.args(args); + Ok(cmd) +} + +#[cfg(windows)] +#[tracing::instrument(level = "trace")] +pub fn sign_command_default + Debug>( path: P, params: &SignParams, ) -> crate::Result { @@ -125,88 +182,86 @@ pub fn sign_command + Debug>( Ok(cmd) } -pub(crate) trait ConfigSignExt { - fn can_sign(&self) -> bool; - fn sign_params(&self) -> SignParams; -} +#[tracing::instrument(level = "trace")] +pub fn sign_command + Debug>( + path: P, + params: &SignParams, +) -> crate::Result { + match ¶ms.sign_command { + Some(custom_command) => sign_command_custom(path, custom_command), + #[cfg(windows)] + None => sign_command_default(path, params), -impl ConfigSignExt for Config { - fn can_sign(&self) -> bool { - self.windows() - .and_then(|w| w.certificate_thumbprint.as_ref()) - .is_some() + // should not be reachable + #[cfg(not(windows))] + None => Ok(Command::new("")), } +} - fn sign_params(&self) -> SignParams { - let windows = self.windows(); - SignParams { - product_name: self.product_name.clone(), - digest_algorithm: windows - .and_then(|w| w.digest_algorithm.as_ref()) - .as_ref() - .map(|algorithm| algorithm.to_string()) - .unwrap_or_else(|| "sha256".to_string()), - certificate_thumbprint: windows - .and_then(|w| w.certificate_thumbprint.as_ref()) - .cloned() - .unwrap_or_default(), - timestamp_url: windows - .and_then(|w| w.timestamp_url.as_ref()) - .as_ref() - .map(|url| url.to_string()), - tsp: windows.map(|w| w.tsp).unwrap_or_default(), - } - } +#[tracing::instrument(level = "trace")] +pub fn sign_custom + Debug>(path: P, custom_command: &str) -> crate::Result<()> { + let path = path.as_ref(); + + tracing::info!( + "Codesigning {} with a custom signing command", + util::display_path(path), + ); + + let mut cmd = sign_command_custom(path, custom_command)?; + + let output = cmd + .output_ok() + .map_err(crate::Error::CustomSignCommandFailed)?; + + let stdout = String::from_utf8_lossy(output.stdout.as_slice()); + tracing::info!("{:?}", stdout); + + Ok(()) } #[tracing::instrument(level = "trace")] -pub fn sign + Debug>(path: P, params: &SignParams) -> crate::Result<()> { +#[cfg(windows)] +pub fn sign_default + Debug>(path: P, params: &SignParams) -> crate::Result<()> { let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?; let path = path.as_ref(); tracing::info!( - "Signing {} with identity \"{}\"", - display_path(path), + "Codesigning {} with identity \"{}\"", + util::display_path(path), params.certificate_thumbprint ); + let mut cmd = sign_command_default(path, params)?; + tracing::debug!("Running signtool {:?}", signtool); - let mut cmd = sign_command(path, params)?; let output = cmd.output_ok().map_err(crate::Error::SignToolFailed)?; - let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned(); + + let stdout = String::from_utf8_lossy(output.stdout.as_slice()); tracing::info!("{:?}", stdout); Ok(()) } +#[tracing::instrument(level = "trace")] +pub fn sign + Debug>(path: P, params: &SignParams) -> crate::Result<()> { + match ¶ms.sign_command { + Some(custom_command) => sign_custom(path, custom_command), + #[cfg(windows)] + None => sign_default(path, params), + + // should not be reachable + #[cfg(not(windows))] + None => Ok(()), + } +} + #[tracing::instrument(level = "trace")] pub fn try_sign( file_path: &std::path::PathBuf, config: &crate::config::Config, ) -> crate::Result<()> { - let windows_config = config.windows(); - if let Some(certificate_thumbprint) = - windows_config.and_then(|c| c.certificate_thumbprint.as_ref()) - { - tracing::info!("Signing {}", util::display_path(file_path)); - sign( - file_path, - &SignParams { - product_name: config.product_name.clone(), - digest_algorithm: windows_config - .and_then(|c| { - c.digest_algorithm - .as_ref() - .map(|algorithm| algorithm.to_string()) - }) - .unwrap_or_else(|| "sha256".to_string()), - certificate_thumbprint: certificate_thumbprint.to_string(), - timestamp_url: windows_config - .and_then(|c| c.timestamp_url.as_ref()) - .map(|url| url.to_string()), - tsp: windows_config.map(|c| c.tsp).unwrap_or_default(), - }, - )?; + if config.can_sign() { + sign(file_path, &config.sign_params())?; } Ok(()) } diff --git a/crates/packager/src/config/mod.rs b/crates/packager/src/config/mod.rs index 29b7e0b8..fb9164a2 100644 --- a/crates/packager/src/config/mod.rs +++ b/crates/packager/src/config/mod.rs @@ -1225,6 +1225,16 @@ pub struct WindowsConfig { alias = "allow_downgrades" )] pub allow_downgrades: bool, + + /// Specify a custom command to sign the binaries. + /// This command needs to have a `%1` in it which is just a placeholder for the binary path, + /// which we will detect and replace before calling the command. + /// + /// By Default we use `signtool.exe` which can be found only on Windows so + /// if you are on another platform and want to cross-compile and sign you will + /// need to use another tool like `osslsigncode`. + #[serde(alias = "sign-command", alias = "sign_command")] + pub sign_command: Option, } impl Default for WindowsConfig { @@ -1235,6 +1245,7 @@ impl Default for WindowsConfig { timestamp_url: None, tsp: false, allow_downgrades: true, + sign_command: None, } } } diff --git a/crates/packager/src/error.rs b/crates/packager/src/error.rs index 4ffd9e72..b1557e49 100644 --- a/crates/packager/src/error.rs +++ b/crates/packager/src/error.rs @@ -86,9 +86,12 @@ pub enum Error { /// create-dmg script error #[error("Error running create-dmg script: {0}")] CreateDmgFailed(std::io::Error), - /// create-dmg script error + /// signtool.exe error #[error("Error running signtool.exe: {0}")] SignToolFailed(std::io::Error), + /// Custom signing command error + #[error("Error running custom signing command: {0}")] + CustomSignCommandFailed(std::io::Error), /// bundle_appimage script error #[error("Error running bundle_appimage.sh script: {0}")] AppImageScriptFailed(std::io::Error), diff --git a/crates/packager/src/package/app/mod.rs b/crates/packager/src/package/app/mod.rs index 73891bba..a02be710 100644 --- a/crates/packager/src/package/app/mod.rs +++ b/crates/packager/src/package/app/mod.rs @@ -12,7 +12,7 @@ use std::{ use super::Context; use crate::{ - codesign::{self, SignTarget}, + codesign::macos::{self as codesign, SignTarget}, config::Config, shell::CommandExt, util, diff --git a/crates/packager/src/package/dmg/mod.rs b/crates/packager/src/package/dmg/mod.rs index 4e56734d..fdb99f65 100644 --- a/crates/packager/src/package/dmg/mod.rs +++ b/crates/packager/src/package/dmg/mod.rs @@ -7,7 +7,7 @@ use std::{os::unix::fs::PermissionsExt, path::PathBuf, process::Command}; use super::Context; use crate::{ - codesign, + codesign::macos as codesign, shell::CommandExt, util::{self, download}, }; diff --git a/crates/packager/src/package/nsis/mod.rs b/crates/packager/src/package/nsis/mod.rs index c2aab18b..fa7e0c1d 100644 --- a/crates/packager/src/package/nsis/mod.rs +++ b/crates/packager/src/package/nsis/mod.rs @@ -13,8 +13,7 @@ use std::{ use handlebars::{to_json, Handlebars}; use super::Context; -#[cfg(windows)] -use crate::codesign; +use crate::codesign::windows::{self as codesign, ConfigSignExt}; use crate::{ config::{Config, LogLevel, NSISInstallerMode, NsisCompression}, shell::CommandExt, @@ -307,15 +306,14 @@ fn build_nsis_app_installer(ctx: &Context, nsis_path: &Path) -> crate::Result crate::Result crate::Result