Skip to content

feat(windows): zig cross-build knob, AE_TEST_RUNNER hook, fast CI lane - #1596

Merged
paul-hammant merged 2 commits into
mainfrom
feat/windows-cross-lane
Aug 15, 2026
Merged

feat(windows): zig cross-build knob, AE_TEST_RUNNER hook, fast CI lane#1596
paul-hammant merged 2 commits into
mainfrom
feat/windows-cross-lane

Conversation

@paul-hammant

Copy link
Copy Markdown
Collaborator

Closes #1592, closes #1593, closes #1594 — the three-part plan for a fast Linux-hosted Windows signal.

Why: the MSYS2 legs take ~20 minutes, so Windows-only compile breakage is discovered long after the author context-switched away. This lane compiles the same code paths on a Linux runner in minutes. Minutes are free on a public repo — the prize is PR turnaround, and now also not burning 20 minutes of doomed compute when the fast lane fails.

WINDOWS=1 cross knob (#1592)

Mirrors FREEBSD=1, via zig cc -target x86_64-windows-gnu. Simpler than the FreeBSD leg: zig bundles the mingw-w64 headers and CRT, so there is no sysroot to fetch.

make compiler ae stdlib WINDOWS=1 ZIG=/path/to/zig
file build/ae.exe   # PE32+ executable (console) x86-64, for MS Windows

Capability-lean by design (out of scope: cross-compiling OpenSSL/nghttp2, the genuinely expensive part) — those std features ship as their "unavailable" stubs, exactly as on any box without the libraries. Vendored PCRE2 needs no host library, so std.regex survives.

Three host-uname-keyed spots had to opt out or the cross link picked up the host's POSIX libs (zig-lld: unable to find dynamic system library 'pthread'): AUDIO_LDFLAGS, the -pthread block, and __USE_MINGW_ANSI_STDIO — the last of which turned out to be correct as-is, since zig ships mingw-w64's libmingwex.

AE_TEST_RUNNER hook (#1592)

ae run / ae test prefix the binary they just built with the runner instead of exec'ing it — cargo's CARGO_TARGET_<triple>_RUNNER pattern. Empty by default, so every existing caller is byte-for-byte unaffected. 45 lines, two call sites.

The point is that it keeps target-awareness at the edge: no forked _windows spec files, nothing in std.spec or the test sources knows it is under an emulator, and the AE_SPEC_FORMAT/AE_SPEC_REPORT contract is inherited straight through the wrapper.

Fast CI lane (#1593)

windows-cross lives inside windows.yml, not a separate file — needs: only works within one workflow, and gating was the point. Both MSYS2 legs and headers-msvc now needs: windows-cross, so a fast-lane failure skips the slow tier. (needs:-skipped jobs report as skipped, not failed; the PR still reads red from the fast lane's own failure, so branch protection behaves.)

Steps: provision zig (same cache discipline as ci.yml's FreeBSD leg) → cross-build → assert the artifacts really are PE → warm the Wine prefix once → smoke-test hello-world → sweep with AE_TEST_RUNNER=wine.

Exclusions are named in tests/ae_sweep_prune_wine.txt, not skipped silently — fs/path semantics (Wine's case-mapping is not NTFS), sockets/h2/sendfile (IOCP and TransmitFile, not epoll/sendfile), actor-scheduler timing (flake under emulation), and the LD_PRELOAD sandbox. make test-ae gained AE_SWEEP_EXTRA_PRUNE=<file>, applied to both the .ae and .sh sweeps — the .sh sweep had no prune filter at all, so the excluded suites would otherwise have run anyway.

Build-target stamp (fell out of the work)

One build/ tree holds one target's objects and archives; mixing ELF and PE dies deep in the link with unknown file type, and a cross build silently clobbers libaether.a for the next native build. build/.build-target now makes the next mismatching build stop immediately with an actionable message. make clean/help are exempt — the guard must not block its own remedy.

Docs (#1594)

README's "enforced down to libc" names the platform boundary; docs/containment-sandbox.md states Windows is out by construction (there is no LD_PRELOAD, so this is not a port waiting to be written) and that a Wine run does not exercise containment at all — preloading into the wine process intercepts wine's libc calls, not the guest's, so a green run there would be actively misleading. docs/build-system.md documents both cross knobs and the runner hook.

Verification

  • Clean WINDOWS=1 build from scratch → PE32+ ae.exe + aetherc.exe, correct stamp.
  • AE_TEST_RUNNER proven to intercept with a logging wrapper on both ae run and ae test; inert when unset or empty.
  • Native build unaffected; 390 unit tests pass; AE_SWEEP_EXTRA_PRUNE verified end-to-end (687 → 203 tests with a test list, message reports the count).
  • Every pattern in the wine prune list checked to match real paths (no dead entries).
  • Full .ae sweep: 850 passing. The only failures are http_server_h2documented as failing on clean main in asks/h2-50-stream-stress-framing-layer-error.md — plus a load-related http_request_remote_addr flake that passes in isolation, and contrib_vulkan_portability (no GPU on this box).

Honest gap: the Wine execution half is unproven locally (no Wine installed here). The cross-build and the runner hook are both proven; CI is the first real exercise of AE_TEST_RUNNER=wine. If it needs tuning, expect it in the timeout or the prune list rather than the mechanism.

Incidentally the lane already earned its keep: the cross-build surfaced two Windows-only -Wsign-compare diagnostics inside Winsock's own FD_SET macro (SOCKET is unsigned). They originate in zig's bundled headers rather than our code, so the fast lane deliberately does not promote warnings to errors — that enforcement stays with the MSYS2 legs, against the headers Windows users actually compile with.

🤖 Generated with Claude Code

paul-hammant and others added 2 commits August 15, 2026 14:08
Closes #1592, #1593, #1594.

The MSYS2 Windows legs take ~20 minutes, so Windows-only compile
breakage surfaces long after the author moved on. This adds a fast lane
that compiles the same code paths on a Linux runner in minutes and runs
the base of the test pyramid under Wine.

WINDOWS=1 (#1592) — cross-build the ae/aetherc toolchain FOR Windows via
`zig cc -target x86_64-windows-gnu`. Companion to FREEBSD=1 and simpler:
zig bundles the mingw-w64 headers and CRT, so there is no sysroot to
fetch. Capability-lean like the FreeBSD leg (OpenSSL/zlib/nghttp2/YAML
forced off — host pkg-config would resolve the HOST's libs and poison
the target); vendored PCRE2 keeps std.regex. Three host-keyed spots
needed opting out or the cross link picked up host POSIX libs:
AUDIO_LDFLAGS, the -pthread block, and __USE_MINGW_ANSI_STDIO (which
turned out to be correct as-is under zig's libmingwex).

AE_TEST_RUNNER (#1592) — `ae run` / `ae test` prefix the binary they
just built with this runner instead of exec'ing it directly, modelled on
cargo's CARGO_TARGET_<triple>_RUNNER. Empty by default, so every
existing caller is unaffected. Keeps target-awareness at the EDGE:
nothing in std.spec or the test sources knows it is under an emulator,
and the AE_SPEC_* structured-report contract is inherited through the
wrapper.

CI fast lane (#1593) — `windows-cross` job in windows.yml (not a
separate file: `needs:` only works within one workflow). Cross-builds,
asserts the artifacts really are PE, warms a Wine prefix once, smoke-
tests hello-world, then sweeps with AE_TEST_RUNNER=wine. Both MSYS2 legs
and headers-msvc now `needs:` it, so a fast-lane failure skips ~20
minutes of doomed compute. Exclusions are named in
tests/ae_sweep_prune_wine.txt — fs/path semantics, sockets/h2/sendfile,
actor timing and the LD_PRELOAD sandbox, all areas where a green Wine
run would be misleading. `make test-ae` gained AE_SWEEP_EXTRA_PRUNE to
layer that list, applied to BOTH the .ae and the .sh sweeps (the latter
had no prune filter at all).

Build-target stamp — one build/ tree holds one target's objects; mixing
ELF and PE died deep in the link with "unknown file type". build/
now carries .build-target and a mismatching build stops immediately with
an actionable message. `make clean`/`help` are exempt, or the guard
would block its own remedy.

Docs (#1594) — README's "enforced down to libc" now names the platform
boundary, and docs/containment-sandbox.md states that Windows is out BY
CONSTRUCTION and that a Wine run does not exercise containment at all
(preloading into the wine process intercepts wine's libc, not the
guest's). docs/build-system.md documents both cross knobs and the runner.

Verified locally: clean WINDOWS=1 build produces PE32+ ae.exe/aetherc.exe;
AE_TEST_RUNNER proven to intercept via a logging wrapper on both `ae run`
and `ae test`, and inert when unset; native build + 390 unit tests
unaffected; every prune pattern matches real paths. The Wine half is
unproven locally (no Wine on this box) — CI exercises it first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI proved the Wine half of #1593 doesn't work as designed, so ship the
half that does rather than a lane that fails on every PR.

What the run showed: provisioning, cross-build, PE verification and the
Wine prefix all succeeded; the hello-world smoke step failed because
`ae.exe` under Wine tried to download 250 MB of MinGW-w64. That is
structural, not a config slip — `ae` is a compile-and-run driver, so
running it inside a Wine prefix needs a WINDOWS C toolchain there to
compile the C the compiler emits. The obvious alternative (drive the
NATIVE `ae` with AE_CC="zig cc -target x86_64-windows-gnu", so only the
resulting binary goes through Wine) needs a Windows libaether.a living
beside the native one — verified locally: it fails to link against the
Linux archive — and one build/ tree holds exactly one target's archives.
The sweep step would have failed too, independently: test-ae shells out
to build/ae, which is build/ae.exe in a WINDOWS=1 tree.

So windows-cross now cross-builds and asserts the artifacts are PE, and
stops there. The gating is unchanged and is most of the value: the MSYS2
legs still `needs:` it, so a compile break skips ~20 minutes of doomed
compute and surfaces at minute ~4.

AE_TEST_RUNNER stays — it works, it is tested, and it is exactly what a
future execution lane needs. tests/ae_sweep_prune_wine.txt stays too,
reframed as the design record: the durable part was always the ANALYSIS
of which areas a Wine lane must never claim to cover, not the wiring.
CHANGELOG and both docs now describe what ships instead of what was
planned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@paul-hammant

Copy link
Copy Markdown
Collaborator Author

Scope correction after the first CI run: the Wine execution half is deferred; this PR is now compile-only.

The run is worth reading as a result rather than a failure — it invalidated a premise in #1593's plan that neither of us had tested:

step outcome
Install toolchain + Wine
Provision zig
Cross-build (WINDOWS=1)
Verify artifacts are PE
Warm Wine prefix
Smoke: hello-world via ae.exe under Wine [ae] GCC not found. Downloading MinGW-w64 GCC (~250 MB)...

ae is a compile-and-run driver. Running ae.exe inside a Wine prefix means the compiler runs there too, so it wants a Windows C toolchain in that prefix to compile the C it emits. Wine alone was never enough.

I checked the obvious alternative before giving up on it: drive the native ae with AE_CC="zig cc -target x86_64-windows-gnu" so only the resulting binary goes through Wine. That needs a Windows libaether.a beside the native one — confirmed locally, it fails to link against the Linux archive — and one build/ tree holds exactly one target's archives (the very collision this PR's .build-target stamp exists to catch). The sweep step would also have failed independently: test-ae shells out to build/ae, which is build/ae.exe in a WINDOWS=1 tree.

That is a real piece of design work, not a tweak, so it belongs in its own issue rather than blocking this one.

What still ships, and why it is still worth it:

CHANGELOG, docs/build-system.md and docs/containment-sandbox.md all describe what ships rather than what was planned, and the deferred work is written down where the next person will find it.

Happy to close #1593 as partially-delivered and open a follow-up for the execution lane, or leave #1593 open scoped down to just that — your call on which reads better in the tracker.

@paul-hammant
paul-hammant merged commit fe5513f into main Aug 15, 2026
26 checks passed
@paul-hammant
paul-hammant deleted the feat/windows-cross-lane branch August 15, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant