From 000ceb3ac70e78a2f0651a800aa64831e0485364 Mon Sep 17 00:00:00 2001 From: sunrioa <178722768+sunrioa@users.noreply.github.com> Date: Sun, 20 Sep 2026 15:27:14 +0800 Subject: [PATCH] fix(release): restore the macOS icon at 16px and 32px MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit electron-builder 26.15.2 replaced its icon generator, and from then until 26.15.3 the .icns it produced stored the 16px and 32px faces as PNG data in the legacy icp4/icp5/icp6 slots. macOS does not decode those, so every place that draws the icon small — Finder list rows, the Trash, Activity Monitor, and the installer DMG's own volume icon — showed noise, while 128px and up stayed correct and hid the breakage. 26.16.1 carries the upstream fix (icons toolset 1.2.3), which writes those sizes as ic04/ic05 ARGB again. The icon comes from a toolset the builder downloads at build time, so a version pin alone says nothing about what was produced. The macOS packaging verifier now reads the bundled icon and rejects the broken shape, which is the check that would have caught this before it shipped. The reproducible-ZIP patch moves to the new version unchanged. 26.16.1 sets -mtm=off and -mta=off for 7z archives upstream but still not for zip, which is the branch Windows artifacts take. Refs: https://github.com/electron-userland/electron-builder/issues/9940 Generated-by: Claude Code Co-Authored-By: Claude Opus 5 --- LICENSE | 6 +- apps/desktop/package.json | 2 +- package-lock.json | 135 +++++++++--------- ....3.patch => app-builder-lib+26.16.1.patch} | 3 +- scripts/verify-macos-dmg.mjs | 62 ++++++++ scripts/verify-packaged-app.test.mjs | 71 +++++++++ 6 files changed, 208 insertions(+), 71 deletions(-) rename patches/{app-builder-lib+26.15.3.patch => app-builder-lib+26.16.1.patch} (84%) diff --git a/LICENSE b/LICENSE index f8269bdb75..c1a01ff34c 100644 --- a/LICENSE +++ b/LICENSE @@ -467,11 +467,11 @@ THE SOFTWARE. electron-builder dependency patches -Source: https://www.npmjs.com/package/app-builder-lib/v/26.15.3 +Source: https://www.npmjs.com/package/app-builder-lib/v/26.16.1 https://www.npmjs.com/package/electron-updater/v/6.8.9 Repository: https://github.com/electron-userland/electron-builder -Versions: app-builder-lib 26.15.3; electron-updater 6.8.9 -Dependency patch: patches/app-builder-lib+26.15.3.patch +Versions: app-builder-lib 26.16.1; electron-updater 6.8.9 +Dependency patch: patches/app-builder-lib+26.16.1.patch patches/electron-updater+6.8.9.patch License: MIT diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f4ddef4303..fb3204b9a2 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -89,7 +89,7 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "electron": "43.4.1", - "electron-builder": "26.15.3", + "electron-builder": "26.16.1", "esbuild": "^0.28.1", "linkedom": "^0.18.13", "react": "^19.3.0", diff --git a/package-lock.json b/package-lock.json index c8d825607c..9a8297d1ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,7 +79,7 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "electron": "43.4.1", - "electron-builder": "26.15.3", + "electron-builder": "26.16.1", "esbuild": "^0.28.1", "linkedom": "^0.18.13", "react": "^19.3.0", @@ -1971,9 +1971,9 @@ } }, "node_modules/@electron/universal/node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -2002,9 +2002,9 @@ "license": "MIT" }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -2059,9 +2059,9 @@ } }, "node_modules/@electron/universal/node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -3691,13 +3691,13 @@ } }, "node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 20.19.0" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -4390,15 +4390,18 @@ ] }, "node_modules/@peculiar/asn1-schema": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", - "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz", + "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, "node_modules/@peculiar/json-schema": { @@ -6464,9 +6467,9 @@ } }, "node_modules/app-builder-lib": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", - "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "version": "26.16.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.16.1.tgz", + "integrity": "sha512-FhaO6YOup01ZfQW0Z6gt3AyukJjv1gW4uFK47jTgwcHZKqyN/fSlK2LqPf9tAeZYLP2bRJLDzeOkRImsw2X4Pg==", "dev": true, "license": "MIT", "dependencies": { @@ -6478,13 +6481,13 @@ "@electron/rebuild": "^4.0.4", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", - "@noble/hashes": "^2.2.0", + "@noble/hashes": "^1.8.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", - "builder-util": "26.15.3", + "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", @@ -6492,7 +6495,7 @@ "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", - "electron-publish": "26.15.3", + "electron-publish": "26.16.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", @@ -6516,8 +6519,8 @@ "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "26.15.3", - "electron-builder-squirrel-windows": "26.15.3" + "dmg-builder": "26.16.1", + "electron-builder-squirrel-windows": "26.16.1" } }, "node_modules/app-builder-lib/node_modules/@electron/asar": { @@ -6606,9 +6609,9 @@ "license": "MIT" }, "node_modules/app-builder-lib/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -7326,9 +7329,9 @@ "license": "MIT" }, "node_modules/builder-util": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", - "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "version": "26.16.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.16.0.tgz", + "integrity": "sha512-RLyJhB7Si3YkzKR9ubQslWuXW3Vhs3CGe1i+SeixBZ0qTd1mk3XBmssvY22TlB6CS5blyko8Gu1JzpYk8UkYAg==", "dev": true, "license": "MIT", "dependencies": { @@ -8847,9 +8850,9 @@ "license": "MIT" }, "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -8887,14 +8890,14 @@ } }, "node_modules/dmg-builder": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", - "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "version": "26.16.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.16.1.tgz", + "integrity": "sha512-pnI/3Qb24Uk+rMTgIUrsVUKosVgwmBUdF8Zeb8TexOSbpq8MWc7v6l+n+FrEqVkjNZwzBN+XpDS9ENgZ/rkWAw==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.15.3", - "builder-util": "26.15.3", + "app-builder-lib": "26.16.1", + "builder-util": "26.16.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } @@ -9101,18 +9104,18 @@ } }, "node_modules/electron-builder": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", - "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "version": "26.16.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.16.1.tgz", + "integrity": "sha512-LrLK65QX5PUYYODXqp23FKrV7CILTtVY7mrJckNknO9jLNSMiqFkKbSMiDRw4CjOADMPVDdWLxY4mezOZWswxg==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.15.3", - "builder-util": "26.15.3", + "app-builder-lib": "26.16.1", + "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", - "dmg-builder": "26.15.3", + "dmg-builder": "26.16.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", @@ -9127,15 +9130,15 @@ } }, "node_modules/electron-builder-squirrel-windows": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", - "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "version": "26.16.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.16.1.tgz", + "integrity": "sha512-w0y44wSaT1l6R7CAGmeHn4nHPfvzDyCAU1xJyi1w9SbPYJpYn76SmHDzqHf8Y7l91cPWTdPYBpGQtB2T5mJ08A==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "app-builder-lib": "26.15.3", - "builder-util": "26.15.3", + "app-builder-lib": "26.16.1", + "builder-util": "26.16.0", "electron-winstaller": "5.4.0" } }, @@ -9212,15 +9215,15 @@ } }, "node_modules/electron-publish": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", - "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "version": "26.16.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.16.0.tgz", + "integrity": "sha512-Vt3KzQIiw9BImvNOYtndg9Mjki+tl4+1sQiC/+G5j8khWaENOJFWodiB+sUl6yyHwtd37avehskdtPw7f8y/+Q==", "dev": true, "license": "MIT", "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", - "builder-util": "26.15.3", + "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", @@ -9314,9 +9317,9 @@ "peer": true }, "node_modules/electron-winstaller/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "peer": true, @@ -9890,9 +9893,9 @@ "license": "MIT" }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -12720,9 +12723,9 @@ } }, "node_modules/node-abi": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz", - "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.35.0.tgz", + "integrity": "sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -14451,9 +14454,9 @@ "peer": true }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "peer": true, diff --git a/patches/app-builder-lib+26.15.3.patch b/patches/app-builder-lib+26.16.1.patch similarity index 84% rename from patches/app-builder-lib+26.15.3.patch rename to patches/app-builder-lib+26.16.1.patch index bbd3bb6520..32abdf80ec 100644 --- a/patches/app-builder-lib+26.15.3.patch +++ b/patches/app-builder-lib+26.16.1.patch @@ -1,7 +1,8 @@ diff --git a/node_modules/app-builder-lib/out/targets/archive.js b/node_modules/app-builder-lib/out/targets/archive.js +index 9595b4f..fd8fe92 100644 --- a/node_modules/app-builder-lib/out/targets/archive.js +++ b/node_modules/app-builder-lib/out/targets/archive.js -@@ -101,6 +101,7 @@ function compute7zCompressArgs(format, options = {}) { +@@ -144,6 +144,7 @@ function compute7zCompressArgs(format, options = {}) { // For all other formats the codec is implicit from the output file extension. args.push(`-mm=${storeOnly ? "Copy" : "Deflate"}`); args.push("-mcu"); diff --git a/scripts/verify-macos-dmg.mjs b/scripts/verify-macos-dmg.mjs index 21a5e6a05b..fb1dbb1470 100644 --- a/scripts/verify-macos-dmg.mjs +++ b/scripts/verify-macos-dmg.mjs @@ -121,6 +121,67 @@ function assertSingleArchitecture(output, subject, expectedArch) { } } +/** + * macOS draws the 16px and 32px faces of an ICNS from the ARGB slots `ic04` + * and `ic05`. PNG data packed into the legacy `icp4`/`icp5`/`icp6` slots is + * not decoded, so it renders as noise — which is what a Finder list row, the + * Dock's Trash, Activity Monitor and the DMG's own volume icon show. Every + * larger size keeps working, so nothing else in this verifier notices. + * + * electron-builder shipped that exact shape from 26.15.2 through 26.15.3 + * (electron-userland/electron-builder#9940). The icon is generated by a + * toolset the builder downloads at build time, so the bundle is the only + * place the result can be seen: assert on it here rather than on a version + * pin that says nothing about what was produced. + */ +const UNRENDERABLE_ICNS_SLOTS = new Map([ + ['icp4', '16x16'], + ['icp5', '32x32'], + ['icp6', '64x64'], +]); +const RENDERED_ICNS_SLOTS = new Map([ + ['ic04', '16x16'], + ['ic05', '32x32'], +]); +const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + +function readIcnsSlots(icns) { + if (icns.length < 8 || icns.subarray(0, 4).toString('latin1') !== 'icns') { + throw new Error('Maka icon is not an ICNS archive.'); + } + const slots = new Map(); + // Every entry declares its own length including the 8-byte header, so a zero + // or truncated length is the difference between a parse error and a loop. + for (let offset = 8; offset + 8 <= icns.length; ) { + const type = icns.subarray(offset, offset + 4).toString('latin1'); + const length = icns.readUInt32BE(offset + 4); + if (length < 8 || offset + length > icns.length) { + throw new Error(`Maka icon entry ${type} declares an unusable length of ${length}.`); + } + slots.set(type, icns.subarray(offset + 8, offset + length)); + offset += length; + } + return slots; +} + +export async function assertRenderableAppIcon(resourcesPath, { readIcon = readFile } = {}) { + const slots = readIcnsSlots(await readIcon(join(resourcesPath, 'icon.icns'))); + const unrenderable = [...UNRENDERABLE_ICNS_SLOTS] + .filter(([type]) => slots.get(type)?.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) + .map(([type, size]) => `${size} (${type})`); + if (unrenderable.length > 0) { + throw new Error( + `Maka icon stores PNG data in ICNS slots macOS does not render: ${unrenderable.join(', ')}.`, + ); + } + const missing = [...RENDERED_ICNS_SLOTS] + .filter(([type]) => !slots.has(type)) + .map(([type, size]) => `${size} (${type})`); + if (missing.length > 0) { + throw new Error(`Maka icon is missing the sizes macOS draws smallest: ${missing.join(', ')}.`); + } +} + async function readPlistValue(run, infoPlist, key) { const { stdout } = await run('plutil', ['-extract', key, 'raw', '-o', '-', infoPlist]); return stdout.trim(); @@ -167,6 +228,7 @@ export async function verifyPackagedMacApp( await requirePath(executable); await assertPackagedResources(resources, { requirePath, forbidPath }); + await assertRenderableAppIcon(resources); await assertPackagedUpdateConfiguration(resources, { channel }); await assertPackagedDependencyClosure(resources); diff --git a/scripts/verify-packaged-app.test.mjs b/scripts/verify-packaged-app.test.mjs index 268ef84cdc..06f79b02c5 100644 --- a/scripts/verify-packaged-app.test.mjs +++ b/scripts/verify-packaged-app.test.mjs @@ -237,6 +237,31 @@ describe('asarLookupPath', () => { const roots = []; +const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + +/** An ICNS archive carrying `slots` verbatim, so a test can state the exact + * entry shape a generator produced without shipping a binary fixture. */ +function icnsWith(slots) { + const entries = slots.map(([type, payload]) => { + const entry = Buffer.alloc(8 + payload.length); + entry.write(type, 0, 'latin1'); + entry.writeUInt32BE(entry.length, 4); + payload.copy(entry, 8); + return entry; + }); + const body = Buffer.concat(entries); + const header = Buffer.alloc(8); + header.write('icns', 0, 'latin1'); + header.writeUInt32BE(header.length + body.length, 4); + return Buffer.concat([header, body]); +} + +const RENDERABLE_ICNS = icnsWith([ + ['ic04', Buffer.from('ARGBfixture')], + ['ic05', Buffer.from('ARGBfixture')], + ['ic07', Buffer.concat([PNG_SIGNATURE, Buffer.from('128px')])], +]); + const PTY_PACKAGES = ['@xterm/headless', '@xterm/addon-unicode11']; const COVERING_NOTICES = 'Header\n\nPackage: react@19.2.0\nDeclared license: MIT\n'; @@ -332,6 +357,7 @@ test('accepts the Intel Mach-O architecture for an x64 package', async () => { join(resources, 'app-update.yml'), 'provider: github\nowner: apache\nrepo: maka\nchannel: dev\nupdaterCacheDirName: "@makadesktop-updater"\n', ); + await writeFile(join(resources, 'icon.icns'), RENDERABLE_ICNS); const version = '0.2.0-dev.14.20260902'; const app = join(dirname(resources), 'Maka.app'); await mkdir(join(app, 'Contents'), { recursive: true }); @@ -366,6 +392,51 @@ test('accepts the Intel Mach-O architecture for an x64 package', async () => { }); }); +describe('assertRenderableAppIcon', () => { + const withIcon = async (t, icns) => { + const resources = await mkdtemp(join(tmpdir(), 'maka-icon-')); + t.after(() => rm(resources, { recursive: true, force: true })); + await writeFile(join(resources, 'icon.icns'), icns); + return resources; + }; + + test('accepts an icon whose small sizes are stored as ARGB', async (t) => { + const { assertRenderableAppIcon } = await import('./verify-macos-dmg.mjs'); + await assertRenderableAppIcon(await withIcon(t, RENDERABLE_ICNS)); + }); + + test('rejects PNG data in the legacy slots macOS does not decode', async (t) => { + const { assertRenderableAppIcon } = await import('./verify-macos-dmg.mjs'); + // The shape electron-builder 26.15.2-26.15.3 produced: every size present, + // the large ones fine, and 16px/32px unreadable where a person sees them. + const resources = await withIcon( + t, + icnsWith([ + ['icp4', Buffer.concat([PNG_SIGNATURE, Buffer.from('16px')])], + ['icp5', Buffer.concat([PNG_SIGNATURE, Buffer.from('32px')])], + ['ic07', Buffer.concat([PNG_SIGNATURE, Buffer.from('128px')])], + ]), + ); + await assert.rejects(assertRenderableAppIcon(resources), /16x16 \(icp4\), 32x32 \(icp5\)/); + }); + + test('rejects an icon that carries no small sizes at all', async (t) => { + const { assertRenderableAppIcon } = await import('./verify-macos-dmg.mjs'); + const resources = await withIcon( + t, + icnsWith([['ic07', Buffer.concat([PNG_SIGNATURE, Buffer.from('128px')])]]), + ); + await assert.rejects(assertRenderableAppIcon(resources), /missing the sizes/); + }); + + test('refuses to guess at a truncated entry instead of looping on it', async (t) => { + const { assertRenderableAppIcon } = await import('./verify-macos-dmg.mjs'); + const icns = icnsWith([['ic04', Buffer.from('ARGBfixture')]]); + icns.writeUInt32BE(0, 12); + await assert.rejects(assertRenderableAppIcon(await withIcon(t, icns)), /unusable length/); + }); +}); + describe('assertPackagedDependencyClosure', () => { test('accepts an artifact whose asar, bundle record, and shipped notices match', async () => { const resources = await makeResources();