docs(ci): ADR-ci-003 — cross-platform release builds (D1 matrix)
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.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# 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 `.sha256` each.
|
||||
|
||||
**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 `.exe` is **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`**:
|
||||
|
||||
- **`test`** runs once on the host (`cargo test`) — a tag never publishes
|
||||
untested code;
|
||||
- **`build`** is a **matrix over the four targets** (`needs: test`,
|
||||
`fail-fast: false`), each `cargo zigbuild --release --target <triple>`, then
|
||||
packages the binary (`.exe` for Windows) + a `.sha256` and 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 `.exe`s 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 `kernel32` forwarding `WaitOnAddress`** (true on
|
||||
Windows 8+), which covers every supported target.
|
||||
- **Asset naming** `rdbms-playground-<tag>-<target>[.exe]` is close to what
|
||||
`cargo-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* ship `libsynchronization.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 a `rust-mingw`
|
||||
component) 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 via `kernel32`.
|
||||
|
||||
## Deferred / out of scope
|
||||
|
||||
- **macOS** (x86_64 + aarch64) — the SDK/runner decision above.
|
||||
- **D3 packaging** — Homebrew / Scoop / winget / `cargo-binstall` manifests
|
||||
(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` +
|
||||
`zig` and the four release targets.
|
||||
- **Fills in ADR-ci-001 §3 (Release)** — that single-target job is now this
|
||||
matrix.
|
||||
- **Advances `requirements.md`** **D1** (4/6) and **D2** (Linux, done).
|
||||
Reference in New Issue
Block a user