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.
This commit is contained in:
1 parent
1fe3fdc428
commit
2d784297ff
13 files changed
+348
-3
No files matched your search
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -18,7 +18,9 @@ fileAssociations:
|
||||
description: Open a Markdown document in EdgeEver
|
||||
role: Editor
|
||||
asar: true
|
||||
icon: ../web/public/pwa-512x512.png
|
||||
# Cross-platform master (1024). macOS uses the prebuilt full ICNS below so Dock
|
||||
# always receives ic10/ic14 instead of electron-builder's thin 512-only pack.
|
||||
icon: assets/icon.png
|
||||
directories:
|
||||
output: ../../release/desktop
|
||||
files:
|
||||
@@ -34,6 +36,11 @@ extraResources:
|
||||
filter:
|
||||
- "trayTemplate.png"
|
||||
- "trayTemplate@2x.png"
|
||||
- from: assets
|
||||
to: .
|
||||
filter:
|
||||
- "icon.icns"
|
||||
- "icon.png"
|
||||
- from: ../../migrations
|
||||
to: migrations
|
||||
filter:
|
||||
@@ -45,6 +52,7 @@ extraResources:
|
||||
- edgeever-sidecar.exe
|
||||
mac:
|
||||
category: public.app-category.productivity
|
||||
icon: assets/icon.icns
|
||||
target:
|
||||
- target: dmg
|
||||
- target: zip
|
||||
|
||||
@@ -30,6 +30,29 @@ if (requestedUserDataDirectory) app.setPath("userData", requestedUserDataDirecto
|
||||
|
||||
const currentDirectory = fileURLToPath(new URL(".", import.meta.url));
|
||||
const projectRoot = join(currentDirectory, "../../..");
|
||||
/**
|
||||
* Force Dock to use our multi-resolution app icon. Bundle Info.plist is still
|
||||
* the primary source; this covers cases where Launch Services/Dock cache a
|
||||
* blank tile after overwrite installs.
|
||||
*/
|
||||
const applyMacDockIcon = () => {
|
||||
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({
|
||||
|
||||
+3
-2
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
]);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user