Documentation
¶
Overview ¶
Package containerbuilder provides the on-demand container-based Linux builder for the macOS container runtimes. When a `packages:` build isn't cached, nix must offload to Linux; instead of a second hypervisor, a tiny nix+sshd builder runs as a container on the runtime already up, and the host drives it via nix's ssh-ng remote-builder protocol. The argv/URI/`builders`-line builders and the `container ls` ADDR parse are byte-exact contracts (the nix --builders line is as runbook-critical as internal/builder's); the pull/run/wait/stop lifecycle and the session context manager stay in the run wiring until the macos_user port lands.
Index ¶
- Constants
- func BuilderKey() string
- func BuilderKeyDir() string
- func BuilderSystem() string
- func BuilderURI(host string, port int, keyPath string) string
- func BuildersLine(host string, port, maxJobs int, keyPath, publicHostKey string) string
- func EncodeHostKey(keyscanStdout string) string
- func HostKeyScanArgv(host string, port int) []string
- func PullArgv(runtime, image string) []string
- func ReachableAddressFromContainerLs(stdout, name string) (string, int, bool)
- func RunArgv(runtime, pubkey, image, name string, hostPort int) []string
- func StopArgv(runtime, name string) []string
- type Deps
- type Session
Constants ¶
const ( BuilderImage = "ghcr.io/mschulkind-oss/yolo-jail-builder:latest" BuilderContainer = "yolo-linux-builder" BuilderSSHUser = "root" BuilderGuestPort = 22 BuilderHostPort = 31022 )
Frozen constants (byte-identical to container_builder.py).
Variables ¶
This section is empty.
Functions ¶
func BuilderKey ¶
func BuilderKey() string
BuilderKey is the private-key path; its .pub half is authorized in the container.
func BuilderKeyDir ¶
func BuilderKeyDir() string
BuilderKeyDir is the per-workspace host-daemon key dir under GLOBAL_STORAGE. that reads GLOBAL_STORAGE at import).
func BuilderSystem ¶ added in v0.8.0
func BuilderSystem() string
BuilderSystem is the nix system the container builder advertises: the LINUX system matching this host's architecture, because a Linux container on an arm64 Mac runs aarch64-linux and on an x86_64 Mac runs x86_64-linux.
This used to be hardcoded `aarch64-linux`, with the comment "the arch a Mac needs" — true of an Apple Silicon Mac and wrong of every other host. The consequence was specific and silent: nix would connect to a working builder, be told it serves aarch64-linux, and then decline to offload an x86_64-linux build to it — so on an x86_64 host the builder appeared healthy and did nothing. That is the second half of BACKLOG E8; the first half (publishing the builder image for both arches) landed in 7cc54a0, and a multi-arch image is useless while the advertised system is fixed.
Derived from GOARCH rather than probed: the builder runs a Linux container on THIS machine, so its system is a fact about the local architecture, known without asking anything. An unrecognized GOARCH passes through as `<goarch>-linux`, which is wrong in the same way a hardcoded constant is wrong but at least names what it saw — and nix rejects an unknown system loudly rather than silently not offloading.
func BuilderURI ¶
BuilderURI is the ssh-ng store/builder URI nix uses to reach the container. The key path is percent-encoded (nixQueryEscape), which nix decodes, so a path holding a space or a '&' is still the one ssh is handed.
func BuildersLine ¶
BuildersLine is a nix --builders spec pointing at the container. Format: "ssh-ng://user@host:port <system> key maxjobs", where <system> is BuilderSystem() — the Linux system for this host's arch, NOT a constant. keyPath falls back to BuilderKey() when empty; port 0 / maxJobs 0 fall back to defaults.
publicHostKey is nix's EIGHTH field, and it is the whole reason this offload can work at all on a normal macOS install. `builders` is a RESTRICTED nix setting: a client that passes it does not act on it — the setting crosses the daemon socket and the NIX-DAEMON, running as root, forks `ssh` itself. So the NIX_SSHOPTS the caller exports never reaches that ssh (the launchd plist sets no environment, and a fork inherits the daemon's), and root meets an unknown host key — this builder regenerates one on every boot — with StrictHostKeyChecking at OpenSSH's default `ask`, no tty and no askpass. That is a deterministic
Host key verification failed. cannot build on 'ssh-ng://root@127.0.0.1:31022': error: failed to start SSH connection
and, because nix wires ssh's stderr to a log fd only for the legacy `ssh://` scheme, the first of those two lines lands in /var/log/nix-daemon.log and never in the caller's output. Both halves measured 2026-09-13 against nix 2.34.8 and reproduced byte-for-byte from the macOS nightly's logs.
Filling field 8 replaces the option nix cannot deliver with one it can: nix writes "<host> <key>" to a temp file and passes `-oUserKnownHostsFile=<that>`, so the connection is VERIFIED rather than unchecked, in the daemon's process, with nothing to configure on the host. Fields 5-7 (speedFactor, supported, mandatory) have to be spelled to reach it, and "-" is nix's own "default" token. Empty publicHostKey keeps the historical 4-field line.
A KEY PATH NIX WOULD SPLIT MOVES INTO THE STORE URI. The key sits under the state dir in the user's home, so a home holding a space put that space in field 3: every later field moved one place right and nix refused the whole line ("bad machine specification: failed to convert column #3 … to 'unsigned int'", measured against nix 2.34.8), while a ';' or '#' cut the line at that byte, so ssh got a truncated key path and no host-key pin. nix has no quoting for a field, but it percent-decodes the URI's query, and a key field of "-" leaves the URI's ssh-key in force. So such a path is spelled `?ssh-key=<encoded>` on field 1 with "-" in field 3, and every other path keeps the field-3 line, byte for byte. builderslineparse_test.go reads both forms as nix does and records the measurement.
func EncodeHostKey ¶ added in v0.9.0
EncodeHostKey turns ssh-keyscan stdout into the base64 blob BuildersLine's eighth field wants: base64 of "<type> <key>", which is what nix base64-DECODES and writes after the hostname into the known-hosts file it hands ssh. Returns "" when the output carries no key line — ssh-keyscan prints a "# host:port SSH-2.0-…" banner comment on stderr AND stdout, and prints nothing at all when the far end never answers.
The comment the scan emits alongside the key ("root@<container id>") is dropped: nix writes the decoded bytes verbatim, and a known-hosts line is <host> <type> <key>, with anything after the key ignored — but keeping it would put a container-id into a launch's argv for no gain.
func HostKeyScanArgv ¶ added in v0.9.0
HostKeyScanArgv reads the builder's SSH host key off the wire, from the same address nix is about to be pointed at.
It is deliberately NOT `podman exec cat /etc/ssh/ssh_host_ed25519_key.pub`, which would be a stronger provenance claim and a WEAKER probe: this runs end to end over the address that has to work, so it fails when the path to the builder is broken even though the container is healthy — which is the failure this offload actually has on macOS. It also needs no per-runtime argv, so podman and Apple Container share one spelling.
ed25519 only, because that is the one HostKey the builder image's sshd config declares. Asking for a type the server does not have returns nothing rather than a wrong key, and Start treats "nothing" as "not ready yet".
func ReachableAddressFromContainerLs ¶
ReachableAddressFromContainerLs parses `container ls` stdout for the running builder's VM IP:22 (Apple Container has no host port-publish). container branch of reachable_address: skip header, find the row whose first field == name, then the first token containing exactly 3 dots is the ADDR (e.g. "192.168.64.2/24"); strip the "/mask". Returns (host, port, true) or (,,false). The podman branch (always 127.0.0.1:BUILDER_HOST_PORT) is a constant the caller applies directly.
func RunArgv ¶
RunArgv returns the argv to start the builder container detached. podman publishes sshd to 127.0.0.1:<hostPort>; Apple Container has no -p (each container gets its own VM IP), so the publish is omitted there. Mirrors run_argv. Empty image/name/0 hostPort fall back to the frozen defaults.
Types ¶
type Deps ¶
type Deps struct {
// Run runs argv (inherit stdio) and returns the return code.
Run func(argv []string) int
// Output runs argv and returns (stdout, rc) — `container ls` ADDR discovery on
// Apple Container, and the `ssh-keyscan` readiness probe on BOTH runtimes.
Output func(argv []string) (string, int)
// Reachable reports whether host:port accepts a TCP connection. A cheap negative
// filter ONLY: on podman machine an accepted connection says nothing about
// whether sshd exists behind it (see Start).
Reachable func(host string, port int) bool
// Sleep pauses for the given seconds (poll backoff). Injectable for tests.
Sleep func(seconds float64)
// Now returns a monotonic-ish wall clock in seconds (poll deadline).
Now func() float64
// Out receives human progress lines. nil => io.Discard.
Out io.Writer
}
Deps are the injectable subprocess/clock seams for a builder Session.
type Session ¶
type Session struct {
Runtime string // "podman" | "container"
Pubkey string // authorized_keys public half baked into the container
Deps Deps
// contains filtered or unexported fields
}
Session drives one builder container's lifecycle for one build.
func (*Session) BuildersLine ¶
BuildersLine returns the nix --builders spec for this session's resolved address, carrying the host key Start observed (convenience wrapper over the pure constructor). Called before Start, it yields the unpinned four-field line — which is the line that cannot authenticate against a daemon nix, so do not.
func (*Session) Start ¶
Start pulls the builder image and runs the container detached, then polls until its sshd is reachable. Returns (host, port, ok). On podman the sshd is published to 127.0.0.1:BuilderHostPort; on Apple Container (no -p) the VM IP is discovered from `container ls`. ok=false means the builder never came up (the caller falls back to the plain build + failure diagnosis).