250 líneas de comentario en 151 scripts. Control verificado: el diff no toca NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa. El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces: las tres eran el MOTD que el script escribe DENTRO de la imagen construida — texto del producto, no comentario del script. Se cambiaron aparte y a propósito, que es rebranding, no limpieza. Y el hallazgo caro: casaba contra , que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate. La etapa 4 lo movió a y el script quedó casando NADA. No fallaba: imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja. Además 14 rutas de módulo en docs, que el barrido anterior no tocó porque no es frontera de palabra.
304 lines
20 KiB
Markdown
304 lines
20 KiB
Markdown
# SDD 11 — Bootstrap from-scratch (track posterior)
|
||
|
||
> **Estado:** diseño. Abre el *track posterior* del [SDD 10](10-roadmap.md): bajar de "takana
|
||
> sobre Alpine" a "takana sobre sí mismo". No bloquea ninguna Fase 0–6 (ya cerradas); reusa
|
||
> el laboratorio de build ([SDD 02](02-build-lab.md)) sin retrabajo.
|
||
|
||
## 1. El problema: el cordón umbilical
|
||
|
||
Hoy el userland que `takana` muta es el de Alpine (musl + busybox ya cocidos). Eso fue
|
||
deliberado ([ADR 0002](adr/0002-alpine-first.md)): validar la capa AI-nativa antes de gastar
|
||
meses en un bootstrap. Validada esa capa, el track posterior responde una sola pregunta:
|
||
|
||
> **¿Puede `takana` compilar el sistema completo que ejecuta, sin pedirle nada al host más
|
||
> que un toolchain semilla fijado por hash?**
|
||
|
||
Si la respuesta es sí *y* el resultado es bit-reproducible, takana deja de heredar la
|
||
confianza de Alpine y la deriva de su propio [log de transparencia](09-trust-model.md).
|
||
|
||
## 2. Principios (heredados, no nuevos)
|
||
|
||
- **Todo es una receta sellada.** Cada pieza del bootstrap es un `Recipe` ([SDD 02 §1](02-build-lab.md))
|
||
cuyo artefacto se direcciona por hash en el `Store`. El bootstrap es un *subgrafo* del grafo
|
||
de dependencias que ya construye el lab, no un mecanismo aparte.
|
||
- **Nada del host entra al hash.** El único insumo externo es el **toolchain semilla**, y entra
|
||
como un artefacto importado con su sha256 **fijado** ([ADR 0006](adr/0006-pinned-commits.md)),
|
||
igual que un tarball upstream. El host aporta un kernel para correr procesos y nada más.
|
||
- **`zig cc` es la semilla primaria** ([ADR 0003](adr/0003-zig-cc-builder.md)): un binario
|
||
hermético = compilador C/C++ + musl + headers, cross al target. Escotilla `musl-cross-make`
|
||
(gcc+musl) detrás de la misma interfaz de receta (`compiler = "gcc"`) para los paquetes con
|
||
gcc-ismos.
|
||
|
||
## 3. Las tres etapas
|
||
|
||
```
|
||
host (sólo kernel + seed pinned)
|
||
│
|
||
┌─────────────▼─────────────┐
|
||
│ Stage 0 — toolchain semilla│ zig (o musl-cross-make) ingerido al store,
|
||
│ sellado por hash │ fijado por sha256. Cross → x86_64-linux-musl.
|
||
└─────────────┬─────────────┘
|
||
│ artifact_hash(seed) ← dependencia de build de todo lo demás
|
||
┌─────────────▼─────────────┐
|
||
│ Stage 1 — userland mínimo │ musl, busybox/toybox, init arje, hammerd,
|
||
│ cross-compilado │ cross-compilados CON Stage 0. Sale un rootfs sellado.
|
||
└─────────────┬─────────────┘
|
||
│ rootfs_hash(stage1)
|
||
┌─────────────▼─────────────┐
|
||
│ Stage 2 — rebuild nativo │ DENTRO del rootfs Stage 1 (bwrap/chroot/VM),
|
||
│ y verificación │ recompila toolchain+userland con las herramientas
|
||
└────────────────────────────┘ de Stage 1, NO las del host. Compara hashes.
|
||
```
|
||
|
||
### Stage 0 — Toolchain semilla
|
||
|
||
Modelamos el toolchain semilla como una **fuente fijada**, no como un build: un `Source` de
|
||
tipo tarball con `sha256` pinned (`zig-<ver>-linux-x86_64.tar.xz`, o el tarball de
|
||
`musl-cross-make`). `takana-bootstrap` lo descarga (fuera del sandbox, como `takana_build::download`
|
||
ya hace para `patch_url`/`content_url`), verifica el sha256 **antes** de sellarlo, y lo deposita
|
||
en el store con un `artifact_hash` derivado de ese sha256. A partir de ahí el resto del
|
||
bootstrap depende del *hash del store*, no de la ruta del host.
|
||
|
||
Salida: `seed_hash` (artefacto del toolchain en el store).
|
||
|
||
### Stage 1 — Userland mínimo
|
||
|
||
Un conjunto de recetas cuyo `[deps].build` incluye `seed_hash`. El conjunto mínimo que arranca
|
||
y se administra solo:
|
||
|
||
| Pieza | Por qué | Compilador |
|
||
|---|---|---|
|
||
| `musl` | libc del target | zig cc (estático) |
|
||
| `busybox` o `toybox` | coreutils + shell | zig cc (estático) |
|
||
| `arje` | init: PID 1 + supervisión ([ADR 0007](adr/0007-arje-como-init-propio.md)) | toolchain Rust |
|
||
| `hammerd` | diario + bus de agente + watcher, **servicio supervisado por arje** | toolchain Rust |
|
||
|
||
Todo se enlaza estático o con rpath controlado (sin depender del loader del host). El resultado
|
||
se ensambla en un **rootfs** (árbol FHS) que a su vez se sella como un artefacto del store
|
||
(`stage1_hash`). El init **no se escribe de cero**: se adopta `arje`
|
||
([ADR 0007](adr/0007-arje-como-init-propio.md)), cuyo PID 1 con supervisión real entrega el
|
||
`CRASHED` real que la Fase 5 dejó diferido — `hammerd` corre como servicio bajo arje.
|
||
|
||
> El **kernel** no es el foco de este hito: se importa pinned (como la semilla) para poder
|
||
> correr el rootfs. Construirlo desde fuente es un sub-ítem posterior, ortogonal al
|
||
> auto-alojamiento del userland.
|
||
|
||
### Stage 2 — Rebuild nativo y verificación
|
||
|
||
El corte del cordón. Dentro del rootfs Stage 1 (primero vía `bwrap`/`chroot`, luego en la VM
|
||
destino), se reconstruyen Stage 0' y Stage 1' usando **sólo** las herramientas de Stage 1.
|
||
Se compara:
|
||
|
||
```
|
||
hash(stage1) vs hash(stage1')
|
||
```
|
||
|
||
- **Iguales** ⇒ el sistema se compila a sí mismo bit a bit: reproducible y auto-alojado. Hito
|
||
cumplido.
|
||
- **Distintos** ⇒ hay no-determinismo (timestamps, paths embebidos, orden de enlace). Se
|
||
caza y se elimina; es exactamente el trabajo que [SDD 09](09-trust-model.md) §2 exige.
|
||
|
||
## 4. Manifiesto de bootstrap (embrión del log de transparencia) ✅
|
||
|
||
Cada etapa anota una línea `(stage, recipe_hash, artifact_hash, seed_hash, ts)` en un
|
||
`bootstrap.json` versionado (en la raíz del store). Ese manifiesto **es** la semilla del log de
|
||
transparencia ([SDD 09 §4](09-trust-model.md)): un tercero reproduce el bootstrap y verifica que
|
||
sus hashes coinciden con los publicados, sin confiar en el binario del autor.
|
||
|
||
Implementado en `takana-bootstrap::manifest` (`BootstrapManifest` / `StageEntry`): append-only e
|
||
**idempotente** (de-dup por `(stage, artifact_hash)`), escritura atómica, y `ts` que **no** entra
|
||
en ningún hash (dos reproducciones del mismo bootstrap difieren a lo sumo en ese campo). Stage 0
|
||
ya anota su línea: `recipe_hash = None` (la semilla se ingiere, no se compila) y
|
||
`artifact_hash == seed_hash` (la semilla es a la vez insumo y producto). **`takana bootstrap
|
||
manifest`** imprime el `bootstrap.json` actual: es la superficie de **publicación** del log: el autor
|
||
lo comparte y un tercero reproduce el bootstrap y compara sus hashes contra estas líneas, sin confiar
|
||
en el binario del autor ([SDD 09 §4](09-trust-model.md)).
|
||
|
||
## 5. Interfaz
|
||
|
||
Nuevo crate `takana-bootstrap` (orquestador) sobre `takana-build` + `takana-core::Store`.
|
||
No introduce mecanismo nuevo de build: encadena recetas y persiste el manifiesto.
|
||
|
||
```rust
|
||
// takana-bootstrap
|
||
pub fn stage0(seed: &SeedSpec, store: &Store) -> Result<ArtifactHash>; // ✅ toolchain semilla
|
||
pub fn stage1(spec: &Stage1Spec, cfg: &BuildConfig, store: &Store)
|
||
-> Result<RootfsHash>; // ◑ musl+busybox+hammerd; init arje ☐
|
||
pub fn stage2(stage1: &RootfsHash, store: &Store) -> Result<VerifyReport>; // ✅ ancla+verifica; rebuild in-rootfs en VM ✓ REPRODUCIBLE
|
||
pub fn all(seed, recipes_dir, base_cfg, store) -> Result<AllReport>; // ✅ encadena stage0→1→2 + manifiesto
|
||
```
|
||
|
||
`all` encadena las tres etapas del host en una corrida (stage0 ingiere la semilla, stage1 construye y
|
||
sella el rootfs, stage2 ancla la referencia) y devuelve los tres hashes + el `BootstrapManifest`
|
||
poblado. El veredicto `✓ REPRODUCIBLE` **no** sale de aquí: el rebuild nativo que lo decide corre
|
||
dentro del rootfs (en la VM, `scripts/selfhost-verify.sh`); `all` deja la referencia anclada lista para
|
||
que ese rebuild la compare con `stage2 --verify`. Idempotente (compone las idempotencias de cada etapa).
|
||
|
||
`stage1` cross-compila `musl` + `busybox` + `hammerd` con la semilla (no el zig del host),
|
||
ensambla un rootfs FHS por hardlink y lo sella; el `RootfsHash` se deriva del **contenido** (hashes
|
||
de componentes + init), no del árbol en disco, así que es reproducible. El PID 1 provisional es el
|
||
`init` de busybox vía `/etc/inittab`, que arranca `hammerd` como servicio respawn. `hammerd` es una
|
||
receta Cargo (repo pinned + deps vendoreadas en el fetch para build `--offline`). `arje`
|
||
([ADR 0007](adr/0007-arje-como-init-propio.md)) ya tiene su **receta puente** `recipes/arje-zero.toml`
|
||
(Cargo, fuente = monorepo tawasuyu pinned al commit de la migración del CAS a BLAKE3, `-p arje-zero`):
|
||
prueba que el lab construye el init, pero todavía **no** es PID 1. El paso "init real" —arje-zero
|
||
como PID 1 del rootfs con su seed card, `arje-bus` y hammerd como Card de servicio supervisada, que
|
||
entrega el `CRASHED` real— está diseñado en el [SDD 12](12-init-real.md) y **validado en QEMU**
|
||
(ver [runbook](runbooks/stage1-vm-boot.md)).
|
||
|
||
`stage2` ancla el **content-hash** del rootfs de Stage 1 (`ArtifactHash::of_tree` — los bytes
|
||
reales, no el hash input-addressed del store) como la referencia a reproducir, y la anota en el
|
||
manifiesto. `verify_against` la compara con un rebuild `stage1'` producido **dentro** del rootfs (en
|
||
la VM, recompilando con sólo las herramientas de Stage 1): iguales ⇒ auto-alojado bit a bit. El
|
||
determinismo necesario lo dan las rutas fijas (`/src`) + `SOURCE_DATE_EPOCH` del sandbox +
|
||
`CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1` (impuesto por el sandbox tras cazar el no-determinismo del
|
||
codegen paralelo de rustc en `arje-zero`). **El rebuild in-rootfs ya se ejecutó end-to-end**: `KVM=1
|
||
MEM=24576 ./scripts/selfhost-verify.sh` reconstruyó los 4/4 dentro del rootfs y su
|
||
`of_tree(stage1') = b3:0039b2b9…` igualó la referencia ⇒ `✓ REPRODUCIBLE: stage1' == stage1`
|
||
(ver [runbook](runbooks/stage1-vm-boot.md) §8).
|
||
|
||
`SeedSpec { kind, version, url, sha256 }` es la identidad pinned de la semilla; `seed_hash()`
|
||
deriva el `ArtifactHash` de `(kind, version, sha256)` — no del `url` ni del host, así que
|
||
cualquier espejo del mismo tarball produce el mismo artefacto.
|
||
|
||
CLI (implementado lo de Stage 0; el resto pendiente):
|
||
|
||
```
|
||
takana bootstrap stage0 --url URL --sha256 HEX --version V [--seed zig|musl-cross-make] # ✅
|
||
takana bootstrap stage1 --seed-hash HASH [--seed zig] [--recipes DIR] # ✅ booteado en QEMU
|
||
takana bootstrap stage2 --rootfs HASH [--verify CONTENT_HASH] # ✅ ancla+verifica; rebuild en VM ✓ REPRODUCIBLE
|
||
takana bootstrap all --url URL --sha256 HEX --version V [--seed zig] [--recipes DIR] # ✅ las tres etapas + manifiesto
|
||
takana bootstrap manifest # imprime el bootstrap.json (log de transparencia, §4) # ✅
|
||
```
|
||
|
||
## 6. Decisiones (fijadas en [ADR 0008](adr/0008-bootstrap-stages.md))
|
||
|
||
- **Semilla primaria:** `zig` por hermeticidad (ADR 0003), con `musl-cross-make` como escotilla
|
||
por receta. ✅
|
||
- **coreutils+shell = `busybox`** (no toybox): el Stage 1 es una balsa desechable hacia el
|
||
userland nativo en Rust/Zig; se prioriza compatibilidad de flags y velocidad, y su GPLv2 no
|
||
contamina el sistema final. ✅
|
||
- **init `arje` diferido:** Stage 1 con init mínimo provisional; `arje` (y el `CRASHED` real) entran
|
||
como receta git pinned en un lote posterior. ✅
|
||
- **Layout de la semilla:** ingesta pura en Stage 0 + resolución del toolchain al usarlo
|
||
(`SeedSpec::toolchain_dir`, localiza `zig` en raíz o hijo versionado). ✅
|
||
- **Particionado/montaje** de `/store` y `/var/lib/hammer` en la imagen destino: ✅ (Etapa B3).
|
||
`scripts/disk-image.sh` arma una imagen GPT con 3 particiones ext4 dedicadas —
|
||
`/dev/vda1`→`/`, `/dev/vda2`→`/store` (CAS inmutable), `/dev/vda3`→`/var/lib/hammer` (estado
|
||
mutable). El kernel monta vda1; un wrapper `/sbin/init` monta vda2/vda3 y hace `exec` del init
|
||
real (arje-zero). Consecuencia que hubo que resolver: con `/store` en su propia partición, el
|
||
sellado del store cruza filesystems y `rename(2)` da EXDEV ⇒ `Store::seal` cae a copia-a-staging
|
||
dentro del store + rename store-interno (atómico). Verificado in-VM: rebuild ✓ REPRODUCIBLE.
|
||
- **Kernel:** importado pinned ahora; from-source después. ☐
|
||
|
||
## 7. Auto-alojamiento: el builder rootfs (camino a Stage 2 pleno)
|
||
|
||
La verificación de reproducibilidad de Stage 2 está operativa, probada y **ejecutada end-to-end**:
|
||
`of_tree` (content-hash de bytes) + `verify_against`, los 4 componentes reconstruyen
|
||
**bit-idéntico**, y el **rebuild *dentro* del rootfs** ya corrió en la VM dando `✓ REPRODUCIBLE`
|
||
(variante (a) abajo; ver [runbook §8](runbooks/stage1-vm-boot.md)). El obstáculo que hubo que
|
||
resolver para llegar ahí:
|
||
|
||
> El Stage 1 que booteamos es un **runtime** (musl+busybox+hammerd+arje-zero): **no trae compilador**
|
||
> (ni `zig`, ni `make`/autotools, ni `cargo`/rust, ni `linux-headers`, ni `bwrap`). No puede
|
||
> reconstruir nada. El rebuild in-rootfs exige un **builder rootfs**: Stage 1 + el toolchain adentro.
|
||
|
||
### 7.1 Qué necesita el builder rootfs
|
||
|
||
Para correr `takana bootstrap stage1` **dentro** de sí mismo: el binario `takana` (estático), las
|
||
recetas, la **semilla** (`zig`, ya un artefacto sellado), `make`+autotools, `cargo`+rust,
|
||
`linux-headers`, y `bwrap` (el lab anida un sandbox ⇒ userns sin privilegios dentro del chroot/VM).
|
||
|
||
### 7.2 Dos niveles de pureza (fasable)
|
||
|
||
- **(a) Builder pragmático** — el toolchain entra al rootfs **desde Alpine** (apk), no construido por
|
||
takana. Demuestra el **mecanismo** (rebuild in-rootfs + `of_tree(stage1)==of_tree(stage1')`) y
|
||
cierra la reproducibilidad *end-to-end*, sin ser aún auto-alojamiento puro.
|
||
- **(b) Auto-alojamiento puro** — el toolchain lo **construye takana desde fuente** (recetas para
|
||
make/autotools, y el gran tramo: rust/llvm o un rustc bootstrappeado). Es el end-state; el más
|
||
caro (construir rust desde cero). Se llega **incrementalmente**, reemplazando una a una las piezas
|
||
Alpine por componentes takana, con Stage 2 verificando cada paso.
|
||
- **Pieza 1 — GNU make `4.4.1`** (`recipes/make.toml`): arranque de (b). Build estático musl con
|
||
`zig cc` (mismo camino que `grep`: tarball release con `configure` → AutoconfReady), sellado en
|
||
`b3:fbad44ac…` y **reproducible bit-a-bit** (dos builds en stores distintos ⇒ árbol idéntico).
|
||
Es la herramienta base de toda receta autotools. **Swap implementado:** `BuilderSpec.swaps`
|
||
(`ToolchainSwap { name, artifact, rel_path }`) monta el binario sellado sobre el path Alpine en
|
||
`/toolchain` (estático musl ⇒ sin shim del loader) y lo ancla en el **hash lógico** del builder
|
||
(`swaps_digest`, ordenado por nombre): la procedencia deja de ser "todo Alpine" y se vuelve
|
||
auditable para el log de transparencia. CLI: `takana bootstrap builder --swap make=<hash>[:rel]`
|
||
(repetible; `rel_path` por defecto `usr/bin/<name>`). En el host: pura `b3:8a370f5d…` vs swapped
|
||
`b3:e7e2282c…`, y `/toolchain/usr/bin/make` queda hardlinkeado al artefacto `fbad44ac…`. El
|
||
`selfhost-verify.sh` lo expone opt-in con `SWAP_MAKE=1` (construye make y lo swapea). **Validado en
|
||
host:** con make de takana los 4/4 reproducen `of_tree=9adefb82…` (idéntico al baseline determinista).
|
||
- **Pieza 2 — busybox `1.36.1`** (ya es componente de Stage 1, `recipes/busybox.toml`): provee el
|
||
`sh`+coreutils que el build usa del `/toolchain`. El `/toolchain` de Alpine es **mixto** — busybox
|
||
para `sh/sed/grep/awk/tar/find` (symlinks → `/bin/busybox`) y GNU coreutils para `cp/mkdir/install`.
|
||
El swap monta el busybox estático de takana sobre `/toolchain/bin/busybox` (`--swap
|
||
busybox=<hash>:bin/busybox`; sin código nuevo — el mecanismo `--swap` ya soporta `rel_path`): los
|
||
applets busybox-backed pasan a usar takana-busybox, coreutils GNU intacto. `SWAP_BUSYBOX=1` en el
|
||
verify. **Validado en host:** con make+busybox de takana los 4/4 (incl. busybox compilándose a sí
|
||
mismo con takana-busybox de shell) reproducen `of_tree=9adefb82…`.
|
||
- **Determinismo (transversal):** `codegen-units=1` NO bastaba — el backend paralelo de rustc/LLVM
|
||
(ThinLTO) divergía ~62 KB en `arje-zero` a >1 CPU. Fix: `CARGO_BUILD_JOBS=1` en el sandbox
|
||
serializa el jobserver ⇒ reproducible e independiente del nº de CPUs (SDD 09 §2). El baseline
|
||
reproducible verdadero es `of_tree=9adefb82…` (el viejo `0039b2b9…` era un build paralelo no-fiable).
|
||
- **✅ VARIANTE (b) CERRADA** (ver [runbook self-hosting-toolchain.md](runbooks/self-hosting-toolchain.md)):
|
||
se completaron todas las piezas en-camino — make, busybox, **coreutils, bwrap, linux-headers** y
|
||
el gran tramo **rust/cargo 1.91.1** (cadena purista mrustc→1.90→1.91.0→1.91.1, LLVM 20.1.8 externo
|
||
reusado; ver [`scripts/rust-frontier/`](../scripts/rust-frontier/README.md)). El **capstone** corrió
|
||
los 6 swaps juntos in-VM (`SWAP_{MAKE,BUSYBOX,COREUTILS,BWRAP,LINUX_HEADERS,RUST}=1`) dando
|
||
`✓ REPRODUCIBLE` con `of_tree(stage1')=7fa6cb4e…` bit a bit. Dos anclajes de `of_tree`: `9adefb82…`
|
||
(rustc Alpine, los 5 tools lo mantienen) y `7fa6cb4e…` (takana-rust, auto-consistente — el
|
||
compilador emite los bytes). binutils desde fuente (`recipes/binutils.toml`, 2.45.1) cierra la
|
||
provenance pero es **inerte** al `of_tree` (zig provee as/ld; verificado por bisección). No quedan
|
||
piezas del toolchain por de-Alpinizar.
|
||
|
||
### 7.3 Enganche con lo ya hecho
|
||
|
||
`stage2` ya ancla `of_tree(stage1)`; el builder produce `stage1'` y `verify_against` emite el
|
||
veredicto. El determinismo necesario está cubierto (paths fijos `/src`, `SOURCE_DATE_EPOCH`, locks
|
||
deterministas). El sub-ítem era: **ensamblar el builder** (variante de `assemble_rootfs` que hidrata
|
||
el toolchain) y **correr el rebuild anidado** en la VM.
|
||
|
||
### 7.4 Estado: builder ensamblado + rebuild in-VM ejecutado ✅ ✓ REPRODUCIBLE
|
||
|
||
El **ensamblado** del builder está implementado (`takana_bootstrap::builder_rootfs`, variante a):
|
||
|
||
```
|
||
takana bootstrap builder --stage1 <H> --seed-hash <H> \
|
||
[--toolchain .dev-fs/alpine] [--toolchain-tag alpine-3.23.4] \
|
||
[--hammer-bin <static-musl-hammer>] [--ref-content of_tree(stage1)] \
|
||
[--work-cache work/] [--out work/builder-rootfs]
|
||
```
|
||
|
||
Sobre el rootfs de Stage 1 (hidratado como base) monta: `/toolchain` (el rootfs Alpine, el sandbox
|
||
de build), `/store` con la **semilla replicada** (el `takana` de adentro resuelve zig por hash),
|
||
`/usr/bin/hammer` + `/etc/hammer/recipes`, y el driver **`/usr/bin/rebuild-stage1`** que lee
|
||
`/etc/hammer/rebuild.env` (`SEED_HASH`/`SEED_KIND`/`REF_CONTENT`), apunta `TAKANA_ROOTFS=/toolchain`,
|
||
corre `takana bootstrap stage1` y compara `stage1'` contra la referencia con `stage2 --verify`.
|
||
|
||
El builder **no se sella** (su `/toolchain` Alpine no es content-addressed); se ensambla en un
|
||
`out_dir` para empaquetar como initramfs. Su identidad sí es reproducible: un `ArtifactHash` *lógico*
|
||
de sus insumos (stage1 + semilla + recetas + binario + tag del toolchain + el driver), anotado en el
|
||
manifiesto (línea stage 2). Validado contra el store real (Stage 1 `73d7a9be…` + semilla
|
||
`3ce721ec…` ⇒ builder `81dad3d9…`, 1.2 GB con el toolchain). **El paso operacional ya se ejecutó:**
|
||
`scripts/selfhost-verify.sh` empaqueta el builder, lo bootea con KVM y corre `rebuild-stage1`
|
||
no-interactivo; in-VM reconstruyó los 4/4 (arje-zero cu=1 en ~27 min) y `of_tree(stage1') =
|
||
b3:0039b2b9…` igualó la referencia ⇒ `✓ REPRODUCIBLE: stage1' == stage1`. Lo que queda es la
|
||
variante (b), el auto-alojamiento *puro* (toolchain construido por takana desde fuente, §7.2).
|
||
**Arrancada:** la pieza 1, GNU make `4.4.1` (`recipes/make.toml`), ya se construye desde fuente
|
||
con el lab — estática, reproducible bit-a-bit (§7.2b). Ver [runbook §8](runbooks/stage1-vm-boot.md).
|
||
|
||
## 8. Hecho cuando
|
||
|
||
`takana bootstrap --all` produce un rootfs que, ejecutado en la VM destino, **reconstruye su
|
||
propio toolchain y userland con hashes idénticos a los publicados**, sin que ninguna
|
||
herramienta del host entre al resultado. Ese día Alpine deja de ser una dependencia y pasa a
|
||
ser, a lo sumo, una conveniencia de desarrollo.
|
||
|
||
**Estado (2026-06-12):** el **mecanismo** está demostrado end-to-end con la variante (a) — el
|
||
rootfs reconstruye su userland in-VM con `of_tree` idéntico (`✓ REPRODUCIBLE`, §7.4). Falta sólo
|
||
la variante (b): que el **toolchain** dentro del builder lo construya takana desde fuente (no
|
||
provenga de Alpine), reemplazando una a una las piezas con Stage 2 verificando cada paso. Ese es
|
||
el corte pleno del cordón.
|