Files
rdbms-playground/docs/ci/adr/20260613-adr-ci-003.md
T
claude@clouddev1 aeb92f56a7
ci / gate (push) Successful in 3m23s
docs(ci): record macOS implementation in ADR-ci-003 (D1 complete)
macOS is no longer deferred — built natively on a Tart (Apple-Silicon)
runner (real hardware → licensed SDK, no grey area). Amendment documents
release-macos.yaml (dispatch-only, needs main), the libiconv de-nix +
ad-hoc re-sign, the runner-label `:host` backend nuance, generation-based
cache pruning, and D2-on-macOS (system libs only). All six D1 targets now
produce artifacts. Updates the deferred list + index entry.
2026-06-15 15:56:38 +00:00

196 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).
## Amendment — 2026-06-14: macOS implemented (closes D1)
macOS is no longer deferred. The two `*-apple-darwin` targets now build on a
**Tart (Apple-Silicon) macOS runner** registered to Gitea — building on **real
Apple hardware** makes the SDK fully licensed, so the whole osxcross / SDK
grey-area + public-image-redistribution problem (§5 below) simply **does not
arise**. With all six D1 targets producing artifacts, **D1 is complete.**
Details, all verified on the runner via a throwaway smoke-test before wiring the
release leg:
- **`release-macos.yaml`** — `workflow_dispatch` with a `tag` input,
`runs-on: macos`. The runner registered as `macos:host`, but `:host` is
act_runner's execution-backend schema (run on host, no container), **not** part
of the label, so the label is `macos`. Steps: `cargo test` (macOS gets the only
automated test coverage outside the Linux gate — user choice) → build both
darwin targets natively through the flake (`apple-sdk` added to the devShell so
the toolchain links AppKit) → **upload to the same release** via the idempotent
create-or-get.
- **De-nix + re-sign.** The darwin stdenv bakes a `/nix/store` `libiconv` load
path into the binary (the *only* non-system dependency; everything else is
AppKit/Foundation/CoreGraphics/IOKit + `libSystem`/`libobjc`). The release step
rewrites it to `/usr/lib/libiconv.2.dylib` with `install_name_tool` and
**re-signs ad-hoc** (`codesign -f -s -`) — `install_name_tool` invalidates the
signature and Apple Silicon refuses an unsigned binary. A guard fails the build
if any `/nix/store` path remains. Result: portable, signed binaries (the native
one was confirmed to launch).
- **Dispatch-only, intermittent runner.** The Mac isn't always on, so macOS is a
separate dispatched workflow (not a job in `release.yaml`) — a release always
carries the four Linux/Windows assets regardless of the Mac, and the two macOS
assets are added by dispatching `release-macos` for that tag. **Caveat:** Gitea
exposes `workflow_dispatch` only for workflows on the **default branch**, so
`release-macos` becomes triggerable once the CI work is merged to `main`.
- **Cache hygiene (host-execution runner).** The runner wipes the workspace each
run, so cargo `target/` never accumulates; the persistent cache is the nix
store, bounded by **generation** — record the current devShell in a persistent
profile, keep the 2 newest generations (`nix-env --delete-generations +2`),
reclaim the rest. (The first sweep reclaimed a ~3.8 GB one-time backlog of
build scaffolding — source + build-only deps, not re-installed toolchains.)
- **D2 on macOS.** macOS binaries cannot be fully static (`libSystem` is always
dynamic); "no runtime deps" there means *system libraries only*, which the
de-nix step guarantees.
## 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** (~24 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)~~ — **done** via the Tart runner (see the
2026-06-14 amendment); §5 below is the as-deferred rationale, kept for history.
- **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).