# PLAN-DIST — bundle `zig` as sx's hermetic link/libc backend ## Goal `sx build` produces a native binary by driving a **bundled `zig`** (`zig cc`) as the linker, so a distributed sx on Linux needs no system `cc`/lld/libc/CRT. `sx run` (JIT) is unaffected — it never links. This is the "be like Zig" move: reuse Zig's hermetic toolchain (lld + crt objects + musl/glibc, all bundled in the `zig` distribution) instead of building our own lld-in-process + libc-from-source pipeline. > **Configuration surface** (env vars, flags, resolution order, > activation truth table, target→ABI map, distribution layout) is > specified in [../design/bundled-zig-link-backend-design.md](../design/bundled-zig-link-backend-design.md) — the design-of-record > for how the backend is configured. Keep the two files in sync. ## Locked decisions 1. **Default Linux output ABI = static musl** (`x86_64-linux-musl`, `-static`). Output runs on ANY Linux with zero deps — the property that makes Zig binaries portable. glibc/dynamic only via explicit `--target x86_64-linux-gnu`. 2. **Activation = auto** when a bundled/resolvable `zig` exists AND the user passed no `--linker`. Falls back to system `cc` otherwise. 3. **Dev uses PATH `zig`** (0.16.0 already installed). Defer copying a vendored toolchain into `libexec/` until Phase 3 packaging. ## Why `zig cc`, not raw `ld.lld` `zig cc` is a clang-compatible driver, so it slots into the **existing** cc-style argv branch in `src/target.zig` almost unchanged, and supplies lld + crt objects + musl/glibc automatically per `-target`. Driving `ld.lld` directly would force us to locate/pass crt1.o/crti.o/libc ourselves — exactly the work we're avoiding. ## Key code anchors (verified) - Linker selection hook: `TargetConfig.getLinker()` — `src/target.zig:194-196` (`self.linker orelse "cc"`). - Unix `cc`-style link branch: `src/target.zig:524-564` (this is where the zig backend hooks in; `-o`/`-L`/`-l`/extra objects already pass through clang-compatibly). - Exe-relative resolution pattern to mirror for finding zig: `src/imports.zig:204-227` (`discoverStdlibPaths`, `$SX_STDLIB_PATH` override + `/..` candidates). - `--linker` CLI flag parsing: `src/main.zig:87-90`. - Emit triple (must agree with link target): `src/ir/emit_llvm.zig` (`LLVMSetTarget`, ~L246-284). ## Phases ### Phase 0 — Resolve a bundled/host zig - New `src/zig_backend.zig`: `discoverZig(alloc) -> ?[]const u8`. Resolution order: 1. `$SX_ZIG` env override. 2. `/../libexec/zig/zig` (install layout, Phase 3). 3. `/../../zig-bundle/zig` (dev vendored layout, Phase 3). 4. `zig` on `PATH` (dev fallback — active now). - Add `SX_DEBUG_ZIG` trace, matching existing `SX_DEBUG_*` hooks. - No behavior change yet; just resolution + a debug/print hook to confirm. ### Phase 1 — `zig cc` link backend (core change) - `src/target.zig`: generalize the linker from a single token to a **driver argv**. Today `getLinker()` returns one string at `argv[0]`; introduce a `LinkBackend` so the internal backend contributes `{zigPath, "cc"}` as leading entries. - In the Unix branch (L524-564), when backend = zig: - prepend `zig cc`, - append `-target `, - add `-static` for musl, - everything else (`-o`, `-L`, `-l`, extra objects, extra link flags) passes through unchanged. - Add `sxTripleToZig()` mapping (sx shorthand/triple → zig `-target`); unspecified-on-Linux → `x86_64-linux-musl`. - Align emit triple: when the zig backend is selected, set the LLVM module triple in `emit_llvm.zig` to match the link target (x86_64-linux), so the `.o` links cleanly against musl crt. ### Phase 2 — Activation - Auto-enable: if `discoverZig()` succeeds and no `--linker` override, use the zig backend for `sx build`. System `cc` remains the fallback. - Optional explicit `--self-contained` / `--no-self-contained` to force. - Confirm `sx run`/JIT path is untouched (no link step). ### Phase 3 — Distribution packaging - `build.zig`: a `dist` step assembling - `bin/sx` (built with `-Dstatic-llvm`), - `libexec/zig/` (vendored zig binary **and its `lib/`**, copied from a pinned ziglang.org release per host arch), - `library/` (stdlib), into a relocatable tarball. - Pin the zig version (currently 0.16.0). ### Phase 4 — Verify & lock - Manual first: `sx build hello.sx` (auto zig backend) then `file`/`ldd` the output → expect "statically linked". - Honor snapshot-integrity + FFI-cadence rules before adding a corpus test (host/arch-gated, likely a `.build` sidecar). ## Risks / watch - **Bundle size**: zig + its `lib/` ≈ 50–60 MB. - **gnu vs musl ABI**: pure codegen objects link fine against musl; TLS/stack-protector are the only realistic friction. Aligning the emit triple (Phase 1) covers the common path. - **macOS/Windows cross** via the same `zig cc -target` is nearly free after Phase 1, but Apple-SDK linking has caveats — scope to Linux target first; treat the rest as follow-up. - **c_import.zig** also shells `cc` for C imports (JIT). Out of scope here; same backend can absorb it later. ## Status - [x] Phase 0 — resolve zig (`src/zig_backend.zig`) - [x] Phase 1 — zig cc link backend (`target.zig` + `emit_llvm` triple normalize) - [x] Phase 2 — activation (`--self-contained`/`--no-self-contained`; auto on bundled zig) - [ ] Phase 3 — dist packaging (vendor `zig` into `libexec/`) - [ ] Phase 4 — verify & lock (manual ✓ macOS/Linux/Windows; corpus test pending runner `--self-contained` support) Scope landed as **macOS + Linux + Windows** (not Linux-first). See the "Implementation status" section in [../design/bundled-zig-link-backend-design.md](../design/bundled-zig-link-backend-design.md) for what refined the original locked decisions.