diff --git a/cmd/gui/package-config.test.mjs b/cmd/gui/package-config.test.mjs new file mode 100644 index 00000000..a38a41fe --- /dev/null +++ b/cmd/gui/package-config.test.mjs @@ -0,0 +1,139 @@ +// GUI 发行打包配置的一致性判据。 +// +// ## 要判的是什么 +// +// 两条打包路径的配置各自漏过东西,且都是**静默**失败: +// +// 1) electron-packager(make build-gui,Windows 发行 + 本机安装都走它) +// · 曾漏 --icon=icon.ico ⇒ exe 一直是 Electron 默认图标 +// (已修:Makefile e79eff6) +// +// 2) package.json 的 build 段(electron-builder,Linux 发行走它) +// · files 白名单漏了 icon.ico / icon-tray*.png ⇒ 图标不进包 +// · win.icon 路径写错(electron-builder 相对**项目目录**解析, +// 写 'build/icon.ico' 会找不到文件) +// +// 3) deploy/packaging/package-linux.sh 的手工组装曾逐文件 cp 七个文件, +// 漏掉 renderer/vendor/*(本地化的 four 个库)与全部图标文件 +// ⇒ Linux 发行包装完星图与托盘图标直接坏,且**没有任何构建报错** +// (已修:改目录整体同步 + 必备文件校验) +// +// 打包配置类问题的特点是"改了没人知道、坏了要等用户装上才发现", +// 所以在这里钉死。 +// +// 运行:node package-config.test.mjs + +import { readFileSync, existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(here, "..", ".."); +const guiDir = here; + +let failures = 0; +const check = (name, ok, detail) => { + if (ok) console.log(` ✓ ${name}`); + else { + failures++; + console.log(` ✗ ${name}${detail ? " — " + detail : ""}`); + } +}; + +// ── 1) Makefile: electron-packager 必须带 --icon ────────────────────── +const mk = readFileSync(join(repoRoot, "Makefile"), "utf8"); +const buildGui = mk.slice( + mk.indexOf("build-gui:"), + mk.indexOf("\nbuild-static:"), +); + +check("能定位 build-gui 目标", buildGui.length > 0); +check( + "★ build-gui 传 --icon(否则 exe 一直是 Electron 默认图标)", + /--icon[= ]\S*icon\.ico/.test(buildGui), + "缺 --icon ⇒ exe 内嵌图标为 Electron 默认", +); +check( + "build-gui 仍带 --no-sandbox", + /--no-sandbox/.test(buildGui), + "--no-sandbox 没了,本机可能起不来", +); + +// ── 2) package.json build 段 ───────────────────────────────────────── +const pkg = JSON.parse(readFileSync(join(guiDir, "package.json"), "utf8")); +const files = pkg.build?.files || []; + +check( + "★ files 白名单含 icon.ico", + files.includes("icon.ico"), + "files = " + JSON.stringify(files), +); +check( + "files 白名单含托盘图标", + files.includes("icon-tray.png") && files.includes("icon-tray@2x.png"), + "缺 icon-tray*.png ⇒ 托盘图标在发行包里丢失", +); +check( + "files 含 renderer/**/*(含 vendor/)", + files.some((f) => f.startsWith("renderer/")), + "renderer 未列入 ⇒ vendor 库不会进包", +); + +// ── 3) win.icon 路径语义 ───────────────────────────────────────────── +// electron-builder 的 icon 相对**项目目录**(package.json 所在)解析。 +// 写成 'build/icon.ico'(相对输出目录)会找不到文件。 +const winIcon = pkg.build?.win?.icon; +check("配置了 win.icon", !!winIcon, "未配置 win.icon"); +if (winIcon) { + check( + "★ win.icon 路径不是误写的 build/ 前缀", + !winIcon.startsWith("build/"), + `win.icon='${winIcon}' —— electron-builder 相对项目目录解析,` + + "带 build/ 前缀会找不到文件", + ); + check( + "win.icon 指向的文件真实存在", + existsSync(join(guiDir, winIcon)), + `${winIcon} 不存在于 cmd/gui/`, + ); +} + +// ── 4) icon.ico 本身要含足够尺寸 ──────────────────────────────────── +const ico = readFileSync(join(guiDir, "icon.ico")); +const isIco = ico.readUInt16LE(0) === 0 && ico.readUInt16LE(2) === 1; +check("icon.ico 是合法 ICO", isIco, "头部不是 00 00 01 00"); +const icoCount = ico.readUInt16LE(4); +check( + "icon.ico 含多尺寸(含 256x256,electron-builder 需要)", + icoCount >= 2, + `只含 ${icoCount} 个尺寸`, +); + +// ── 5) package-linux.sh 不得再逐文件手列 ──────────────────────────── +const pkgLinux = readFileSync( + join(repoRoot, "deploy", "packaging", "package-linux.sh"), + "utf8", +); +check( + "★ package-linux.sh 按目录整体同步 renderer", + /cp -r "\$gui_dir\/renderer\/\."/.test(pkgLinux), + "仍在逐文件 cp ⇒ 新增资源(如 vendor/)不会被打进 Linux 包", +); +check( + "package-linux.sh 同步根级图标", + /cp "\$gui_dir"\/\*\.ico/.test(pkgLinux) && + /icon-tray/.test(pkgLinux), + "未同步根级 ico / 托盘图标", +); +check( + "package-linux.sh 有必备文件校验", + /vendor\/three\.min\.js/.test(pkgLinux) && /rm -rf "\$gui_out"/.test(pkgLinux), + "缺必备文件校验 ⇒ 装完才发现坏包", +); + +console.log(""); +if (failures > 0) { + console.log(`全部失败:${failures} 条`); + process.exit(1); +} +console.log("全部通过"); \ No newline at end of file diff --git a/cmd/gui/package.json b/cmd/gui/package.json index 4dcb97de..d34b900b 100644 --- a/cmd/gui/package.json +++ b/cmd/gui/package.json @@ -8,7 +8,7 @@ "scripts": { "start": "electron . --no-sandbox", "dev": "electron . --no-sandbox --dev", - "test": "node sse-backoff.test.mjs && node sse-backoff-behavior.test.mjs && node retry-guard.test.mjs && node endpoint-align.test.mjs && node connections-io.test.mjs && node silent-start.test.mjs", + "test": "node sse-backoff.test.mjs && node sse-backoff-behavior.test.mjs && node retry-guard.test.mjs && node endpoint-align.test.mjs && node connections-io.test.mjs && node silent-start.test.mjs && node package-config.test.mjs", "test-live": "node chat-perf.test.mjs && node protocol-align.test.mjs" }, "dependencies": { @@ -40,7 +40,16 @@ "renderer/**/*", "icon.svg", "node_modules/**/*", - "!node_modules/electron-builder/**" - ] + "!node_modules/electron-builder/**", + "icon.ico", + "icon-tray.png", + "icon-tray@2x.png" + ], + "win": { + "target": [ + "nsis" + ], + "icon": "icon.ico" + } } } diff --git a/deploy/packaging/package-linux.sh b/deploy/packaging/package-linux.sh index e86349cd..61fad774 100644 --- a/deploy/packaging/package-linux.sh +++ b/deploy/packaging/package-linux.sh @@ -261,13 +261,43 @@ print(m.group(1) if m else '') rm -f "$gui_out/resources/default_app.asar" 2>/dev/null # copy app source + # + # ★ 改成**按目录整体同步**,不再逐文件手列。 + # + # 原实现手工 cp 七个文件,每加一个新文件就要记得补一行,漏了不会有 + # 任何报错,只会做出一个"装完缺东西"的包。已实际漏掉过: + # · renderer/mascot.svg —— 源码里根本不存在(真名 mascot.webp) + # · icon.svg / icon.ico / icon-tray*.png —— 托盘与窗口图标 + # · renderer/icon.svg + # · renderer/vendor/* —— three/OrbitControls/marked/purify + # (这四个是本地化后的第三方库,缺了 Linux 发行版的星图直接坏) + # + # 整体同步还顺带保证 .hmap 之外的任何新增资源都会进包。 cp "$gui_dir/main.js" "$gui_out/resources/app/" cp "$gui_dir/preload.js" "$gui_out/resources/app/" cp "$gui_dir/package.json" "$gui_out/resources/app/" - cp "$gui_dir/renderer/index.html" "$gui_out/resources/app/renderer/" - cp "$gui_dir/renderer/app.js" "$gui_out/resources/app/renderer/" - cp "$gui_dir/renderer/style.css" "$gui_out/resources/app/renderer/" 2>/dev/null || true - cp "$gui_dir/renderer/mascot.svg" "$gui_out/resources/app/renderer/" 2>/dev/null || true + cp "$gui_dir"/*.svg "$gui_out/resources/app/" 2>/dev/null || true + cp "$gui_dir"/*.ico "$gui_out/resources/app/" 2>/dev/null || true + cp "$gui_dir"/icon-tray*.png "$gui_out/resources/app/" 2>/dev/null || true + + # renderer 整目录同步(含 vendor/ 与全部静态资源) + cp -r "$gui_dir/renderer/." "$gui_out/resources/app/renderer/" + + # ★ 必备文件校验:缺了就是"装完坏掉"的包,且不会有任何构建报错。 + # renderer/vendor 是本地化后的第三方库(three/OrbitControls/marked/purify), + # 缺任何一份都会让星图或 Markdown 渲染在断网/离线环境下直接坏掉。 + local missing="" + for _f in index.html app.js style.css vendor/three.min.js vendor/OrbitControls.js \ + vendor/marked.min.js vendor/purify.min.js; do + [ -f "$gui_out/resources/app/renderer/$_f" ] || missing="$_f " + done + [ -f "$gui_out/resources/app/icon.svg" ] || missing="icon.svg " + if [ -n "$missing" ]; then + echo " ERROR: GUI 资源缺失:$missing" + echo " 宁可不发,也不发装了跑不起来的包。" + rm -rf "$gui_out" + return + fi # production node_modules for app if [ -d "$gui_dir/node_modules" ]; then