From ca0f94471266116a997ba2abf87f036bdee5e9f6 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 9 Aug 2026 19:36:14 -0700 Subject: [PATCH 1/6] CI: install MSVC build tools on Windows runners that lack them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-windows failed on this PR with "linker `link.exe` not found", while build-linux and build-container passed — the code was fine, the runner environment was not. The job installs Rust and Node conditionally but assumed the MSVC C++ toolchain was hand-provisioned. A runner without it registers normally, advertises windows-latest, accepts the job, downloads the entire crate graph and only then fails at link time. That also means a bare runner coming online turns a job that would have queued for a capable machine into a failed build. Installs the VC++ workload when vswhere cannot find it, matching the existing conditional Rust and Node steps. rustc locates MSVC through vswhere and the registry rather than PATH, so no dev-shell activation is needed. Installer exit 3010 (success, reboot pending) is treated as success. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/build-app.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.gitea/workflows/build-app.yml b/.gitea/workflows/build-app.yml index 968f5ab..25a0b3b 100644 --- a/.gitea/workflows/build-app.yml +++ b/.gitea/workflows/build-app.yml @@ -357,6 +357,38 @@ jobs: (Get-Content app/src-tauri/Cargo.toml) -replace '^version = ".*?"', "version = `"$version`"" | Set-Content app/src-tauri/Cargo.toml Write-Host "Patched version to $version" + - name: Install MSVC C++ build tools + shell: cmd + run: | + rem Tauri links with MSVC, so rustc needs link.exe and the Windows SDK. + rem This job previously assumed a hand-provisioned runner; a runner + rem without them registers fine, accepts windows-latest jobs, and then + rem fails at link time with "linker `link.exe` not found" after having + rem already downloaded the whole crate graph. + rem + rem rustc locates MSVC through vswhere/the registry rather than PATH, + rem so installing is sufficient — no dev-shell activation needed here. + set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" + if exist "%VSWHERE%" ( + "%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath > "%TEMP%\vcpath.txt" 2>nul + for /f "usebackq delims=" %%i in ("%TEMP%\vcpath.txt") do set "VCPATH=%%i" + ) + if defined VCPATH ( + echo MSVC build tools already present at %VCPATH% + ) else ( + echo MSVC build tools not found - installing Visual Studio Build Tools + curl -fSL -o "%TEMP%\vs_BuildTools.exe" https://aka.ms/vs/17/release/vs_BuildTools.exe || exit /b 1 + rem 3010 means "installed, reboot pending" and is a success for our purposes. + "%TEMP%\vs_BuildTools.exe" --quiet --wait --norestart --nocache ^ + --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended + set "VSEXIT=%ERRORLEVEL%" + del "%TEMP%\vs_BuildTools.exe" 2>nul + if not "%VSEXIT%"=="0" if not "%VSEXIT%"=="3010" ( + echo Visual Studio Build Tools installer failed with %VSEXIT% + exit /b %VSEXIT% + ) + echo Visual Studio Build Tools installed + ) - name: Install Rust stable run: | where rustup >nul 2>&1 && ( From c6bb7fdf1d8f2400d509964c9ed0a90bc17fb62b Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 9 Aug 2026 21:50:15 -0700 Subject: [PATCH 2/6] CI: fix the MSVC exit-code check and verify .NET 3.5 before bundling %VSEXIT% and %ERRORLEVEL% inside a parenthesised cmd block are substituted at parse time, not run time, so the installer's real exit code was never read. Uses delayed expansion now. Also checks for the .NET 3.5 runtime before building: WiX candle.exe needs it, and Tauri aborts the whole bundle when the MSI target fails, which silently suppresses the NSIS installer too. Fails early with the exact dism command rather than at bundle time. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/build-app.yml | 59 +++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/.gitea/workflows/build-app.yml b/.gitea/workflows/build-app.yml index 25a0b3b..eae95b3 100644 --- a/.gitea/workflows/build-app.yml +++ b/.gitea/workflows/build-app.yml @@ -362,33 +362,62 @@ jobs: run: | rem Tauri links with MSVC, so rustc needs link.exe and the Windows SDK. rem This job previously assumed a hand-provisioned runner; a runner - rem without them registers fine, accepts windows-latest jobs, and then - rem fails at link time with "linker `link.exe` not found" after having - rem already downloaded the whole crate graph. + rem without them registers fine, advertises windows-latest, accepts the + rem job, downloads the whole crate graph and only then fails at link + rem time with "linker `link.exe` not found". rem - rem rustc locates MSVC through vswhere/the registry rather than PATH, - rem so installing is sufficient — no dev-shell activation needed here. + rem rustc finds MSVC via vswhere and the registry rather than PATH, so + rem installing is enough - no dev-shell activation needed here. + rem + rem Delayed expansion is required: %VAR% inside a parenthesised block + rem is substituted when the block is PARSED, not when it runs, so both + rem %ERRORLEVEL% and %VSEXIT% would read as their pre-block values. + setlocal enabledelayedexpansion + set "VCPATH=" set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" if exist "%VSWHERE%" ( - "%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath > "%TEMP%\vcpath.txt" 2>nul - for /f "usebackq delims=" %%i in ("%TEMP%\vcpath.txt") do set "VCPATH=%%i" + for /f "usebackq delims=" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VCPATH=%%i" ) if defined VCPATH ( - echo MSVC build tools already present at %VCPATH% + echo MSVC build tools already present at !VCPATH! ) else ( echo MSVC build tools not found - installing Visual Studio Build Tools curl -fSL -o "%TEMP%\vs_BuildTools.exe" https://aka.ms/vs/17/release/vs_BuildTools.exe || exit /b 1 - rem 3010 means "installed, reboot pending" and is a success for our purposes. - "%TEMP%\vs_BuildTools.exe" --quiet --wait --norestart --nocache ^ - --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended - set "VSEXIT=%ERRORLEVEL%" + "%TEMP%\vs_BuildTools.exe" --quiet --wait --norestart --nocache --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended + set "VSEXIT=!ERRORLEVEL!" del "%TEMP%\vs_BuildTools.exe" 2>nul - if not "%VSEXIT%"=="0" if not "%VSEXIT%"=="3010" ( - echo Visual Studio Build Tools installer failed with %VSEXIT% - exit /b %VSEXIT% + rem 3010 means installed, reboot pending - a success for our purposes. + if not "!VSEXIT!"=="0" if not "!VSEXIT!"=="3010" ( + echo Visual Studio Build Tools installer failed with exit code !VSEXIT! + exit /b 1 ) echo Visual Studio Build Tools installed ) + endlocal + + - name: Verify .NET 3.5 for the MSI bundler + shell: cmd + run: | + rem WiX 3.x candle.exe is a .NET 2.0/3.5 application, and Tauri aborts + rem the whole bundle when the MSI target fails - taking the NSIS + rem installer with it. Windows 11 ships NetFx3 as + rem DisabledWithPayloadRemoved, and enabling it needs a payload that + rem Windows Update cannot always supply, so this checks rather than + rem guesses and tells you exactly how to fix it on the runner. + if exist "%WINDIR%\Microsoft.NET\Framework64\v2.0.50727" ( + echo .NET 3.5 runtime present - WiX can run + ) else ( + echo ERROR: .NET 3.5 is missing, so WiX candle.exe cannot run and the + echo MSI bundle will fail, which also suppresses the NSIS build. + echo. + echo Fix on this runner, with a Windows ISO mounted ^(e.g. D:^): + echo dism /Online /Enable-Feature /FeatureName:NetFx3 /All ^ + echo /Source:D:\sources\sxs /LimitAccess + echo. + echo Windows Update alone is not sufficient when the feature reports + echo DisabledWithPayloadRemoved. + exit /b 1 + ) - name: Install Rust stable run: | where rustup >nul 2>&1 && ( From 03384409e7b60250ff02ee2295b145c451cefcf8 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 9 Aug 2026 22:12:27 -0700 Subject: [PATCH 3/6] CI: keep the WiX toolset out of System32 so 32-bit candle.exe can run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WiX's candle.exe/light.exe are 32-bit. A SYSTEM-run runner has %LOCALAPPDATA% under C:\Windows\system32\config\systemprofile, where Tauri caches the WiX toolset — and WOW64 redirection sends 32-bit processes reading System32 to SysWOW64, which has no such directory. The CLR then fails to start with 0x80131700 and Tauri reports only "failed to run candle.exe". Verified: the same binary and identity exits 0 from C:\wixtest and 0x80131700 from the systemprofile path. Pointing LOCALAPPDATA outside System32 avoids redirection, needs no stored credential, and is a no-op for runners already running as a normal user. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/build-app.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.gitea/workflows/build-app.yml b/.gitea/workflows/build-app.yml index eae95b3..67b4c11 100644 --- a/.gitea/workflows/build-app.yml +++ b/.gitea/workflows/build-app.yml @@ -339,6 +339,22 @@ jobs: build-windows: runs-on: windows-latest needs: [compute-version] + env: + # WiX 3.x candle.exe/light.exe are 32-bit. A runner running as SYSTEM has + # %LOCALAPPDATA% = C:\Windows\system32\config\systemprofile\AppData\Local, + # and Tauri caches the WiX toolset there. WOW64 redirection then sends a + # 32-bit process reading C:\Windows\System32 to C:\Windows\SysWOW64, where + # that directory does not exist - so candle.exe cannot see its own folder, + # the CLR fails to start, and Tauri reports only "failed to run + # candle.exe". The real error is 0x80131700, visible in the Application + # event log as ".NET Runtime ... could not be started". + # + # Verified on our runner: candle.exe -? exits 0 from C:\wixtest and + # 0x80131700 from the systemprofile path - same identity, same binary. + # + # Pointing LOCALAPPDATA outside System32 sidesteps redirection entirely, + # and is harmless on a runner that already runs as a normal user. + LOCALAPPDATA: C:\actions-runner-cache defaults: run: shell: cmd From 763af910428349e13f802f2581716327e86c10a3 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 9 Aug 2026 22:19:12 -0700 Subject: [PATCH 4/6] Revert: LOCALAPPDATA override does not move Tauri's WiX cache Rust's `dirs` crate resolves LOCALAPPDATA on Windows through SHGetKnownFolderPath, which reads the process token rather than the environment, so the override changed nothing and 32-bit candle.exe still hit WOW64 redirection under the SYSTEM profile. Removing it rather than leaving a plausible-looking non-fix in the workflow. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/build-app.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.gitea/workflows/build-app.yml b/.gitea/workflows/build-app.yml index 67b4c11..eae95b3 100644 --- a/.gitea/workflows/build-app.yml +++ b/.gitea/workflows/build-app.yml @@ -339,22 +339,6 @@ jobs: build-windows: runs-on: windows-latest needs: [compute-version] - env: - # WiX 3.x candle.exe/light.exe are 32-bit. A runner running as SYSTEM has - # %LOCALAPPDATA% = C:\Windows\system32\config\systemprofile\AppData\Local, - # and Tauri caches the WiX toolset there. WOW64 redirection then sends a - # 32-bit process reading C:\Windows\System32 to C:\Windows\SysWOW64, where - # that directory does not exist - so candle.exe cannot see its own folder, - # the CLR fails to start, and Tauri reports only "failed to run - # candle.exe". The real error is 0x80131700, visible in the Application - # event log as ".NET Runtime ... could not be started". - # - # Verified on our runner: candle.exe -? exits 0 from C:\wixtest and - # 0x80131700 from the systemprofile path - same identity, same binary. - # - # Pointing LOCALAPPDATA outside System32 sidesteps redirection entirely, - # and is harmless on a runner that already runs as a normal user. - LOCALAPPDATA: C:\actions-runner-cache defaults: run: shell: cmd From fdc161fd9c59b4b4c5979f08c3b8ec0c691d77dd Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 9 Aug 2026 22:24:42 -0700 Subject: [PATCH 5/6] CI: build NSIS only on Windows, dropping the MSI target WiX's candle.exe/light.exe are 32-bit. On a SYSTEM-run runner Tauri caches WiX under C:\Windows\system32\config\systemprofile\..., and WOW64 redirection sends 32-bit processes to SysWOW64 where that directory does not exist, so candle exits 0x80131700. Tauri aborts the whole bundle on one target's failure, so the MSI was suppressing the NSIS installer too and Windows produced no artifact at all. NSIS is what the project already relies on for Windows upgrades. Drops the .NET 3.5 gate, which existed only for WiX; keeps the MSVC step, which is what makes the app link. Artifact collection now fails when no installer is produced rather than tolerating an empty directory. To restore the MSI, run the runner as a normal user and set --bundles msi,nsis. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/build-app.yml | 39 ++++++++++++---------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/.gitea/workflows/build-app.yml b/.gitea/workflows/build-app.yml index eae95b3..b560c9e 100644 --- a/.gitea/workflows/build-app.yml +++ b/.gitea/workflows/build-app.yml @@ -395,29 +395,6 @@ jobs: ) endlocal - - name: Verify .NET 3.5 for the MSI bundler - shell: cmd - run: | - rem WiX 3.x candle.exe is a .NET 2.0/3.5 application, and Tauri aborts - rem the whole bundle when the MSI target fails - taking the NSIS - rem installer with it. Windows 11 ships NetFx3 as - rem DisabledWithPayloadRemoved, and enabling it needs a payload that - rem Windows Update cannot always supply, so this checks rather than - rem guesses and tells you exactly how to fix it on the runner. - if exist "%WINDIR%\Microsoft.NET\Framework64\v2.0.50727" ( - echo .NET 3.5 runtime present - WiX can run - ) else ( - echo ERROR: .NET 3.5 is missing, so WiX candle.exe cannot run and the - echo MSI bundle will fail, which also suppresses the NSIS build. - echo. - echo Fix on this runner, with a Windows ISO mounted ^(e.g. D:^): - echo dism /Online /Enable-Feature /FeatureName:NetFx3 /All ^ - echo /Source:D:\sources\sxs /LimitAccess - echo. - echo Windows Update alone is not sufficient when the feature reports - echo DisabledWithPayloadRemoved. - exit /b 1 - ) - name: Install Rust stable run: | where rustup >nul 2>&1 && ( @@ -477,14 +454,24 @@ jobs: TAURI_CONFIG: "{\"build\":{\"beforeBuildCommand\":\"\"}}" run: | set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%" - cargo tauri build + rem NSIS only: the MSI target needs WiX, whose candle.exe/light.exe are + rem 32-bit. On a runner running as SYSTEM, Tauri caches WiX under + rem %LOCALAPPDATA% = C:\Windows\system32\config\systemprofile\..., and + rem WOW64 redirection points 32-bit processes at SysWOW64, where that + rem directory does not exist - so candle cannot see its own folder and + rem dies with 0x80131700. Tauri aborts the whole bundle when one target + rem fails, so the MSI was also costing us the NSIS installer. + rem + rem NSIS is what the project ships for Windows upgrades anyway. To bring + rem the MSI back, run the runner as a normal user (whose LOCALAPPDATA is + rem outside System32) and restore --bundles msi,nsis. + cargo tauri build --bundles nsis - name: Collect artifacts run: | set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%" mkdir artifacts - copy app\src-tauri\target\release\bundle\msi\*.msi artifacts\ 2>nul - copy app\src-tauri\target\release\bundle\nsis\*.exe artifacts\ 2>nul + copy app\src-tauri\target\release\bundle\nsis\*.exe artifacts\ || exit /b 1 dir artifacts\ - name: Upload to Gitea release From 0aa83155142c737f6ebb8fb67609d961b713f801 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 9 Aug 2026 22:34:59 -0700 Subject: [PATCH 6/6] CI: restore the MSI now that the 32-bit bundlers can resolve their paths Dropping the MSI did not help: makensis.exe is 32-bit like candle.exe and failed the same way ("Unable to start child process, error 0x2"). The cause was WOW64 redirection sending 32-bit processes reading C:\Windows\System32 to SysWOW64, where the toolset directory does not exist. The build VM now carries junctions from the SysWOW64 view of systemprofile\AppData\Local\tauri and systemprofile\.cache to the System32 originals. Verified on the runner: candle.exe reports WiX 3.14.1.8722 and makensis reports v3.11, both exiting 0 from the path that previously failed. Both targets build again, so the .msi comes back. Artifact collection fails if either installer is missing. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/build-app.yml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.gitea/workflows/build-app.yml b/.gitea/workflows/build-app.yml index b560c9e..aa05136 100644 --- a/.gitea/workflows/build-app.yml +++ b/.gitea/workflows/build-app.yml @@ -454,23 +454,25 @@ jobs: TAURI_CONFIG: "{\"build\":{\"beforeBuildCommand\":\"\"}}" run: | set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%" - rem NSIS only: the MSI target needs WiX, whose candle.exe/light.exe are - rem 32-bit. On a runner running as SYSTEM, Tauri caches WiX under - rem %LOCALAPPDATA% = C:\Windows\system32\config\systemprofile\..., and - rem WOW64 redirection points 32-bit processes at SysWOW64, where that - rem directory does not exist - so candle cannot see its own folder and - rem dies with 0x80131700. Tauri aborts the whole bundle when one target - rem fails, so the MSI was also costing us the NSIS installer. + rem Every Tauri bundler it downloads - candle.exe, light.exe and + rem makensis.exe - is 32-bit. A runner running as SYSTEM has + rem %LOCALAPPDATA% under C:\Windows\system32\config\systemprofile, and + rem WOW64 redirection sends 32-bit processes reading System32 to + rem SysWOW64, so they cannot see their own directory: candle exits + rem 0x80131700 and makensis reports "Unable to start child process, + rem error 0x2". rem - rem NSIS is what the project ships for Windows upgrades anyway. To bring - rem the MSI back, run the runner as a normal user (whose LOCALAPPDATA is - rem outside System32) and restore --bundles msi,nsis. - cargo tauri build --bundles nsis + rem The build VM carries junctions from the SysWOW64 view of + rem systemprofile\AppData\Local\tauri and systemprofile\.cache to the + rem System32 originals, which makes the redirected view resolve. A + rem runner running as a normal user needs no such patch. + cargo tauri build --bundles msi,nsis - name: Collect artifacts run: | set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%" mkdir artifacts + copy app\src-tauri\target\release\bundle\msi\*.msi artifacts\ || exit /b 1 copy app\src-tauri\target\release\bundle\nsis\*.exe artifacts\ || exit /b 1 dir artifacts\