Record the multi-platform build strategy as its own decision: cargo-zigbuild for the four non-macOS targets, the static/standalone posture per platform, the Windows synchronization stub, the test->build matrix workflow, and the macOS deferral with its licensing rationale (the public CI image can't carry the SDK). Shrinks the ci-001 amendment to a pointer; updates the index. Runtime-verified by the user: Linux x86_64 + Windows aarch64 run correctly.
7.2 KiB
ADR-ci-003: Cross-platform release builds (the D1 matrix)
Status
Accepted (2026-06-13); implemented the same day on the ci branch. Every
fork was settled with the user. Verified end-to-end:
- all four targets cross-build locally from Linux x86_64;
- the Linux binaries are statically linked (D2); the Windows artifacts are valid PE32+ (x86-64 / Aarch64);
- a real release-matrix run (tag
v.0.0.0-citest3) published 8 assets — the four binaries + a.sha256each.
Runtime-verified (2026-06-13, by the user): the Linux x86_64 and Windows aarch64 binaries launch and run correctly — one of each OS family and both architectures. The remaining two (Linux aarch64, Windows x86_64) are link-clean and valid format but not yet runtime smoke-tested.
This ADR records the cross-platform build strategy; it sits on top of ADR-ci-002 (the nix flake, which now carries the cross toolchain) and ADR-ci-001 (the pipeline, whose release job this fills in).
Context
requirements.md D1 asks for binaries on Linux, macOS, Windows × x86_64
and aarch64 (six targets); D2 asks for a single static binary, no
runtime deps. The CI runner executes jobs in a Linux x86_64 container
(ADR-ci-001), so every target is cross-compiled from Linux.
What's feasible is decided almost entirely by one dependency — arboard
(the clipboard backend for the copy command). Its per-platform backends in
Cargo.lock:
| Target family | arboard backend | Needs a platform SDK to cross-link? |
|---|---|---|
| Linux x86_64 / aarch64 | x11rb (pure Rust) |
No |
| Windows x86_64 / aarch64 | clipboard-win + windows-sys (import libs bundled) |
No |
| macOS x86_64 / aarch64 | objc2-app-kit → links AppKit |
Yes — Apple's SDK |
So four targets cross-compile with no SDK; macOS is the hard wall — AppKit can only be linked against Apple's SDK.
Decision
1. Tooling — cargo-zigbuild
Cross-compile with cargo-zigbuild (Zig's bundled clang + libc as a single
universal cross cc/linker), added to the flake devShell alongside zig. One
tool serves every non-macOS target, including the cc-crate compile of
rusqlite's bundled SQLite C, with no per-target toolchain. It replaced the
earlier single-target musl cc (ADR-ci-002's first cut).
2. Targets this iteration — the four non-macOS
Added to rust-toolchain.toml and the release matrix:
x86_64-unknown-linux-musl,aarch64-unknown-linux-musl— musl +crt-static, so fully static portable binaries (D2);x86_64-pc-windows-gnu,aarch64-pc-windows-gnullvm— Zig statically links its libc, so the.exeis standalone (no mingw runtime DLLs).
3. The Windows synchronization stub
Rust's std links -lsynchronization (its WaitOnAddress-based thread
parking). That import library is normally supplied by Rust's rust-mingw
"self-contained" component — which rust-overlay does not ship — and Zig's
mingw doesn't carry it either, so the link fails with "unable to find dynamic
system library 'synchronization'". The functions (WaitOnAddress,
WakeByAddress*) are forwarded by kernel32 (already linked), so an
empty stub libsynchronization.a (committed at ci/winstub/, 8 bytes,
wired via .cargo/config.toml for the Windows targets only) satisfies the
linker without contributing symbols. Host and Linux builds are untouched by it.
4. Workflow shape — test once, then a build matrix
release.yaml is test → build:
testruns once on the host (cargo test) — a tag never publishes untested code;buildis a matrix over the four targets (needs: test,fail-fast: false), eachcargo zigbuild --release --target <triple>, then packages the binary (.exefor Windows) + a.sha256and uploads both to the shared release via an idempotent create-or-get (the first matrix job creates the release; the rest fetch it).
5. macOS — deferred, with rationale
macOS is not in this iteration. arboard→AppKit needs the macOS SDK, and:
- the SDK ships only inside Xcode; Apple's license ties its use to Apple-branded hardware, so using it on a Linux runner is a grey area (widely done, low enforcement, but technically against the terms);
- redistributing the SDK is a clearer violation — and our CI image is public, so the SDK cannot be baked into it even if the grey area were accepted; it would have to live in a private store;
- the clean path is building on real Apple hardware (a Mac registered as a Gitea runner, or hosted Mac CI), where the SDK is fully licensed.
macOS therefore becomes its own step, choosing between (a) osxcross + a private SDK kept out of the public image, or (b) a Mac runner. The user decides when we get there.
Consequences
- D1: four of six targets met from a single Linux runner; D2 met on
Linux (static musl). Windows
.exes are standalone. - Runtime coverage: Linux x86_64 + Windows aarch64 confirmed running (user, 2026-06-13); Linux aarch64 + Windows x86_64 are the outstanding runtime checks.
- Each matrix target recompiles from scratch (~2–4 min; ~10 min total on the single runner), and Zig's per-target libc cache is cold each run. Fine at release frequency; cacheable later if it matters.
- The empty stub depends on
kernel32forwardingWaitOnAddress(true on Windows 8+), which covers every supported target. - Asset naming
rdbms-playground-<tag>-<target>[.exe]is close to whatcargo-binstall/ the D3 package managers will want.
Alternatives considered
cross(cross-rs). Docker-image-per-target; covers Linux + Windows but not macOS (no legally redistributable Apple images), and needs DinD orchestration inside our job. Rejected — no macOS, more moving parts than zigbuild.- Per-target nix cross (
pkgsCross). Clean for Linux-musl and Windows-x86_64 (mingw-w64, which does shiplibsynchronization.a), but Windows-aarch64 isn't readily packaged and macOS-from-Linux is unsupported in nixpkgs. Rejected — incomplete. - Native runners per OS. Cleanest for macOS/Windows, but needs mac/windows runners we don't have. Kept on the table specifically for the deferred macOS step.
- A real
libsynchronization.a(from nixpkgs mingw or arust-mingwcomponent) instead of the empty stub. More principled, but more flake machinery, doesn't cover Windows-aarch64, and unnecessary — the stub links clean because the symbols resolve viakernel32.
Deferred / out of scope
- macOS (x86_64 + aarch64) — the SDK/runner decision above.
- D3 packaging — Homebrew / Scoop / winget /
cargo-binstallmanifests (and binstall-friendly archive naming). - CI speed — caching per-target builds / Zig's libc cache.
- Runtime smoke test of the two not-yet-checked targets (Linux aarch64, Windows x86_64).
Relationship to other decisions
- Extends ADR-ci-002 — the flake devShell now carries
cargo-zigbuild+zigand the four release targets. - Fills in ADR-ci-001 §3 (Release) — that single-target job is now this matrix.
- Advances
requirements.mdD1 (4/6) and D2 (Linux, done).