From 4c2dd5c6e1d0d06320c71acf5d86cadb1c5cc931 Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 20:42:21 -0700 Subject: [PATCH 1/6] fix(ci): unpack vc_redist with Burn /layout instead of hanging on /extract vc_redist.x64.exe is a WiX Burn bundle with no /extract switch. Burn ran a normal install, returned 1638 because the runner already has the runtime, and the script then retried without /quiet, which opened an installer window that nobody answers on CI. The win7 job has hung until cancelled since the script was introduced. Use /layout to copy the embedded MSIs, extract them with an administrative install, drop the interactive retry, and time-limit every external tool plus the CI step. --- .github/workflows/ci-release.yml | 2 + .../scripts/prepare-win7-runtime.ps1 | 63 +++++++++++-------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 5bd32c5..c3fb4e3 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -238,6 +238,8 @@ jobs: if: matrix.target == 'windows-x86_64-win7' working-directory: tauri-app/src-tauri shell: pwsh + # Backstop: an installer waiting on a UI prompt once held this step for hours. + timeout-minutes: 15 run: powershell -File scripts/prepare-win7-runtime.ps1 - name: Build installer diff --git a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 index a0d060c..d45b215 100644 --- a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 +++ b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 @@ -2,7 +2,8 @@ # # windows-latest currently ships Visual Studio 2026 (18). Its Redist\MSVC\v145 # folder is not the classic x64\Microsoft.VC*.CRT layout, so this script: -# 1. downloads the VS 2015-2022 x64 redistributable and extracts CRT DLLs +# 1. downloads the VS 2015-2022 x64 redistributable, unpacks its packages with Burn's +# /layout and extracts the CRT DLLs from the MSIs with an administrative install # 2. falls back to any VS install that still has Microsoft.VC143.CRT (or later) # 3. copies UCRT app-local files from the Windows SDK # @@ -90,6 +91,23 @@ function Find-VcRedistCrtDirFromVisualStudio { return $null } +# Runs an installer non-interactively and kills it after $TimeoutSeconds. Every external tool +# here must stay quiet: on a CI runner a UI prompt never gets an answer and the job hangs for hours. +function Invoke-Quiet { + param( + [Parameter(Mandatory = $true)][string]$FilePath, + [Parameter(Mandatory = $true)][string[]]$ArgumentList, + [int]$TimeoutSeconds = 300 + ) + + $process = Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -PassThru -NoNewWindow + if (-not $process.WaitForExit($TimeoutSeconds * 1000)) { + $process.Kill() + throw "$FilePath $($ArgumentList -join ' ') did not finish within ${TimeoutSeconds}s" + } + return $process.ExitCode +} + function Extract-VcRedistCrtDir { param( [Parameter(Mandatory = $true)] @@ -98,35 +116,28 @@ function Extract-VcRedistCrtDir { [string]$ExtractRoot ) - $extractDir = Join-Path $ExtractRoot "vc_redist" - New-Item -ItemType Directory -Force -Path $extractDir | Out-Null - Write-Host "Extracting $VcRedistPath to $extractDir" - $process = Start-Process -FilePath $VcRedistPath -ArgumentList @("/quiet", "/norestart", "/extract:$extractDir") -Wait -PassThru -NoNewWindow - if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) { - Write-Warning "vc_redist /extract exited with code $($process.ExitCode); trying without /quiet" - $process = Start-Process -FilePath $VcRedistPath -ArgumentList @("/extract:$extractDir") -Wait -PassThru -NoNewWindow - if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) { - Write-Warning "vc_redist /extract failed with exit code $($process.ExitCode)" - } + # vc_redist is a WiX Burn bundle. It has no /extract switch (Burn treats an unknown switch as + # a plain install and returns 1638 when the runtime is already present); /layout copies the + # embedded MSI and CAB payloads to a directory without installing anything. + $layoutDir = Join-Path $ExtractRoot "vc_redist" + New-Item -ItemType Directory -Force -Path $layoutDir | Out-Null + Write-Host "Laying out $VcRedistPath to $layoutDir" + $exitCode = Invoke-Quiet -FilePath $VcRedistPath -ArgumentList @("/layout", "`"$layoutDir`"", "/quiet", "/norestart") + if ($exitCode -ne 0) { + throw "vc_redist /layout exited with code $exitCode" } - $found = Find-CrtDirectoryInTree -Root $extractDir - if ($found) { - return $found + $msis = @(Get-ChildItem -Path $layoutDir -Recurse -Filter "*.msi" -File -ErrorAction SilentlyContinue) + if (-not $msis) { + throw "vc_redist /layout produced no MSI packages under $layoutDir" } - - Get-ChildItem -Path $extractDir -Recurse -Filter "*.cab" -File -ErrorAction SilentlyContinue | ForEach-Object { - $cabOut = Join-Path $ExtractRoot ("cab_" + $_.BaseName) - New-Item -ItemType Directory -Force -Path $cabOut | Out-Null - & expand.exe $_.FullName -F:* $cabOut | Out-Null - } - - Get-ChildItem -Path $extractDir -Recurse -Filter "*.msi" -File -ErrorAction SilentlyContinue | ForEach-Object { - $msiOut = Join-Path $ExtractRoot ("msi_" + $_.BaseName) + foreach ($msiFile in $msis) { + $msiOut = Join-Path $ExtractRoot ("msi_" + $msiFile.BaseName) New-Item -ItemType Directory -Force -Path $msiOut | Out-Null - $msi = Start-Process -FilePath "msiexec.exe" -ArgumentList @("/a", $_.FullName, "/qn", "TARGETDIR=$msiOut") -Wait -PassThru -NoNewWindow - if ($msi.ExitCode -ne 0) { - Write-Warning "msiexec /a $($_.Name) exited with code $($msi.ExitCode)" + Write-Host "Administrative install of $($msiFile.Name)" + $exitCode = Invoke-Quiet -FilePath "msiexec.exe" -ArgumentList @("/a", "`"$($msiFile.FullName)`"", "/qn", "TARGETDIR=`"$msiOut`"") + if ($exitCode -ne 0) { + Write-Warning "msiexec /a $($msiFile.Name) exited with code $exitCode" } } From 8d19c4f07fefc0bf6747b42e46621b642fda668f Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 20:47:00 -0700 Subject: [PATCH 2/6] fix(ci): keep the process handle so installer exit codes are readable --- tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 index d45b215..a2dfda3 100644 --- a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 +++ b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 @@ -101,6 +101,8 @@ function Invoke-Quiet { ) $process = Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -PassThru -NoNewWindow + # Without holding the handle, ExitCode reads back as $null once the process has exited. + $null = $process.Handle if (-not $process.WaitForExit($TimeoutSeconds * 1000)) { $process.Kill() throw "$FilePath $($ArgumentList -join ' ') did not finish within ${TimeoutSeconds}s" From 0a7948bfed3909f0bbe7f17f620ed29cf42b809c Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 20:52:49 -0700 Subject: [PATCH 3/6] fix(ci): log Burn layout output and directory contents when vc_redist unpack fails --- .../src-tauri/scripts/prepare-win7-runtime.ps1 | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 index a2dfda3..6d1da5b 100644 --- a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 +++ b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 @@ -124,14 +124,17 @@ function Extract-VcRedistCrtDir { $layoutDir = Join-Path $ExtractRoot "vc_redist" New-Item -ItemType Directory -Force -Path $layoutDir | Out-Null Write-Host "Laying out $VcRedistPath to $layoutDir" - $exitCode = Invoke-Quiet -FilePath $VcRedistPath -ArgumentList @("/layout", "`"$layoutDir`"", "/quiet", "/norestart") - if ($exitCode -ne 0) { - throw "vc_redist /layout exited with code $exitCode" - } - + $burnLog = Join-Path $ExtractRoot "vc_redist_layout.log" + $exitCode = Invoke-Quiet -FilePath $VcRedistPath -ArgumentList @("/layout", "`"$layoutDir`"", "/quiet", "/norestart", "/log", "`"$burnLog`"") $msis = @(Get-ChildItem -Path $layoutDir -Recurse -Filter "*.msi" -File -ErrorAction SilentlyContinue) - if (-not $msis) { - throw "vc_redist /layout produced no MSI packages under $layoutDir" + if ($exitCode -ne 0 -or -not $msis) { + Write-Host "----- files under $ExtractRoot -----" + Get-ChildItem -Path $ExtractRoot -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object { Write-Host "$($_.FullName) $($_.Length)" } + Get-ChildItem -Path (Split-Path -Parent $burnLog) -Filter "vc_redist_layout*.log" -File -ErrorAction SilentlyContinue | ForEach-Object { + Write-Host "----- $($_.Name) (tail) -----" + Get-Content $_.FullName -Tail 60 | ForEach-Object { Write-Host $_ } + } + throw "vc_redist /layout exited with code $exitCode and produced $($msis.Count) MSI packages" } foreach ($msiFile in $msis) { $msiOut = Join-Path $ExtractRoot ("msi_" + $msiFile.BaseName) From 464d0bf766cefbcb718e23f776c9920c7e6d6334 Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 20:59:41 -0700 Subject: [PATCH 4/6] fix(ci): build the Win7 installer on windows-2022 with VS 2022's x64 VC143 CRT windows-latest now carries VS 2026: no classic Microsoft.VC*.CRT folder, and its system VC++ 14.5x runtime makes vc_redist 14.44 mark every package obsolete, so /layout copies nothing. Run the win7 job on windows-2022, take the CRT from VS 2022 first, and only accept the x64 folder (the Redist tree also has x86/arm64/onecore copies with the same folder name). --- .github/workflows/ci-release.yml | 6 ++++- .../scripts/prepare-win7-runtime.ps1 | 23 ++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index c3fb4e3..2590adf 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -107,7 +107,11 @@ jobs: build_script: build:installer:windows artifact_glob: tauri-app/src-tauri/target/release/bundle/nsis/*.exe - target: windows-x86_64-win7 - os: windows-latest + # windows-latest moved to Visual Studio 2026: its v145 redist has no classic + # Microsoft.VC*.CRT folder and its system VC++ 14.5x runtime makes vc_redist 14.44 + # treat its own packages as obsolete, so nothing can be unpacked. windows-2022 still + # carries VS 2022's Microsoft.VC143.CRT (14.4x), the last line that supports Windows 7. + os: windows-2022 build_script: build:installer:windows:win7 artifact_glob: tauri-app/src-tauri/target/x86_64-win7-windows-msvc/release/bundle/nsis/*.exe needs_fixed_runtime: true diff --git a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 index 6d1da5b..4ca3249 100644 --- a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 +++ b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 @@ -1,10 +1,11 @@ # Prepare app-local VC++ / Universal CRT files for the Windows 7 installer. # -# windows-latest currently ships Visual Studio 2026 (18). Its Redist\MSVC\v145 -# folder is not the classic x64\Microsoft.VC*.CRT layout, so this script: -# 1. downloads the VS 2015-2022 x64 redistributable, unpacks its packages with Burn's -# /layout and extracts the CRT DLLs from the MSIs with an administrative install -# 2. falls back to any VS install that still has Microsoft.VC143.CRT (or later) +# windows-latest ships Visual Studio 2026 (18). Its Redist\MSVC\v145 folder is not the classic +# x64\Microsoft.VC*.CRT layout, so this script: +# 1. takes the CRT DLLs from a Visual Studio install that has Microsoft.VC143.CRT (the CI job +# runs on windows-2022 for this) +# 2. otherwise unpacks the downloaded VS 2015-2022 x64 redistributable with Burn's /layout and +# extracts the CRT DLLs from its MSIs with an administrative install # 3. copies UCRT app-local files from the Windows SDK # # Output: src-tauri/win7-runtime/*.dll plus vc_redist.x64.exe (and KB2999226 when available). @@ -37,6 +38,9 @@ function Find-CrtDirectoryInTree { $candidates = @(Get-ChildItem -Path $Root -Recurse -Filter "vcruntime140.dll" -File -ErrorAction SilentlyContinue | Where-Object { + # Visual Studio's Redist tree also holds x86, arm64, onecore and debug copies under + # identically named Microsoft.VC*.CRT folders; only the plain x64 set ships. + $_.DirectoryName -notmatch '\\(x86|arm64|arm64ec|onecore|debug_nonredist)\\' -and (Test-Path (Join-Path $_.DirectoryName "msvcp140.dll")) -and ( $_.Directory.Name -like "Microsoft.VC*.CRT" -or @@ -179,10 +183,13 @@ if ((Get-Item $vcRedistPath).Length -lt 1MB) { throw "vc_redist.x64.exe download looks truncated" } -$crtDir = Extract-VcRedistCrtDir -VcRedistPath $vcRedistPath -ExtractRoot $workDir +# Prefer Visual Studio 2022's Microsoft.VC143.CRT (the CI job runs on windows-2022). Unpacking +# vc_redist only works where the machine's own VC++ runtime is not newer than the bundle; otherwise +# Burn marks every package obsolete and lays out nothing. +$crtDir = Find-VcRedistCrtDirFromVisualStudio if (-not $crtDir) { - Write-Warning "Could not extract CRT DLLs from vc_redist.x64.exe; searching Visual Studio installs" - $crtDir = Find-VcRedistCrtDirFromVisualStudio + Write-Warning "No Microsoft.VC14x.CRT under Visual Studio; unpacking vc_redist.x64.exe" + $crtDir = Extract-VcRedistCrtDir -VcRedistPath $vcRedistPath -ExtractRoot $workDir } if (-not $crtDir) { throw "Could not locate vcruntime140.dll / msvcp140.dll from vc_redist.x64.exe or Visual Studio redistributables" From 2724a20a5de0218156ae514530d9982d1e33f1c4 Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 21:16:15 -0700 Subject: [PATCH 5/6] fix(ci): fetch KB2999226 from the Download Center and require it The download.windowsupdate.com URL now returns 404, so the update was silently skipped and the installer check rejected the Win7 bundle. Use the Download Center file (id=49093), pin its SHA-256, retry, and fail the build if it cannot be fetched. --- .../scripts/prepare-win7-runtime.ps1 | 35 ++++++++++++------- tauri-app/src-tauri/win7-runtime/README.md | 2 +- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 index 4ca3249..f6a98e8 100644 --- a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 +++ b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 @@ -8,7 +8,7 @@ # extracts the CRT DLLs from its MSIs with an administrative install # 3. copies UCRT app-local files from the Windows SDK # -# Output: src-tauri/win7-runtime/*.dll plus vc_redist.x64.exe (and KB2999226 when available). +# Output: src-tauri/win7-runtime/*.dll plus vc_redist.x64.exe and Windows6.1-KB2999226-x64.msu. [CmdletBinding()] param( @@ -202,20 +202,28 @@ Write-Host "UCRT: $ucrtDir" Copy-Item -Path (Join-Path $crtDir "*.dll") -Destination $OutputDir -Force Copy-Item -Path (Join-Path $ucrtDir "*.dll") -Destination $OutputDir -Force -$kbUrl = "https://download.windowsupdate.com/d/msdownload/update/software/updt/2015/11/windows6.1-kb2999226-x64_7da58c4d261ad48a1367c60c81481ea2a8bae7e3.msu" +# Microsoft Download Center, "Update for Windows 7 for x64-based Systems (KB2999226)", id=49093. +# The old download.windowsupdate.com path now 404s. The installer verification step requires this +# file, so a failed or altered download stops the build instead of being skipped. +$kbUrl = "https://download.microsoft.com/download/1/1/5/11565a9a-ea09-4f0a-a57e-520d5d138140/Windows6.1-KB2999226-x64.msu" +$kbSha256 = "43234D2986CA9B0DE75D5183977964D161A8395C3396279DDFC9B20698E5BC34" $kbPath = Join-Path $OutputDir "Windows6.1-KB2999226-x64.msu" -try { - Write-Host "Downloading Universal CRT update KB2999226 (optional)" - Invoke-WebRequest -Uri $kbUrl -OutFile $kbPath - if ((Get-Item $kbPath).Length -lt 100KB) { - throw "KB2999226 download looks truncated" - } -} catch { - Write-Warning "KB2999226 could not be downloaded; app-local UCRT DLLs still cover launch. $_" - if (Test-Path $kbPath) { - Remove-Item $kbPath -Force +Write-Host "Downloading Universal CRT update KB2999226" +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +for ($attempt = 1; $attempt -le 3; $attempt++) { + try { + Invoke-WebRequest -Uri $kbUrl -OutFile $kbPath -UseBasicParsing + break + } catch { + if ($attempt -eq 3) { throw "KB2999226 download failed after 3 attempts: $_" } + Write-Warning "KB2999226 download attempt $attempt failed: $_" + Start-Sleep -Seconds (5 * $attempt) } } +$kbHash = (Get-FileHash -Path $kbPath -Algorithm SHA256).Hash +if ($kbHash -ne $kbSha256) { + throw "KB2999226 checksum mismatch: expected $kbSha256, got $kbHash" +} Remove-Item -Path $workDir -Recurse -Force -ErrorAction SilentlyContinue @@ -229,7 +237,8 @@ $required = @( "api-ms-win-crt-stdio-l1-1-0.dll", "api-ms-win-crt-locale-l1-1-0.dll", "api-ms-win-crt-heap-l1-1-0.dll", - "vc_redist.x64.exe" + "vc_redist.x64.exe", + "Windows6.1-KB2999226-x64.msu" ) foreach ($name in $required) { diff --git a/tauri-app/src-tauri/win7-runtime/README.md b/tauri-app/src-tauri/win7-runtime/README.md index 43eacf1..9b6a6c7 100644 --- a/tauri-app/src-tauri/win7-runtime/README.md +++ b/tauri-app/src-tauri/win7-runtime/README.md @@ -9,7 +9,7 @@ The script copies: - Universal CRT app-local files from the Windows SDK (`ucrtbase.dll` and `api-ms-win-*.dll`) - `vc_redist.x64.exe` for an optional system-wide install -- `Windows6.1-KB2999226-x64.msu` when Microsoft's download is reachable +- `Windows6.1-KB2999226-x64.msu`(必需,下载后校验 SHA-256) The NSIS hook then copies every `*.dll` next to `fpsmaster-launcher.exe`. On Windows 7 the UCRT API-set forwarders only resolve `ucrtbase.dll` from the From 0546ff97e1b611870f84017975b6cfdf0ff0fdba Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 21:31:08 -0700 Subject: [PATCH 6/6] fix(ci): check Win7 installer contents from the 7-Zip file list The raw-bytes search only saw file names while NSIS left its header uncompressed; once the payload grew, every name check failed even though the files were packed. List the installer with 7-Zip (native NSIS support, present on Windows runners) and fall back to the byte search when 7-Zip is unavailable. --- tauri-app/src-tauri/scripts/verify-win7-pe.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tauri-app/src-tauri/scripts/verify-win7-pe.py b/tauri-app/src-tauri/scripts/verify-win7-pe.py index db613c5..bdd4789 100644 --- a/tauri-app/src-tauri/scripts/verify-win7-pe.py +++ b/tauri-app/src-tauri/scripts/verify-win7-pe.py @@ -4,6 +4,8 @@ from __future__ import annotations import argparse +import subprocess +import shutil import json import struct import sys @@ -262,8 +264,26 @@ def contains_text(data: bytes, text: str) -> bool: return text.encode("ascii") in data or text.encode("utf-16le") in data +def installer_listing(path: Path) -> str | None: + """File list of an NSIS installer via 7-Zip, which reads NSIS archives natively. + + Searching the raw installer bytes only works while NSIS happens to leave the file-name table + uncompressed; with solid LZMA the names vanish and every check fails. Returns None when 7-Zip + is unavailable or cannot read the file, so the caller can fall back to the byte search. + """ + candidates = [shutil.which("7z"), r"C:\Program Files\7-Zip\7z.exe"] + for exe in candidates: + if not exe or not Path(exe).exists(): + continue + result = subprocess.run([exe, "l", "-slt", str(path)], capture_output=True, text=True, errors="replace") + if result.returncode == 0 and "Path = " in result.stdout: + return result.stdout + return None + + def verify_installer_strings(path: Path) -> list[str]: - data = path.read_bytes() + listing = installer_listing(path) + data = listing.encode("utf-8") if listing is not None else path.read_bytes() errors = [] for needle in ( "ucrtbase.dll",