From 5869eec4f4275d35abf3cc01727edd7f4bba142d Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Sat, 13 Jun 2026 19:11:29 +0000 Subject: [PATCH] =?UTF-8?q?docs(ci):=20ADR-ci-003=20=E2=80=94=20cross-plat?= =?UTF-8?q?form=20release=20builds=20(D1=20matrix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/ci/adr/20260612-adr-ci-001.md | 34 ++----- docs/ci/adr/20260613-adr-ci-003.md | 151 +++++++++++++++++++++++++++++ docs/ci/adr/README.md | 3 +- 3 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 docs/ci/adr/20260613-adr-ci-003.md diff --git a/docs/ci/adr/20260612-adr-ci-001.md b/docs/ci/adr/20260612-adr-ci-001.md index 8389a59..1d64ccf 100644 --- a/docs/ci/adr/20260612-adr-ci-001.md +++ b/docs/ci/adr/20260612-adr-ci-001.md @@ -24,34 +24,14 @@ it rather than restating it. > previously forced website ADRs to be renumbered (see that namespace's > history note and ADR-0000 "Numbering discipline"). -## Amendment — 2026-06-13: D1 matrix expanded (non-macOS targets) +## Amendment — 2026-06-13: D1 matrix (non-macOS) -The release now builds the **four non-macOS D1 targets**, all cross-compiled -from the Linux x86_64 runner with **`cargo-zigbuild`** (Zig's bundled clang + -libc as one universal cross cc/linker — including the `cc`-crate compile of -rusqlite's bundled SQLite C — added to the flake devShell, replacing the -single-target musl cc): - -- `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl` — static (D2); -- `x86_64-pc-windows-gnu`, `aarch64-pc-windows-gnullvm` — standalone `.exe`. - -`release.yaml` became a **`test` (once, host) → `build` (matrix over the four -targets)** workflow; each matrix job uploads its artifact + `.sha256` to the -shared release (idempotent create-or-get). - -**Windows link fix:** Rust's std links `-lsynchronization` (WaitOnAddress -thread-parking), an import lib that rust-overlay's toolchain doesn't ship and -Zig's mingw lacks. Those symbols are forwarded by `kernel32` (already linked), -so an **empty stub** `libsynchronization.a` (committed at `ci/winstub/`, wired -via `.cargo/config.toml` for the Windows targets only) satisfies the linker. -Verified locally: all four build; the Linux binaries are statically linked; the -Windows artifacts are valid PE32+ (x86-64 / Aarch64) — not yet runtime -smoke-tested on Windows. - -**macOS stays deferred** (see Deferred): `arboard`→AppKit needs Apple's SDK, -which a Linux runner can't supply cleanly — and the CI image is *public*, so the -SDK can't be baked in even if the licensing grey area were accepted. macOS is -its own step (osxcross + a private SDK, or a real Mac runner). +§3 (Release) below describes the original **single-target** (x86_64 Linux) job. +The release is now a **`test` → `build` matrix** over the four non-macOS D1 +targets (Linux + Windows × x86_64/aarch64), cross-built with `cargo-zigbuild`. +The full decision — tooling, targets, the Windows `synchronization` stub, the +matrix shape, and the macOS deferral with its licensing rationale — is recorded +in its own record: **[ADR-ci-003](20260613-adr-ci-003.md)**. ## Context diff --git a/docs/ci/adr/20260613-adr-ci-003.md b/docs/ci/adr/20260613-adr-ci-003.md new file mode 100644 index 0000000..f840fc3 --- /dev/null +++ b/docs/ci/adr/20260613-adr-ci-003.md @@ -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 `, 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--[.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). diff --git a/docs/ci/adr/README.md b/docs/ci/adr/README.md index 0a2339c..74a2223 100644 --- a/docs/ci/adr/README.md +++ b/docs/ci/adr/README.md @@ -18,5 +18,6 @@ here too). ## Index -- [ADR-ci-001 — CI + release pipeline on Gitea Actions](20260612-adr-ci-001.md) — **Accepted 2026-06-12** (implemented the same day on the `ci` branch). Establishes the CI/release pipeline on the self-hosted Gitea instance's Actions runner (`ci-public`). **Runner model** (established by a throwaway probe): jobs execute *inside* a container (`catthehacker/ubuntu:act-22.04` by default), as root, so the runner host's nix is **not** reachable from steps. **Toolchain delivery:** a **baked CI image** — `node:22-bookworm-slim` (satisfies the act_runner job-container contract: `/bin/sleep` keep-alive, `bash`, `node` for JS actions; a bare `nixos/nix` image lacks these and won't start) **+ single-user nix + the flake's devShell pre-warmed** — built by `build-ci-image.yaml` via DinD and pushed to the Gitea container registry as a **public** package, so CI runs `nix develop -c …` against the **same pinned toolchain as dev** (the flake, ADR-ci-002) with a warm store (~1.4 s to a ready toolchain). **Gate** (`ci.yaml`): `clippy -D warnings` + `cargo test` inside that image on branch pushes + PRs; **fmt deliberately not gated** (the tree isn't stock-rustfmt-clean — user decision, revisit on `main`; see ADR-ci-002). **Release** (`release.yaml`): on a `v*` tag, runs the tests, builds the **static `x86_64-unknown-linux-musl` binary** (D2: single static binary, no runtime deps — the glibc/nix build is non-portable), strips it, and publishes it + a `.sha256` to a Gitea release via the API and the auto-provided `GITEA_TOKEN`. **Triggers:** gate + image-build are scoped to **branch** pushes (`branches: ['**']`) so a release tag doesn't spuriously re-run them; the image-build additionally path-filters to its inputs (Dockerfile/flake/toolchain); the release owns tags. **Auth:** a dedicated PAT (`REGISTRY_USERNAME`/`REGISTRY_TOKEN` secrets) pushes the image; the auto `GITEA_TOKEN` publishes releases. **Scope:** the **four non-macOS D1 targets** (Linux + Windows × x86_64/aarch64) cross-built from Linux via cargo-zigbuild (2026-06-13 amendment); macOS, D3 package-manager manifests, CI-speed dependency caching, and the website's static→Cloudflare deploy remain deferred, added step by step. Verified live: probe → runner facts; image built + checked locally; gate green (**2424 tests**); release exercised end-to-end (`v0.0.0-citest2` published with binary + checksum). Builds on **ADR-ci-002** (the nix flake, relocated here from main's ADR-0049 to avoid exactly this cross-branch collision). +- [ADR-ci-001 — CI + release pipeline on Gitea Actions](20260612-adr-ci-001.md) — **Accepted 2026-06-12** (implemented the same day on the `ci` branch). Establishes the CI/release pipeline on the self-hosted Gitea instance's Actions runner (`ci-public`). **Runner model** (established by a throwaway probe): jobs execute *inside* a container (`catthehacker/ubuntu:act-22.04` by default), as root, so the runner host's nix is **not** reachable from steps. **Toolchain delivery:** a **baked CI image** — `node:22-bookworm-slim` (satisfies the act_runner job-container contract: `/bin/sleep` keep-alive, `bash`, `node` for JS actions; a bare `nixos/nix` image lacks these and won't start) **+ single-user nix + the flake's devShell pre-warmed** — built by `build-ci-image.yaml` via DinD and pushed to the Gitea container registry as a **public** package, so CI runs `nix develop -c …` against the **same pinned toolchain as dev** (the flake, ADR-ci-002) with a warm store (~1.4 s to a ready toolchain). **Gate** (`ci.yaml`): `clippy -D warnings` + `cargo test` inside that image on branch pushes + PRs; **fmt deliberately not gated** (the tree isn't stock-rustfmt-clean — user decision, revisit on `main`; see ADR-ci-002). **Release** (`release.yaml`): on a `v*` tag, runs the tests, builds the **static `x86_64-unknown-linux-musl` binary** (D2: single static binary, no runtime deps — the glibc/nix build is non-portable), strips it, and publishes it + a `.sha256` to a Gitea release via the API and the auto-provided `GITEA_TOKEN`. **Triggers:** gate + image-build are scoped to **branch** pushes (`branches: ['**']`) so a release tag doesn't spuriously re-run them; the image-build additionally path-filters to its inputs (Dockerfile/flake/toolchain); the release owns tags. **Auth:** a dedicated PAT (`REGISTRY_USERNAME`/`REGISTRY_TOKEN` secrets) pushes the image; the auto `GITEA_TOKEN` publishes releases. **Scope:** the original release job was Linux x86_64 only; it's now the **four non-macOS D1 targets** (Linux + Windows × x86_64/aarch64) cross-built via cargo-zigbuild — see **ADR-ci-003**. macOS, D3 package-manager manifests, CI-speed dependency caching, and the website's static→Cloudflare deploy remain deferred, added step by step. Verified live: probe → runner facts; image built + checked locally; gate green (**2424 tests**); release exercised end-to-end (`v0.0.0-citest2` published with binary + checksum). Builds on **ADR-ci-002** (the nix flake, relocated here from main's ADR-0049 to avoid exactly this cross-branch collision). - [ADR-ci-002 — Nix flake for a reproducible dev + build environment](20260612-adr-ci-002.md) — **Accepted 2026-06-12** (relocated from main's **ADR-0049** on the same day — content unchanged — to keep CI/dev-env decisions out of `main`'s integer sequence). The single, version-pinned declaration of the **dev *and* build toolchain** so CI never relies on whatever Rust is on the build machine — mirroring **datamage ADR 0046**, but far simpler (pure-Rust TUI). Root **Nix flake** with two outputs: **`devShells.default`** (pinned **Rust 1.95.0** via `rust-toolchain.toml` + `rust-overlay`, `cargo-sweep`, and the musl cc for the static release build) and **`packages.default`** (`rustPlatform.buildRustPackage` from the committed `Cargo.lock`; `doCheck = false`). Exact-pin (not floating `stable`) so `nix flake update` can't surprise-bump clippy past the `-D warnings` gate. System inputs near-empty by design (`libsqlite3-sys bundled` → stdenv cc only; `arboard`→`x11rb` pure-Rust). `.envrc` (`use flake`) for direnv parity. Verified through the flake: `nix build` yields a working binary, clippy clean, **2424 tests pass / 0 fail / 1 intentional ignored doctest**. Consumed by **ADR-ci-001** (the pipeline). Alternatives rejected: dev-shell-only; a standard `rust:1.95` CI image (a second toolchain definition = drift); `rustup` on the build host (non-reproducible). +- [ADR-ci-003 — Cross-platform release builds (the D1 matrix)](20260613-adr-ci-003.md) — **Accepted 2026-06-13** (implemented + a real matrix release verified the same day — tag `v.0.0.0-citest3` published 8 assets). Cross-compiles the **four non-macOS D1 targets** from the Linux x86_64 runner with **`cargo-zigbuild`** (Zig's bundled clang + libc as one universal cross cc/linker, incl. rusqlite's bundled SQLite C; added to the flake devShell, replacing the single-target musl cc): **`x86_64`/`aarch64-unknown-linux-musl`** (musl + crt-static → fully static, **D2**) and **`x86_64-pc-windows-gnu`** / **`aarch64-pc-windows-gnullvm`** (Zig statically links libc → standalone `.exe`). **Windows `synchronization` stub:** Rust std links `-lsynchronization` (WaitOnAddress thread-parking), an import lib rust-overlay's toolchain doesn't ship and Zig's mingw lacks; the symbols are forwarded by `kernel32`, so an **empty 8-byte stub** `libsynchronization.a` (`ci/winstub/`, wired via `.cargo/config.toml` for the Windows targets only) satisfies the linker. **Workflow:** `release.yaml` = **`test` once (host) → `build` matrix** over the four targets (`needs: test`, `fail-fast: false`); each job packages binary (`.exe` for Windows) + `.sha256` and uploads to the **shared release** via idempotent create-or-get. **macOS deferred** — `arboard`→AppKit needs Apple's SDK, a licensing grey area on a Linux runner, and the **public** CI image can't carry it; its own step (osxcross + a private SDK, or a Mac runner). Alternatives rejected: `cross` (no macOS, needs DinD), per-target nix cross (Windows-aarch64 unpackaged, macOS-from-Linux unsupported), a real `libsynchronization.a` (more machinery, doesn't cover Windows-aarch64). Runtime-verified by the user (2026-06-13): Linux x86_64 + Windows aarch64 run correctly; Linux aarch64 + Windows x86_64 are the outstanding runtime checks. Builds on ADR-ci-002 (flake) and fills in ADR-ci-001 §3 (Release).