From 3a570a0b07552705749c6e2e9a246db9bebb81aa Mon Sep 17 00:00:00 2001 From: iamEvan Date: Wed, 17 Sep 2025 18:29:21 +0100 Subject: [PATCH 01/21] feat: support Icon Composer icons for macOS --- .changeset/tender-berries-fix.md | 5 ++ packages/app-builder-lib/scheme.json | 14 ++++ packages/app-builder-lib/src/macPackager.ts | 11 ++- .../app-builder-lib/src/options/macOptions.ts | 5 ++ .../src/util/macosIconComposer.ts | 73 +++++++++++++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 .changeset/tender-berries-fix.md create mode 100644 packages/app-builder-lib/src/util/macosIconComposer.ts diff --git a/.changeset/tender-berries-fix.md b/.changeset/tender-berries-fix.md new file mode 100644 index 00000000000..a7dcfa2dab5 --- /dev/null +++ b/.changeset/tender-berries-fix.md @@ -0,0 +1,5 @@ +--- +"app-builder-lib": minor +--- + +feat: support Icon Composer icons for macOS diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index fe7fe2a2aa2..d3db3a898da 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -2834,6 +2834,13 @@ "string" ] }, + "iconComposerIcon": { + "description": "The path to application icon made with Apple's Icon Composer.", + "type": [ + "null", + "string" + ] + }, "identity": { "description": "The name of certificate to use when signing. Consider using environment variables [CSC_LINK or CSC_NAME](./code-signing.md) instead of specifying this option.\nMAS installer identity is specified in the [mas](./mas.md).\n\nSet to `-` to use an ad-hoc identity for signing. Set to `null` to skip signing entirely.", "type": [ @@ -3469,6 +3476,13 @@ "string" ] }, + "iconComposerIcon": { + "description": "The path to application icon made with Apple's Icon Composer.", + "type": [ + "null", + "string" + ] + }, "identity": { "description": "The name of certificate to use when signing. Consider using environment variables [CSC_LINK or CSC_NAME](./code-signing.md) instead of specifying this option.\nMAS installer identity is specified in the [mas](./mas.md).\n\nSet to `-` to use an ad-hoc identity for signing. Set to `null` to skip signing entirely.", "type": [ diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index 0bc3b492bb3..a4cc3c73562 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -22,6 +22,7 @@ import { isMacOsHighSierra } from "./util/macosVersion" import { getTemplatePath } from "./util/pathManager" import { resolveFunction } from "./util/resolve" import { expandMacro as doExpandMacro } from "./util/macroExpander" +import { generateAssetCatalogForIcon } from "./util/macosIconComposer" export type CustomMacSignOptions = SignOptions export type CustomMacSign = (configuration: CustomMacSignOptions, packager: MacPackager) => Promise @@ -502,10 +503,11 @@ export class MacPackager extends PlatformPackager { // https://github.com/electron-userland/electron-builder/issues/1278 appPlist.CFBundleExecutable = appFilename.endsWith(" Helper") ? appFilename.substring(0, appFilename.length - " Helper".length) : appFilename + const resourcesPath = path.join(contentsPath, "Resources") + const icon = await this.getIconPath() if (icon != null) { const oldIcon = appPlist.CFBundleIconFile - const resourcesPath = path.join(contentsPath, "Resources") if (oldIcon != null) { await unlinkIfExists(path.join(resourcesPath, oldIcon)) } @@ -516,6 +518,13 @@ export class MacPackager extends PlatformPackager { appPlist.CFBundleName = appInfo.productName appPlist.CFBundleDisplayName = appInfo.productName + const iconComposerIcon = this.platformSpecificBuildOptions.iconComposerIcon + if (iconComposerIcon) { + const assetCatalog = await generateAssetCatalogForIcon(iconComposerIcon) + appPlist.CFBundleIconName = "Icon" + await fs.writeFile(path.join(resourcesPath, "Assets.car"), assetCatalog) + } + const minimumSystemVersion = this.platformSpecificBuildOptions.minimumSystemVersion if (minimumSystemVersion != null) { appPlist.LSMinimumSystemVersion = minimumSystemVersion diff --git a/packages/app-builder-lib/src/options/macOptions.ts b/packages/app-builder-lib/src/options/macOptions.ts index b03f1460193..f2738ffd33b 100644 --- a/packages/app-builder-lib/src/options/macOptions.ts +++ b/packages/app-builder-lib/src/options/macOptions.ts @@ -32,6 +32,11 @@ export interface MacConfiguration extends PlatformSpecificBuildOptions { */ readonly icon?: string | null + /** + * The path to application icon made with Apple's Icon Composer. + */ + readonly iconComposerIcon?: string | null + /** * The path to entitlements file for signing the app. `build/entitlements.mac.plist` will be used if exists (it is a recommended way to set). * MAS entitlements is specified in the [mas](./mas.md). diff --git a/packages/app-builder-lib/src/util/macosIconComposer.ts b/packages/app-builder-lib/src/util/macosIconComposer.ts new file mode 100644 index 00000000000..99382c30cf4 --- /dev/null +++ b/packages/app-builder-lib/src/util/macosIconComposer.ts @@ -0,0 +1,73 @@ +// Adapted from https://github.com/electron/packager/pull/1806 + +import { spawn } from "builder-util" +import * as fs from "fs/promises" +import * as os from "node:os" +import * as path from "node:path" +import * as plist from "plist" +import * as semver from "semver" + +export async function generateAssetCatalogForIcon(inputPath: string) { + if (!semver.gte(os.release(), "25.0.0")) { + throw new Error(`actool .icon support is currently limited to macOS 26 and higher`) + } + + const acToolVersionOutput = await spawn("actool", ["--version"]) + const versionInfo = plist.parse(acToolVersionOutput) as Record> + if (!versionInfo || !versionInfo["com.apple.actool.version"] || !versionInfo["com.apple.actool.version"]["short-bundle-version"]) { + throw new Error("Unable to query actool version. Is Xcode 26 or higher installed? See output of the `actool --version` CLI command for more details.") + } + + const acToolVersion = versionInfo["com.apple.actool.version"]["short-bundle-version"] + if (!semver.gte(semver.coerce(acToolVersion)!, "26.0.0")) { + throw new Error(`Unsupported actool version. Must be on actool 26.0.0 or higher but found ${acToolVersion}. Install XCode 26 or higher to get a supported version of actool.`) + } + + const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icon-compile-")) + const iconPath = path.resolve(tmpDir, "Icon.icon") + const outputPath = path.resolve(tmpDir, "out") + + try { + await fs.cp(inputPath, iconPath, { + recursive: true, + }) + + await fs.mkdir(outputPath, { + recursive: true, + }) + + await spawn("actool", [ + iconPath, + "--compile", + outputPath, + "--output-format", + "human-readable-text", + "--notices", + "--warnings", + "--output-partial-info-plist", + path.resolve(outputPath, "assetcatalog_generated_info.plist"), + "--app-icon", + "Icon", + "--include-all-app-icons", + "--accent-color", + "AccentColor", + "--enable-on-demand-resources", + "NO", + "--development-region", + "en", + "--target-device", + "mac", + "--minimum-deployment-target", + "26.0", + "--platform", + "macosx", + ]) + + return await fs.readFile(path.resolve(outputPath, "Assets.car")) + } finally { + await fs.rm(tmpDir, { + recursive: true, + force: true, + }) + } +} From 0ad8833e312d91962243ef54656cfa75383ce649 Mon Sep 17 00:00:00 2001 From: iamEvan Date: Thu, 18 Sep 2025 22:31:07 +0100 Subject: [PATCH 02/21] feat: use existing `icon` property --- packages/app-builder-lib/scheme.json | 18 ++---------------- packages/app-builder-lib/src/macPackager.ts | 19 +++++++++++++------ .../app-builder-lib/src/options/macOptions.ts | 7 ++----- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index d3db3a898da..376456fa50c 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -2828,14 +2828,7 @@ }, "icon": { "default": "build/icon.icns", - "description": "The path to application icon.", - "type": [ - "null", - "string" - ] - }, - "iconComposerIcon": { - "description": "The path to application icon made with Apple's Icon Composer.", + "description": "The path to application icon.\nAccepts `.icns` (legacy) or `.icon` (Icon Composer asset).\nIf a `.icon` asset is provided, it will be preferred and compiled to an asset catalog.", "type": [ "null", "string" @@ -3470,14 +3463,7 @@ }, "icon": { "default": "build/icon.icns", - "description": "The path to application icon.", - "type": [ - "null", - "string" - ] - }, - "iconComposerIcon": { - "description": "The path to application icon made with Apple's Icon Composer.", + "description": "The path to application icon.\nAccepts `.icns` (legacy) or `.icon` (Icon Composer asset).\nIf a `.icon` asset is provided, it will be preferred and compiled to an asset catalog.", "type": [ "null", "string" diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index a4cc3c73562..05bc773a7cb 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -505,7 +505,12 @@ export class MacPackager extends PlatformPackager { const resourcesPath = path.join(contentsPath, "Resources") - const icon = await this.getIconPath() + // Support both legacy `.icns` and modern `.icon` (Icon Composer) inputs via `mac.icon`. + // Prefer `.icon` if provided; still accept `.icns`. + const configuredIcon = (this.platformSpecificBuildOptions.icon ?? (this.config as any).icon) as string | null | undefined + const isIconComposer = typeof configuredIcon === "string" && configuredIcon.toLowerCase().endsWith(".icon") + + const icon = isIconComposer ? null : await this.getIconPath() if (icon != null) { const oldIcon = appPlist.CFBundleIconFile if (oldIcon != null) { @@ -518,11 +523,13 @@ export class MacPackager extends PlatformPackager { appPlist.CFBundleName = appInfo.productName appPlist.CFBundleDisplayName = appInfo.productName - const iconComposerIcon = this.platformSpecificBuildOptions.iconComposerIcon - if (iconComposerIcon) { - const assetCatalog = await generateAssetCatalogForIcon(iconComposerIcon) - appPlist.CFBundleIconName = "Icon" - await fs.writeFile(path.join(resourcesPath, "Assets.car"), assetCatalog) + if (isIconComposer && configuredIcon) { + const iconComposerPath = await this.getResource(configuredIcon) + if (iconComposerPath) { + const assetCatalog = await generateAssetCatalogForIcon(iconComposerPath) + appPlist.CFBundleIconName = "Icon" + await fs.writeFile(path.join(resourcesPath, "Assets.car"), assetCatalog) + } } const minimumSystemVersion = this.platformSpecificBuildOptions.minimumSystemVersion diff --git a/packages/app-builder-lib/src/options/macOptions.ts b/packages/app-builder-lib/src/options/macOptions.ts index f2738ffd33b..3fd756bd8e1 100644 --- a/packages/app-builder-lib/src/options/macOptions.ts +++ b/packages/app-builder-lib/src/options/macOptions.ts @@ -28,15 +28,12 @@ export interface MacConfiguration extends PlatformSpecificBuildOptions { /** * The path to application icon. + * Accepts `.icns` (legacy) or `.icon` (Icon Composer asset). + * If a `.icon` asset is provided, it will be preferred and compiled to an asset catalog. * @default build/icon.icns */ readonly icon?: string | null - /** - * The path to application icon made with Apple's Icon Composer. - */ - readonly iconComposerIcon?: string | null - /** * The path to entitlements file for signing the app. `build/entitlements.mac.plist` will be used if exists (it is a recommended way to set). * MAS entitlements is specified in the [mas](./mas.md). From 48145b1230267d4c288a21baf32d95b0e568d6c0 Mon Sep 17 00:00:00 2001 From: iamEvan Date: Thu, 18 Sep 2025 22:31:31 +0100 Subject: [PATCH 03/21] docs: update icon docs --- pages/icons.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pages/icons.md b/pages/icons.md index ebbfd244652..053d9d2a4f7 100644 --- a/pages/icons.md +++ b/pages/icons.md @@ -1,14 +1,20 @@ -Recommended tools: [AppIcon Generator](http://www.tweaknow.com/appicongenerator.php), [MakeAppIcon](https://makeappicon.com/). +Recommended tools: [Icon Composer](https://developer.apple.com/icon-composer/), [AppIcon Generator](http://www.tweaknow.com/appicongenerator.php), [MakeAppIcon](https://makeappicon.com/). ## macOS Files -* *Optional* `icon.icns` (macOS app icon) or `icon.png`. Icon size should be at least 512x512. +* *Optional* `icon.icon` (Apple Icon Composer asset), `icon.icns` (legacy macOS app icon), or `icon.png`. Icon size should be at least 512x512. * *Optional* `background.png` (macOS DMG background). * *Optional* `background@2x.png` (macOS DMG Retina background). -need to be placed in the [buildResources](./contents.md#extraresources) directory (defaults to `build`). All files are optional — but it is important to provide `icon.icns` (or `icon.png`), as otherwise the default Electron icon will be used. +need to be placed in the [buildResources](./contents.md#extraresources) directory (defaults to `build`). All files are optional — but it is important to provide a macOS-capable icon: `.icon` (preferred), `.icns` (legacy), or `icon.png`; otherwise the default Electron icon will be used. + +Notes + +- `.icon` preferred: If you set `mac.icon` to a `.icon` file, electron-builder compiles it into an asset catalog (`Assets.car`) and wires it via `CFBundleIconName`. This requires Xcode 26+ (`actool` 26+) on macOS 15+. +- `.icns` accepted: If you set `mac.icon` to `.icns`, it is copied into the app bundle and referenced via `CFBundleIconFile`. +- DMG volume icon: If you rely on the default DMG volume icon and only provide `.icon`, consider setting `dmg.icon` explicitly to an `.icns` file, as the DMG volume icon still uses `.icns`. ## Windows (NSIS) From d83658ad312a41fe0d15b400c5a89756f3d53f52 Mon Sep 17 00:00:00 2001 From: iamEvan Date: Thu, 18 Sep 2025 22:33:25 +0100 Subject: [PATCH 04/21] fix types --- packages/app-builder-lib/src/macPackager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index 05bc773a7cb..a11a89143d2 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -507,9 +507,10 @@ export class MacPackager extends PlatformPackager { // Support both legacy `.icns` and modern `.icon` (Icon Composer) inputs via `mac.icon`. // Prefer `.icon` if provided; still accept `.icns`. - const configuredIcon = (this.platformSpecificBuildOptions.icon ?? (this.config as any).icon) as string | null | undefined + const configuredIcon = this.platformSpecificBuildOptions.icon const isIconComposer = typeof configuredIcon === "string" && configuredIcon.toLowerCase().endsWith(".icon") + // Bundle legacy `icns` format const icon = isIconComposer ? null : await this.getIconPath() if (icon != null) { const oldIcon = appPlist.CFBundleIconFile @@ -523,6 +524,7 @@ export class MacPackager extends PlatformPackager { appPlist.CFBundleName = appInfo.productName appPlist.CFBundleDisplayName = appInfo.productName + // Bundle new `icon` format if (isIconComposer && configuredIcon) { const iconComposerPath = await this.getResource(configuredIcon) if (iconComposerPath) { From 004571492fc5bdff673fad8d828e3167ead3063d Mon Sep 17 00:00:00 2001 From: iamEvan Date: Fri, 26 Sep 2025 21:24:35 +0100 Subject: [PATCH 05/21] feat: add icon composer test --- .../electron.icon/Assets/electron-stroke.png | Bin 0 -> 40321 bytes .../electron.icon/icon.json | 41 ++++++++++++++++++ test/src/mac/macIconTest.ts | 39 +++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 test/fixtures/macos-icon-composer-assets/electron.icon/Assets/electron-stroke.png create mode 100644 test/fixtures/macos-icon-composer-assets/electron.icon/icon.json diff --git a/test/fixtures/macos-icon-composer-assets/electron.icon/Assets/electron-stroke.png b/test/fixtures/macos-icon-composer-assets/electron.icon/Assets/electron-stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e371a0531cc2d6381bff62cc5266d3c70512cf GIT binary patch literal 40321 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%0AUW>j8c$US-`tQisNO1?cF65Vw6ye-QzX79{s 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 { + return app( + expect, + { + targets, + config: { + mac: { + icon: "icon.icon", + }, + }, + }, + { + projectDirCreated: async projectDir => { + await Promise.all([fs.unlink(path.join(projectDir, "build", "icon.icns")), fs.unlink(path.join(projectDir, "build", "icon.ico"))]) + + await fs.cp(iconComposerFixture, path.join(projectDir, "build", "icon.icon"), { + recursive: true, + }) + }, + packed: async context => { + const resourcesDir = context.getResources(Platform.MAC, Arch.arm64) + const contentsDir = context.getContent(Platform.MAC, Arch.arm64) + const infoPlistPath = path.join(contentsDir, "Info.plist") + + const info = await parsePlistFile(infoPlistPath) + expect(info.CFBundleIconName).toBe("Icon") + expect(info.CFBundleIconFile).toBeUndefined() + + const assetCatalogPath = path.join(resourcesDir, "Assets.car") + const writtenCatalog = await fs.readFile(assetCatalogPath) + expect(writtenCatalog.length).toBeGreaterThan(0) + }, + } + ) +}) + test.ifMac("icon set", ({ expect }) => { let platformPackager: CheckingMacPackager | null = null return app( From 74102f43593a96da1d89bfd60d6218321f2d03c2 Mon Sep 17 00:00:00 2001 From: iamEvan Date: Fri, 26 Sep 2025 21:42:57 +0100 Subject: [PATCH 06/21] refactor: fix and verify tests --- test/snapshots/mac/macIconTest.js.snap | 12 ++++++++++++ test/src/mac/macIconTest.ts | 7 +++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/test/snapshots/mac/macIconTest.js.snap b/test/snapshots/mac/macIconTest.js.snap index a41831b453b..5f79a8f6dc2 100644 --- a/test/snapshots/mac/macIconTest.js.snap +++ b/test/snapshots/mac/macIconTest.js.snap @@ -1,5 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Icon Composer asset catalog 1`] = ` +{ + "mac": [], +} +`; + exports[`custom icon set 1`] = ` [ { @@ -99,6 +105,12 @@ exports[`default png icon 1`] = ` ] `; +exports[`icon composer generate asset catalog 1`] = ` +{ + "mac": [], +} +`; + exports[`icon set 1`] = ` [ { diff --git a/test/src/mac/macIconTest.ts b/test/src/mac/macIconTest.ts index 38ba7f8b190..258a23a2805 100644 --- a/test/src/mac/macIconTest.ts +++ b/test/src/mac/macIconTest.ts @@ -21,7 +21,7 @@ const targets = Platform.MAC.createTarget(DIR_TARGET, Arch.x64) const iconComposerFixture = path.join(__dirname, "..", "..", "fixtures", "macos-icon-composer-assets", "electron.icon") -test.ifMac("Icon Composer asset catalog", ({ expect }) => { +test.ifMac("icon composer generate asset catalog", ({ expect }) => { return app( expect, { @@ -41,13 +41,12 @@ test.ifMac("Icon Composer asset catalog", ({ expect }) => { }) }, packed: async context => { - const resourcesDir = context.getResources(Platform.MAC, Arch.arm64) - const contentsDir = context.getContent(Platform.MAC, Arch.arm64) + const resourcesDir = context.getResources(Platform.MAC, Arch.x64) + const contentsDir = context.getContent(Platform.MAC, Arch.x64) const infoPlistPath = path.join(contentsDir, "Info.plist") const info = await parsePlistFile(infoPlistPath) expect(info.CFBundleIconName).toBe("Icon") - expect(info.CFBundleIconFile).toBeUndefined() const assetCatalogPath = path.join(resourcesDir, "Assets.car") const writtenCatalog = await fs.readFile(assetCatalogPath) From 9c933312c6dc6ab74d3d9d02c9ccdf4c30dae971 Mon Sep 17 00:00:00 2001 From: iamEvan Date: Sat, 27 Sep 2025 12:35:21 +0100 Subject: [PATCH 07/21] feat: run macIconTest in CI --- .github/workflows/test.yaml | 30 ++++++++++++++++++++++++++ test/snapshots/mac/macIconTest.js.snap | 6 ------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0f3d16f8e46..1721b83e02a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -227,3 +227,33 @@ jobs: env: TEST_FILES: ${{ matrix.testFiles }} FORCE_COLOR: 1 + + test-mac-icons: + runs-on: macos-26 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + testFiles: + - macIconTest + steps: + - name: Checkout code repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Setup Tests + uses: ./.github/actions/pretest + with: + cache-path: ~/Library/Caches/electron + cache-key: v-23.3.10-macos-electron-icons + + - name: Install toolset via brew + run: | + brew install powershell/tap/powershell + brew install --cask wine-stable + brew install rpm + + - name: Test + run: pnpm ci:test + env: + TEST_FILES: ${{ matrix.testFiles }} + FORCE_COLOR: 1 diff --git a/test/snapshots/mac/macIconTest.js.snap b/test/snapshots/mac/macIconTest.js.snap index 5f79a8f6dc2..2abecbf60ab 100644 --- a/test/snapshots/mac/macIconTest.js.snap +++ b/test/snapshots/mac/macIconTest.js.snap @@ -1,11 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Icon Composer asset catalog 1`] = ` -{ - "mac": [], -} -`; - exports[`custom icon set 1`] = ` [ { From 3c835607cd20e6da7e8538936333e6fc9aaba5ff Mon Sep 17 00:00:00 2001 From: iamEvan Date: Sat, 27 Sep 2025 18:06:35 +0100 Subject: [PATCH 08/21] feat: add macIconTest to test-mac & upgrade to macos-26 runners --- .github/workflows/test.yaml | 36 +++--------------------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1721b83e02a..b5d47ba0655 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -2,7 +2,7 @@ name: Test on: push: - branches: master + branches: [master] pull_request: workflow_dispatch: # Allows you to run this workflow manually from the Actions tab inputs: @@ -196,7 +196,7 @@ jobs: FORCE_COLOR: 1 test-mac: - runs-on: macos-latest + runs-on: macos-26 timeout-minutes: 20 strategy: fail-fast: false @@ -204,7 +204,7 @@ jobs: testFiles: - oneClickInstallerTest,assistedInstallerTest,webInstallerTest - winPackagerTest,winCodeSignTest,BuildTest,blackboxUpdateTest - - masTest,dmgTest,filesTest,macPackagerTest,differentialUpdateTest,macArchiveTest + - masTest,dmgTest,filesTest,macPackagerTest,differentialUpdateTest,macArchiveTest,macIconTest - concurrentBuildsTest steps: - name: Checkout code repository @@ -227,33 +227,3 @@ jobs: env: TEST_FILES: ${{ matrix.testFiles }} FORCE_COLOR: 1 - - test-mac-icons: - runs-on: macos-26 - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - testFiles: - - macIconTest - steps: - - name: Checkout code repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - - name: Setup Tests - uses: ./.github/actions/pretest - with: - cache-path: ~/Library/Caches/electron - cache-key: v-23.3.10-macos-electron-icons - - - name: Install toolset via brew - run: | - brew install powershell/tap/powershell - brew install --cask wine-stable - brew install rpm - - - name: Test - run: pnpm ci:test - env: - TEST_FILES: ${{ matrix.testFiles }} - FORCE_COLOR: 1 From a4ce5305c54f0b279ddae87cc6c8daac82bbed79 Mon Sep 17 00:00:00 2001 From: iamEvan Date: Sun, 12 Oct 2025 00:09:28 +0100 Subject: [PATCH 09/21] chore: remove macos version check --- packages/app-builder-lib/src/util/macosIconComposer.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/app-builder-lib/src/util/macosIconComposer.ts b/packages/app-builder-lib/src/util/macosIconComposer.ts index 99382c30cf4..1e6ae5a69ab 100644 --- a/packages/app-builder-lib/src/util/macosIconComposer.ts +++ b/packages/app-builder-lib/src/util/macosIconComposer.ts @@ -8,10 +8,6 @@ import * as plist from "plist" import * as semver from "semver" export async function generateAssetCatalogForIcon(inputPath: string) { - if (!semver.gte(os.release(), "25.0.0")) { - throw new Error(`actool .icon support is currently limited to macOS 26 and higher`) - } - const acToolVersionOutput = await spawn("actool", ["--version"]) const versionInfo = plist.parse(acToolVersionOutput) as Record> if (!versionInfo || !versionInfo["com.apple.actool.version"] || !versionInfo["com.apple.actool.version"]["short-bundle-version"]) { From b92eec3b768b92025d0f7df48e4d03e97172d6b3 Mon Sep 17 00:00:00 2001 From: iamEvan Date: Sun, 12 Oct 2025 00:16:58 +0100 Subject: [PATCH 10/21] feat: copy icns file too as fallback --- packages/app-builder-lib/src/macPackager.ts | 8 +++++++- packages/app-builder-lib/src/util/macosIconComposer.ts | 5 ++++- test/src/mac/macIconTest.ts | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index a11a89143d2..ad9ab23b84a 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -528,9 +528,15 @@ export class MacPackager extends PlatformPackager { if (isIconComposer && configuredIcon) { const iconComposerPath = await this.getResource(configuredIcon) if (iconComposerPath) { - const assetCatalog = await generateAssetCatalogForIcon(iconComposerPath) + const { assetCatalog, icnsFile } = await generateAssetCatalogForIcon(iconComposerPath) + + // Create and setup the asset catalog appPlist.CFBundleIconName = "Icon" await fs.writeFile(path.join(resourcesPath, "Assets.car"), assetCatalog) + + // Create and setup the icns file + appPlist.CFBundleIconFile = "Icon" + await fs.writeFile(path.join(resourcesPath, "Icon.icns"), icnsFile) } } diff --git a/packages/app-builder-lib/src/util/macosIconComposer.ts b/packages/app-builder-lib/src/util/macosIconComposer.ts index 1e6ae5a69ab..fe1c8b9782d 100644 --- a/packages/app-builder-lib/src/util/macosIconComposer.ts +++ b/packages/app-builder-lib/src/util/macosIconComposer.ts @@ -59,7 +59,10 @@ export async function generateAssetCatalogForIcon(inputPath: string) { "macosx", ]) - return await fs.readFile(path.resolve(outputPath, "Assets.car")) + const assetCatalog = await fs.readFile(path.resolve(outputPath, "Assets.car")) + const icnsFile = await fs.readFile(path.resolve(outputPath, "Icon.icns")) + + return { assetCatalog, icnsFile } } finally { await fs.rm(tmpDir, { recursive: true, diff --git a/test/src/mac/macIconTest.ts b/test/src/mac/macIconTest.ts index 258a23a2805..c5ac360e574 100644 --- a/test/src/mac/macIconTest.ts +++ b/test/src/mac/macIconTest.ts @@ -47,6 +47,7 @@ test.ifMac("icon composer generate asset catalog", ({ expect }) => { const info = await parsePlistFile(infoPlistPath) expect(info.CFBundleIconName).toBe("Icon") + expect(info.CFBundleIconFile).toBe("Icon") const assetCatalogPath = path.join(resourcesDir, "Assets.car") const writtenCatalog = await fs.readFile(assetCatalogPath) From fdcd1cf540c6832d7e5431969d9be0c3d4785e2f Mon Sep 17 00:00:00 2001 From: iamEvan Date: Sun, 12 Oct 2025 00:23:47 +0100 Subject: [PATCH 11/21] fix: linux builds failing when .icon files exists --- packages/app-builder-lib/src/targets/LinuxTargetHelper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app-builder-lib/src/targets/LinuxTargetHelper.ts b/packages/app-builder-lib/src/targets/LinuxTargetHelper.ts index a9aa128a5ef..14f24257c71 100644 --- a/packages/app-builder-lib/src/targets/LinuxTargetHelper.ts +++ b/packages/app-builder-lib/src/targets/LinuxTargetHelper.ts @@ -69,7 +69,9 @@ export class LinuxTargetHelper { // need to put here and not as default because need to resolve image size const result = await packager.resolveIcon(sources, fallbackSources, "set") this.maxIconPath = result[result.length - 1].file - return result + + // Ignore .icon files for linux (they are exclusive for macOS) + return result.filter(icon => !icon.file.endsWith(".icon")) } getDescription(options: LinuxTargetSpecificOptions) { From 6b0b46c0793f4701284ef3db101a69b892181349 Mon Sep 17 00:00:00 2001 From: iamEvan Date: Sun, 12 Oct 2025 00:28:52 +0100 Subject: [PATCH 12/21] feat: add temporary icns file handling for compatibility in MacPackager --- packages/app-builder-lib/src/macPackager.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index ad9ab23b84a..e8ce3fd2823 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -8,6 +8,7 @@ import * as fs from "fs/promises" import { mkdir, readdir } from "fs/promises" import { Lazy } from "lazy-val" import * as path from "path" +import * as os from "os" import { AppInfo } from "./appInfo" import { CertType, CodeSigningInfo, createKeychain, CreateKeychainOptions, findIdentity, isSignAllowed, removeKeychain, reportError, sign } from "./codeSign/macCodeSign" import { DIR_TARGET, Platform, Target } from "./core" @@ -537,6 +538,14 @@ export class MacPackager extends PlatformPackager { // Create and setup the icns file appPlist.CFBundleIconFile = "Icon" await fs.writeFile(path.join(resourcesPath, "Icon.icns"), icnsFile) + + // Override configuration to use the generated icns file for compatibility + const tempDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icon-compile-")) + const tempIcnsFile = path.resolve(tempDir, "Icon.icns") + await fs.writeFile(tempIcnsFile, icnsFile) + + // @ts-expect-error - this is an override for compatibility + this.platformSpecificBuildOptions.icon = tempIcnsFile } } From 4f66e03374a953fdf235d6797154ff34c1e306cf Mon Sep 17 00:00:00 2001 From: iamEvan Date: Sun, 12 Oct 2025 01:09:07 +0100 Subject: [PATCH 13/21] fix: improve process of cloning the icns file --- packages/app-builder-lib/src/macPackager.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index e8ce3fd2823..bdc6fef972e 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -512,15 +512,19 @@ export class MacPackager extends PlatformPackager { const isIconComposer = typeof configuredIcon === "string" && configuredIcon.toLowerCase().endsWith(".icon") // Bundle legacy `icns` format - const icon = isIconComposer ? null : await this.getIconPath() - if (icon != null) { + const setIcnsFile = async (iconPath: string) => { const oldIcon = appPlist.CFBundleIconFile if (oldIcon != null) { await unlinkIfExists(path.join(resourcesPath, oldIcon)) } const iconFileName = "icon.icns" appPlist.CFBundleIconFile = iconFileName - await copyFile(icon, path.join(resourcesPath, iconFileName)) + await copyFile(iconPath, path.join(resourcesPath, iconFileName)) + } + + const icon = isIconComposer ? null : await this.getIconPath() + if (icon != null) { + await setIcnsFile(icon) } appPlist.CFBundleName = appInfo.productName appPlist.CFBundleDisplayName = appInfo.productName @@ -535,17 +539,15 @@ export class MacPackager extends PlatformPackager { appPlist.CFBundleIconName = "Icon" await fs.writeFile(path.join(resourcesPath, "Assets.car"), assetCatalog) - // Create and setup the icns file - appPlist.CFBundleIconFile = "Icon" - await fs.writeFile(path.join(resourcesPath, "Icon.icns"), icnsFile) - // Override configuration to use the generated icns file for compatibility const tempDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icon-compile-")) const tempIcnsFile = path.resolve(tempDir, "Icon.icns") await fs.writeFile(tempIcnsFile, icnsFile) - // @ts-expect-error - this is an override for compatibility this.platformSpecificBuildOptions.icon = tempIcnsFile + + // Setup the icns file + await setIcnsFile(tempIcnsFile) } } From f788939108725ec227d04522f32d74c83ef5deba Mon Sep 17 00:00:00 2001 From: iamEvan Date: Sun, 12 Oct 2025 01:19:58 +0100 Subject: [PATCH 14/21] fix --- packages/app-builder-lib/src/macPackager.ts | 2 +- packages/app-builder-lib/src/util/macosIconComposer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index bdc6fef972e..9f9f1181448 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -540,7 +540,7 @@ export class MacPackager extends PlatformPackager { await fs.writeFile(path.join(resourcesPath, "Assets.car"), assetCatalog) // Override configuration to use the generated icns file for compatibility - const tempDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icon-compile-")) + const tempDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icns-storage")) const tempIcnsFile = path.resolve(tempDir, "Icon.icns") await fs.writeFile(tempIcnsFile, icnsFile) // @ts-expect-error - this is an override for compatibility diff --git a/packages/app-builder-lib/src/util/macosIconComposer.ts b/packages/app-builder-lib/src/util/macosIconComposer.ts index fe1c8b9782d..b9b543f0d27 100644 --- a/packages/app-builder-lib/src/util/macosIconComposer.ts +++ b/packages/app-builder-lib/src/util/macosIconComposer.ts @@ -19,7 +19,7 @@ export async function generateAssetCatalogForIcon(inputPath: string) { throw new Error(`Unsupported actool version. Must be on actool 26.0.0 or higher but found ${acToolVersion}. Install XCode 26 or higher to get a supported version of actool.`) } - const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icon-compile-")) + const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icon-compile")) const iconPath = path.resolve(tmpDir, "Icon.icon") const outputPath = path.resolve(tmpDir, "out") From 4c2ac7fc542aa660891e2a5d4acec9cff5563b4a Mon Sep 17 00:00:00 2001 From: iamEvan Date: Sun, 12 Oct 2025 15:09:26 +0100 Subject: [PATCH 15/21] fix: universal builds not working --- packages/app-builder-lib/src/macPackager.ts | 24 ++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index 9f9f1181448..a91702ac51f 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -2,7 +2,21 @@ import { notarize } from "@electron/notarize" import { NotarizeOptionsNotaryTool, NotaryToolKeychainCredentials } from "@electron/notarize/lib/types" import { PerFileSignOptions, SignOptions } from "@electron/osx-sign/dist/cjs/types" import { Identity } from "@electron/osx-sign/dist/cjs/util-identities" -import { Arch, AsyncTaskManager, copyFile, deepAssign, exec, getArchSuffix, InvalidConfigurationError, log, orIfFileNotExist, statOrNull, unlinkIfExists, use } from "builder-util" +import { + Arch, + AsyncTaskManager, + copyFile, + deepAssign, + exec, + exists, + getArchSuffix, + InvalidConfigurationError, + log, + orIfFileNotExist, + statOrNull, + unlinkIfExists, + use, +} from "builder-util" import { MemoLazy, Nullish } from "builder-util-runtime" import * as fs from "fs/promises" import { mkdir, readdir } from "fs/promises" @@ -164,6 +178,14 @@ export class MacPackager extends PlatformPackager { `packaging` ) const appFile = `${this.appInfo.productFilename}.app` + + // Make sure the Assets.car file is the same for both architectures + const sourceCatalogPath = path.join(x64AppOutDir, appFile, "Contents/Resources/Assets.car") + if (await exists(sourceCatalogPath)) { + const targetCatalogPath = path.join(arm64AppOutPath, appFile, "Contents/Resources/Assets.car") + await fs.copyFile(sourceCatalogPath, targetCatalogPath) + } + const { makeUniversalApp } = require("@electron/universal") await makeUniversalApp({ x64AppPath: path.join(x64AppOutDir, appFile), From 0b18cc7ce1e6b05fe316495732193ad81705bb6a Mon Sep 17 00:00:00 2001 From: iamEvan Date: Tue, 14 Oct 2025 18:24:30 +0100 Subject: [PATCH 16/21] refactor: cache asset catalog generation and improve icon resolver --- packages/app-builder-lib/src/macPackager.ts | 28 ++++------- .../app-builder-lib/src/platformPackager.ts | 49 +++++++++++++++++++ .../src/util/macosIconComposer.ts | 12 ++++- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/packages/app-builder-lib/src/macPackager.ts b/packages/app-builder-lib/src/macPackager.ts index a91702ac51f..003dfc74ecb 100644 --- a/packages/app-builder-lib/src/macPackager.ts +++ b/packages/app-builder-lib/src/macPackager.ts @@ -22,7 +22,6 @@ import * as fs from "fs/promises" import { mkdir, readdir } from "fs/promises" import { Lazy } from "lazy-val" import * as path from "path" -import * as os from "os" import { AppInfo } from "./appInfo" import { CertType, CodeSigningInfo, createKeychain, CreateKeychainOptions, findIdentity, isSignAllowed, removeKeychain, reportError, sign } from "./codeSign/macCodeSign" import { DIR_TARGET, Platform, Target } from "./core" @@ -37,7 +36,6 @@ import { isMacOsHighSierra } from "./util/macosVersion" import { getTemplatePath } from "./util/pathManager" import { resolveFunction } from "./util/resolve" import { expandMacro as doExpandMacro } from "./util/macroExpander" -import { generateAssetCatalogForIcon } from "./util/macosIconComposer" export type CustomMacSignOptions = SignOptions export type CustomMacSign = (configuration: CustomMacSignOptions, packager: MacPackager) => Promise @@ -533,7 +531,11 @@ export class MacPackager extends PlatformPackager { const configuredIcon = this.platformSpecificBuildOptions.icon const isIconComposer = typeof configuredIcon === "string" && configuredIcon.toLowerCase().endsWith(".icon") - // Bundle legacy `icns` format + // Set the app name + appPlist.CFBundleName = appInfo.productName + appPlist.CFBundleDisplayName = appInfo.productName + + // Bundle legacy `icns` format - this should also run when `.icon` is provided const setIcnsFile = async (iconPath: string) => { const oldIcon = appPlist.CFBundleIconFile if (oldIcon != null) { @@ -544,32 +546,20 @@ export class MacPackager extends PlatformPackager { await copyFile(iconPath, path.join(resourcesPath, iconFileName)) } - const icon = isIconComposer ? null : await this.getIconPath() - if (icon != null) { - await setIcnsFile(icon) + const icnsFilePath = await this.getIconPath() + if (icnsFilePath != null) { + await setIcnsFile(icnsFilePath) } - appPlist.CFBundleName = appInfo.productName - appPlist.CFBundleDisplayName = appInfo.productName // Bundle new `icon` format if (isIconComposer && configuredIcon) { const iconComposerPath = await this.getResource(configuredIcon) if (iconComposerPath) { - const { assetCatalog, icnsFile } = await generateAssetCatalogForIcon(iconComposerPath) + const { assetCatalog } = await this.generateAssetCatalogData(iconComposerPath) // Create and setup the asset catalog appPlist.CFBundleIconName = "Icon" await fs.writeFile(path.join(resourcesPath, "Assets.car"), assetCatalog) - - // Override configuration to use the generated icns file for compatibility - const tempDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icns-storage")) - const tempIcnsFile = path.resolve(tempDir, "Icon.icns") - await fs.writeFile(tempIcnsFile, icnsFile) - // @ts-expect-error - this is an override for compatibility - this.platformSpecificBuildOptions.icon = tempIcnsFile - - // Setup the icns file - await setIcnsFile(tempIcnsFile) } } diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index c3895d78858..6f4daf988ea 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -20,6 +20,8 @@ import { readdir } from "fs/promises" import { Lazy } from "lazy-val" import { Minimatch } from "minimatch" import * as path from "path" +import * as fs from "fs/promises" +import * as os from "os" import { AppInfo } from "./appInfo" import { checkFileInArchive } from "./asar/asarFileChecker" import { AsarPackager } from "./asar/asarUtil" @@ -46,6 +48,7 @@ import { import { executeAppBuilderAsJson } from "./util/appBuilder" import { computeFileSets, computeNodeModuleFileSets, copyAppFiles, ELECTRON_COMPILE_SHIM_FILENAME, transformFiles } from "./util/appFileCopier" import { expandMacro as doExpandMacro } from "./util/macroExpander" +import { AssetCatalogResult, generateAssetCatalogForIcon } from "./util/macosIconComposer" export type DoPackOptions = { outDir: string @@ -779,7 +782,53 @@ export abstract class PlatformPackager return (forceCodeSigningPlatform == null ? this.config.forceCodeSigning : forceCodeSigningPlatform) || false } + private assetCatalogResults = new Map>() + protected generateAssetCatalogData(iconPath: string): Promise { + // Cache results + const cachedPromise = this.assetCatalogResults.get(iconPath) + if (cachedPromise) { + return cachedPromise + } + + const promise = generateAssetCatalogForIcon(iconPath) + this.assetCatalogResults.set(iconPath, promise) + return promise + } + + private cachedIcnsFromIconFile = new Map>() + private async generateIcnsFromIcon(iconPath: string): Promise { + const cachedPromise = this.cachedIcnsFromIconFile.get(iconPath) + if (cachedPromise) { + return cachedPromise + } + + const runner = async () => { + const { icnsFile } = await this.generateAssetCatalogData(iconPath) + + // Generate icns file + const tempDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icon-compile-")) + const tempIcnsPath = path.resolve(tempDir, "Icon.icns") + await fs.writeFile(tempIcnsPath, icnsFile) + + return tempIcnsPath + } + const promise = runner() + this.cachedIcnsFromIconFile.set(iconPath, promise) + return promise + } + protected async getOrConvertIcon(format: IconFormat): Promise { + if (format === "icns") { + const configuredIcon = this.platformSpecificBuildOptions.icon + // If it is a .icon file, generate the icns file and return the path to the icns file + if (configuredIcon?.endsWith(".icon")) { + const iconPath = await this.getResource(configuredIcon) + if (iconPath) { + return this.generateIcnsFromIcon(iconPath) + } + } + } + const result = await this.resolveIcon(asArray(this.platformSpecificBuildOptions.icon || this.config.icon), [], format) if (result.length === 0) { const framework = this.info.framework diff --git a/packages/app-builder-lib/src/util/macosIconComposer.ts b/packages/app-builder-lib/src/util/macosIconComposer.ts index b9b543f0d27..6e27026f950 100644 --- a/packages/app-builder-lib/src/util/macosIconComposer.ts +++ b/packages/app-builder-lib/src/util/macosIconComposer.ts @@ -7,7 +7,17 @@ import * as path from "node:path" import * as plist from "plist" import * as semver from "semver" -export async function generateAssetCatalogForIcon(inputPath: string) { +export interface AssetCatalogResult { + assetCatalog: Buffer + icnsFile: Buffer +} + +/** + * Generates an asset catalog and extra assets that are useful for packaging the app. + * @param inputPath The path to the `.icon` file + * @returns The asset catalog and extra assets + */ +export async function generateAssetCatalogForIcon(inputPath: string): Promise { const acToolVersionOutput = await spawn("actool", ["--version"]) const versionInfo = plist.parse(acToolVersionOutput) as Record> if (!versionInfo || !versionInfo["com.apple.actool.version"] || !versionInfo["com.apple.actool.version"]["short-bundle-version"]) { From aeb42415d16392b281ea84b0d8e7c7dd745efc28 Mon Sep 17 00:00:00 2001 From: iamEvan Date: Tue, 14 Oct 2025 18:38:15 +0100 Subject: [PATCH 17/21] fix: better checks for actool version --- .../src/util/macosIconComposer.ts | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/app-builder-lib/src/util/macosIconComposer.ts b/packages/app-builder-lib/src/util/macosIconComposer.ts index 6e27026f950..80340ea628a 100644 --- a/packages/app-builder-lib/src/util/macosIconComposer.ts +++ b/packages/app-builder-lib/src/util/macosIconComposer.ts @@ -12,24 +12,55 @@ export interface AssetCatalogResult { icnsFile: Buffer } +const INVALID_ACTOOL_VERSION_ERROR = new Error( + "Failed to check actool version. Is Xcode 26 or higher installed? See output of the `actool --version` CLI command for more details." +) + +async function checkActoolVersion(tmpDir: string) { + const acToolOutputFileName = path.resolve(tmpDir, "actool.log") + + let versionInfo: Record> | undefined = undefined + + try { + const acToolOutputFile = await fs.open(acToolOutputFileName, "w") + await spawn("actool", ["--version"], { stdio: ["ignore", acToolOutputFile.fd, acToolOutputFile.fd] }) + const acToolVersionOutput = await fs.readFile(acToolOutputFileName, "utf8") + versionInfo = plist.parse(acToolVersionOutput) as Record> + } catch { + throw INVALID_ACTOOL_VERSION_ERROR + } + + if (!versionInfo || !versionInfo["com.apple.actool.version"] || !versionInfo["com.apple.actool.version"]["short-bundle-version"]) { + throw INVALID_ACTOOL_VERSION_ERROR + } + + const acToolVersion = versionInfo["com.apple.actool.version"]["short-bundle-version"] + if (!semver.gte(semver.coerce(acToolVersion)!, "26.0.0")) { + throw new Error(`Unsupported actool version. Must be on actool 26.0.0 or higher but found ${acToolVersion}. Install Xcode 26 or higher to get a supported version of actool.`) + } +} + /** * Generates an asset catalog and extra assets that are useful for packaging the app. * @param inputPath The path to the `.icon` file * @returns The asset catalog and extra assets */ export async function generateAssetCatalogForIcon(inputPath: string): Promise { - const acToolVersionOutput = await spawn("actool", ["--version"]) - const versionInfo = plist.parse(acToolVersionOutput) as Record> - if (!versionInfo || !versionInfo["com.apple.actool.version"] || !versionInfo["com.apple.actool.version"]["short-bundle-version"]) { - throw new Error("Unable to query actool version. Is Xcode 26 or higher installed? See output of the `actool --version` CLI command for more details.") + const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icon-compile-")) + const cleanup = async () => { + await fs.rm(tmpDir, { + recursive: true, + force: true, + }) } - const acToolVersion = versionInfo["com.apple.actool.version"]["short-bundle-version"] - if (!semver.gte(semver.coerce(acToolVersion)!, "26.0.0")) { - throw new Error(`Unsupported actool version. Must be on actool 26.0.0 or higher but found ${acToolVersion}. Install XCode 26 or higher to get a supported version of actool.`) + try { + await checkActoolVersion(tmpDir) + } catch (error) { + await cleanup() + throw error } - const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), "icon-compile")) const iconPath = path.resolve(tmpDir, "Icon.icon") const outputPath = path.resolve(tmpDir, "out") @@ -74,9 +105,6 @@ export async function generateAssetCatalogForIcon(inputPath: string): Promise Date: Tue, 14 Oct 2025 21:48:33 +0100 Subject: [PATCH 18/21] fix: `.icon` files causing builds to fail --- packages/app-builder-lib/src/platformPackager.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index 6f4daf988ea..4889b51722e 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -863,9 +863,17 @@ export abstract class PlatformPackager path.resolve(this.projectDir, output, `.icon-${outputFormat}`), ] for (const source of sources) { + if (source.endsWith(".icon")) { + // Ignore .icon files: they will cause the format conversion to fail + continue + } args.push("--input", source) } for (const source of fallbackSources) { + if (source.endsWith(".icon")) { + // Ignore .icon files: they will cause the format conversion to fail + continue + } args.push("--fallback-input", source) } From 0ef3fde5545c793b2d60f41803cd2034c7004e47 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 20 Oct 2025 23:31:59 +0100 Subject: [PATCH 19/21] fix: tests --- test/src/mac/macIconTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/mac/macIconTest.ts b/test/src/mac/macIconTest.ts index c5ac360e574..18473bd5b7d 100644 --- a/test/src/mac/macIconTest.ts +++ b/test/src/mac/macIconTest.ts @@ -47,7 +47,7 @@ test.ifMac("icon composer generate asset catalog", ({ expect }) => { const info = await parsePlistFile(infoPlistPath) expect(info.CFBundleIconName).toBe("Icon") - expect(info.CFBundleIconFile).toBe("Icon") + expect(info.CFBundleIconFile).toBe("icon.icns") const assetCatalogPath = path.join(resourcesDir, "Assets.car") const writtenCatalog = await fs.readFile(assetCatalogPath) From 6edafc7c7f9f9949861a5442d1c3ada768e8aa40 Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 8 Nov 2025 22:25:49 +0000 Subject: [PATCH 20/21] fix: e2e test --- packages/app-builder-lib/src/util/macosIconComposer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-builder-lib/src/util/macosIconComposer.ts b/packages/app-builder-lib/src/util/macosIconComposer.ts index 80340ea628a..5330b650ced 100644 --- a/packages/app-builder-lib/src/util/macosIconComposer.ts +++ b/packages/app-builder-lib/src/util/macosIconComposer.ts @@ -8,8 +8,8 @@ import * as plist from "plist" import * as semver from "semver" export interface AssetCatalogResult { - assetCatalog: Buffer - icnsFile: Buffer + assetCatalog: Buffer + icnsFile: Buffer } const INVALID_ACTOOL_VERSION_ERROR = new Error( From bd097f4d0be2414281d51dfc29204bf2c8b6efa5 Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 8 Nov 2025 22:28:34 +0000 Subject: [PATCH 21/21] fix: broken mac tests --- .github/workflows/test.yaml | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index dbe21bba3e7..0d8892c939b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -210,8 +210,9 @@ jobs: TEST_FILES: ${{ matrix.testFiles }} FORCE_COLOR: 1 + # Some tests fails while on macOS 26, so we'll keep it this way for now test-mac: - runs-on: macos-26 + runs-on: macos-latest timeout-minutes: 20 strategy: fail-fast: false @@ -219,7 +220,7 @@ jobs: testFiles: - oneClickInstallerTest,assistedInstallerTest,webInstallerTest - winPackagerTest,winCodeSignTest,BuildTest,blackboxUpdateTest - - masTest,dmgTest,filesTest,macPackagerTest,differentialUpdateTest,macArchiveTest,macIconTest + - masTest,dmgTest,filesTest,macPackagerTest,differentialUpdateTest,macArchiveTest - concurrentBuildsTest steps: - name: Checkout code repository @@ -242,3 +243,33 @@ jobs: env: TEST_FILES: ${{ matrix.testFiles }} FORCE_COLOR: 1 + + test-mac-26: + runs-on: macos-26 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + testFiles: + - macIconTest + steps: + - name: Checkout code repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Tests + uses: ./.github/actions/pretest + with: + cache-path: ~/Library/Caches/electron + cache-key: v-23.3.10-macos-electron + + - name: Install toolset via brew + run: | + brew install powershell/tap/powershell + brew install --cask wine-stable + brew install rpm + + - name: Test + run: pnpm ci:test + env: + TEST_FILES: ${{ matrix.testFiles }} + FORCE_COLOR: 1