gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1173Shell 64Rust 62Markdown 441482 Sutichaykuna

⎇ a51d49a342ccbae8d8f4464bce62a000e0e829e8 docs/02-build-lab.md Markdown · 153 siq'ikuna · 7.2 KB Ñawpa willay ch'uya

takana etapa 5b: los 59 docs de diseño, runbooks y ADR 97ceb7241 · Sergio ·

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/cargo/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`, `Cargo.toml`, `Makefile`) y se puede sobreescribir por receta.
97
98### Cargo (crates Rust)
99
100Un árbol con `Cargo.toml` se construye con `cargo` (Cargo gana sobre un `Makefile` huérfano,
101pero cede ante un `CMakeLists.txt` de un políglota C+Rust). Particularidades:
102
103- **Triple traducido.** El `target` estilo zig de la receta (`x86_64-linux-musl`) se traduce al
104  de rustc (`x86_64-unknown-linux-musl`).
105- **Link con `zig cc`.** `compile` instala un wrapper `zig cc` como
106  `CARGO_TARGET_<triple>_LINKER` — cargo no acepta un linker de dos palabras, y zig provee el
107  `libgcc_s` que el link **dinámico** musl exige (sin él, `cannot find -lgcc_s`). `link =
108  "dynamic"` añade `RUSTFLAGS=-C target-feature=-crt-static` (necesario para `dlopen`, p. ej.
109  apps gráficas que cargan Vulkan/Wayland en runtime).
110- **Hermético ⇒ deps vendoreadas.** `compile` corre `cargo build --release --locked --offline`:
111  el sandbox no tiene red, así que la fuente debe traer `vendor/` + `.cargo/config.toml`
112  (`cargo vendor`) o las deps pre-pobladas. `--locked` exige `Cargo.lock` fijo (coherente con
113  ADR 0006).
114- **Install.** Copia los ejecutables del top de `target/<triple>/release/` a `/out/usr/bin`.
115
116## 5. El grafo de dependencias
117
118Las recetas forman un DAG por sus `deps.build`. El algoritmo de build:
119
120```
121build(recipe):
122    h = artifact_hash(recipe)          # requiere resolver deps primero (recursivo)
123    if store.has(h): return h          # caché
124    for dep in recipe.deps.build:
125        build(dep)                     # post-orden: deps antes que el nodo
126    sandbox = make_sandbox(recipe, deps)
127    run_phases(sandbox)                # patch → configure → compile → install
128    return store.seal(sandbox.out, h)
129```
130
131Builds de nodos independientes se paralelizan (sin dependencia mutua en el DAG). El cap de
132concurrencia se ajusta a los cores disponibles.
133
134## 6. Bootstrap (romper el cordón umbilical)
135
136El mayor error es usar las herramientas del host para compilar el sistema final. `zig cc`
137reduce drásticamente este problema: un solo binario provee compilador C/C++ + musl + headers,
138hermético, cross-compilando al target. El Stage 0 tradicional (cross-compiler mínimo) queda
139en gran parte resuelto por `zig`.
140
141Para la fase Alpine no necesitamos bootstrap completo: Alpine ya provee musl y un toolchain;
142usamos `zig cc` para los builds de takana y validamos el flujo. El bootstrap from-scratch real
143es trabajo del track de distro propia ([SDD 10](10-roadmap.md), [ADR 0003](adr/0003-zig-cc-builder.md)).
144
145## 7. Interfaz (qué expone el crate)
146
147```rust
148// takana-build
149pub fn build(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;
150pub fn artifact_hash(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;
151```
152
153CLI: `takana 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:

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
fetchNo (red permitida)Clona/actualiza el repo al commit fijado; verifica hash
patchSíAplica source.patches sobre el árbol read-only (en copia tmpfs)
configureSíDetecta sistema de build (autotools/cmake/meson/cargo/make) y configura
compileSíCompila con el compilador de la receta hacia target/link
installSímake install DESTDIR=/out (o equivalente)
sealNoCalcula 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, Cargo.toml, Makefile) y se puede sobreescribir por receta.

Cargo (crates Rust)

Un árbol con Cargo.toml se construye con cargo (Cargo gana sobre un Makefile huérfano, pero cede ante un CMakeLists.txt de un políglota C+Rust). Particularidades:

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

// takana-build
pub fn build(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;
pub fn artifact_hash(recipe: &Recipe, store: &Store) -> Result<ArtifactHash>;

CLI: takana build <recipe.toml> → imprime el hash del artefacto sellado.