Files
hammer/docs/02-build-lab.md
T
sergioandClaude Opus 4.8 8bf1623044 Scaffold inicial: workspace Rust + SDDs completos
Arranque del proyecto hammer (distro AI-nativa: laboratorio funcional en el
sótano, terminal mutable clásica arriba, integración de IA programadora).

- Workspace Rust (compila, tests verdes): hammer-core, hammer-build,
  hammer-cli (bin `hammer`), hammerd.
- SDDs 00-10 + 6 ADRs en docs/ con toda la arquitectura.
- Esqueletos navegables mapeados a las fases del roadmap; Fase 0/1 listas
  para implementar el sandbox real.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 19:06:04 +00:00

6.0 KiB

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:

  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 Aplica source.patches sobre el árbol read-only (en copia tmpfs)
configure Detecta sistema de build (autotools/cmake/meson/make) y configura
compile Compila con el compilador de la receta hacia target/link
install 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.