From 5aec74653bd6df6009103b7baf32d50296e23b41 Mon Sep 17 00:00:00 2001 From: baiqing Date: Wed, 6 May 2026 10:41:07 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(windows):=20NSIS=20=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=8C=85=E6=B3=A8=E5=86=8C=20OpenLess=20TSF=20DLL=20=E2=80=94?= =?UTF-8?q?=20=E4=BF=AE"=E8=BE=93=E5=85=A5=E6=B3=95=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=EF=BC=9A=E4=B8=8D=E5=8F=AF=E7=94=A8"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 现象 1.2.15 起在 Settings 页"Windows 输入法后端"出现 Pill「不可用」。`get_windows_ime_status()` 查 HKLM 注册表四件套(COM CLSID + TSF LanguageProfile + 三个 Category),缺一即报 NotInstalled。 ## 根因 - MSI 通过 wix/openless-ime.wxs 的 RegisterOpenLessIme* CustomAction 跑 regsvr32 → 注册齐全。 - NSIS 包既没把 OpenLessIme.dll 打进去(wxs.componentRefs 是 MSI-only),也没 hook 跑 regsvr32 → 用 setup.exe 安装的用户、走 updater 自动升级的存量用户全部命中"不可用"。 - 由于 updater latest-windows-x86_64.json 是 NSIS pass 写的,老 MSI 用户每次更新后也 会被替换为没 TSF 的 NSIS 版本。 ## 修复 - 加 src-tauri/installer.nsh:NSIS_HOOK_POSTINSTALL/PREUNINSTALL 跑 regsvr32 注册/反注册。 - bundle.resources map 把 OpenLessIme.dll 拷贝到 $INSTDIR\tsf-ime\OpenLessIme.dll (target 路径避开 wxs File 已声明的 windows-ime\x64\,避免 MSI component 路径冲突)。 - bundle.windows.nsis.installMode = "perMachine",让 NSIS 以管理员身份运行(写 HKLM 必需)。 - CI 在 build native dll 后把真 dll 复制到 src-tauri/openless-ime-payload/x64/OpenLessIme.dll; 仓库 commit 0 字节占位文件,让本地 mac 端 tauri build 也能 resolve resources 路径。 - build-mac.sh 与 Linux build 步骤加 --config '{"bundle":{"resources":{}}}' 跳过 Windows-only 的 dll 占位,避免污染 mac .app / Linux .deb/.rpm/.AppImage。 ## Trade-off - NSIS perMachine 升级需要 UAC,老 currentUser 用户首次升级时多一次 UAC 提示。 - MSI 包里 dll 会有两份(wxs File + bundle.resources),磁盘冗余 ~几百 KB,无功能影响。 --- .github/workflows/release-tauri.yml | 15 ++++++++-- openless-all/app/scripts/build-mac.sh | 4 ++- openless-all/app/src-tauri/installer.nsh | 29 +++++++++++++++++++ .../openless-ime-payload/x64/OpenLessIme.dll | 0 openless-all/app/src-tauri/tauri.conf.json | 7 +++++ 5 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 openless-all/app/src-tauri/installer.nsh create mode 100644 openless-all/app/src-tauri/openless-ime-payload/x64/OpenLessIme.dll diff --git a/.github/workflows/release-tauri.yml b/.github/workflows/release-tauri.yml index 4f87aaea..55e5ac4c 100644 --- a/.github/workflows/release-tauri.yml +++ b/.github/workflows/release-tauri.yml @@ -214,6 +214,14 @@ jobs: Write-Host "[ok] built $dll (exported $($t.EnvName))" } + # bundle.resources 引用的是 src-tauri/openless-ime-payload/x64/OpenLessIme.dll + # (仓库里 commit 的是 0 字节占位让 mac 本地 build 也能 resolve)。 + # Windows CI 这里用真 x64 dll 覆盖占位,让 NSIS / MSI 包装的是真 dll。 + $payloadDir = Join-Path $appRoot 'src-tauri\openless-ime-payload\x64' + New-Item -ItemType Directory -Force -Path $payloadDir | Out-Null + Copy-Item -Force -Path $env:OPENLESS_IME_DLL_X64 -Destination (Join-Path $payloadDir 'OpenLessIme.dll') + Write-Host "[ok] copied real x64 dll into bundle.resources payload path" + # ── Windows tauri build:保持 bash shell,因为 PowerShell 调外部命令 # 会把 '{"bundle":...}' 的内部双引号吃掉、让 tauri 收到无效 JSON。 # 用 set +e + GITHUB_ENV 把 exit code 传到下一步给 Repair 判断。 @@ -299,6 +307,7 @@ jobs: Write-Host "[ok] MSI rebuilt at $msiPath" # ── Linux:产 deb / rpm / AppImage ── + # bundle.resources 里的 Windows TSF DLL 占位对 Linux 包没意义,用空 map 覆盖跳过。 - name: Build (Linux) if: matrix.platform == 'ubuntu-22.04' shell: bash @@ -308,9 +317,11 @@ jobs: TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then - npm run tauri -- build --bundles deb,rpm,appimage --config '{"bundle":{"createUpdaterArtifacts":true}}' + npm run tauri -- build --bundles deb,rpm,appimage \ + --config '{"bundle":{"resources":{},"createUpdaterArtifacts":true}}' else - npm run tauri -- build --bundles deb,rpm,appimage + npm run tauri -- build --bundles deb,rpm,appimage \ + --config '{"bundle":{"resources":{}}}' fi - name: Disambiguate macOS updater bundle filename diff --git a/openless-all/app/scripts/build-mac.sh b/openless-all/app/scripts/build-mac.sh index 63b7c94e..3a51ea81 100755 --- a/openless-all/app/scripts/build-mac.sh +++ b/openless-all/app/scripts/build-mac.sh @@ -25,7 +25,9 @@ else fi echo "▶ tauri build" -TAURI_BUILD_ARGS=(build) +# bundle.resources 里有 Windows TSF DLL(仅 NSIS/MSI 需要),mac 端用空 map 覆盖避免 +# 把 0 字节占位 OpenLessIme.dll 打进 .app/Contents/Resources。 +TAURI_BUILD_ARGS=(build --config '{"bundle":{"resources":{}}}') if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ] || [ -n "${TAURI_SIGNING_PRIVATE_KEY_PATH:-}" ]; then TAURI_BUILD_ARGS+=(--config '{"bundle":{"createUpdaterArtifacts":true}}') fi diff --git a/openless-all/app/src-tauri/installer.nsh b/openless-all/app/src-tauri/installer.nsh new file mode 100644 index 00000000..e3eaf310 --- /dev/null +++ b/openless-all/app/src-tauri/installer.nsh @@ -0,0 +1,29 @@ +; NSIS installer hook:注册 / 反注册 OpenLess TSF 输入法 DLL。 +; +; 背景:MSI 包通过 wix/openless-ime.wxs 的 CustomAction 跑 regsvr32,可正常注册到 +; HKLM 下的 TSF 注册表四件套;NSIS 包没有等价钩子 → NSIS 安装的用户在 +; "设置 → 权限"页面看到 "Windows 输入法后端:不可用"。 +; +; 这里的 hook 在 NSIS 安装/卸载流程里调 regsvr32 把 OpenLessIme.dll 注册到 HKLM。 +; bundle.resources 把 DLL 拷到 $INSTDIR\tsf-ime\OpenLessIme.dll +; (resources map 的 target 必须避开 wxs fragment 已声明的 windows-ime\x64\ +; 路径,否则 MSI 包里同一路径会被两个 component 占用)。 +; tauri.conf.json 的 nsis.installMode = "perMachine" 让 NSIS 以管理员身份运行 +; (写 HKLM 必需)。 +; +; regsvr32 失败时不阻塞安装:用户仍可以靠 SendInput / 粘贴兜底完成上屏。 + +!macro NSIS_HOOK_POSTINSTALL + DetailPrint "Registering OpenLess TSF IME ..." + nsExec::ExecToLog '"$SYSDIR\regsvr32.exe" /s "$INSTDIR\tsf-ime\OpenLessIme.dll"' + Pop $0 + ${If} $0 != 0 + DetailPrint "OpenLess TSF IME registration failed (exit $0); fallback insertion paths still work." + ${EndIf} +!macroend + +!macro NSIS_HOOK_PREUNINSTALL + DetailPrint "Unregistering OpenLess TSF IME ..." + nsExec::ExecToLog '"$SYSDIR\regsvr32.exe" /s /u "$INSTDIR\tsf-ime\OpenLessIme.dll"' + Pop $0 +!macroend diff --git a/openless-all/app/src-tauri/openless-ime-payload/x64/OpenLessIme.dll b/openless-all/app/src-tauri/openless-ime-payload/x64/OpenLessIme.dll new file mode 100644 index 00000000..e69de29b diff --git a/openless-all/app/src-tauri/tauri.conf.json b/openless-all/app/src-tauri/tauri.conf.json index 438315e7..031f75e5 100644 --- a/openless-all/app/src-tauri/tauri.conf.json +++ b/openless-all/app/src-tauri/tauri.conf.json @@ -79,6 +79,9 @@ "icons/icon.icns", "icons/icon.ico" ], + "resources": { + "openless-ime-payload/x64/OpenLessIme.dll": "tsf-ime/OpenLessIme.dll" + }, "macOS": { "minimumSystemVersion": "12.0", "infoPlist": "Info.plist", @@ -93,6 +96,10 @@ "OpenLessImeDllX64Component", "OpenLessImeDllX86Component" ] + }, + "nsis": { + "installMode": "perMachine", + "installerHooks": "installer.nsh" } } }, From c52658b08b18c55220dc816730200cc19142b535 Mon Sep 17 00:00:00 2001 From: baiqing Date: Wed, 6 May 2026 10:47:03 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(windows):=20NSIS=20=E4=B9=9F=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=2032-bit=20TSF=20DLL=20=E2=80=94=20=E4=BF=AE=20P1=20r?= =?UTF-8?q?eview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review 发现:windows_ime_profile.rs::inspect_windows_ime_registration() 同时 用 KEY_WOW64_64KEY 和 KEY_WOW64_32KEY 两次检查 HKLM\Software\Classes\CLSID\{...}\ InprocServer32,少了任一边就返回 RegistrationBroken("OpenLess 32-bit COM registration is missing")。MSI 流程通过 wxs 同时跑了 64-bit 和 SysWOW64\regsvr32 注册两份;之前的 NSIS hook 只注册了 x64,所以 NSIS / updater 用户依然在 Settings 页看到「不可用」。 修复: - bundle.resources 拆出 x64 / x86 两份 dll target;x64 → tsf-ime\x64,x86 → tsf-ime\x86 - installer.nsh PostInstall/PreUninstall 增加 x86 分支, x86 用 $WINDIR\SysWOW64\regsvr32.exe(写 KEY_WOW64_32KEY 必需) - CI Build Windows IME native DLLs 步骤把复制 dll 移到循环里,x64 + x86 都覆盖 仓库占位 - 仓库新增 0 字节 x86 占位 dll,让本地 mac/linux 构建也能 resolve resources --- .github/workflows/release-tauri.yml | 18 ++++++------ openless-all/app/src-tauri/installer.nsh | 28 +++++++++++++++---- .../openless-ime-payload/x86/OpenLessIme.dll | 0 openless-all/app/src-tauri/tauri.conf.json | 3 +- 4 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 openless-all/app/src-tauri/openless-ime-payload/x86/OpenLessIme.dll diff --git a/.github/workflows/release-tauri.yml b/.github/workflows/release-tauri.yml index 55e5ac4c..c5f4099d 100644 --- a/.github/workflows/release-tauri.yml +++ b/.github/workflows/release-tauri.yml @@ -212,15 +212,17 @@ jobs: } "$($t.EnvName)=$dll" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 Write-Host "[ok] built $dll (exported $($t.EnvName))" - } - # bundle.resources 引用的是 src-tauri/openless-ime-payload/x64/OpenLessIme.dll - # (仓库里 commit 的是 0 字节占位让 mac 本地 build 也能 resolve)。 - # Windows CI 这里用真 x64 dll 覆盖占位,让 NSIS / MSI 包装的是真 dll。 - $payloadDir = Join-Path $appRoot 'src-tauri\openless-ime-payload\x64' - New-Item -ItemType Directory -Force -Path $payloadDir | Out-Null - Copy-Item -Force -Path $env:OPENLESS_IME_DLL_X64 -Destination (Join-Path $payloadDir 'OpenLessIme.dll') - Write-Host "[ok] copied real x64 dll into bundle.resources payload path" + # bundle.resources 引用的是 src-tauri/openless-ime-payload/{x64,x86}/OpenLessIme.dll + # (仓库里 commit 的是 0 字节占位让 mac 本地 build 也能 resolve)。 + # 这里用真 dll 覆盖占位,让 NSIS / MSI 都装上真文件;NSIS hook 的 regsvr32 + # 同时会把 64 / 32 位 COM 注册到 HKLM\Software\Classes\CLSID 的 + # KEY_WOW64_64KEY / KEY_WOW64_32KEY 两侧(windows_ime_profile.rs 都会查)。 + $payloadDir = Join-Path $appRoot "src-tauri\openless-ime-payload\$($t.Folder)" + New-Item -ItemType Directory -Force -Path $payloadDir | Out-Null + Copy-Item -Force -Path $dll -Destination (Join-Path $payloadDir 'OpenLessIme.dll') + Write-Host "[ok] copied real $($t.Folder) dll into bundle.resources payload path" + } # ── Windows tauri build:保持 bash shell,因为 PowerShell 调外部命令 # 会把 '{"bundle":...}' 的内部双引号吃掉、让 tauri 收到无效 JSON。 diff --git a/openless-all/app/src-tauri/installer.nsh b/openless-all/app/src-tauri/installer.nsh index e3eaf310..cefbcea6 100644 --- a/openless-all/app/src-tauri/installer.nsh +++ b/openless-all/app/src-tauri/installer.nsh @@ -5,25 +5,41 @@ ; "设置 → 权限"页面看到 "Windows 输入法后端:不可用"。 ; ; 这里的 hook 在 NSIS 安装/卸载流程里调 regsvr32 把 OpenLessIme.dll 注册到 HKLM。 -; bundle.resources 把 DLL 拷到 $INSTDIR\tsf-ime\OpenLessIme.dll +; bundle.resources 把 x64 / x86 DLL 拷到 $INSTDIR\tsf-ime\{x64,x86}\OpenLessIme.dll ; (resources map 的 target 必须避开 wxs fragment 已声明的 windows-ime\x64\ ; 路径,否则 MSI 包里同一路径会被两个 component 占用)。 ; tauri.conf.json 的 nsis.installMode = "perMachine" 让 NSIS 以管理员身份运行 ; (写 HKLM 必需)。 ; +; 必须同时注册 x64 + x86 两份 dll:windows_ime_profile.rs 的 +; inspect_windows_ime_registration() 会用 KEY_WOW64_64KEY 和 KEY_WOW64_32KEY 两次 +; 检查 HKLM CLSID InprocServer32,少了任何一边都会被判 RegistrationBroken。 +; - x64 用 $SYSDIR\regsvr32.exe (System32 = 64-bit on x64 Windows) +; - x86 用 $WINDIR\SysWOW64\regsvr32.exe (32-bit regsvr32 → 写 KEY_WOW64_32KEY) +; ; regsvr32 失败时不阻塞安装:用户仍可以靠 SendInput / 粘贴兜底完成上屏。 !macro NSIS_HOOK_POSTINSTALL - DetailPrint "Registering OpenLess TSF IME ..." - nsExec::ExecToLog '"$SYSDIR\regsvr32.exe" /s "$INSTDIR\tsf-ime\OpenLessIme.dll"' + DetailPrint "Registering OpenLess TSF IME (x64) ..." + nsExec::ExecToLog '"$SYSDIR\regsvr32.exe" /s "$INSTDIR\tsf-ime\x64\OpenLessIme.dll"' + Pop $0 + ${If} $0 != 0 + DetailPrint "OpenLess TSF IME x64 registration failed (exit $0); fallback insertion paths still work." + ${EndIf} + + DetailPrint "Registering OpenLess TSF IME (x86) ..." + nsExec::ExecToLog '"$WINDIR\SysWOW64\regsvr32.exe" /s "$INSTDIR\tsf-ime\x86\OpenLessIme.dll"' Pop $0 ${If} $0 != 0 - DetailPrint "OpenLess TSF IME registration failed (exit $0); fallback insertion paths still work." + DetailPrint "OpenLess TSF IME x86 registration failed (exit $0); fallback insertion paths still work." ${EndIf} !macroend !macro NSIS_HOOK_PREUNINSTALL - DetailPrint "Unregistering OpenLess TSF IME ..." - nsExec::ExecToLog '"$SYSDIR\regsvr32.exe" /s /u "$INSTDIR\tsf-ime\OpenLessIme.dll"' + DetailPrint "Unregistering OpenLess TSF IME (x86) ..." + nsExec::ExecToLog '"$WINDIR\SysWOW64\regsvr32.exe" /s /u "$INSTDIR\tsf-ime\x86\OpenLessIme.dll"' + Pop $0 + DetailPrint "Unregistering OpenLess TSF IME (x64) ..." + nsExec::ExecToLog '"$SYSDIR\regsvr32.exe" /s /u "$INSTDIR\tsf-ime\x64\OpenLessIme.dll"' Pop $0 !macroend diff --git a/openless-all/app/src-tauri/openless-ime-payload/x86/OpenLessIme.dll b/openless-all/app/src-tauri/openless-ime-payload/x86/OpenLessIme.dll new file mode 100644 index 00000000..e69de29b diff --git a/openless-all/app/src-tauri/tauri.conf.json b/openless-all/app/src-tauri/tauri.conf.json index 031f75e5..f61d699c 100644 --- a/openless-all/app/src-tauri/tauri.conf.json +++ b/openless-all/app/src-tauri/tauri.conf.json @@ -80,7 +80,8 @@ "icons/icon.ico" ], "resources": { - "openless-ime-payload/x64/OpenLessIme.dll": "tsf-ime/OpenLessIme.dll" + "openless-ime-payload/x64/OpenLessIme.dll": "tsf-ime/x64/OpenLessIme.dll", + "openless-ime-payload/x86/OpenLessIme.dll": "tsf-ime/x86/OpenLessIme.dll" }, "macOS": { "minimumSystemVersion": "12.0", From 81fa80da693bb29ac4a2dfb117ba92fac711352e Mon Sep 17 00:00:00 2001 From: baiqing Date: Wed, 6 May 2026 10:52:12 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(windows):=20NSIS=20x64=20dll=20?= =?UTF-8?q?=E7=94=A8=20Sysnative=20=E7=BB=95=E8=BF=87=20WOW64=20=E9=87=8D?= =?UTF-8?q?=E5=AE=9A=E5=90=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NSIS installer 是 32-bit 进程,进程内的 $SYSDIR / $WINDIR\System32 都因 WOW64 文件系统重定向变成 $WINDIR\SysWOW64,跑的是 32-bit regsvr32 → x64 dll 被注册到 HKLM\Software\Wow6432Node\Classes\CLSID(KEY_WOW64_32KEY 视图)。 但 windows_ime_profile.rs 用 KEY_WOW64_64KEY 校验时只看 HKLM\Software\Classes \CLSID,永远查不到 → 仍判 RegistrationBroken("OpenLess COM registration is missing")。 修复:x64 改用 $WINDIR\Sysnative\regsvr32.exe — 这是 32-bit 进程访问真 64-bit System32 的专用 alias,写入会落在 KEY_WOW64_64KEY 视图。x86 保持 SysWOW64 不变(确实需要 32-bit regsvr32 写 Wow6432Node)。 --- openless-all/app/src-tauri/installer.nsh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openless-all/app/src-tauri/installer.nsh b/openless-all/app/src-tauri/installer.nsh index cefbcea6..e6675c39 100644 --- a/openless-all/app/src-tauri/installer.nsh +++ b/openless-all/app/src-tauri/installer.nsh @@ -14,14 +14,21 @@ ; 必须同时注册 x64 + x86 两份 dll:windows_ime_profile.rs 的 ; inspect_windows_ime_registration() 会用 KEY_WOW64_64KEY 和 KEY_WOW64_32KEY 两次 ; 检查 HKLM CLSID InprocServer32,少了任何一边都会被判 RegistrationBroken。 -; - x64 用 $SYSDIR\regsvr32.exe (System32 = 64-bit on x64 Windows) -; - x86 用 $WINDIR\SysWOW64\regsvr32.exe (32-bit regsvr32 → 写 KEY_WOW64_32KEY) +; +; ⚠️ NSIS installer 自身是 32-bit 进程,会触发 WOW64 文件系统重定向: +; - 32-bit 进程里的 $SYSDIR / $WINDIR\System32 都被重定向到 $WINDIR\SysWOW64, +; 运行的是 32-bit regsvr32 → 注册结果落在 HKLM\Software\Wow6432Node(即 +; KEY_WOW64_32KEY 视图),Rust 端 KEY_WOW64_64KEY 查不到 → 仍判"不可用"。 +; - 32-bit 进程访问真正的 64-bit System32 必须走 $WINDIR\Sysnative 这个 alias。 +; 因此: +; - x64 dll → $WINDIR\Sysnative\regsvr32.exe → 写 KEY_WOW64_64KEY 视图 +; - x86 dll → $WINDIR\SysWOW64\regsvr32.exe → 写 KEY_WOW64_32KEY 视图 ; ; regsvr32 失败时不阻塞安装:用户仍可以靠 SendInput / 粘贴兜底完成上屏。 !macro NSIS_HOOK_POSTINSTALL DetailPrint "Registering OpenLess TSF IME (x64) ..." - nsExec::ExecToLog '"$SYSDIR\regsvr32.exe" /s "$INSTDIR\tsf-ime\x64\OpenLessIme.dll"' + nsExec::ExecToLog '"$WINDIR\Sysnative\regsvr32.exe" /s "$INSTDIR\tsf-ime\x64\OpenLessIme.dll"' Pop $0 ${If} $0 != 0 DetailPrint "OpenLess TSF IME x64 registration failed (exit $0); fallback insertion paths still work." @@ -40,6 +47,6 @@ nsExec::ExecToLog '"$WINDIR\SysWOW64\regsvr32.exe" /s /u "$INSTDIR\tsf-ime\x86\OpenLessIme.dll"' Pop $0 DetailPrint "Unregistering OpenLess TSF IME (x64) ..." - nsExec::ExecToLog '"$SYSDIR\regsvr32.exe" /s /u "$INSTDIR\tsf-ime\x64\OpenLessIme.dll"' + nsExec::ExecToLog '"$WINDIR\Sysnative\regsvr32.exe" /s /u "$INSTDIR\tsf-ime\x64\OpenLessIme.dll"' Pop $0 !macroend