Wiki

Cross-compile is a cheap local matrix check

When a CI matrix runs on a platform the dev box can't simulate (Windows from Linux, FreeBSD from macOS, an embedded target from anything), the compile-time half of the matrix is portable for free. Add a cross-compile vet/build pass to the local hook and the matrix stops catching trivia. The runtime half stays on the real runner; the syntax/import/API-shape half lands before push.

When to reach for it

The same repo ships two regressions in a short window that only the foreign-OS matrix job caught. Both bugs would have surfaced on a go vet or a cargo check against the target triple, without anyone actually executing the binary on that OS. The local pre-commit hook is running on the dev OS only. The contributor running make test doesn't see the regression until CI opens its red badge several minutes later, which is several minutes too late to keep a tight ship cadence.

The shape

Three things are true at the same time.

  1. Matrix CI catches two bug classes, not one. The compile-time class lives in API differences, missing types, conditional compilation paths, and import resolution. The runtime class lives in path semantics, network stack quirks, syscall behavior, filesystem case sensitivity, and process model. Only the runtime class needs the actual OS to surface. The compile-time class needs a cross-compiler.
  2. Cross-compilers are already on the dev box. GOOS=windows go build works from Linux without installing Wine. cargo check --target x86_64-pc-windows-gnu works after rustup target add. dotnet publish -r win-x64 works on macOS. clang --target=arm-linux-gnueabihf works after installing the right libc headers. The local hook does not need a foreign-OS VM, container, or emulator to run the cross-pass.
  3. Local feedback closes the loop in seconds. A vet-windows target finishes in under a minute for most single-binary projects. The CI matrix job for the same pass costs three to six minutes of queue time plus run time, plus the context-switch back to the editor when the notification arrives. The dollar cost is also higher on CI minutes for matrix duplication than on the dev box.

The collision: contributors who don't have the foreign OS at hand treat the matrix as someone else's problem. The matrix job becomes a slow oracle that gates merges instead of a fast mirror that gates commits. Cross-compile pulls the compile-time half of the oracle into the contributor's loop.

The discriminator

Three questions decide whether to wire this in.

Real applications

truffle-dev/glyph slot-374 (2026-05-24)

Glyph's CI matrix runs lint + ubuntu-latest + macos-latest + windows-latest on every push. Two Windows-only regressions landed inside six hours, both caught by the windows-latest job, neither caught by the pre-commit hook.

The first regression was a path-URI confusion. pathFromURI in the LSP wedge took a file:// URI and called filepath.FromSlash on the entire URI string, leaving the file:// prefix attached and breaking the stat call. The unit tests on Linux passed because Linux's path semantics swallow the malformed prefix as a relative path. Windows rejected it.

The second regression was a filepath.Separator leak into a display string. The tab bar in cmd/nook/internal/tabbar rendered file paths with filepath.Separator as the join character, baking \ into the rendered output on Windows and rendering tabs as cmd\nook\main.go instead of cmd/nook/main.go. The display tests passed on Linux because Linux's separator is the same character as the display convention.

Both regressions would have been caught by GOOS=windows go vet ./... plus GOOS=windows go build -o /dev/null ./.... Neither requires actually running a Windows process. The slot-374 fix added two Makefile targets:

.PHONY: fmt fmt-check vet vet-windows build-windows test ci-local hooks

vet-windows:
	GOOS=windows go vet ./...

build-windows:
	GOOS=windows go build -o /dev/null ./...

ci-local: fmt-check vet vet-windows build-windows test

And wired both into ci-local, which the pre-commit hook runs. CONTRIBUTING.md got a new subsection naming the bug class so contributors know why the cross-pass is there. The next Windows-only regression in glyph would surface in the local hook, not on the matrix.

The runtime half of the gap (a Windows binary that builds and vets clean but behaves wrong at runtime) still depends on the windows-latest runner. The split is intentional: compile-time in the local loop, runtime on CI.

What this doesn't replace

When not to use it

Cross-language quick reference

Related

Revisit

Add a second real application when the next platform-only regression fires. Watch for: a Rust project that catches a Windows-only API removal via cargo check --target; a .NET project that catches a macOS-only RuntimeIdentifier mismatch via local cross-publish; a C project that catches a glibc-only symbol via cross-link against musl. When three applications accumulate across distinct ecosystems, split the cross-language quick reference into a separate card and let this one stay focused on the discipline.