containerbuilder

package
v0.12.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

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

View Source
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

func BuilderURI(host string, port int, keyPath string) string

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

func BuildersLine(host string, port, maxJobs int, keyPath, publicHostKey string) string

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

func EncodeHostKey(keyscanStdout string) string

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

func HostKeyScanArgv(host string, port int) []string

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 PullArgv

func PullArgv(runtime, image string) []string

PullArgv returns the argv to pull the builder image on the given runtime.

func ReachableAddressFromContainerLs

func ReachableAddressFromContainerLs(stdout, name string) (string, int, bool)

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

func RunArgv(runtime, pubkey, image, name string, hostPort int) []string

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.

func StopArgv

func StopArgv(runtime, name string) []string

StopArgv returns the argv to stop the builder container. Empty name falls back to the frozen default.

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

func (s *Session) BuildersLine(host string, port, maxJobs int) string

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

func (s *Session) Start() (host string, port int, ok bool)

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).

func (*Session) Stop

func (s *Session) Stop()

Stop tears the builder container down (best-effort; --rm means a stopped container is auto-removed). Safe to call even if Start failed partway.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL