1# SDD 02 — El laboratorio de build
2
3El laboratorio es la fábrica de artefactos: toma una **receta** (qué compilar y cómo) y
4produce un **árbol de archivos** depositado en el store, direccionado por su hash. Es la única
5parte del sistema que se permite ser funcional/determinista, porque es donde paga.
6
7## 1. Receta (`Recipe`)
8
9Una receta es una descripción declarativa y pura de *un* build. Es el átomo del grafo.
10
11```toml
12# ejemplo: recipes/grep.toml
13name = "grep"
14version = "3.11"
15
16[source]
17repo = "git://git.savannah.gnu.org/grep.git"
18commit = "a1b2c3d4e5f6..." # FIJADO. Nunca "HEAD". Ver ADR 0006.
19patches = ["patches/grep-perl-default.patch"]
20
21[build]
22compiler = "zig-cc" # por defecto; escotilla: "clang" | "gcc"
23target = "x86_64-linux-musl"
24link = "static" # "static" | "dynamic"
25flags = ["--enable-perl-regexp"]
26# fases override opcionales; si se omiten, se usan las heurísticas estándar
27# configure = "..." compile = "..." install = "..."
28
29[deps]
30build = ["pcre2", "musl"] # dependencias de compilación (otras recetas)
31runtime = [] # vacío si link = static
32```
33
34El **compilador es por receta** (no global): `zig-cc` por defecto por hermeticidad y musl
35incluido, con escotilla a `clang`/`gcc` para paquetes con gcc-ismos. Ver
36[ADR 0003](adr/0003-zig-cc-builder.md).
37
38## 2. El identificador de contenido (CAS)
39
40Cada artefacto se direcciona por un hash que captura **todo** lo que influye en su salida:
41
42```
43artifact_hash = BLAKE3(
44 source_commit // commit git fijado
45 ⧺ canonical(patches) // contenido de los parches, no su ruta
46 ⧺ canonical(build_flags) // flags + compilador + target + link
47 ⧺ Σ dep.artifact_hash // hashes de las dependencias de build (recursivo)
48)
49```
50
51Consecuencia: **entrada idéntica ⇒ hash idéntico ⇒ salida idéntica**. Esto es lo que permite:
52
53- **Caché:** ¿el hash ya existe en el store? Se reusa, no se recompila.
54- **Avance ordenado del grafo:** si un upstream avanza su commit fijado, sólo cambian los
55 hashes de ese nodo y sus dependientes; el resto del grafo se reusa.
56- **Verificación de `.swm`:** dos máquinas con la misma receta producen el mismo hash; así se
57 verifica un cambio compartido sin confiar en binarios (ver [SDD 09](09-trust-model.md)).
58
59> BLAKE3 por velocidad y paralelismo; el hash no es un secreto, es un identificador.
60
61## 3. El sandbox de build
62
63Cada build corre en su propio entorno aislado, montado con **bubblewrap** (`bwrap`), que usa
64namespaces de Linux (mount, pid, net, user) sin requerir root real cuando el kernel lo permite.
65
66Receta del aislamiento:
67
681. **Raíz tmpfs.** Se monta un `/` temporal vacío en tmpfs. Nada del host es visible salvo lo
69 que montemos explícitamente.
702. **Compilador inyectado.** `zig` (o el compilador de la receta) se monta read-only en el
71 `PATH` del sandbox. En el bootstrap es el único compilador presente — así se rompe el
72 cordón umbilical con el host.
733. **Fuentes read-only.** El árbol del repo (en el commit fijado) se monta read-only.
744. **Dependencias read-only.** Los artefactos de las deps de build, ya en el store, se montan
75 read-only en rutas conocidas.
765. **Sin red.** El namespace de red se aísla: el `fetch` ocurre *fuera* del sandbox (ver §4),
77 de modo que el build en sí no puede descargar nada no declarado → hermeticidad real.
786. **Salida vía `DESTDIR`.** El build instala en `DESTDIR=/out`; ese árbol es el artefacto que
79 se extrae y se sella en el store.
80
81## 4. Fases del build
82
83Inspiradas en BitBake (`do_fetch`/`do_unpack`/`do_configure`/`do_compile`/`do_install`) pero
84mínimas. Sólo `fetch` toca la red, y ocurre **antes** de entrar al sandbox:
85
86| Fase | ¿En sandbox? | Qué hace |
87|---|---|---|
88| `fetch` | No (red permitida) | Clona/actualiza el repo al commit fijado; verifica hash |
89| `patch` | Sí | Aplica `source.patches` sobre el árbol read-only (en copia tmpfs) |
90| `configure` | Sí | Detecta sistema de build (autotools/cmake/meson/make) y configura |
91| `compile` | Sí | Compila con el compilador de la receta hacia `target`/`link` |
92| `install` | Sí | `make install DESTDIR=/out` (o equivalente) |
93| `seal` | No | Calcula el hash final del árbol `/out`, lo deposita en el store |
94
95La detección de sistema de build en `configure` es heurística (presencia de `configure.ac`,
96`CMakeLists.txt`, `meson.build`, `Makefile`) y se puede sobreescribir por receta.
97
98## 5. El grafo de dependencias
99
100Las recetas forman un DAG por sus `deps.build`. El algoritmo de build:
101
102```
103build(recipe):
104 h = artifact_hash(recipe) # requiere resolver deps primero (recursivo)
105 if store.has(h): return h # caché
106 for dep in recipe.deps.build:
107 build(dep) # post-orden: deps antes que el nodo
108 sandbox = make_sandbox(recipe, deps)
109 run_phases(sandbox) # patch → configure → compile → install
110 return store.seal(sandbox.out, h)
111```
112
113Builds de nodos independientes se paralelizan (sin dependencia mutua en el DAG). El cap de
114concurrencia se ajusta a los cores disponibles.
115
116## 6. Bootstrap (romper el cordón umbilical)
117
118El mayor error es usar las herramientas del host para compilar el sistema final. `zig cc`
119reduce drásticamente este problema: un solo binario provee compilador C/C++ + musl + headers,
120hermético, cross-compilando al target. El Stage 0 tradicional (cross-compiler mínimo) queda
121en gran parte resuelto por `zig`.
122
123Para la fase Alpine no necesitamos bootstrap completo: Alpine ya provee musl y un toolchain;
124usamos `zig cc` para los builds de hammer y validamos el flujo. El bootstrap from-scratch real
125es trabajo del track de distro propia ([SDD 10](10-roadmap.md), [ADR 0003](adr/0003-zig-cc-builder.md)).
126
127## 7. Interfaz (qué expone el crate)
128
129```rust
130// hammer-build
131pub fn build(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;
132pub fn artifact_hash(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;
133```
134
135CLI: `hammer build <recipe.toml>` → imprime el hash del artefacto sellado.
02-build-lab.md
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.
# 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.
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).
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:
- Raíz tmpfs. Se monta un
/ temporal vacío en tmpfs. Nada del host es visible salvo lo
que montemos explícitamente.
- 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.
- Fuentes read-only. El árbol del repo (en el commit fijado) se monta read-only.
- Dependencias read-only. Los artefactos de las deps de build, ya en el store, se montan
read-only en rutas conocidas.
- 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.
- 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, ADR 0003).
7. Interfaz (qué expone el crate)
// hammer-build
pub fn build(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;
pub fn artifact_hash(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;
CLI: hammer build <recipe.toml> → imprime el hash del artefacto sellado.