// 构建前准备三样东西: // 1) 把 docs/ 下的使用说明与报名名单导入规范暂存到 console/extra/,供 electron-builder 的 extraFiles 平铺到部署包根目录; // 2) 生成安装器自定义脚本(console/resources/installer-custom.nsh):许可页顶部文案(署名来自构建机本地文件) // + 记住上次安装目录(重装时沿用,而不是退回系统默认路径); // 3) 读仓库根 VERSION 文件:生成 deploy/version.gen.bat(供 build.bat 注入 Go 二进制) // 并同步 console/package.json 的 version(决定安装包/免安装包的文件名)。 // 用 Node 而不是批处理:中文文件名/中文内容在 .bat 里会被 cmd 解析错乱。 import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' const root = dirname(import.meta.dirname) // ---- 1. usage docs ---------------------------------------------------------- // 都暂存成 .txt:部署包根目录里给现场双击打开,.md 在记事本里全是符号。 // 源文件优先取 docs/ 下同名 .txt(便于临时替换),否则用仓库里的 .md。 const extraDir = join(root, 'console', 'extra') mkdirSync(extraDir, { recursive: true }) const docsToStage = [ { target: '使用说明.txt', sources: ['使用说明.txt', '使用手册.md'], label: 'usage doc' }, { target: '报名名单导入规范.txt', sources: ['报名名单导入规范.md'], label: 'entry import spec' }, ] for (const doc of docsToStage) { const source = doc.sources.map((name) => join(root, 'docs', name)).find((file) => existsSync(file)) if (!source) { console.log(` [docs] no ${doc.label}, skip`) continue } copyFileSync(source, join(extraDir, doc.target)) console.log(` ${doc.label} staged from ${source}`) } // ---- 2. installer custom script --------------------------------------------- // electron-builder 的 nsis.include 只收一个文件,所以许可页文案与安装目录记忆写在同一个 .nsh 里。 const signatureFile = join(root, 'deploy', 'signature.local.txt') const nshDir = join(root, 'console', 'resources') const nshFile = join(nshDir, 'installer-custom.nsh') const legacyNshFile = join(nshDir, 'installer-signature.nsh') mkdirSync(nshDir, { recursive: true }) let topText = '请阅读并接受许可协议' if (existsSync(signatureFile)) { const signature = readFileSync(signatureFile, 'utf8').trim() if (signature !== '') topText = `内部署名:${signature}` } // 换行用 CRLF:.gitattributes 里 *.nsh 就是 CRLF,NSIS 在 Windows 上按CRLF 读最稳。 const nsh = [ '!include "LogicLib.nsh"', '', '; 安装器自定义脚本:由 deploy/prepare-extra.mjs 生成,不要手改。', '; 内容只有三块:许可页文案、上次安装目录记忆、卸载钩子。', '', '; 自己记的安装目录(per-user 落 HKCU,per-all-users 落 HKLM)。', '; electron-builder 自带的 ${INSTALL_REGISTRY_KEY} 在卸载时会被整键删掉,', '; 「先卸载再装新版」或换过安装模式就会退回系统默认路径,所以另存一份不会被卸载器清理的。', '!define SCOREBOARD_INSTALL_KEY "Software\\Scoreboard Console"', '', '!macro customHeader', ` !define MUI_LICENSEPAGE_TEXT_TOP "${topText}"`, '!macroend', '', '!macro customInit', ' ; 在 .onInit 里、initMultiUser 之后执行:此时改 $INSTDIR 还来得及影响后面的目录页。', ' ReadRegStr $R9 HKCU "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"', ' ${If} $R9 != ""', ' StrCpy $INSTDIR $R9', ' ; 安装模式页(per-user / per-all-users)离开时,electron-builder 会用自己那个键', ' ; 重新算一遍 $INSTDIR;卸载后它已经被删了,把记忆值补写回同一个 hive 里。', ' ; 不跨 hive 写:否则会把 per-machine 装的机器悄悄带成默认 per-user 模式(反之亦然)。', ' WriteRegStr HKCU "${INSTALL_REGISTRY_KEY}" "InstallLocation" "$R9"', ' ${Else}', ' ReadRegStr $R9 HKLM "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"', ' ${If} $R9 != ""', ' StrCpy $INSTDIR $R9', ' WriteRegStr HKLM "${INSTALL_REGISTRY_KEY}" "InstallLocation" "$R9"', ' ${Else}', ' ; 没有自己的记忆时,退回 electron-builder 的键(老版本装过、没卸载过的情况)', ' ReadRegStr $R9 HKCU "${INSTALL_REGISTRY_KEY}" "InstallLocation"', ' ${If} $R9 == ""', ' ReadRegStr $R9 HKLM "${INSTALL_REGISTRY_KEY}" "InstallLocation"', ' ${EndIf}', ' ; 只认非空值;旧目录已经被删掉也照样用(这就是「记住我的路径」的语义)。', ' ${If} $R9 != ""', ' StrCpy $INSTDIR $R9', ' ${EndIf}', ' ${EndIf}', ' ${EndIf}', ' ClearErrors', '!macroend', '', '!macro customInstall', ' ; SHELL_CONTEXT 跟随安装模式:per-user → HKCU,per-all-users → HKLM。', ' WriteRegStr SHELL_CONTEXT "${SCOREBOARD_INSTALL_KEY}" "InstallLocation" "$INSTDIR"', '!macroend', '', '!macro customUnInstall', ' ; 故意什么都不删:需求就是「卸载后重装仍沿用上次的目录」。', ' ; 若改成卸载即遗忘,在这里补两行(卸载时 SHELL_CONTEXT 不一定已设定,要写死根键):', ' ; DeleteRegValue HKCU "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"', ' ; DeleteRegValue HKLM "${SCOREBOARD_INSTALL_KEY}" "InstallLocation"', '!macroend', '', ].join('\r\n') writeFileSync(nshFile, nsh, 'utf8') // 改名前生成的旧脚本已经没人引用,构建机上顺手清掉,免得看起来还在用。 rmSync(legacyNshFile, { force: true }) console.log(` installer custom script ready (${existsSync(signatureFile) ? 'with' : 'without'} internal signature)`) // ---- 3. version ------------------------------------------------------------- // 版本号只维护仓库根的 VERSION 文件(唯一真相):这里读出来分两路用—— // deploy/version.gen.bat 给 build.bat,编译期用 -X main.version 写进二进制; // console/package.json electron-builder 据此给安装包/免安装包命名。 // 二进制里的版本在编译那一刻定死,运行期不再读任何文件。 const versionFile = join(root, 'VERSION') if (!existsSync(versionFile)) { console.error(' [version] missing VERSION file at repo root') process.exit(1) } const version = (readFileSync(versionFile, 'utf8') .replace(/^\uFEFF/, '') .split(/\r?\n/) .find((line) => line.trim() !== '') ?? '').trim() if (version === '') { console.error(' [version] VERSION file is empty') process.exit(1) } // 必须纯 ASCII:cmd.exe 会错解 .bat 里的非 ASCII 文本(见本文件头部说明)。 // 用 CRLF:与 .gitattributes 里 *.bat 的约定一致,cmd 在 Windows 上按 CRLF 读最稳。 // 只 set 不加 setlocal:build.bat 用 call 调它,变量要留在调用方的环境里。 const genBatFile = join(root, 'deploy', 'version.gen.bat') const genBat = [ '@echo off', 'REM Generated by deploy/prepare-extra.mjs from the VERSION file - do not edit.', `set "SCOREBOARD_VERSION=${version}"`, '', ].join('\r\n') writeFileSync(genBatFile, genBat, 'utf8') console.log(` version ${version} -> deploy/version.gen.bat`) // console/package.json 的 version 是 semver,不带 dev_ 前缀(dev_ 只用于二进制版本号)。 // 逐字替换而不是 JSON.parse/stringify:保住原有缩进与末尾换行,改动最小。 const pkgVersion = version.replace(/^dev_/, '') const pkgFile = join(root, 'console', 'package.json') const pkgRaw = readFileSync(pkgFile, 'utf8') const pkgNext = pkgRaw.replace(/^(\s*"version"\s*:\s*)"[^"]*"/m, `$1"${pkgVersion}"`) if (pkgNext === pkgRaw) { console.log(` console/package.json version is ${pkgVersion}`) } else { writeFileSync(pkgFile, pkgNext, 'utf8') console.log(` console/package.json version -> ${pkgVersion}`) }