Files
takana/docs/31-compilador-de-rust.md
T
SergioandClaude Opus 5 72ba86c290 la cadena rehecha: llvm21 arreglado, lld enlaza, rust reconstruido — los seis controles pasan
Un cambio de dos líneas en `llvm21` re-selló tres artefactos y los molió la granja sola, en orden,
por dependencia: llvm21 95956a16→bd4a4094, lld21 27be53c1→95a4794b, rust 015a07fb→702094a8.

    llc --version        Stack dump: + segfault   →   LLVM 21.1.2, Optimized build
    opt --version        ídem                     →   LLVM 21.1.2
    ld.lld               «not built with zlib»    →   ENLAZA
    rustc --version                               →   1.97.0 (built from a source tarball)
    --print target-list                           →   x86_64-alpine-linux-musl
    cargo build + correr                          →   0,16 s y el binario habla

La config del sitio pasa a usar `lld` (más rápido); el `ld` de binutils queda documentado como
alternativa que también funciona.

LA REGLA QUE SE GANÓ SU LUGAR: **`link = "static"` en el encabezado decide cosas que la receta no
dice**. Van tres, todas medidas: apagó los threads de openssl, le mintió a libtool, y acá rompió los
registros estáticos de LLVM (`cl::opt`, `TargetRegistry`) porque el `static-pie` descarta sus
constructores globales. Con C++ grande la postura por defecto pasa a ser `dynamic` y medirlo — y el
control no es que selle: es CORRER UNA HERRAMIENTA del artefacto.

Y el corolario de por qué nadie lo vio en meses: el único consumidor de llvm21 era rust, que usa
`llvm-config` y las `.a`, nunca una herramienta. **Un artefacto puede estar roto en todo lo que nadie
usa** — como los 44 `cargo-*` sin cargo y los 23 binarios sin cargador.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 11:19:18 +00:00

257 lines
15 KiB
Markdown

# SDD 31 — El compilador de Rust: de «lo presta el lab» a «es nuestro»
> «sin compilador de rust, takana no tiene sentido» — el usuario, 2026-09-14.
>
> Tiene razón por una vía que no es de gusto, y conviene escribirla entera antes de discutir cómo.
## 1. El hueco, medido
| pregunta | respuesta (2026-09-14) |
|---|---|
| ¿cuánto de takana es Rust? | **12 crates, 42 555 líneas** — todo el instrumental |
| recetas `cargo-*` en el corpus | **44** |
| recetas de `rustc` o `cargo` | **0** |
| ¿de dónde sale el `rustc` que construye todo eso? | del **LAB** (`.dev-fs/alpine`), un rootfs ajeno |
Y el lab **entra en el `ArtifactHash`**. O sea: la distro depende de bytes de otro para poder
reconstruir su propio instrumental. No se ve porque funciona — es la misma forma de
`subcomando-sin-driver`, a escala de toolchain: 44 subcomandos de `cargo` y ningún `cargo`.
## 2. Los tres escalones
1. **`rust-toolchain-bin`** — el tarball oficial de rust-lang, verificado por su sha256 y sellado
tal cual con `foreign = true` (bytes ajenos: clase `ajeno`, fuera del recuento del corpus).
**Hecho y probado en la caja**: `rustc 1.97.0`, `cargo 1.97.0`, y un binario compilado que corre.
2. **`rust` desde fuente** — con `llvm21` del corpus y el stage0 del lab. *En curso.*
3. **La cadena `mrustc → 1.90 → 1.91.0 → 1.91.1`** del selfhost (`docs/10-roadmap.md`,
`docs/11-bootstrap.md`), que quita hasta el escalón 1. Pendiente.
## 3. Lo que costó el escalón 1, y no estaba en el grafo
El artefacto sella (799 M) y **no ejecuta** hasta tener dos cosas que la distro publicaba por
separado — las dos `sealed`, las dos en el perfil equivocado:
- **El cargador musl.** `rustc` es PIE dinámico (`NEEDED librustc_driver-*.so`, `NEEDED libc.so`).
En el worker ni arranca (`cannot execute: required file not found`, que es como se ve la falta del
intérprete); en la caja sí, porque ahí está `musl-shared`.
- **`libgcc_s.so.1`** (`_Unwind_Resume: symbol not found`). Lo publica `gcc-libs`, que estaba
declarado **sólo en los cuatro perfiles de escritorio**.
**Y no hace falta un `cc`.** El primer `rustc hola.rs` falla con `linker \`cc\` not found`, y la
respuesta no es traer un compilador de C: el toolchain **trae su propio `lld`**
(`rustlib/<target>/bin/rust-lld`). Con `-C linker-flavor=ld.lld -C link-self-contained=yes` enlaza y
el binario corre. Una distro sin compilador de C puede compilar Rust igual.
## 4. Lo que costó el escalón 2 — cinco muros, todos con su mensaje engañoso
1. **`llvm18` no sirve.** El bootstrap de rustc lo dice en una línea:
`panic!("bad LLVM version: {version}, need >=21")`. Mínimos medidos leyendo
`src/bootstrap/src/core/build_steps/llvm.rs`: **1.87 → ≥18 · 1.90 → ≥19 · 1.93/1.95 → ≥20 ·
1.97 → ≥21**. Y bajar de rustc no es salida: el stage0 de rustc *N* es *N-1* o *N*, y lo que
tenemos es 1.97. Por eso existe `llvm21` — y `llvm18` se queda, porque lo usa `mesa-llvmpipe` y
mesa 24.0.9 no soporta 21. Dos consumidores con rangos incompatibles ⇒ dos artefactos.
2. **El toolchain musl oficial NO puede producir proc-macros.** Está compilado con
`crt-static = true`, y los proc-macros son dylibs:
error: cannot produce proc-macro for `clap_derive` as the target
`x86_64-unknown-linux-musl` does not support these crate types
…compilando el **propio bootstrap de rustc**, que usa `clap` con derive. O sea: el prebuilt
sirve para compilar programas y **no** para arrancar el build de rustc. El stage0 es el `rustc`
del LAB, que Alpine construye con soporte de dylib. (Y por eso el rustc que salga de acá lleva
`crt-static = false`: con `true` heredaría el mismo defecto y no podría compilar ninguna de las
44 recetas `cargo-*` que usan `derive`.)
3. **x.py DESCARGA el stage0 si no le nombras el local.** Sin `build.rustc`/`build.cargo`, en un
sandbox sin red muere con `RuntimeError: failed verification` en `download_toolchain()` — un
mensaje que no menciona ni la red ni el stage0.
4. **El triple de Alpine no es el canónico.** Sin fijar `build`, x.py detecta
`x86_64-alpine-linux-musl` y entonces una sección `[target.x86_64-unknown-linux-musl]` **no
aplica a nada**: ignora el `llvm-config` externo y se pone a construir LLVM él solo. Y al forzar
el canónico, el stage0 del lab **no tiene la std de ese triple** (`can't find crate for std`).
Se usa el de Alpine en todo, que además es con el que ya se compila el corpus entero.
5. **cmake + zig = «compiler broken».** Los build scripts nativos (jemalloc, compiler-rt) usan
cmake, y con el wrapper de zig la prueba de ABI falla. `CC=gcc CXX=g++`, igual que en `llvm21`
y `cmake`.
## 4.bis El SEXTO muro, que la granja encontró sola *(2026-09-15)*
`rust` entró a la cola del worker y llegó **mucho más lejos**: compiló las **386 crates del stage1** y
murió justo al empezar `Building stage1 library artifacts (stage1 -> stage1)`:
error: error loading target specification: could not find specification for target
"x86_64-alpine-linux-musl"
= help: did you mean `x86_64-unknown-linux-musl`?
**Es la otra mitad del muro 4, y sólo se ve en el stage 2.** Usar el triple de Alpine funciona
mientras el compilador que manda es el del LAB —Alpine le PARCHEA esa especificación a su rustc— y
deja de funcionar en el instante en que toma el relevo **el rustc que acabamos de construir**, que es
upstream puro y no la conoce. O sea: el triple que hace falta para arrancar el build es el que el
producto del build no tiene.
Las salidas, por orden de honestidad:
1. **Parchear `rustc_target` para registrar `x86_64-alpine-linux-musl`**, que es literalmente lo que
hace Alpine en su APKBUILD. Un fichero de spec copiado de `x86_64_unknown_linux_musl.rs` y una
línea en `supported_targets!`. El triple queda DENTRO del compilador, así que vale en todos los
stages.
2. `RUST_TARGET_PATH` + un `.json` generado por el propio stage0
(`--print target-spec-json`) — más barato, pero un target por JSON como **host** es terreno que
rustc no soporta bien: la std del host se construye con el spec built-in.
3. Volver al triple canónico y resolver el «can't find crate for std» del stage0 por otro lado. Hoy
no hay candidato: el prebuilt oficial trae esa std pero **no puede compilar proc-macros** (muro 2),
así que no sirve de stage0 para el bootstrap.
⇒ La 1 es la que Alpine ya demostró que funciona, y es la que sigue.
## 4.ter ✅ EL ESCALÓN 2 SELLÓ *(2026-09-16 00:18)*
**`rust` 1.97.0 desde fuente: `b3:015a07fb…`, 353 M.** Construido por la granja, en el worker, desde
`llvm21` del corpus y el stage0 del lab.
Los controles, corridos en la caja (que sí tiene el cargador musl; el worker no):
$ rustc --version
rustc 1.97.0 (2d8144b78 2026-07-07) (built from a source tarball) ← NUESTRO
$ rustc --print target-list | grep alpine
x86_64-alpine-linux-musl ← EL PARCHE, DENTRO
$ cargo --version
cargo 1.97.0 (c980f4866 2026-06-30) (built from a source tarball)
Y compila: `--emit=obj` da un objeto de 3,5 K y `--crate-type=rlib` una biblioteca de 6,4 K, las dos
sin tocar el enlazador.
### Los dos muros de esta tanda
6. **El triple de Alpine no existía en el producto.** Resuelto con `rust-alpine-target.patch`, que lo
registra en `rustc_target` — un spec copiado del canónico con `crt_static_default = false` (sin
eso no hay dylibs y sin dylibs no hay proc-macros) y una línea en `supported_targets!`. Es lo que
hace Alpine. **El control no es que el build pase: es que `--print target-list` lo liste.**
7. **`openssl-sys`, a la hora y media de build.** `tools = ["cargo"]` lo arrastra. La dep que entró es
**`openssl-threads` y no la canónica**: el `Configure` de openssl apaga los threads en cuanto ve
`-static` en LDFLAGS, y cargo es masivamente multihilo. Más `OPENSSL_STATIC=1`/`OPENSSL_DIR=/usr`,
porque el lab publica `.a` y ningún `.so`.
### 🧱 Muro 8, ya medido: el compilador no trae con qué ENLAZAR
En una caja sin `cc` —o sea, en cualquier takana que no sea un hub— el enlace falla:
$ rustc hola.rs -C linker-flavor=ld.lld
error: linker `lld` not found
$ rustc hola.rs -C linker=/usr/bin/ld -C link-self-contained=yes
/usr/bin/ld: cannot find -lgcc
La causa está en el log del propio build, en una línea que parece informativa:
skipping llvm-tools (x86_64-alpine-linux-musl): external LLVM
Al usar un LLVM externo —que es lo que queremos, porque `llvm21` es del corpus— **x.py se salta las
herramientas de LLVM, y `rust-lld` es una de ellas**. El prebuilt oficial sí lo trae, y por eso el
escalón 1 enlazaba y éste no. Y `llvm21` tampoco sirve de repuesto: **no publica ningún `lld`**
(medido: 1,6 G de artefacto, cero binarios `lld`).
⇒ Las salidas son dos, y hay que elegir: **(a)** `[rust] lld = true` en el `bootstrap.toml`, que
construye `rust-lld` desde el `llvm-project` vendoreado en el tarball —el toolchain queda completo y
autosuficiente—; o **(b)** publicar `lld` desde `llvm21` y fijar el linker en un
`/etc/cargo/config.toml` del sitio. La (a) es la que hace que «tener compilador de Rust» signifique
lo que parece.
⚠ Y lo que esto enseña del método: **el artefacto selló, reproduce y no enlaza**. Es la familia de
[[subcomando-sin-driver]] en el corazón del toolchain — un compilador que compila y no produce un
ejecutable. Un `build ok` no es la prueba; la prueba es el objeto en el disco y el binario corriendo.
## 4.quater ✅ EL TOOLCHAIN COMPILA, ENLAZA Y CORRE *(2026-09-16)*
$ cargo build --offline
Compiling prueba v0.1.0
Finished `dev` profile in 0.44s
$ ./target/debug/prueba
cargo, rustc y ld: los tres nuestros
**En una caja sin compilador de C.** `cargo` y `rustc` son los que construyó la granja; el enlazador
es el `ld` de nuestro `binutils`. El binario sale estático (880 K el de `rustc` a secas) y corre.
### El enlazador no era `lld`: era el `ld` que ya teníamos
El muro 8 se cerró **sin** `rust-lld` y sin `lld21`. La cadena de intentos, que es la parte útil:
| intento | resultado |
|---|---|
| `-C linker-flavor=ld.lld` | `linker \`lld\` not found` — el build con LLVM externo **no genera** `rust-lld` |
| `rust.lld = true` | el bootstrap lo RECHAZA: «Cannot enable LLD … when using external llvm-config» |
| `lld21` (receta nueva) | selló, y **crasheaba con cualquier entrada** (ver abajo) |
| `lld21` dinámico + zlib | el zlib **no se puede encender desde acá**: `LLVMConfig.cmake` dice `LLVM_ENABLE_ZLIB 0` y el build standalone lo hereda |
| **`-C linker-flavor=ld` + `+crt-static`** | **funciona** |
Lo que faltaba no era un enlazador: era **decirle a rustc que enlace en estático**. Con
`crt-static`, rustc usa su propio `libunwind`/`compiler_builtins` y **deja de pedir `-lgcc`** — que
es lo que rompía el intento con `ld`, porque la distro se construye con zig (compiler-rt) y libgcc
no existe. Estático es además el modo natural de musl.
⇒ La config del sitio (el pendiente del §5) ya está escrita: `scripts/servidor/cargo-config.toml`,
instalada en `/etc/cargo/config.toml`, con las tres flags y el porqué de cada una.
### 🧨 Lo que `lld21` destapó de paso: las 79 herramientas de `llvm21` están ROTAS
`lld21` selló y se moría en cuanto hacía algo: `--version` y `--help` bien, y **cualquier** enlace
—incluso uno que sólo debía dar un error— en SIGSEGV con un «Stack dump:» vacío. No era la receta:
$ llc --version (del artefacto llvm21, sellado hace días)
PLEASE submit a bug report … Stack dump: ← también crashea, y también en el worker
Salen **`static-pie`**, y el enlace estático descarta los constructores globales de los que dependen
los registros de LLVM (`cl::opt`, `TargetRegistry`): funciona lo que no los necesita (`llvm-ar`,
`llvm-config`) y revienta lo que sí. **Nadie lo había visto porque `rust` usa `llvm-config` y las
`.a`, nunca una herramienta.** Con `link = "dynamic"`, `lld21` pasa a dar errores limpios.
⚠ **Queda como deuda DECLARADA, no arreglada**: encender zlib en `llvm21` —lo que haría a `lld21`
usable— **re-hashea `llvm21` y con él `rust`**, o sea dos builds largos. Y arreglar las 79
herramientas es la misma operación. Hoy no bloquea nada: el toolchain enlaza con `ld`.
## 4.quinquies ✅ LA CADENA REHECHA: `llvm21` arreglado, y todo detrás *(2026-09-16)*
Un cambio de dos líneas en `llvm21` re-selló tres artefactos, y los seis controles pasan:
| | antes | ahora |
|---|---|---|
| `llc --version` | `Stack dump:` + segfault | **LLVM 21.1.2, Optimized build** |
| `opt --version` | ídem | **LLVM 21.1.2** |
| `ld.lld` contra la libc del lab | «not built with zlib support» | **enlaza** |
| `rustc --version` | — | 1.97.0 *(built from a source tarball)* |
| `--print target-list \| grep alpine` | — | `x86_64-alpine-linux-musl` |
| `cargo build` + correr | — | **0,16 s y el binario habla** |
Los hashes: `llvm21` `95956a16…`→`bd4a4094…`, `lld21` `27be53c1…`→`95a4794b…`,
`rust` `015a07fb…`→`702094a8…`. La granja los molió sola, en orden, por dependencia.
### Lo que se arregló, y por qué estaba roto
1. **`link = "dynamic"`.** Con `static` los binarios salen `static-pie` y el enlace descarta los
constructores globales de los que dependen los registros de LLVM (`cl::opt`, `TargetRegistry`).
Por eso `llvm-ar` y `llvm-config` funcionaban —no los usan— y `llc`/`opt`/`lld` reventaban.
2. **`LLVM_ENABLE_ZLIB=ON` + dep `zlib`.** Un `lld` construido contra este LLVM **hereda**
`LLVM_ENABLE_ZLIB` de `LLVMConfig.cmake`: encenderlo en la receta de lld era inerte. Sin zlib no
puede leer las secciones de depuración comprimidas que trae la libc del lab, o sea que no enlaza
nada real.
### La regla que se ganó su lugar
**`link = "static"` en el encabezado decide cosas que la receta no dice.** Van tres, todas medidas:
apagó los threads de openssl (`Configure` lo hace al ver `-static`), le mintió a libtool
(`link-static-mentira-libtool`), y acá rompió los registros estáticos de LLVM. Con C++ grande, la
postura por defecto pasa a ser **`dynamic` y medirlo** — y el control no es que selle: es **correr una
herramienta del artefacto**.
⚠ Y el corolario de por qué nadie lo vio en meses: **el único consumidor de `llvm21` era `rust`, que
usa `llvm-config` y las `.a`**. Un artefacto puede estar roto en todo lo que nadie usa. Vale para los
44 `cargo-*` sin `cargo`, para los 23 binarios sin cargador, y ahora para 79 herramientas de LLVM.
## 5. Lo que falta decidir
- **Dónde vive el toolchain.** Hoy `rust-toolchain-bin` está en `perfil.servidor` (799 M; un
escritorio no compila nada). El de fuente irá donde se decida — y probablemente sustituya al
prebuilt en ese perfil en vez de sumarse.
- **La config del sitio**: que las flags del linker (`-C linker-flavor=ld.lld`) sean el default en
un `/etc/cargo/config.toml`, para que `cargo build` funcione sin recordarlas.
- **El escalón 3**: cuándo se conecta la cadena del selfhost al corpus. Es lo único que quita la
dependencia de bytes ajenos, y es el trabajo más caro de los tres.