From 2d4c34d0f3324d5ce9e14c85a6c67efbdf377eac Mon Sep 17 00:00:00 2001 From: Lucas Nogueira <118899497+lucasfernog-crabnebula@users.noreply.github.com> Date: Mon, 11 Dec 2023 15:43:59 -0300 Subject: [PATCH] fix(packager/nodejs): rename Electron binary on macOS (#95) --- .github/workflows/integration-tests.yml | 1 - bindings/packager/nodejs/src-ts/config.d.ts | 304 +++++++++++------- .../nodejs/src-ts/plugins/electron/index.ts | 18 +- .../updater/nodejs/__test__/index.spec.mjs | 46 ++- bindings/updater/nodejs/index.d.ts | 2 +- crates/updater/tests/update.rs | 2 +- 6 files changed, 235 insertions(+), 138 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6b2ee367..83f7bc25 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -62,7 +62,6 @@ jobs: sudo apt-get update sudo apt-get install -y fuse libfuse2 - name: updater integration tests - if: matrix.platform != 'macos-latest' run: | cd bindings/updater/nodejs pnpm test diff --git a/bindings/packager/nodejs/src-ts/config.d.ts b/bindings/packager/nodejs/src-ts/config.d.ts index c9784ae3..a740f7ab 100644 --- a/bindings/packager/nodejs/src-ts/config.d.ts +++ b/bindings/packager/nodejs/src-ts/config.d.ts @@ -96,15 +96,27 @@ export type Resource = [k: string]: unknown; }; /** - * The languages to build using WiX. + * A wix language. */ -export type WixLanguages = [string, WixLanguageConfig][]; +export type WixLanguage = + | string + | { + /** + * Idenitifier of this language, for example `en-US` + */ + identifier: string; + /** + * The path to a locale (`.wxl`) file. See . + */ + path?: string | null; + [k: string]: unknown; + }; /** * Compression algorithms used in the NSIS installer. * * See */ -export type NsisCompression = "zlib" | "bzip2" | "lzma"; +export type NsisCompression = "zlib" | "bzip2" | "lzma" | "off"; /** * Install Modes for the NSIS installer. */ @@ -114,84 +126,88 @@ export type NSISInstallerMode = "currentUser" | "perMachine" | "both"; * The packaging config. */ export interface Config { - /** - * Whether this config is enabled or not. Defaults to `true`. - */ - enabled?: boolean; /** * 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. + * Setting this field has no effect, this just exists so we can parse the JSON correctly when it has `$schema` field set. */ $schema?: string | null; /** * 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. + * If `unset`, the CLI will try to auto-detect it from `Cargo.toml` or `package.json` otherwise, it will keep it unset. */ name?: string | null; /** - * Specify a command to run before starting to package an application. + * Whether this config is enabled or not. Defaults to `true`. + */ + enabled?: boolean; + /** + * The package's product name, for example "My Awesome App". + */ + productName?: string; + /** + * The package's version. + */ + version?: string; + /** + * The binaries to package. + */ + binaries?: Binary[]; + /** + * 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 (.). + */ + identifier?: string | null; + /** + * The command to run before starting to package an application. * * This runs only once. */ beforePackagingCommand?: HookCommand | null; /** - * 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. */ beforeEachPackageCommand?: HookCommand | null; /** - * The log level. + * The logging level. */ logLevel?: LogLevel | null; /** - * 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. */ formats?: PackageFormat[] | null; /** - * 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. */ outDir?: string; /** - * 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. */ targetTriple?: string | null; /** - * the package's product name, for example "My Awesome App". - */ - productName?: string; - /** - * the package's version. - */ - version?: string; - /** - * the package's description. + * The package's description. */ description?: string | null; /** - * the app's long description. + * The app's long description. */ longDescription?: string | null; /** - * the package's homepage. + * The package's homepage. */ homepage?: string | null; /** - * the package's authors. + * The package's authors. */ - authors?: string[]; + authors?: string[] | null; /** - * 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 (.). - */ - identifier?: string | null; - /** - * The app's publisher. Defaults to the second element in the identifier string. Currently maps to the Manufacturer property of the Windows Installer. + * 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. */ publisher?: string | null; /** @@ -199,23 +215,19 @@ export interface Config { */ licenseFile?: string | null; /** - * the app's copyright. + * The app's copyright. */ copyright?: string | null; /** - * the app's category. + * The app's category. */ category?: AppCategory | null; /** - * the app's icon list. + * The app's icon list. */ icons?: string[] | null; /** - * the binaries to package. - */ - binaries?: Binary[]; - /** - * the file associations + * The file associations */ fileAssociations?: FileAssociation[] | null; /** @@ -227,19 +239,29 @@ export interface Config { */ resources?: Resource[] | null; /** - * 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, 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. + * 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 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. * * If 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 */ externalBinaries?: string[] | null; /** - * Debian-specific settings. + * Windows-specific configuration. + */ + windows?: WindowsConfig | null; + /** + * MacOS-specific configuration. + */ + macos?: MacOsConfig | null; + /** + * Debian-specific configuration. */ deb?: DebianConfig | null; /** - * Debian-specific settings. + * AppImage configuration. */ appimage?: AppImageConfig | null; /** @@ -251,13 +273,9 @@ export interface Config { */ nsis?: NsisConfig | null; /** - * MacOS-specific settings. - */ - macos?: MacOsConfig | null; - /** - * Windows-specific settings. + * Dmg configuration. */ - windows?: WindowsConfig | null; + dmg?: DmgConfig | null; } /** * A binary to package within the final package. @@ -279,30 +297,100 @@ export interface FileAssociation { /** * File extensions to associate with this app. e.g. 'png' */ - ext: string[]; + extensions: string[]; /** - * The name. Maps to `CFBundleTypeName` on macOS. Default to the first item in `ext` + * The mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**. */ - name?: string | null; + mimeType?: string | null; /** * The association description. **Windows-only**. It is displayed on the `Type` column on Windows Explorer. */ description?: string | null; /** - * The app’s role with respect to the type. Maps to `CFBundleTypeRole` on macOS. + * The name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext` + */ + name?: string | null; + /** + * The app’s role with respect to the type. Maps to `CFBundleTypeRole` on macOS. Defaults to [`BundleTypeRole::Editor`] */ role?: BundleTypeRole & string; +} +/** + * The Windows configuration. + */ +export interface WindowsConfig { /** - * The mime-type e.g. 'image/png' or 'text/plain'. Linux-only. + * The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended. */ - mimeType?: string | null; + digestAlgorithm?: string | null; + /** + * The SHA1 hash of the signing certificate. + */ + certificateThumbprint?: string | null; + /** + * 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. + */ + tsp?: boolean; + /** + * Server to use during timestamping. + */ + timestampUrl?: string | null; + /** + * 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`. + */ + allowDowngrades?: boolean; +} +/** + * The macOS configuration. + */ +export interface MacOsConfig { + /** + * 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) + */ + frameworks?: string[] | null; + /** + * 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`. + */ + minimumSystemVersion?: string | null; + /** + * 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. + */ + exceptionDomain?: string | null; + /** + * Code signing identity. + */ + signingIdentity?: string | null; + /** + * Provider short name for notarization. + */ + providerShortName?: string | null; + /** + * Path to the entitlements.plist file. + */ + entitlements?: string | null; + /** + * Path to the Info.plist file for the package. + */ + infoPlistPath?: string | null; } /** * The Linux debian configuration. */ export interface DebianConfig { /** - * the list of debian dependencies. + * The list of debian dependencies. */ depends?: string[] | null; /** @@ -325,7 +413,7 @@ export interface DebianConfig { */ export interface 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 `find -L /usr/lib* -name ` + * 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 ` */ libs?: string[] | null; /** @@ -333,15 +421,15 @@ export interface AppImageConfig { */ bins?: string[] | null; /** - * 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. + * List of custom files to add to the appimage package. Maps a dir/file to a dir/file inside the appimage package. */ - linuxdeployPlugins?: { + files?: { [k: string]: string; } | null; /** - * List of custom files to add to the appimage package. Maps a dir/file to a dir/file inside the appimage package. + * 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. */ - files?: { + linuxdeployPlugins?: { [k: string]: string; } | null; } @@ -352,7 +440,7 @@ export interface WixConfig { /** * The app languages to build. See . */ - languages?: WixLanguages; + languages?: WixLanguage[] | null; /** * By default, the packager uses an internal template. This option allows you to define your own wix file. */ @@ -412,15 +500,6 @@ export interface WixConfig { */ fipsCompliant?: boolean; } -/** - * Configuration for a target language for the WiX build. - */ -export interface WixLanguageConfig { - /** - * The path to a locale (`.wxl`) file. See . - */ - localePath?: string | null; -} /** * The NSIS format configuration. */ @@ -497,72 +576,53 @@ export interface NsisConfig { appdataPaths?: string[] | null; } /** - * The macOS configuration. + * The Apple Disk Image (.dmg) configuration. */ -export interface MacOsConfig { +export interface DmgConfig { /** - * 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) + * Image to use as the background in dmg file. Accepted formats: `png`/`jpg`/`gif`. */ - frameworks?: string[] | null; + background?: string | null; /** - * 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`. + * Position of volume window on screen. */ - minimumSystemVersion?: string | null; + windowPosition?: Position | null; /** - * 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. - */ - exceptionDomain?: string | null; - /** - * Code signing identity. + * Size of volume window. */ - signingIdentity?: string | null; + windowSize?: Size | null; /** - * Provider short name for notarization. - */ - providerShortName?: string | null; - /** - * Path to the entitlements.plist file. + * Position of application file on window. */ - entitlements?: string | null; + appPosition?: Position | null; /** - * Path to the Info.plist file for the package. + * Position of application folder on window. */ - infoPlistPath?: string | null; + appFolderPosition?: Position | null; } /** - * The Windows configuration. + * Position coordinates struct. */ -export interface WindowsConfig { - /** - * The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended. - */ - digestAlgorithm?: string | null; +export interface Position { /** - * The SHA1 hash of the signing certificate. + * X coordinate. */ - certificateThumbprint?: string | null; + x: number; /** - * Server to use during timestamping. + * Y coordinate. */ - timestampUrl?: string | null; + y: number; +} +/** + * Size struct. + */ +export interface Size { /** - * 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. + * Width. */ - tsp?: boolean; + width: number; /** - * Validates 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`. + * Height. */ - allowDowngrades?: boolean; + height: number; } diff --git a/bindings/packager/nodejs/src-ts/plugins/electron/index.ts b/bindings/packager/nodejs/src-ts/plugins/electron/index.ts index 87a29096..35b9fd01 100644 --- a/bindings/packager/nodejs/src-ts/plugins/electron/index.ts +++ b/bindings/packager/nodejs/src-ts/plugins/electron/index.ts @@ -78,6 +78,12 @@ export default async function run( switch (platformName) { case "darwin": + var binaryName: string = + userConfig.name || + packageJson.productName || + packageJson.name || + "Electron"; + var standaloneElectronPath = path.join(zipDir, "Electron.app"); const resourcesPath = path.join( @@ -103,7 +109,17 @@ export default async function run( path.join(frameworksPath, p), ); - binaryPath = path.join(standaloneElectronPath, "Contents/MacOS/Electron"); + binaryPath = path.join( + standaloneElectronPath, + `Contents/MacOS/${binaryName}`, + ); + + // rename the electron binary + await fs.rename( + path.join(standaloneElectronPath, "Contents/MacOS/Electron"), + binaryPath, + ); + break; case "win32": var binaryName: string = diff --git a/bindings/updater/nodejs/__test__/index.spec.mjs b/bindings/updater/nodejs/__test__/index.spec.mjs index de619988..46cea789 100644 --- a/bindings/updater/nodejs/__test__/index.spec.mjs +++ b/bindings/updater/nodejs/__test__/index.spec.mjs @@ -15,14 +15,18 @@ const isMac = process.platform === "darwin"; test("it updates correctly", async (t) => { const UPDATER_PRIVATE_KEY = await fs.readFile( path.join(__dirname, "../../../../crates/updater/tests/dummy.key"), - { encoding: "utf8" }, + { + encoding: "utf8", + }, ); process.chdir(path.join(__dirname, "app")); await execa("yarn", ["install"]); const buildApp = async (version, updaterFormats) => { - const content = await fs.readFile("main.js", { encoding: "utf8" }); + const content = await fs.readFile("main.js", { + encoding: "utf8", + }); await fs.writeFile("main.js", content.replace("{{version}}", version)); try { @@ -35,13 +39,17 @@ test("it updates correctly", async (t) => { privateKey: UPDATER_PRIVATE_KEY, password: "", }, - { verbosity: 2 }, + { + verbosity: 2, + }, ); } catch (e) { console.error("failed to package app"); console.error(e); } finally { - const content = await fs.readFile("main.js", { encoding: "utf8" }); + const content = await fs.readFile("main.js", { + encoding: "utf8", + }); await fs.writeFile("main.js", content.replace(version, "{{version}}")); } }; @@ -50,7 +58,7 @@ test("it updates correctly", async (t) => { const formats = isWin ? ["nsis", "wix"] : isMac ? ["app"] : ["appimage"]; await buildApp("1.0.0", formats); - const gneratedPackages = isWin + const generatedPackages = isWin ? [ ["nsis", path.join("dist", `ElectronApp_1.0.0_x64-setup.exe`)], ["wix", path.join("dist", `ElectronApp_1.0.0_x64_en-US.msi`)], @@ -59,11 +67,16 @@ test("it updates correctly", async (t) => { ? [["app", path.join("dist", "ElectronApp.app.tar.gz")]] : [["appimage", path.join("dist", `electron-app_1.0.0_x86_64.AppImage`)]]; - for (let [format, updatePackagePath] of gneratedPackages) { - const signaturePath = path.format({ name: updatePackagePath, ext: ".sig" }); - const signature = await fs.readFile(signaturePath, { encoding: "utf8" }); + for (let [format, updatePackagePath] of generatedPackages) { + const signaturePath = path.format({ + name: updatePackagePath, + ext: ".sig", + }); + const signature = await fs.readFile(signaturePath, { + encoding: "utf8", + }); - // on macOS, gnerated bundle doesn't have the version in its name + // on macOS, generated bundle doesn't have the version in its name // so we need to move it otherwise it'll be overwritten when we build the next app if (isMac) { const info = path.parse(updatePackagePath); @@ -80,7 +93,11 @@ test("it updates correctly", async (t) => { .get("/", (_, res) => { const platforms = {}; const target = `${isWin ? "windows" : isMac ? "macos" : "linux"}-${ - process.arch === "x64" ? "x86_64" : "i686" + process.arch === "x64" + ? "x86_64" + : process.arch === "arm64" + ? "aarch64" + : "i686" }`; platforms[target] = { signature, @@ -106,7 +123,10 @@ test("it updates correctly", async (t) => { // install the inital app on Windows to `installdir` if (isWin) { const installDir = path.join(__dirname, "app", "dist", "installdir"); - if (existsSync(installDir)) await fs.rm(installDir, { recursive: true }); + if (existsSync(installDir)) + await fs.rm(installDir, { + recursive: true, + }); await fs.mkdir(installDir); const isNsis = format === "nsis"; @@ -150,7 +170,9 @@ test("it updates correctly", async (t) => { await execa(app, { stdio: "inherit", // This is read by the updater app test - env: { UPDATER_FORMAT: format }, + env: { + UPDATER_FORMAT: format, + }, }); } catch (e) { console.error(`failed to start initial app: ${e}`); diff --git a/bindings/updater/nodejs/index.d.ts b/bindings/updater/nodejs/index.d.ts index bec12bbe..a35915cf 100644 --- a/bindings/updater/nodejs/index.d.ts +++ b/bindings/updater/nodejs/index.d.ts @@ -68,7 +68,7 @@ export class Update { /** Update format */ format: UpdateFormat /** The Windows options for the updater. */ - windows: UpdaterWindowsOptions + windows?: UpdaterWindowsOptions /** Update description */ body?: string /** Update publish date */ diff --git a/crates/updater/tests/update.rs b/crates/updater/tests/update.rs index 73b04750..18a1730e 100644 --- a/crates/updater/tests/update.rs +++ b/crates/updater/tests/update.rs @@ -130,7 +130,7 @@ fn update_app() { panic!("failed to read signature file {}", signature_path.display()) }); - // on macOS, gnerated bundle doesn't have the version in its name + // on macOS, generated bundle doesn't have the version in its name // so we need to move it otherwise it'll be overwritten when we build the next app #[cfg(target_os = "macos")] let update_package_path = {