修复 AGC NSIS 安装钩子在模板常量与插件目录之前编译导致的出包失败
Project CI / AI game creator shell Rust crates (push) Successful in 1m32s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / Backend tests (push) Successful in 4m6s
Project CI / Frontend tests (push) Successful in 2m11s
Project CI / Native shell tests (push) Successful in 6m11s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m0s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m19s
Project CI / AI game creator shell web tests (push) Failing after 1m48s
Project CI / Repository checks (push) Successful in 2m33s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled

- installer-hooks.nsh 顶层收敛为 !define / Var / !macro:迁移逻辑改成 !macro AgcMigrateLegacyIdentity,由 !macro NSIS_HOOK_POSTINSTALL 在模板插入点(Section Install,addplugindir 之后)展开,避免 include 时 INSTALLMODE 未定义走错分支、nsis_tauri_utils 插件目录还没 add 导致 makensis 报 Plugin not found 中断
- 旧展示名改走运行期变量 $AgcLegacyIdentity(替换 Push/Pop + Function),去掉 section 上下文里不可用的 Return 提前返回,清理与补建语义保持不变
- 文件头补编译期位置纪律:模板在 installer.nsi 第 28 行 include 本文件,早于 !define INSTALLMODE / MAINBINARYNAME 与 !addplugindir,顶层只能放声明
- build-release.test.mjs 新增守卫用例:顶层不得出现 Function、插件调用、模板常量引用或裸可执行语句(对修复前形态命中 36 条,含第 68 行顶层插件调用)
- pitfalls 与 decision-log 记录该踩坑、根因、本机逐字复现与处理口径
- AGC 渠道安装身份隔离里程碑补 2026-09-29 钩子编译位置修正小节与本机探针取证
This commit is contained in:
kdletters
2026-09-29 17:59:36 +08:00
parent 4a0f18e8a2
commit 30a190d183
5 changed files with 159 additions and 47 deletions
@@ -310,6 +310,79 @@ test('dev 渠道的 Windows 包注入改名迁移钩子,其它渠道与目标
});
});
/**
* Tauri 2.11 的 NSIS 模板在 `installer.nsi` 第 28 行 include 钩子文件,位置**早于**
* `!define INSTALLMODE` / `!define MAINBINARYNAME` 等模板常量,也早于
* `!addplugindir "${ADDITIONALPLUGINSPATH}"`(nsis_tauri_utils 插件目录)。
* 所以顶层只能放声明:顶层 Function 会在 include 处就编译(模板常量还是未定义的,
* makensis 静默替换成空文本),顶层调 `nsis_tauri_utils::*` 会直接 `Plugin not found`
* 并中断整个打包(2026-09-29 Jenkins 打包失败的形态)。可执行逻辑必须待在 !macro 里,
* 等模板的 `!insertmacro NSIS_HOOK_*`(Section Install,addplugindir 之后)展开。
*/
test('安装钩子的可执行逻辑必须全在 !macro 内,顶层不得引用模板常量或插件', () => {
const templateConstants = [
'MAINBINARYNAME',
'PRODUCTNAME',
'MANUFACTURER',
'INSTALLMODE',
'BUNDLEID',
'ADDITIONALPLUGINSPATH',
'VERSION',
'OUTFILE',
];
const hooksPath = createChannelConfig('dev', windowsTarget).bundle.windows
.nsis.installerHooks;
const offenders = [];
let macroDepth = 0;
for (const [index, rawLine] of readFileSync(hooksPath, 'utf8')
.split('\n')
.entries()) {
const line = rawLine.trim();
if (line === '' || line.startsWith(';')) continue;
if (/^!macroend\b/u.test(line)) {
macroDepth = Math.max(0, macroDepth - 1);
continue;
}
if (/^!macro\b/u.test(line)) {
macroDepth += 1;
continue;
}
if (macroDepth > 0) continue;
const where = `第 ${index + 1} 行`;
if (/^Function(?:End)?\b/u.test(line)) {
offenders.push(`${where} 顶层定义 Function:函数体在 include 处就编译`);
continue;
}
if (line.includes('::')) {
offenders.push(`${where} 顶层调用插件:此时 addplugindir 还没执行`);
continue;
}
const usedConstant = templateConstants.find((name) =>
line.includes(`\${${name}}`),
);
if (usedConstant) {
offenders.push(`${where} 顶层引用模板常量 \${${usedConstant}}`);
continue;
}
if (
!/^(?:!define\b|!macro\b|!macroend\b|!ifn?def\b|!if\b|!else\b|!endif\b|!undef\b|Var\b)/u.test(
line,
)
) {
offenders.push(`${where} 顶层出现可执行语句:${line}`);
}
}
assert.deepEqual(
offenders,
[],
`installer-hooks.nsh 顶层只能放 !define / Var / !macro;` +
`模板在 installer.nsi 第 28 行 include 它,早于模板常量与 addplugindir,` +
`可执行逻辑一律写进 !macro,等 !insertmacro NSIS_HOOK_* 时再展开:\n` +
offenders.join('\n'),
);
});
/**
* RFC 7386(tauri-utils 用 `json_patch::merge`)语义:对象递归合并,数组整体替换。
* 这里按同样语义复现 Tauri CLI 的 `--config` 合并,用来守住"渠道配置不得丢窗口契约"。
@@ -15,71 +15,79 @@
;
; 该文件只注入 dev 渠道(改名发生在 dev 渠道)的 Windows 包;其它渠道的展示名从未变过,
; 不能拿这份旧身份表去删它们的安装。
;
; ⚠️ 编译期位置(改本文件前必读):Tauri 2.11 的 NSIS 模板在 installer.nsi 第 28 行
; `!include` 本文件,位置早于 `!define INSTALLMODE` / `!define MAINBINARYNAME` 等模板常量,
; 也早于 `!addplugindir "${ADDITIONALPLUGINSPATH}"`(nsis_tauri_utils 插件目录)。所以顶层只能放
; !define / Var / !macro 这类声明:
; - 顶层引用 `${MAINBINARYNAME}`、`${PRODUCTNAME}` 会命中未定义常量(makensis warning 6000),
; 字符串被静默替换成空文本;
; - 顶层写 `!if "${INSTALLMODE}" == "currentUser"` 会走错分支(此时 INSTALLMODE 还是空值);
; - 顶层调用 nsis_tauri_utils::* 会直接 `Plugin not found`,打包在 makensis 这一步整条中断。
; 可执行逻辑一律写进 !macro:宏体在模板的 `!insertmacro NSIS_HOOK_*` 处展开(Section Install,
; 位于 addplugindir 之后),那时模板常量与插件都已就绪。
; 旧展示名表:以后再改展示名,必须在这里追加旧名,否则升级后旧快捷方式会继续指向旧安装。
!define AGC_LEGACY_IDENTITY_1 "陶泥儿"
!define AGC_LEGACY_IDENTITY_2 "Genarrative AI Game Creator"
Var AgcLegacyIdentity
Var AgcLegacyInstallDir
Var AgcMigratedLegacyDesktopShortcut
; 清理一个旧身份。调用方把旧展示名压栈(Push),本函数不返回错误。
; 清理一个旧身份。调用方先把旧展示名写进 $AgcLegacyIdentity,再 !insertmacro 本宏;宏不返回错误。
; 只在「旧展示名目录下确实存在我们的主程序」时才动手,避免误删同名目录;
; 当前安装目录 $INSTDIR 永不进入删除路径。
Function AgcMigrateLegacyIdentity
Pop $R0
!macro AgcMigrateLegacyIdentity
StrCpy $AgcLegacyInstallDir ""
${If} ${FileExists} "$LOCALAPPDATA\$R0\${MAINBINARYNAME}.exe"
StrCpy $AgcLegacyInstallDir "$LOCALAPPDATA\$R0"
${ElseIf} ${FileExists} "$PROGRAMFILES64\$R0\${MAINBINARYNAME}.exe"
StrCpy $AgcLegacyInstallDir "$PROGRAMFILES64\$R0"
${Else}
Return
${If} ${FileExists} "$LOCALAPPDATA\$AgcLegacyIdentity\${MAINBINARYNAME}.exe"
StrCpy $AgcLegacyInstallDir "$LOCALAPPDATA\$AgcLegacyIdentity"
${ElseIf} ${FileExists} "$PROGRAMFILES64\$AgcLegacyIdentity\${MAINBINARYNAME}.exe"
StrCpy $AgcLegacyInstallDir "$PROGRAMFILES64\$AgcLegacyIdentity"
${EndIf}
${If} $AgcLegacyInstallDir == $INSTDIR
Return
${EndIf}
; 桌面图标:只有目标正好指向旧目录的主程序时才删,避免误删同名的用户快捷方式。
!insertmacro IsShortcutTarget "$DESKTOP\$R0.lnk" "$AgcLegacyInstallDir\${MAINBINARYNAME}.exe"
Pop $0
${If} $0 = 1
StrCpy $AgcMigratedLegacyDesktopShortcut 1
!insertmacro UnpinShortcut "$DESKTOP\$R0.lnk"
Delete "$DESKTOP\$R0.lnk"
${EndIf}
; 开始菜单图标:必须先删掉,否则它会在当前身份图标补建后继续指向旧 exe。
!insertmacro IsShortcutTarget "$SMPROGRAMS\$R0.lnk" "$AgcLegacyInstallDir\${MAINBINARYNAME}.exe"
Pop $0
${If} $0 = 1
!insertmacro UnpinShortcut "$SMPROGRAMS\$R0.lnk"
Delete "$SMPROGRAMS\$R0.lnk"
${EndIf}
; 旧安装目录与卸载项整体清理:旧 exe 留着就会继续把新版本装进新目录。
; 先结束残留的旧实例(同名主程序):更新路径下调用方通常已退出,这里兜住手动启动的情况。
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
${If} $AgcLegacyInstallDir != ""
${AndIf} $AgcLegacyInstallDir != $INSTDIR
; 桌面图标:只有目标正好指向旧目录的主程序时才删,避免误删同名的用户快捷方式。
!insertmacro IsShortcutTarget "$DESKTOP\$AgcLegacyIdentity.lnk" "$AgcLegacyInstallDir\${MAINBINARYNAME}.exe"
Pop $0
!else
nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe"
${If} $0 = 1
StrCpy $AgcMigratedLegacyDesktopShortcut 1
!insertmacro UnpinShortcut "$DESKTOP\$AgcLegacyIdentity.lnk"
Delete "$DESKTOP\$AgcLegacyIdentity.lnk"
${EndIf}
; 开始菜单图标:必须先删掉,否则它会在当前身份图标补建后继续指向旧 exe。
!insertmacro IsShortcutTarget "$SMPROGRAMS\$AgcLegacyIdentity.lnk" "$AgcLegacyInstallDir\${MAINBINARYNAME}.exe"
Pop $0
!endif
RMDir /r "$AgcLegacyInstallDir"
; 卸载项按展示名登记,旧名与当前名不同,必须按旧名清掉,否则「应用和功能」里会留下孤儿条目。
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\$R0"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$R0"
DeleteRegKey HKCU "Software\${MANUFACTURER}\$R0"
FunctionEnd
${If} $0 = 1
!insertmacro UnpinShortcut "$SMPROGRAMS\$AgcLegacyIdentity.lnk"
Delete "$SMPROGRAMS\$AgcLegacyIdentity.lnk"
${EndIf}
; 旧安装目录与卸载项整体清理:旧 exe 留着就会继续把新版本装进新目录。
; 先结束残留的旧实例(同名主程序):更新路径下调用方通常已退出,这里兜住手动启动的情况。
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
Pop $0
!else
nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe"
Pop $0
!endif
RMDir /r "$AgcLegacyInstallDir"
; 卸载项按展示名登记,旧名与当前名不同,必须按旧名清掉,否则「应用和功能」里会留下孤儿条目。
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\$AgcLegacyIdentity"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$AgcLegacyIdentity"
DeleteRegKey HKCU "Software\${MANUFACTURER}\$AgcLegacyIdentity"
${EndIf}
!macroend
!macro NSIS_HOOK_POSTINSTALL
Push "${AGC_LEGACY_IDENTITY_1}"
Call AgcMigrateLegacyIdentity
Push "${AGC_LEGACY_IDENTITY_2}"
Call AgcMigrateLegacyIdentity
StrCpy $AgcLegacyIdentity "${AGC_LEGACY_IDENTITY_1}"
!insertmacro AgcMigrateLegacyIdentity
StrCpy $AgcLegacyIdentity "${AGC_LEGACY_IDENTITY_2}"
!insertmacro AgcMigrateLegacyIdentity
; 迁移过的机器补建当前身份的快捷方式(更新模式下模板不会创建)。
StrCpy $R0 "$INSTDIR\${MAINBINARYNAME}.exe"