fix: register Windows IME from NSIS installer
This commit is contained in:
1 parent
c85b1bd4c6
commit
753236dd55
5 files changed
+297
No files matched your search
@@ -298,6 +298,18 @@ jobs:
|
||||
}
|
||||
Write-Host "[ok] MSI rebuilt at $msiPath"
|
||||
|
||||
- name: Verify Windows installers register TSF IME
|
||||
if: matrix.platform == 'windows-latest'
|
||||
shell: pwsh
|
||||
working-directory: 'openless-all/app'
|
||||
run: |
|
||||
$nsis = Get-ChildItem "src-tauri\target\release\bundle\nsis\*.exe" -ErrorAction Stop | Select-Object -First 1
|
||||
$msi = Get-ChildItem "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction Stop | Select-Object -First 1
|
||||
if (-not $nsis) { throw "NSIS installer not found." }
|
||||
if (-not $msi) { throw "MSI installer not found." }
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $nsis.FullName -InstallerKind nsis
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $msi.FullName -InstallerKind msi
|
||||
|
||||
# ── Linux:产 deb / rpm / AppImage ──
|
||||
- name: Build (Linux)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InstallerPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("nsis", "msi")]
|
||||
[string]$InstallerKind,
|
||||
[switch]$SkipUninstall
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$TextServiceClsid = "{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}"
|
||||
$ProfileGuid = "{9B5F5E04-23F6-47DA-9A26-D221F6C3F02E}"
|
||||
$LangId = "0x00000804"
|
||||
$KeyboardCategoryGuid = "{34745C63-B2F0-4784-8B67-5E12C8701A31}"
|
||||
$ImmersiveCategoryGuid = "{13A016DF-560B-46CD-947A-4C3AF1E0E35D}"
|
||||
$SystrayCategoryGuid = "{25504FB4-7BAB-4BC1-9C69-CF81890F0EF5}"
|
||||
|
||||
# Keep this script aligned with the backend status check and the TSF IPC path
|
||||
# used by OpenLessImeSubmit-* named pipes.
|
||||
$ExpectedBackendKeys = @(
|
||||
"Software\Classes\CLSID\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\InprocServer32",
|
||||
"Software\WOW6432Node\Classes\CLSID\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\InprocServer32",
|
||||
"Software\Microsoft\CTF\TIP\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\LanguageProfile\0x00000804\{9B5F5E04-23F6-47DA-9A26-D221F6C3F02E}",
|
||||
"Software\Microsoft\CTF\TIP\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\Category\Category\{34745C63-B2F0-4784-8B67-5E12C8701A31}\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}",
|
||||
"Software\Microsoft\CTF\TIP\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\Category\Category\{13A016DF-560B-46CD-947A-4C3AF1E0E35D}\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}",
|
||||
"Software\Microsoft\CTF\TIP\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\Category\Category\{25504FB4-7BAB-4BC1-9C69-CF81890F0EF5}\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}"
|
||||
)
|
||||
|
||||
function Test-IsAdministrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Invoke-CheckedProcess {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
[string[]]$ArgumentList = @(),
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Label
|
||||
)
|
||||
|
||||
Write-Host "[run] $Label`: $FilePath $($ArgumentList -join ' ')"
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -Wait -PassThru
|
||||
if ($process.ExitCode -ne 0) {
|
||||
throw "$Label failed with exit code $($process.ExitCode)"
|
||||
}
|
||||
}
|
||||
|
||||
function Open-LocalMachineSubKey {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[Microsoft.Win32.RegistryView]$View,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SubKey
|
||||
)
|
||||
|
||||
$baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $View)
|
||||
try {
|
||||
return $baseKey.OpenSubKey($SubKey)
|
||||
} finally {
|
||||
$baseKey.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-RegistryKey {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[Microsoft.Win32.RegistryView]$View,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SubKey,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Label
|
||||
)
|
||||
|
||||
$key = Open-LocalMachineSubKey -View $View -SubKey $SubKey
|
||||
if ($null -eq $key) {
|
||||
throw "Missing $Label registry key ($View): HKLM\$SubKey"
|
||||
}
|
||||
$key.Close()
|
||||
Write-Host "[ok] $Label registry key present ($View)"
|
||||
}
|
||||
|
||||
function Get-DefaultRegistryValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[Microsoft.Win32.RegistryView]$View,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SubKey,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Label
|
||||
)
|
||||
|
||||
$key = Open-LocalMachineSubKey -View $View -SubKey $SubKey
|
||||
if ($null -eq $key) {
|
||||
throw "Missing $Label registry key ($View): HKLM\$SubKey"
|
||||
}
|
||||
try {
|
||||
$value = [string]$key.GetValue("")
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
throw "$Label default registry value is empty ($View): HKLM\$SubKey"
|
||||
}
|
||||
return $value
|
||||
} finally {
|
||||
$key.Close()
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-OpenLessImeInstalled {
|
||||
$comKey = "Software\Classes\CLSID\$TextServiceClsid\InprocServer32"
|
||||
$x64Dll = Get-DefaultRegistryValue -View Registry64 -SubKey $comKey -Label "x64 COM"
|
||||
$x86Dll = Get-DefaultRegistryValue -View Registry32 -SubKey $comKey -Label "x86 COM"
|
||||
|
||||
foreach ($dll in @($x64Dll, $x86Dll)) {
|
||||
if (-not (Test-Path -LiteralPath $dll -PathType Leaf)) {
|
||||
throw "Registered IME DLL path does not exist: $dll"
|
||||
}
|
||||
}
|
||||
|
||||
$installRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $x64Dll))
|
||||
$expectedX64 = Join-Path $installRoot "windows-ime\x64\OpenLessIme.dll"
|
||||
$expectedX86 = Join-Path $installRoot "windows-ime\x86\OpenLessIme.dll"
|
||||
if ($x64Dll -ne $expectedX64) {
|
||||
throw "x64 COM DLL path points outside the installed IME directory. Expected '$expectedX64', got '$x64Dll'"
|
||||
}
|
||||
if ($x86Dll -ne $expectedX86) {
|
||||
throw "x86 COM DLL path points outside the installed IME directory. Expected '$expectedX86', got '$x86Dll'"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $installRoot "openless.exe") -PathType Leaf)) {
|
||||
throw "Installed OpenLess executable not found under $installRoot"
|
||||
}
|
||||
|
||||
Assert-RegistryKey -View Registry64 -SubKey "Software\Microsoft\CTF\TIP\$TextServiceClsid\LanguageProfile\$LangId\$ProfileGuid" -Label "TSF language profile"
|
||||
Assert-RegistryKey -View Registry64 -SubKey "Software\Microsoft\CTF\TIP\$TextServiceClsid\Category\Category\$KeyboardCategoryGuid\$TextServiceClsid" -Label "TSF keyboard category"
|
||||
Assert-RegistryKey -View Registry64 -SubKey "Software\Microsoft\CTF\TIP\$TextServiceClsid\Category\Category\$ImmersiveCategoryGuid\$TextServiceClsid" -Label "TSF immersive category"
|
||||
Assert-RegistryKey -View Registry64 -SubKey "Software\Microsoft\CTF\TIP\$TextServiceClsid\Category\Category\$SystrayCategoryGuid\$TextServiceClsid" -Label "TSF systray category"
|
||||
|
||||
foreach ($key in $ExpectedBackendKeys) {
|
||||
Write-Host "[trace] backend-required key: HKLM\$key"
|
||||
}
|
||||
|
||||
Write-Host "[ok] Windows IME backend would report installed"
|
||||
return $installRoot
|
||||
}
|
||||
|
||||
function Uninstall-OpenLess {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InstallRoot
|
||||
)
|
||||
|
||||
if ($InstallerKind -eq "nsis") {
|
||||
$uninstaller = Join-Path $InstallRoot "uninstall.exe"
|
||||
if (-not (Test-Path -LiteralPath $uninstaller -PathType Leaf)) {
|
||||
throw "NSIS uninstaller not found: $uninstaller"
|
||||
}
|
||||
Invoke-CheckedProcess -FilePath $uninstaller -ArgumentList @("/S") -Label "NSIS uninstall"
|
||||
} else {
|
||||
Invoke-CheckedProcess -FilePath "msiexec.exe" -ArgumentList @("/x", $InstallerPath, "/qn", "/norestart") -Label "MSI uninstall"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-IsAdministrator)) {
|
||||
throw "Windows IME install smoke must run from an elevated Administrator PowerShell."
|
||||
}
|
||||
|
||||
$InstallerPath = (Resolve-Path -LiteralPath $InstallerPath).Path
|
||||
if ($InstallerKind -eq "nsis") {
|
||||
Invoke-CheckedProcess -FilePath $InstallerPath -ArgumentList @("/S", "/AllUsers") -Label "NSIS install"
|
||||
} else {
|
||||
Invoke-CheckedProcess -FilePath "msiexec.exe" -ArgumentList @("/i", $InstallerPath, "/qn", "/norestart") -Label "MSI install"
|
||||
}
|
||||
|
||||
$installRoot = Assert-OpenLessImeInstalled
|
||||
if (-not $SkipUninstall) {
|
||||
Uninstall-OpenLess -InstallRoot $installRoot
|
||||
}
|
||||
@@ -5,9 +5,12 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
||||
const appRoot = join(scriptsDir, "..");
|
||||
const repoRoot = join(appRoot, "..", "..");
|
||||
const scriptPath = join(scriptsDir, "windows-package-msvc.ps1");
|
||||
const launcherPath = join(scriptsDir, "windows-package-msvc.cmd");
|
||||
const ciWorkflowPath = join(repoRoot, ".github", "workflows", "release-tauri.yml");
|
||||
const imeBuildPath = join(scriptsDir, "windows-ime-build.ps1");
|
||||
const imeInstallSmokePath = join(scriptsDir, "windows-ime-install-smoke.ps1");
|
||||
const imeRegisterPath = join(scriptsDir, "windows-ime-register.ps1");
|
||||
const imeUnregisterPath = join(scriptsDir, "windows-ime-unregister.ps1");
|
||||
const imeSolutionPath = join(appRoot, "windows-ime", "OpenLessIme.sln");
|
||||
@@ -15,11 +18,14 @@ const imeProjectPath = join(appRoot, "windows-ime", "OpenLessIme.vcxproj");
|
||||
const imeEditSessionPath = join(appRoot, "windows-ime", "src", "edit_session.cpp");
|
||||
const imeTextServicePath = join(appRoot, "windows-ime", "src", "text_service.cpp");
|
||||
const tauriConfigPath = join(appRoot, "src-tauri", "tauri.conf.json");
|
||||
const nsisHookPath = join(appRoot, "src-tauri", "nsis", "openless-ime-hooks.nsh");
|
||||
const wixFragmentPath = join(appRoot, "src-tauri", "wix", "openless-ime.wxs");
|
||||
|
||||
const script = readFileSync(scriptPath, "utf8");
|
||||
const launcher = readFileSync(launcherPath, "utf8");
|
||||
const ciWorkflow = readFileSync(ciWorkflowPath, "utf8");
|
||||
const imeBuild = readFileSync(imeBuildPath, "utf8");
|
||||
const imeInstallSmoke = readFileSync(imeInstallSmokePath, "utf8");
|
||||
const imeRegister = readFileSync(imeRegisterPath, "utf8");
|
||||
const imeUnregister = readFileSync(imeUnregisterPath, "utf8");
|
||||
const imeSolution = readFileSync(imeSolutionPath, "utf8");
|
||||
@@ -27,6 +33,7 @@ const imeProject = readFileSync(imeProjectPath, "utf8");
|
||||
const imeEditSession = readFileSync(imeEditSessionPath, "utf8");
|
||||
const imeTextService = readFileSync(imeTextServicePath, "utf8");
|
||||
const tauriConfig = JSON.parse(readFileSync(tauriConfigPath, "utf8"));
|
||||
const nsisHook = readFileSync(nsisHookPath, "utf8");
|
||||
const wixFragment = readFileSync(wixFragmentPath, "utf8");
|
||||
|
||||
const requiredFragments = [
|
||||
@@ -84,6 +91,8 @@ assert.deepEqual(tauriConfig.bundle.windows.wix.componentRefs, [
|
||||
"OpenLessImeDllX64Component",
|
||||
"OpenLessImeDllX86Component",
|
||||
]);
|
||||
assert.equal(tauriConfig.bundle.windows.nsis.installMode, "both", "NSIS must request elevation so TSF can register in HKLM");
|
||||
assert.equal(tauriConfig.bundle.windows.nsis.installerHooks, "nsis/openless-ime-hooks.nsh", "NSIS must install and register the TSF DLLs");
|
||||
|
||||
assert.match(imeSolution, /Release\|Win32/, "IME solution should include a Win32 Release configuration");
|
||||
assert.match(imeProject, /Release\|Win32/, "IME project should include a Win32 Release configuration");
|
||||
@@ -108,6 +117,27 @@ assert.match(wixFragment, /RegisterOpenLessImeX86/, "MSI should register x86 Ope
|
||||
assert.match(wixFragment, /UnregisterOpenLessImeX64/, "MSI should unregister x64 OpenLess IME during uninstall");
|
||||
assert.match(wixFragment, /UnregisterOpenLessImeX86/, "MSI should unregister x86 OpenLess IME during uninstall");
|
||||
|
||||
assert.match(nsisHook, /NSIS_HOOK_PREINSTALL/, "NSIS should copy IME DLLs before install completes");
|
||||
assert.match(nsisHook, /NSIS_HOOK_POSTINSTALL/, "NSIS should register IME DLLs after files are installed");
|
||||
assert.match(nsisHook, /NSIS_HOOK_PREUNINSTALL/, "NSIS should unregister IME DLLs before uninstall removes them");
|
||||
assert.match(nsisHook, /\$%OPENLESS_IME_DLL_X64%/, "NSIS should consume the CI-built x64 IME DLL");
|
||||
assert.match(nsisHook, /\$%OPENLESS_IME_DLL_X86%/, "NSIS should consume the CI-built x86 IME DLL");
|
||||
assert.match(nsisHook, /SetOutPath "\$INSTDIR\\windows-ime\\x64"/, "NSIS should install the x64 IME DLL beside the app");
|
||||
assert.match(nsisHook, /SetOutPath "\$INSTDIR\\windows-ime\\x86"/, "NSIS should install the x86 IME DLL beside the app");
|
||||
assert.match(nsisHook, /File \/oname=OpenLessIme\.dll/, "NSIS should embed OpenLessIme.dll in the installer");
|
||||
assert.match(nsisHook, /Sysnative\\regsvr32\.exe/, "NSIS should use 64-bit regsvr32 for the x64 IME");
|
||||
assert.match(nsisHook, /SysWOW64\\regsvr32\.exe/, "NSIS should use 32-bit regsvr32 for the x86 IME");
|
||||
assert.match(nsisHook, /Abort/, "NSIS install should fail if TSF registration fails");
|
||||
|
||||
assert.match(imeInstallSmoke, /\[ValidateSet\("nsis", "msi"\)\]/, "install smoke should support both Windows installers");
|
||||
assert.match(imeInstallSmoke, /OpenLessImeSubmit/, "install smoke should preserve TSF backend context");
|
||||
assert.match(imeInstallSmoke, /Software\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/, "install smoke should check x64 COM registration");
|
||||
assert.match(imeInstallSmoke, /Software\\WOW6432Node\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/, "install smoke should check x86 COM registration");
|
||||
assert.match(imeInstallSmoke, /LanguageProfile\\0x00000804\\\{9B5F5E04-23F6-47DA-9A26-D221F6C3F02E\}/, "install smoke should check the TSF language profile");
|
||||
assert.match(imeInstallSmoke, /Category\\Category\\\{34745C63-B2F0-4784-8B67-5E12C8701A31\}/, "install smoke should check the keyboard TSF category");
|
||||
assert.match(ciWorkflow, /windows-ime-install-smoke\.ps1[\s\S]*-InstallerKind nsis/, "CI should install and verify the NSIS artifact");
|
||||
assert.match(ciWorkflow, /windows-ime-install-smoke\.ps1[\s\S]*-InstallerKind msi/, "CI should install and verify the MSI artifact");
|
||||
|
||||
assert.match(launcher, /powershell\.exe/, "launcher should call powershell.exe");
|
||||
assert.match(launcher, /-ExecutionPolicy Bypass/, "launcher should bypass execution policy for this process");
|
||||
assert.match(launcher, /windows-package-msvc\.ps1/, "launcher should invoke the packaging script");
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
!macro OPENLESS_IME_ABORT_IF_FAILED EXIT_CODE LABEL
|
||||
${If} ${EXIT_CODE} != 0
|
||||
DetailPrint "OpenLess TSF IME ${LABEL} failed with exit code ${EXIT_CODE}"
|
||||
Abort
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!macro OPENLESS_IME_REGISTER_X64
|
||||
${If} ${RunningX64}
|
||||
DetailPrint "Registering OpenLess x64 TSF IME"
|
||||
ExecWait '"$WINDIR\Sysnative\regsvr32.exe" /s "$INSTDIR\windows-ime\x64\OpenLessIme.dll"' $0
|
||||
${If} $0 != 0
|
||||
${DisableX64FSRedirection}
|
||||
ExecWait '"$WINDIR\System32\regsvr32.exe" /s "$INSTDIR\windows-ime\x64\OpenLessIme.dll"' $0
|
||||
${EnableX64FSRedirection}
|
||||
${EndIf}
|
||||
!insertmacro OPENLESS_IME_ABORT_IF_FAILED $0 "x64 registration"
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!macro OPENLESS_IME_REGISTER_X86
|
||||
DetailPrint "Registering OpenLess x86 TSF IME"
|
||||
ExecWait '"$WINDIR\SysWOW64\regsvr32.exe" /s "$INSTDIR\windows-ime\x86\OpenLessIme.dll"' $0
|
||||
!insertmacro OPENLESS_IME_ABORT_IF_FAILED $0 "x86 registration"
|
||||
!macroend
|
||||
|
||||
!macro OPENLESS_IME_UNREGISTER_X64
|
||||
${If} ${RunningX64}
|
||||
DetailPrint "Unregistering OpenLess x64 TSF IME"
|
||||
ExecWait '"$WINDIR\Sysnative\regsvr32.exe" /s /u "$INSTDIR\windows-ime\x64\OpenLessIme.dll"' $0
|
||||
${If} $0 != 0
|
||||
${DisableX64FSRedirection}
|
||||
ExecWait '"$WINDIR\System32\regsvr32.exe" /s /u "$INSTDIR\windows-ime\x64\OpenLessIme.dll"' $0
|
||||
${EnableX64FSRedirection}
|
||||
${EndIf}
|
||||
DetailPrint "OpenLess x64 TSF IME unregister exit code $0"
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!macro OPENLESS_IME_UNREGISTER_X86
|
||||
DetailPrint "Unregistering OpenLess x86 TSF IME"
|
||||
ExecWait '"$WINDIR\SysWOW64\regsvr32.exe" /s /u "$INSTDIR\windows-ime\x86\OpenLessIme.dll"' $0
|
||||
DetailPrint "OpenLess x86 TSF IME unregister exit code $0"
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_PREINSTALL
|
||||
SetOutPath "$INSTDIR\windows-ime\x64"
|
||||
File /oname=OpenLessIme.dll "$%OPENLESS_IME_DLL_X64%"
|
||||
|
||||
SetOutPath "$INSTDIR\windows-ime\x86"
|
||||
File /oname=OpenLessIme.dll "$%OPENLESS_IME_DLL_X86%"
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_POSTINSTALL
|
||||
!insertmacro OPENLESS_IME_REGISTER_X64
|
||||
!insertmacro OPENLESS_IME_REGISTER_X86
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_PREUNINSTALL
|
||||
!insertmacro OPENLESS_IME_UNREGISTER_X86
|
||||
!insertmacro OPENLESS_IME_UNREGISTER_X64
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_POSTUNINSTALL
|
||||
Delete "$INSTDIR\windows-ime\x64\OpenLessIme.dll"
|
||||
Delete "$INSTDIR\windows-ime\x86\OpenLessIme.dll"
|
||||
RMDir "$INSTDIR\windows-ime\x64"
|
||||
RMDir "$INSTDIR\windows-ime\x86"
|
||||
RMDir "$INSTDIR\windows-ime"
|
||||
!macroend
|
||||
@@ -85,6 +85,10 @@
|
||||
"entitlements": "Entitlements.plist"
|
||||
},
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installMode": "both",
|
||||
"installerHooks": "nsis/openless-ime-hooks.nsh"
|
||||
},
|
||||
"wix": {
|
||||
"fragmentPaths": [
|
||||
"wix/openless-ime.wxs"
|
||||
|
||||
Reference in new issue
Block a user