From 2d784297ffe61007d6e30e10dcd63d5784c5cab4 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Wed, 5 Aug 2026 19:22:00 +0800 Subject: [PATCH] fix(desktop): ship complete macOS ICNS so Dock never blanks Prebuild a full iconutil ICNS (including 1024) instead of converting a lone 512 PWA PNG, validate Dock-critical types at package verify time, and set the Dock icon at runtime as a Launch Services cache fallback. --- apps/desktop/README.md | 17 ++++ apps/desktop/assets/icon.icns | Bin 0 -> 79913 bytes apps/desktop/assets/icon.png | Bin 0 -> 21755 bytes apps/desktop/electron-builder.yml | 10 ++- apps/desktop/src/main/index.mjs | 24 ++++++ package.json | 5 +- scripts/desktop-icns.mjs | 116 +++++++++++++++++++++++++++ scripts/desktop-icns.test.mjs | 61 ++++++++++++++ scripts/plan-native-release.mjs | 2 + scripts/plan-native-release.test.mjs | 2 + scripts/prepare-desktop-icons.mjs | 107 ++++++++++++++++++++++++ scripts/release.mjs | 1 + scripts/verify-desktop-package.mjs | 6 ++ 13 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/assets/icon.icns create mode 100644 apps/desktop/assets/icon.png create mode 100644 scripts/desktop-icns.mjs create mode 100644 scripts/desktop-icns.test.mjs create mode 100644 scripts/prepare-desktop-icons.mjs diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 32a9d3f5..cb426169 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -20,9 +20,26 @@ bun run dev:desktop The sidecar can be overridden for development with `EDGE_EVER_SIDECAR_PATH=/absolute/path/to/edgeever-sidecar`. +## App icon (macOS Dock) + +Dock tiles need a complete multi-resolution `.icns` (including 1024px). Do not +point electron-builder at a lone 512 PWA PNG — that produces a thin icon pack +and macOS can show a blank Dock placeholder forever after installs. + +Regenerate committed icon assets after changing the brand mark: + +```sh +bun run prepare:desktop:icons +``` + +This writes `apps/desktop/assets/icon.icns` (full ICNS) and `icon.png` (1024 +master). Packaging validation rejects incomplete ICNS types. Runtime also calls +`app.dock.setIcon` as a Launch Services / Dock cache fallback. + Build an unsigned installer for the current platform with: ```sh +bun run prepare:desktop:icons bun run build:web bun run build:desktop:sidecar CSC_IDENTITY_AUTO_DISCOVERY=false bun run --cwd apps/desktop dist -- --publish never diff --git a/apps/desktop/assets/icon.icns b/apps/desktop/assets/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..260db2eb7b465c0bb798ce524b69f3486d682c70 GIT binary patch literal 79913 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 R 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 { + if (process.platform !== "darwin" || !app.dock) return; + const candidates = app.isPackaged + ? [join(process.resourcesPath, "icon.icns"), join(process.resourcesPath, "icon.png")] + : [ + join(projectRoot, "apps/desktop/assets/icon.icns"), + join(projectRoot, "apps/desktop/assets/icon.png"), + join(projectRoot, "apps/web/public/pwa-512x512.png"), + ]; + for (const iconPath of candidates) { + if (!existsSync(iconPath)) continue; + const image = nativeImage.createFromPath(iconPath); + if (!image.isEmpty()) { + app.dock.setIcon(image); + return; + } + } +}; const webUrl = process.env.EDGE_EVER_DESKTOP_WEB_URL || "http://127.0.0.1:5173"; // A packaged desktop app is self-hosted-client software: its instance URL must // come from the user-facing first-run setup, never from the build environment. @@ -564,6 +587,7 @@ const confirmMacInstallation = async () => { }; app.whenReady().then(async () => { + applyMacDockIcon(); if (app.isPackaged && isMountedInstallerPath(app.getAppPath())) { const isChinese = app.getLocale().toLowerCase().startsWith("zh"); const result = await dialog.showMessageBox({ diff --git a/package.json b/package.json index da68a250..8118b65f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dev:desktop": "concurrently -k -n web,desktop -c magenta,cyan \"bun run dev:web\" \"bun run --cwd apps/desktop dev\"", "dev:mobile": "bun --cwd apps/mobile expo start", "prepare:mobile:icons": "bun scripts/prepare-mobile-icons.mjs", + "prepare:desktop:icons": "bun scripts/prepare-desktop-icons.mjs", "build:android:fast": "bash scripts/build-android-local.sh fast", "build:android:fast:local": "bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:fast", "build:android:apk": "bash scripts/build-android-local.sh apk", @@ -36,10 +37,10 @@ "dev:extension": "vite --config apps/extension/vite.config.ts", "build:web": "vite build --config apps/web/vite.config.ts", "build:desktop:sidecar": "cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release", - "build:desktop": "EDGE_EVER_DESKTOP_BUILD=1 bun run build:web && bun run build:desktop:sidecar && bun run --cwd apps/desktop dist", + "build:desktop": "EDGE_EVER_DESKTOP_BUILD=1 bun run prepare:desktop:icons && bun run build:web && bun run build:desktop:sidecar && bun run --cwd apps/desktop dist", "benchmark:desktop:sidecar": "bun scripts/benchmark-desktop-sidecar.mjs", "verify:web-performance": "bun scripts/verify-web-performance.mjs", - "test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs scripts/verify-android-apk-signature.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts", + "test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs scripts/verify-android-apk-signature.test.mjs scripts/desktop-icns.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts", "release": "bun scripts/release.mjs", "publish:stores": "bun scripts/store-delivery.mjs", "test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs", diff --git a/scripts/desktop-icns.mjs b/scripts/desktop-icns.mjs new file mode 100644 index 00000000..9f8d5928 --- /dev/null +++ b/scripts/desktop-icns.mjs @@ -0,0 +1,116 @@ +/** + * macOS ICNS inspection helpers. + * + * Dock and Launch Services need a complete multi-resolution icon pack. A single + * 512 PNG converted by electron-builder often omits the 1024 (ic10) slot and + * can leave Dock tiles stuck on the generic "empty" placeholder after installs. + */ + +/** @typedef {{ type: string; length: number; offset: number }} IcnsEntry */ + +/** + * Types that a modern EdgeEver macOS app icon must include so Finder and Dock + * can pick an appropriate bitmap at every scale. + * + * Note: current `iconutil` emits `ic04`/`ic05` for the smallest slots, while + * older electron-builder packs used `icp4`/`icp5`/`icp6`. Either family is OK + * for the small sizes; the high-resolution set below is mandatory. + */ +export const REQUIRED_MAC_ICNS_TYPES = Object.freeze([ + "ic07", // 128 + "ic08", // 256 + "ic09", // 512 + "ic10", // 1024 — required for modern Dock / retina large tiles + "ic11", // 16@2x + "ic12", // 32@2x + "ic13", // 128@2x + "ic14", // 256@2x +]); + +/** At least one type from each group must be present. */ +export const REQUIRED_MAC_ICNS_ANY_OF = Object.freeze([ + Object.freeze(["icp4", "ic04"]), // ~16px family + Object.freeze(["icp5", "ic05", "icp6"]), // ~32–64px family +]); + +/** + * Parse the top-level type table of an ICNS container. + * @param {Buffer | Uint8Array} data + * @returns {IcnsEntry[]} + */ +export const parseIcnsEntries = (data) => { + const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data); + if (buffer.length < 8 || buffer.subarray(0, 4).toString("ascii") !== "icns") { + throw new Error("Not a valid ICNS file (missing icns magic)."); + } + + const declaredLength = buffer.readUInt32BE(4); + if (declaredLength !== buffer.length) { + throw new Error( + `ICNS length mismatch: header claims ${declaredLength} bytes, file has ${buffer.length}.`, + ); + } + + /** @type {IcnsEntry[]} */ + const entries = []; + let offset = 8; + while (offset + 8 <= buffer.length) { + const type = buffer.subarray(offset, offset + 4).toString("ascii"); + const length = buffer.readUInt32BE(offset + 4); + if (length < 8 || offset + length > buffer.length) { + throw new Error(`Invalid ICNS entry at offset ${offset} (type=${JSON.stringify(type)}, length=${length}).`); + } + entries.push({ type, length, offset }); + offset += length; + } + + if (offset !== buffer.length) { + throw new Error(`ICNS trailing bytes: parsed ${offset}, file ${buffer.length}.`); + } + + return entries; +}; + +/** + * @param {Buffer | Uint8Array} data + * @returns {{ types: string[]; missing: string[]; missingGroups: string[][]; ok: boolean }} + */ +export const inspectMacIcns = (data) => { + const entries = parseIcnsEntries(data); + const types = entries.map((entry) => entry.type); + const present = new Set(types); + const missing = REQUIRED_MAC_ICNS_TYPES.filter((type) => !present.has(type)); + const missingGroups = REQUIRED_MAC_ICNS_ANY_OF.filter( + (group) => !group.some((type) => present.has(type)), + ); + return { + types, + missing, + missingGroups, + ok: missing.length === 0 && missingGroups.length === 0, + }; +}; + +/** + * @param {Buffer | Uint8Array} data + * @param {string} [label] + */ +export const assertMacIcnsComplete = (data, label = "icon.icns") => { + const result = inspectMacIcns(data); + if (!result.ok) { + const parts = []; + if (result.missing.length > 0) { + parts.push(`types ${result.missing.join(", ")}`); + } + if (result.missingGroups.length > 0) { + parts.push( + `small-size groups ${result.missingGroups.map((group) => group.join("|")).join("; ")}`, + ); + } + throw new Error( + `${label} is missing Dock-critical ICNS ${parts.join(" and ")}. ` + + `Regenerate with: bun run prepare:desktop:icons`, + ); + } + return result; +}; diff --git a/scripts/desktop-icns.test.mjs b/scripts/desktop-icns.test.mjs new file mode 100644 index 00000000..27d8a1a4 --- /dev/null +++ b/scripts/desktop-icns.test.mjs @@ -0,0 +1,61 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { + REQUIRED_MAC_ICNS_TYPES, + assertMacIcnsComplete, + inspectMacIcns, + parseIcnsEntries, +} from "./desktop-icns.mjs"; + +const incompleteIcnsFromPwa = () => { + // Synthetic ICNS with only a few modern types (mirrors the previous 512-only pack). + const chunks = [ + Buffer.from("icp4"), + (() => { + const body = Buffer.alloc(8); + body.writeUInt32BE(16, 0); + return body; + })(), + ]; + // Build one real-looking entry of length 16 (header 8 + 8 payload) + const entryType = Buffer.from("ic09"); + const entryLen = Buffer.alloc(4); + entryLen.writeUInt32BE(16, 0); + const entryPayload = Buffer.alloc(8, 1); + const entry = Buffer.concat([entryType, entryLen, entryPayload]); + const total = 8 + entry.length; + const header = Buffer.alloc(8); + header.write("icns", 0, 4, "ascii"); + header.writeUInt32BE(total, 4); + return Buffer.concat([header, entry]); +}; + +describe("desktop ICNS helpers", () => { + test("lists the Dock-critical type set including 1024px", () => { + expect(REQUIRED_MAC_ICNS_TYPES).toContain("ic10"); + expect(REQUIRED_MAC_ICNS_TYPES).toContain("ic14"); + }); + + test("rejects buffers that are not ICNS", () => { + expect(() => parseIcnsEntries(Buffer.from("not-an-icon"))).toThrow(/icns magic/i); + }); + + test("reports missing types for incomplete ICNS packs", () => { + const incomplete = incompleteIcnsFromPwa(); + const report = inspectMacIcns(incomplete); + expect(report.ok).toBe(false); + expect(report.missing).toContain("ic10"); + expect(() => assertMacIcnsComplete(incomplete, "test.icns")).toThrow(/missing Dock-critical/i); + }); + + test("committed desktop icon.icns is complete for Dock", () => { + const path = join(import.meta.dir, "../apps/desktop/assets/icon.icns"); + const data = readFileSync(path); + const report = assertMacIcnsComplete(data, path); + for (const type of REQUIRED_MAC_ICNS_TYPES) { + expect(report.types).toContain(type); + } + expect(report.types.some((type) => type === "ic04" || type === "icp4")).toBe(true); + }); +}); diff --git a/scripts/plan-native-release.mjs b/scripts/plan-native-release.mjs index 6e29381c..3c02c976 100644 --- a/scripts/plan-native-release.mjs +++ b/scripts/plan-native-release.mjs @@ -34,6 +34,8 @@ export const planNativeRelease = (platform, changedFiles) => { ".github/workflows/desktop-build.yml", "bun.lock", "scripts/create-mac-update-metadata.mjs", + "scripts/desktop-icns.mjs", + "scripts/prepare-desktop-icons.mjs", "scripts/run-desktop-builder.mjs", "scripts/verify-desktop-package.mjs", ]); diff --git a/scripts/plan-native-release.test.mjs b/scripts/plan-native-release.test.mjs index 8d201bb9..f496c57a 100644 --- a/scripts/plan-native-release.test.mjs +++ b/scripts/plan-native-release.test.mjs @@ -44,6 +44,8 @@ describe("native release planning", () => { const changedFiles = [ ".github/workflows/desktop-build.yml", "scripts/create-mac-update-metadata.mjs", + "scripts/prepare-desktop-icons.mjs", + "scripts/desktop-icns.mjs", "scripts/run-desktop-builder.mjs", ]; expect(planNativeRelease("desktop", changedFiles)).toEqual({ diff --git a/scripts/prepare-desktop-icons.mjs b/scripts/prepare-desktop-icons.mjs new file mode 100644 index 00000000..11227e52 --- /dev/null +++ b/scripts/prepare-desktop-icons.mjs @@ -0,0 +1,107 @@ +/** + * Build a complete macOS ICNS (and 1024 PNG master) for the desktop app. + * + * Why this exists: + * electron-builder converting a lone 512 PWA PNG yields a thin ICNS that often + * omits ic10 (1024). Dock then falls back to the generic empty placeholder — + * including after clean installs. We prebuild a full iconutil pack and commit it. + * + * Usage: bun run prepare:desktop:icons + */ +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; +import { assertMacIcnsComplete } from "./desktop-icns.mjs"; + +const projectRoot = resolve(fileURLToPath(new URL(".", import.meta.url)), ".."); +const sourcePng = join(projectRoot, "apps/web/public/pwa-512x512.png"); +const assetsDir = join(projectRoot, "apps/desktop/assets"); + +/** iconutil expects these exact basenames inside a .iconset directory. */ +const ICONSET_FILES = [ + { name: "icon_16x16.png", size: 16 }, + { name: "icon_16x16@2x.png", size: 32 }, + { name: "icon_32x32.png", size: 32 }, + { name: "icon_32x32@2x.png", size: 64 }, + { name: "icon_128x128.png", size: 128 }, + { name: "icon_128x128@2x.png", size: 256 }, + { name: "icon_256x256.png", size: 256 }, + { name: "icon_256x256@2x.png", size: 512 }, + { name: "icon_512x512.png", size: 512 }, + { name: "icon_512x512@2x.png", size: 1024 }, +]; + +const run = (command, args, options = {}) => { + const result = spawnSync(command, args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + ...options, + }); + if (result.status !== 0) { + throw new Error( + `${command} ${args.join(" ")} failed:\n${result.stderr || result.stdout || result.error}`, + ); + } + return result; +}; + +const writeResizedPng = async (sourceBuffer, size, destination) => { + await sharp(sourceBuffer) + .resize(size, size, { + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 }, + kernel: sharp.kernel.lanczos3, + }) + .png({ compressionLevel: 9 }) + .toFile(destination); +}; + +export const prepareDesktopIcons = async ({ + sourcePath = sourcePng, + assetsDirectory = assetsDir, + requireIconutil = process.platform === "darwin", +} = {}) => { + await mkdir(assetsDirectory, { recursive: true }); + const sourceBuffer = await readFile(sourcePath); + + // Master PNG used by electron-builder on Windows/Linux and as a Dock fallback. + await writeResizedPng(sourceBuffer, 1024, join(assetsDirectory, "icon.png")); + + if (!requireIconutil) { + console.log("[prepare-desktop-icons] skipped iconutil on non-macOS; master PNG written."); + return { icnsPath: join(assetsDirectory, "icon.icns"), pngPath: join(assetsDirectory, "icon.png") }; + } + + const workRoot = await mkdtemp(join(tmpdir(), "edgeever-desktop-icon-")); + const iconsetDir = join(workRoot, "EdgeEver.iconset"); + await mkdir(iconsetDir, { recursive: true }); + + try { + for (const { name, size } of ICONSET_FILES) { + await writeResizedPng(sourceBuffer, size, join(iconsetDir, name)); + } + + const icnsPath = join(assetsDirectory, "icon.icns"); + run("iconutil", ["-c", "icns", iconsetDir, "-o", icnsPath]); + const icnsData = await readFile(icnsPath); + assertMacIcnsComplete(icnsData, icnsPath); + + console.log(`[prepare-desktop-icons] wrote ${icnsPath} (${icnsData.length} bytes)`); + console.log(`[prepare-desktop-icons] wrote ${join(assetsDirectory, "icon.png")} (1024×1024 master)`); + return { icnsPath, pngPath: join(assetsDirectory, "icon.png") }; + } finally { + await rm(workRoot, { recursive: true, force: true }); + } +}; + +if (import.meta.main) { + try { + await prepareDesktopIcons(); + } catch (error) { + console.error(`[prepare-desktop-icons] ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } +} diff --git a/scripts/release.mjs b/scripts/release.mjs index 6932177c..6b77ec36 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -40,6 +40,7 @@ export const RELEASE_VALIDATIONS = [ "scripts/release.test.mjs", "scripts/validate-store-delivery.test.mjs", "scripts/store-delivery.test.mjs", + "scripts/desktop-icns.test.mjs", "apps/web/src/lib/version-check.test.mjs", "apps/mobile/src/lib/mobile-release.test.ts", ], diff --git a/scripts/verify-desktop-package.mjs b/scripts/verify-desktop-package.mjs index 7bd5fc05..96aee5f1 100644 --- a/scripts/verify-desktop-package.mjs +++ b/scripts/verify-desktop-package.mjs @@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process"; import { existsSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs"; import { basename, join } from "node:path"; import { listPackage } from "@electron/asar"; +import { assertMacIcnsComplete } from "./desktop-icns.mjs"; const outputDirectory = join(process.cwd(), "release", "desktop"); const version = JSON.parse( @@ -58,6 +59,11 @@ if (requestedPlatform === "darwin") { const asarFiles = new Set(listPackage(asarPath)); assert.ok(asarFiles.has("/src/preload/index.cjs"), "macOS app bundle must contain the sandbox-compatible CommonJS preload"); assert.ok(!asarFiles.has("/src/preload/index.mjs"), "macOS app bundle must not contain the unsupported ESM preload"); + const appIconPath = join(appResources, "icon.icns"); + assert.ok(existsSync(appIconPath), `macOS app bundle is missing icon.icns: ${appIconPath}`); + assertMacIcnsComplete(readFileSync(appIconPath), appIconPath); + const infoPlist = readFileSync(join(unpackedApp, "Contents", "Info.plist"), "utf8"); + assert.match(infoPlist, /CFBundleIconFile/, "macOS Info.plist must declare CFBundleIconFile"); // LaunchServices registers unpacked build products as additional document // handlers. The signed DMG/ZIP are the release artifacts, so discard the // disposable unpacked bundle once its contents have passed verification.