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>
257 lines
15 KiB
Markdown
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.
|