# SDD 02 — El laboratorio de build El laboratorio es la fábrica de artefactos: toma una **receta** (qué compilar y cómo) y produce un **árbol de archivos** depositado en el store, direccionado por su hash. Es la única parte del sistema que se permite ser funcional/determinista, porque es donde paga. ## 1. Receta (`Recipe`) Una receta es una descripción declarativa y pura de *un* build. Es el átomo del grafo. ```toml # ejemplo: recipes/grep.toml name = "grep" version = "3.11" [source] repo = "git://git.savannah.gnu.org/grep.git" commit = "a1b2c3d4e5f6..." # FIJADO. Nunca "HEAD". Ver ADR 0006. patches = ["patches/grep-perl-default.patch"] [build] compiler = "zig-cc" # por defecto; escotilla: "clang" | "gcc" target = "x86_64-linux-musl" link = "static" # "static" | "dynamic" flags = ["--enable-perl-regexp"] # fases override opcionales; si se omiten, se usan las heurísticas estándar # configure = "..." compile = "..." install = "..." [deps] build = ["pcre2", "musl"] # dependencias de compilación (otras recetas) runtime = [] # vacío si link = static ``` El **compilador es por receta** (no global): `zig-cc` por defecto por hermeticidad y musl incluido, con escotilla a `clang`/`gcc` para paquetes con gcc-ismos. Ver [ADR 0003](adr/0003-zig-cc-builder.md). ## 2. El identificador de contenido (CAS) Cada artefacto se direcciona por un hash que captura **todo** lo que influye en su salida: ``` artifact_hash = BLAKE3( source_commit // commit git fijado ⧺ canonical(patches) // contenido de los parches, no su ruta ⧺ canonical(build_flags) // flags + compilador + target + link ⧺ Σ dep.artifact_hash // hashes de las dependencias de build (recursivo) ) ``` Consecuencia: **entrada idéntica ⇒ hash idéntico ⇒ salida idéntica**. Esto es lo que permite: - **Caché:** ¿el hash ya existe en el store? Se reusa, no se recompila. - **Avance ordenado del grafo:** si un upstream avanza su commit fijado, sólo cambian los hashes de ese nodo y sus dependientes; el resto del grafo se reusa. - **Verificación de `.swm`:** dos máquinas con la misma receta producen el mismo hash; así se verifica un cambio compartido sin confiar en binarios (ver [SDD 09](09-trust-model.md)). > BLAKE3 por velocidad y paralelismo; el hash no es un secreto, es un identificador. ## 3. El sandbox de build Cada build corre en su propio entorno aislado, montado con **bubblewrap** (`bwrap`), que usa namespaces de Linux (mount, pid, net, user) sin requerir root real cuando el kernel lo permite. Receta del aislamiento: 1. **Raíz tmpfs.** Se monta un `/` temporal vacío en tmpfs. Nada del host es visible salvo lo que montemos explícitamente. 2. **Compilador inyectado.** `zig` (o el compilador de la receta) se monta read-only en el `PATH` del sandbox. En el bootstrap es el único compilador presente — así se rompe el cordón umbilical con el host. 3. **Fuentes read-only.** El árbol del repo (en el commit fijado) se monta read-only. 4. **Dependencias read-only.** Los artefactos de las deps de build, ya en el store, se montan read-only en rutas conocidas. 5. **Sin red.** El namespace de red se aísla: el `fetch` ocurre *fuera* del sandbox (ver §4), de modo que el build en sí no puede descargar nada no declarado → hermeticidad real. 6. **Salida vía `DESTDIR`.** El build instala en `DESTDIR=/out`; ese árbol es el artefacto que se extrae y se sella en el store. ## 4. Fases del build Inspiradas en BitBake (`do_fetch`/`do_unpack`/`do_configure`/`do_compile`/`do_install`) pero mínimas. Sólo `fetch` toca la red, y ocurre **antes** de entrar al sandbox: | Fase | ¿En sandbox? | Qué hace | |---|---|---| | `fetch` | No (red permitida) | Clona/actualiza el repo al commit fijado; verifica hash | | `patch` | Sí | Aplica `source.patches` sobre el árbol read-only (en copia tmpfs) | | `configure` | Sí | Detecta sistema de build (autotools/cmake/meson/make) y configura | | `compile` | Sí | Compila con el compilador de la receta hacia `target`/`link` | | `install` | Sí | `make install DESTDIR=/out` (o equivalente) | | `seal` | No | Calcula el hash final del árbol `/out`, lo deposita en el store | La detección de sistema de build en `configure` es heurística (presencia de `configure.ac`, `CMakeLists.txt`, `meson.build`, `Makefile`) y se puede sobreescribir por receta. ## 5. El grafo de dependencias Las recetas forman un DAG por sus `deps.build`. El algoritmo de build: ``` build(recipe): h = artifact_hash(recipe) # requiere resolver deps primero (recursivo) if store.has(h): return h # caché for dep in recipe.deps.build: build(dep) # post-orden: deps antes que el nodo sandbox = make_sandbox(recipe, deps) run_phases(sandbox) # patch → configure → compile → install return store.seal(sandbox.out, h) ``` Builds de nodos independientes se paralelizan (sin dependencia mutua en el DAG). El cap de concurrencia se ajusta a los cores disponibles. ## 6. Bootstrap (romper el cordón umbilical) El mayor error es usar las herramientas del host para compilar el sistema final. `zig cc` reduce drásticamente este problema: un solo binario provee compilador C/C++ + musl + headers, hermético, cross-compilando al target. El Stage 0 tradicional (cross-compiler mínimo) queda en gran parte resuelto por `zig`. Para la fase Alpine no necesitamos bootstrap completo: Alpine ya provee musl y un toolchain; usamos `zig cc` para los builds de hammer y validamos el flujo. El bootstrap from-scratch real es trabajo del track de distro propia ([SDD 10](10-roadmap.md), [ADR 0003](adr/0003-zig-cc-builder.md)). ## 7. Interfaz (qué expone el crate) ```rust // hammer-build pub fn build(recipe: &Recipe, store: &Store) -> Result; pub fn artifact_hash(recipe: &Recipe, store: &Store) -> Result; ``` CLI: `hammer build ` → imprime el hash del artefacto sellado.