Link the C runtime statically on Windows

The Windows release binaries import VCRUNTIME140.dll and the UCRT api-ms-win-crt-*.dll set. On a machine without the Visual C++ Redistributable they fail to load with STATUS_DLL_NOT_FOUND (0xC0000135). That's the failure winget's validator reports on the siGitCode submission, even with VCRedist declared as a package dependency. Set +crt-static for MSVC targets in .cargo/config.toml. The only C code on Windows (ring, aws-lc-sys, onig_sys) builds through cc/cmake, which switch to /MT under crt-static, so nothing else has to change. Add packaging/windows/check-static-crt.ps1, which runs dumpbin /dependents and fails if the exe imports the runtime. CI runs it on the Windows test build and release-github runs it on both Windows release binaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Seto Elkahfi committed Sep 30, 2026 at 22:31 UTC 67f0edde5de7ad343538d16818189a45515ba2d8
7 files changed +71
.agents/AGENTS.md
+8
@@ -351,6 +351,14 @@ GitHub release, so `release-github` builds the binaries, attaches the assets, an
351 is logged as a warning rather than failing the others, so an unconfigured channel does not block a
352 release. Their inputs live in `packaging/`.
353
354 +The Windows binaries link the MSVC C runtime statically (`+crt-static` in `.cargo/config.toml`).
355 +winget, Scoop, npm, PyPI and NuGet all ship the bare exe with no installer, so a dynamic build
356 +that imports `VCRUNTIME140.dll` fails to load on a machine without the VC++ Redistributable
357 +(`0xC0000135`, which is what failed winget's first validation).
358 +`packaging/windows/check-static-crt.ps1` runs in CI and in `release-github` and fails the build if
359 +a Windows exe imports the runtime again. Don't set a `RUSTFLAGS` env var in those workflows: it
360 +replaces the config rustflags instead of adding to them.
361 +
362 Every release asset now carries a `.sha256` sidecar, not just the macOS Homebrew tarball. Scoop,
363 winget, and the AUR PKGBUILD each need one, and they consume the raw binaries rather than the
364 tarball. `release-github` also builds a `.deb` and `.rpm` per Linux target with nfpm
.cargo/config.toml new
+7
@@ -0,0 +1,7 @@
1 +# Link the MSVC C runtime statically on Windows. Without this the binary
2 +# imports VCRUNTIME140.dll and the UCRT api-ms-win-crt-*.dll set, and on a
3 +# machine without the Visual C++ Redistributable it dies at load time with
4 +# STATUS_DLL_NOT_FOUND (0xC0000135) before main runs. winget ships sigit as a
5 +# portable exe, so there is no installer to bring the runtime along.
6 +[target.'cfg(all(windows, target_env = "msvc"))']
7 +rustflags = ["-C", "target-feature=+crt-static"]
.github/workflows/ci.yml
+5
@@ -119,3 +119,8 @@ jobs:
119
120 - name: Test
121 run: cargo test --locked --target ${{ matrix.target }}
122 +
123 + - name: Check Windows binary has no VC++ runtime dependency
124 + if: runner.os == 'Windows'
125 + shell: pwsh
126 + run: ./packaging/windows/check-static-crt.ps1 -Path target/${{ matrix.target }}/debug/sigit.exe
.github/workflows/release-github.yml
+5
@@ -96,6 +96,11 @@ jobs:
96 shell: bash
97 run: cargo build --locked --release --target ${{ matrix.target }}
98
99 + - name: Check Windows binary has no VC++ runtime dependency
100 + if: contains(matrix.target, 'windows')
101 + shell: pwsh
102 + run: ./packaging/windows/check-static-crt.ps1 -Path target/${{ matrix.target }}/release/${{ env.PROJECT_NAME }}.exe
103 +
104 - name: Package binary
105 shell: bash
106 run: |
CHANGELOG.md
+9
@@ -2,6 +2,15 @@
2
3 ## Unreleased
4
5 +### Fixed
6 +
7 +- **The Windows binaries start on machines without the Visual C++
8 + Redistributable.** They used to import `VCRUNTIME140.dll` and the UCRT
9 + `api-ms-win-crt-*` DLLs, so a clean Windows install refused to load them
10 + with `STATUS_DLL_NOT_FOUND` (0xC0000135). That is what failed winget's
11 + post-install check. The C runtime is now linked statically, and CI and the
12 + release build fail if a Windows exe picks up that dependency again.
13 +
14 ## 1.5.11
15
16 ### Added
packaging/windows/check-static-crt.ps1 new
+35
@@ -0,0 +1,35 @@
1 +# Fails if a Windows binary depends on the Visual C++ runtime DLLs.
2 +#
3 +# sigit is linked with +crt-static (see .cargo/config.toml) so it runs on a
4 +# clean Windows install. If RUSTFLAGS overrides that, or a native dependency
5 +# drags in the DLL runtime, the exe still builds and passes tests on a runner
6 +# that has the redistributable installed, then fails to load on users'
7 +# machines. This check catches it before release.
8 +param(
9 + [Parameter(Mandatory = $true)]
10 + [string]$Path
11 +)
12 +
13 +$ErrorActionPreference = 'Stop'
14 +
15 +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
16 +$dumpbin = & $vswhere -latest -products * -find 'VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe' |
17 + Select-Object -First 1
18 +if (-not $dumpbin) {
19 + throw 'dumpbin.exe not found; is the MSVC toolset installed?'
20 +}
21 +
22 +$output = & $dumpbin /nologo /dependents $Path
23 +if ($LASTEXITCODE -ne 0) {
24 + throw "dumpbin failed on $Path"
25 +}
26 +$output | Write-Host
27 +
28 +$runtime = $output |
29 + Where-Object { $_ -match '^\s+(vcruntime|msvcp|concrt|ucrtbase|api-ms-win-crt-)\S*\.dll\s*$' } |
30 + ForEach-Object { $_.Trim() }
31 +if ($runtime) {
32 + Write-Host "::error::$Path depends on the Visual C++ runtime: $($runtime -join ', ')"
33 + exit 1
34 +}
35 +Write-Host "$Path does not depend on the Visual C++ runtime."
packaging/winget/getSigit.siGitCode.installer.yaml.in
+2
@@ -6,6 +6,8 @@ InstallerType: portable
6 # assets are sigit-win-<arch>.exe, but the command has to be `sigit`.
7 Commands:
8 - sigit
9 +# No VCRedist dependency: the exe links the C runtime statically
10 +# (.cargo/config.toml), and CI rejects a build that imports it.
11 ReleaseDate: "@RELEASE_DATE@"
12 Installers:
13 - Architecture: x64