# 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 + `<exe>/..` 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. `<exe>/../libexec/zig/zig` (install layout, Phase 3).
  3. `<exe>/../../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 <mapped triple>`,
  - 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.
