Files
takana/docs/31-compilador-de-rust.md
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

15 KiB

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//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

  1. 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.
  2. 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.