Files
peoplelib/icons/tools/make-icns.js
T
lofyer 381c07733a feat: 支持 macOS arm64 打包,修复图标未入库
新增 build-mac.js,产出 ad-hoc 签名的 .app 与 DMG。只能在 macOS 上
构建:DMG 依赖 hdiutil,且 Apple Silicon 拒绝执行未签名二进制,改名
与改 plist 后必须用 codesign 重签。

按官方 darwin-arm64 运行时的真实结构处理四处易错点:用 ditto 解压以
保留 framework 符号链接、删除重打包后失效的 ElectronAsarIntegrity、
为改名后的 helper 补上 CFBundleExecutable、签名由内向外且单独签
Squirrel 的 ShipIt。

打包版数据目录改为按平台决定:macOS 走 appData,避免写进 DMG 挂载后
只读的 .app 内部并在升级覆盖时丢失书库;Windows 便携版行为不变。
窗口图标在非 Windows 平台改用 PNG。

.gitignore 的 dist 规则补上前导斜杠。此前它同时匹配 icons/dist/,
导致构建与单测依赖的图标从未入库,新克隆的仓库无法构建。
2026-08-03 16:18:47 +08:00

63 lines
2.0 KiB
JavaScript

// 从 icons/dist/<theme>/icon-*.png 生成 macOS 的 .icns。
// 用途:Windows 上没有 iconutil,但 icns 自 10.7 起支持直接内嵌 PNG,
// 因此容器可以手工拼出来,不必依赖 macOS 工具链。
const fs = require('fs');
const path = require('path');
const DIST = path.join(__dirname, '..', 'dist');
// OSType -> 该槽位要求的像素尺寸。ic11/ic12/ic13/ic14 是 @2x 变体,
// 像素数等于逻辑尺寸的两倍,复用同一批 PNG 即可。
const SLOTS = [
['icp4', 16],
['icp5', 32],
['ic11', 32],
['ic12', 64],
['ic07', 128],
['ic13', 256],
['ic08', 256],
['ic14', 512],
['ic09', 512],
['ic10', 1024]
];
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
function pngSize(buf) {
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE)) throw new Error('不是 PNG 文件');
if (buf.readUInt32BE(8) !== 13 || buf.toString('latin1', 12, 16) !== 'IHDR') {
throw new Error('PNG 缺少 IHDR');
}
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
}
function build(theme) {
const chunks = [];
for (const [osType, size] of SLOTS) {
const file = path.join(DIST, theme, `icon-${size}.png`);
const png = fs.readFileSync(file);
const dim = pngSize(png);
if (dim.width !== size || dim.height !== size) {
throw new Error(`${file} 期望 ${size}x${size},实际 ${dim.width}x${dim.height}`);
}
const header = Buffer.alloc(8);
header.write(osType, 0, 4, 'latin1');
header.writeUInt32BE(png.length + 8, 4);
chunks.push(header, png);
}
const body = Buffer.concat(chunks);
const header = Buffer.alloc(8);
header.write('icns', 0, 4, 'latin1');
header.writeUInt32BE(body.length + 8, 4);
const out = path.join(DIST, `book-ai-${theme}.icns`);
fs.writeFileSync(out, Buffer.concat([header, body]));
return out;
}
for (const theme of ['dark', 'light']) {
const out = build(theme);
console.log(`${path.basename(out)} ${(fs.statSync(out).size / 1024).toFixed(1)} KB`);
}