diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 5bd32c5..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 @@ -238,6 +242,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..f6a98e8 100644 --- a/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 +++ b/tauri-app/src-tauri/scripts/prepare-win7-runtime.ps1 @@ -1,12 +1,14 @@ # 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 and extracts CRT DLLs -# 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). +# Output: src-tauri/win7-runtime/*.dll plus vc_redist.x64.exe and Windows6.1-KB2999226-x64.msu. [CmdletBinding()] param( @@ -36,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 @@ -90,6 +95,25 @@ 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 + # 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" + } + return $process.ExitCode +} + function Extract-VcRedistCrtDir { param( [Parameter(Mandatory = $true)] @@ -98,35 +122,31 @@ 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" + $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 ($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" } - - $found = Find-CrtDirectoryInTree -Root $extractDir - if ($found) { - return $found - } - - 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" } } @@ -163,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" @@ -179,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 @@ -206,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/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", 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