# 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 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.