修复 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
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user