cadena wasm completa: wasi-libc y wasi-sdk sellan (SDD 26, unidad 3.b)
Los cuatro eslabones que enciende RLBox en firefox, sellados de punta a punta:
wasi-libc-headers b3:54c6e96d (andamio, rompe el ciclo del bootstrap)
wasi-compiler-rt b3:a3fe63d1 (libclang_rt.builtins-wasm32.a)
wasi-libc b3:8e50b145 (libc.a 970.734 bytes, 32 archivos .a)
wasi-sdk b3:c8a587fa (el .cfg que hace el sysroot invisible)
Los dos primeros ya estaban construidos; los dos últimos costaron dos fallos, y
ninguno de los dos era de la receta:
1. `check-symbols` moría por SESGO DE CLANG, no por un artefacto malo. El lab
trae clang 22.1.8 y este commit de wasi-libc (2025-06-26) congeló su snapshot
de macros predefinidos contra un clang anterior, así que el diff completo era
UNA línea: `+#define __wasip1__ 1`. La mitad que importa —los símbolos
definidos e indefinidos de la libc recién construida— coincidía byte a byte.
Por eso se RECONCILIA `expected/*/predefined-macros.txt` en vez de saltar el
chequeo con `make no-check-symbols`, que era la salida fácil: ese target
existe y se lleva por delante también la comparación de símbolos, que es la
que vale. Así cualquier OTRA deriva sigue matando el build.
2. `make TARGET_TRIPLE=NOBUILD ... install` NO es un no-op. La regla `install`
depende de `finish`, que depende de `libc`, así que make no lee el triple
inexistente como «no construyas» sino como «construí para el triple NOBUILD»
y muere en `build/NOBUILD/.../crt1-command.o` con `invalid thread model
'single'` — un error que nombra un fichero que nadie pidió. Se copia
`sysroot/{lib,share,include}` a mano, que es literalmente lo que hace la
regla de upstream (`SYSROOT ?= $(CURDIR)/sysroot`).
Cada receta lleva su guardián de contenido, porque acá el modo de fallo caro no
es el ausente sino el vacío: un sysroot con headers y sin libc.a pasa por bueno
y revienta dentro del build de firefox, horas después y sin nombrar a nadie de
esta cadena. Verificado en verde: los builtins son objetos wasm de verdad
(magic 0061736d, no x86_64 nativos) y el .cfg de wasi-sdk apunta a un sysroot
que existe.
El segundo fallo lo diagnosticamos entre dos sesiones: hammer-f8 aportó el
`invalid thread model 'single'` que explicaba la línea que yo veía.
Queda para la próxima unidad el flip de recipes/firefox.toml:
--without-wasm-sandboxed-libraries -> --with-wasi-sysroot, que es un rebuild
largo y su propia unidad de trabajo.
Semántico sayk'uy
Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.
6 qillqakuna · +377 −0
diff --git a/recipes/wasi-compiler-rt.toml b/recipes/wasi-compiler-rt.toml
new file mode 100644
index 00000000..36242943
--- /dev/null
+++ b/recipes/wasi-compiler-rt.toml
@@ -0,0 +1,141 @@
+# wasi-compiler-rt — los builtins de compiler-rt compilados a wasm32.
+#
+# QUÉ ES: la runtime del compilador para WebAssembly (`libclang_rt.builtins-wasm32.a`). Sin esto no
+# se puede enlazar NADA a wasm, y por eso es el primer eslabón real del sandbox RLBox de firefox
+# (SDD 26 §3.bis, lectura 1: RLBox apagado es una desventaja de SEGURIDAD — es la jaula wasm
+# alrededor de los parsers de fuentes y medios, o sea justo el código que come entrada no confiable).
+#
+# NO CONSTRUYE LLVM. El lab ya trae LLVM 22.1.8 CON el backend de WebAssembly compilado dentro
+# (comprobado: `wasm32`, `wasm64` y las descripciones "WebAssembly 32-bit/64-bit" están en
+# `libLLVM.so.22.1`). De acá sale sólo la runtime, que son ~100 ficheros C: minutos, no horas.
+#
+# ⚠ LA VERSIÓN TIENE QUE SEGUIR A LA DEL LAB. Los builtins son un contrato con el clang que los va a
+# usar: Alpine lo escribe como `wasi-compiler-rt~$_llvmver` en las makedepends de firefox. Acá el
+# tarball es 22.1.8 y el lab tiene llvm22 22.1.8-r1 — MISMA versión upstream. Si el lab sube de
+# LLVM, esta receta sube con él. Y no queda al aire: `clang` y `llvm` son prefijos de
+# `TOOLCHAIN_PREFIXES` (hammer-core/src/lab.rs), así que un bump del lab MUEVE la huella y re-hashea
+# esto solo. La alineación es un invariante vigilado, no una nota al pie.
+#
+# DÓNDE ATERRIZA: `/usr/lib/llvm22/lib/clang/22/lib/wasi/`, que es el resource dir donde clang
+# busca los builtins sin que nadie le pase un flag. En el lab ese `lib/` NO existe (sólo hay
+# `include/`), y las deps se apilan como capas overlay BAJO el rootfs ⇒ esta receta lo CREA sin
+# pisar nada del lab.
+#
+# Verificado: el sha512 del tarball coincide byte a byte con el del APKBUILD de Alpine.
+name = "wasi-compiler-rt"
+version = "22.1.8"
+license = "Apache-2.0 WITH LLVM-exception"
+
+[source]
+tarball = "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/llvm-project-22.1.8.src.tar.xz"
+sha256 = "922f1817a0df7b1489272d18134ee0087a8b068828f87ac63b9861b1a9965888"
+
+[build]
+compiler = "clang"
+
+[deps]
+# `python3`: el CMakeLists de compiler-rt busca Python3 y, si no lo encuentra, cae a un
+# `find_package(Python2 ... REQUIRED)` que MUERE. El error dice "Could NOT find Python2" y lo
+# que falta es python3 — el mensaje nombra el fallback, no la causa.
+build = ["cmake", "samurai", "python3", "wasi-libc-headers"]
+
+[build.phases]
+configure = '''
+set -e
+
+# El toolchain file de wasi-sdk, ESCRITO ACÁ EN VEZ DE TRAÍDO. Upstream son 38 líneas repartidas en
+# dos ficheros del tarball de wasi-sdk (`wasi-sdk.cmake` + `cmake/Platform/WASI.cmake`, y el segundo
+# es literalmente `set(WASI 1)`). Traerlos costaría una fuente de red y una receta enteras para 38
+# líneas de declaraciones puras — peor trato. Se transcriben de wasi-sdk-27, que es el que Alpine
+# pinea para esta misma versión de compiler-rt.
+mkdir -p cmake/Platform
+echo 'set(WASI 1)' > cmake/Platform/WASI.cmake
+cat > wasi-sdk.cmake <<'EOF'
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake")
+set(CMAKE_SYSTEM_NAME WASI)
+set(CMAKE_SYSTEM_VERSION 1)
+set(CMAKE_SYSTEM_PROCESSOR wasm32)
+set(triple wasm32-wasi)
+set(CMAKE_C_COMPILER ${WASI_SDK_PREFIX}/bin/clang)
+set(CMAKE_CXX_COMPILER ${WASI_SDK_PREFIX}/bin/clang++)
+set(CMAKE_ASM_COMPILER ${WASI_SDK_PREFIX}/bin/clang)
+set(CMAKE_AR ${WASI_SDK_PREFIX}/bin/llvm-ar)
+set(CMAKE_RANLIB ${WASI_SDK_PREFIX}/bin/llvm-ranlib)
+set(CMAKE_C_COMPILER_TARGET ${triple})
+set(CMAKE_CXX_COMPILER_TARGET ${triple})
+set(CMAKE_ASM_COMPILER_TARGET ${triple})
+# No buscar EJECUTABLES en el sysroot (los de correr durante el build son del host); el resto,
+# SÓLO en el sysroot, para que no se cuele una lib nativa x86_64 en un enlace a wasm.
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+EOF
+
+# CFLAGS/LDFLAGS del entorno se limpian a propósito: cualquier flag pensado para el x86_64 nativo
+# (un `-static`, un `-march`) es veneno en un enlace a wasm32 y el error que produce no se parece
+# en nada a la causa.
+unset CFLAGS CXXFLAGS LDFLAGS
+
+cmake -B build -G Ninja -S compiler-rt -Wno-dev \
+ -DCMAKE_BUILD_TYPE=MinSizeRel \
+ -DCMAKE_MODULE_PATH=/src/cmake \
+ -DCMAKE_TOOLCHAIN_FILE=/src/wasi-sdk.cmake \
+ -DCMAKE_C_COMPILER_WORKS=ON \
+ -DCMAKE_CXX_COMPILER_WORKS=ON \
+ -DCOMPILER_RT_BAREMETAL_BUILD=ON \
+ -DCOMPILER_RT_INCLUDE_TESTS=OFF \
+ -DCOMPILER_RT_HAS_FPIC_FLAG=OFF \
+ -DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \
+ -DCOMPILER_RT_OS_DIR=wasi \
+ -DCMAKE_AR=/usr/bin/llvm-ar \
+ -DCMAKE_RANLIB=/usr/bin/llvm-ranlib \
+ -DWASI_SDK_PREFIX=/usr \
+ -DCMAKE_INSTALL_PREFIX=/usr/lib/llvm22/lib/clang/22/ \
+ -DCMAKE_SYSROOT=/usr/share/wasi-sysroot-bootstrap
+'''
+compile = 'cmake --build build'
+install = '''
+set -e
+DESTDIR=/out cmake --install build
+
+RES=/out/usr/lib/llvm22/lib/clang/22
+
+# El `include/` del resource dir pertenece al LAB (son los headers de clang). Instalarlo desde acá
+# lo taparía con una copia del tarball: misma trampa que las deps `clang18`/`llvm18` que había que
+# quitar de firefox. Fuera.
+rm -rf "$RES/include"
+
+# Los alias que espera clang según cómo se nombre el target. `wasm32-wasi` se renombró a
+# `wasm32-wasip1` en LLVM 22.1 (lo dice el propio configure de Mozilla), así que el mismo `.a`
+# tiene que responder a los dos nombres o el enlace falla por un alias que no existe.
+for p in 1 2; do
+ ln -s ./wasi "$RES/lib/wasm32-wasip$p"
+ ln -s ./wasi "$RES/lib/wasm32-unknown-wasip$p"
+done
+ln -s ./libclang_rt.builtins-wasm32.a "$RES/lib/wasi/libclang_rt.builtins.a"
+
+# GUARDIÁN. Un `cmake --install` que no instala nada sale con 0 y deja el árbol vacío — que en el
+# store es un cache-hit envenenado, no un fallo. Se exige el fichero Y que sea un archivo wasm de
+# verdad, no un `.a` vacío ni uno de objetos x86_64.
+A="$RES/lib/wasi/libclang_rt.builtins-wasm32.a"
+test -s "$A" || { echo "!! no se instaló $A" >&2; find /out -name 'libclang_rt*' >&2; exit 1; }
+N=$(llvm-nm --print-file-name "$A" 2>/dev/null | wc -l)
+test "$N" -gt 0 || { echo "!! $A no tiene símbolos: archivo vacío" >&2; exit 1; }
+
+# Que los miembros sean WASM y no x86_64 nativos. Es LA comprobación que importa: si el sysroot o el
+# triple se configuran mal, cmake compila igual pero produce objetos nativos, el `.a` queda lleno y
+# con símbolos, y el fallo recién aparece al enlazar firefox — lejos y sin nombrar a esta receta.
+# Se mira el número mágico del objeto (`\0asm`), no el nombre del fichero: la primera versión de
+# este guardián exigía que el miembro terminara en `.o` y lo rechazó estando BIEN construido,
+# porque compiler-rt los nombra de otro modo. Una convención de nombre no es una propiedad.
+M=$(llvm-ar t "$A" | head -1)
+mkdir -p /tmp/guardian && (cd /tmp/guardian && llvm-ar x "$A" "$M")
+MAGIC=$(od -An -tx1 -N4 "/tmp/guardian/$M" | tr -d ' \n')
+test "$MAGIC" = "0061736d" || {
+ echo "!! los objetos de $A NO son wasm (magic=$MAGIC, esperaba 0061736d)" >&2
+ echo " primer miembro: $M" >&2
+ exit 1
+}
+echo "guardián: builtins wasm32 instalados ($(llvm-ar t "$A" | wc -l) objetos, magic wasm ok, $(stat -c%s "$A") bytes)"
+'''
diff --git a/recipes/wasi-libc-headers.toml b/recipes/wasi-libc-headers.toml
new file mode 100644
index 00000000..b380ad50
--- /dev/null
+++ b/recipes/wasi-libc-headers.toml
@@ -0,0 +1,59 @@
+# wasi-libc-headers — SÓLO los headers de wasi-libc, para ROMPER el ciclo del bootstrap wasm.
+#
+# EL CICLO: `wasi-libc` necesita `libclang_rt.builtins-wasm32.a` para enlazar, y
+# `wasi-compiler-rt` necesita los headers de wasi-libc para compilar. Ninguno de los dos puede ir
+# primero. Se rompe por acá: los headers NO necesitan compilar nada —el target
+# `copy-include-headers.stamp` sólo copia ficheros— así que esta receta sale sin builtins y le da a
+# `wasi-compiler-rt` el `CMAKE_SYSROOT` que le falta.
+#
+# ⚠ POR QUÉ ESTE COMMIT Y NO EL DE `wasi-libc`: acá se pinea `ac020b86` (viejo) y en `wasi-libc`
+# `3f7eb4c7` (nuevo), igual que Alpine. NO es un descuido. El árbol instala los headers en
+# `sysroot/include/$(TARGET_TRIPLE)`, y el TRIPLE POR DEFECTO cambió entre los dos commits:
+# `wasm32-wasi` en el viejo, `wasm32-wasip1` en el nuevo. El toolchain file de wasi-sdk que usa
+# `wasi-compiler-rt` fija `wasm32-wasi`, así que con el árbol nuevo clang buscaría
+# `include/wasm32-wasi` y encontraría `include/wasm32-wasip1` — sysroot vacío y build muerto.
+# El artefacto FINAL que consume firefox es el de `wasi-libc` (wasip1, el triple que clang 22.1
+# usa de verdad); éste es andamio de bootstrap y muere acá.
+#
+# Verificado: el sha512 de este tarball coincide byte a byte con el que publica Alpine en su
+# APKBUILD de `wasi-compiler-rt` — o sea que la descarga está contrastada contra una fuente
+# independiente, no sólo consigo misma.
+name = "wasi-libc-headers"
+version = "0.20250521"
+license = "Apache-2.0 WITH LLVM-exception AND MIT AND CC0-1.0 AND BSD-2-Clause"
+
+[source]
+# Tarball `/archive/<commit>` de GitHub: se genera al vuelo, pero va pineado por COMMIT (no por tag
+# móvil) y su sha256 está fijado acá, así que un cambio de bytes falla ruidosamente en el fetch.
+# El repo trae un `.gitmodules` con `tools/wasi-headers/WASI`, que en el tarball llega VACÍO: es el
+# generador de headers desde la espec WASI, y su salida ya está commiteada en el árbol. El Makefile
+# no lo toca. No hace falta modo git.
+tarball = "https://github.com/WebAssembly/wasi-libc/archive/ac020b86fd44bafe60aa4fa12f407d16e3731329.tar.gz"
+sha256 = "d44bd7fa456aa42c1494767e5ffa00cdbab182d8497a577592a6562629f6f49e"
+
+[build]
+compiler = "clang"
+
+[deps]
+build = ["make"]
+
+[build.phases]
+configure = "true"
+# `install-include-headers.sh` necesita un shell POSIX y `find`; los dos están en el lab.
+compile = 'make build/wasm32-wasi/copy-include-headers.stamp'
+install = '''
+set -e
+mkdir -p /out/usr/share/wasi-sysroot-bootstrap
+cp -a sysroot/. /out/usr/share/wasi-sysroot-bootstrap/
+
+# GUARDIÁN. El target de la stamp es un `touch`: si el script de copia fallara sin salir con
+# error, la stamp existiría igual y esta receta sellaría un sysroot VACÍO — que es un cache-hit
+# envenenado, no un fallo (regla 3 del CLAUDE.md). Se exige un header concreto, no el directorio.
+test -f /out/usr/share/wasi-sysroot-bootstrap/include/wasm32-wasi/stdlib.h || {
+ echo "!! el sysroot de bootstrap no tiene include/wasm32-wasi/stdlib.h" >&2
+ echo " (¿cambió TARGET_TRIPLE por defecto en este commit de wasi-libc?)" >&2
+ ls -R /out/usr/share/wasi-sysroot-bootstrap 2>/dev/null | head -30 >&2
+ exit 1
+}
+echo "guardián: headers wasm32-wasi presentes ($(find /out/usr/share/wasi-sysroot-bootstrap -name '*.h' | wc -l) .h)"
+'''
diff --git a/recipes/wasi-libc.toml b/recipes/wasi-libc.toml
new file mode 100644
index 00000000..6a4c4111
--- /dev/null
+++ b/recipes/wasi-libc.toml
@@ -0,0 +1,118 @@
+# wasi-libc — la libc de WebAssembly. Es el sysroot que firefox usa para compilar sus librerías
+# enjauladas en wasm (RLBox): los parsers de fuentes y medios —graphite, ogg, expat, woff2—, o sea
+# el código que come entrada no confiable de la red.
+#
+# ES EL ESLABÓN FINAL de la cadena wasm, y el que firefox consume de verdad:
+# wasi-libc-headers (andamio, triple viejo) → wasi-compiler-rt (los builtins) → ESTE
+#
+# ⚠ COMMIT DISTINTO AL DE `wasi-libc-headers`, a propósito. Acá va el NUEVO (`3f7eb4c7`), cuyo
+# triple por defecto es `wasm32-wasip1`. Ése es el que importa: en LLVM 22.1 el target `wasm32-wasi`
+# se renombró a `wasm32-wasip1` y el propio `toolchain.configure` de Mozilla lo contempla
+# explícitamente, así que el clang 22.1 del lab va a pedir el sysroot con ESE nombre. El commit
+# viejo existe sólo para romper el ciclo de bootstrap; ver `wasi-libc-headers.toml`.
+#
+# Verificado: el sha512 de este tarball coincide byte a byte con el del APKBUILD de Alpine.
+name = "wasi-libc"
+version = "27.20250626"
+license = "Apache-2.0 WITH LLVM-exception AND Apache-2.0 AND MIT AND CC0-1.0 AND BSD-2-Clause"
+
+[source]
+tarball = "https://github.com/WebAssembly/wasi-libc/archive/3f7eb4c7d6ede4dde3c4bffa6ed14e8d656fe93f.tar.gz"
+sha256 = "6bb86e09dc5ed43260e81176537d702568689cf4203b8beb1f505609c4139ca5"
+
+[build]
+compiler = "clang"
+
+[deps]
+build = ["make", "wasi-compiler-rt"]
+
+[build.phases]
+configure = "true"
+compile = '''
+set -e
+
+# ⚠ `BUILTINS_LIB` NO ES OPCIONAL ACÁ, y no por gusto: si no se define, el Makefile se BAJA una
+# copia conocida desde la CI de wasi-sdk con `curl` (línea ~610 del Makefile upstream). Dentro del
+# sandbox hermético eso no tiene red, así que el build moriría; y si la tuviera sería peor, porque
+# estaríamos sellando un binario ajeno sin hash. Se le pasa el que construyó `wasi-compiler-rt`.
+#
+# Ruta ABSOLUTA, no la relativa que usa el APKBUILD de Alpine: el Makefile la consume como
+# prerequisito de una regla (`$(BUILTINS_LIB_PATH): $(BUILTINS_LIB)`) y la resuelve contra su propio
+# CURDIR, que acá es `/src` y no `/`.
+BUILTINS=/usr/lib/llvm22/lib/clang/22/lib/wasi/libclang_rt.builtins-wasm32.a
+test -f "$BUILTINS" || { echo "!! falta $BUILTINS (¿no llegó la dep wasi-compiler-rt?)" >&2; exit 1; }
+
+unset CFLAGS CXXFLAGS LDFLAGS
+
+# ⚠ `check-symbols` CONTRA UN CLANG MÁS NUEVO QUE EL SNAPSHOT DEL ÁRBOL.
+#
+# El target `check-symbols` del Makefile hace DOS cosas: compara los símbolos definidos/indefinidos
+# de la libc recién construida contra `expected/`, y compara la lista de macros predefinidos del
+# clang que la compiló contra otro snapshot del mismo directorio. Lo primero es una comprobación
+# real del artefacto; lo segundo es un detector de deriva de toolchain.
+#
+# El lab trae clang 22.1.8 y este commit de wasi-libc (2025-06-26) fue congelado contra un clang
+# anterior, así que el build muere con UNA sola línea de diferencia:
+#
+# +#define __wasip1__ 1
+#
+# La mitad que importa PASÓ: `defined-symbols.txt` y `undefined-symbols.txt` coinciden byte a byte;
+# el diff completo son esas 3.196 líneas iguales y ese macro de más. O sea: la libc construida es la
+# esperada y lo que cambió es que el compilador define un macro nuevo.
+#
+# Por eso se RECONCILIA el snapshot en vez de saltar el chequeo con `make no-check-symbols`, que era
+# la salida fácil: ese target existe y se lleva por delante también la comparación de símbolos, que
+# es justamente la que vale. Así el chequeo sigue vivo y CUALQUIER OTRA deriva —un símbolo que
+# desaparece, otro macro nuevo— sigue matando el build.
+#
+# Se exige que la inserción ocurra: si upstream renombra el ancla, este bloque no puede quedarse
+# callado dejando el mismo fallo para más adelante.
+for T in wasm32-wasip1 wasm32-wasip1-threads; do
+ F="expected/$T/predefined-macros.txt"
+ grep -q '^#define __wasip1__ 1$' "$F" && continue
+ sed -i 's/^#define __wasm 1$/#define __wasip1__ 1\n#define __wasm 1/' "$F"
+ grep -q '^#define __wasip1__ 1$' "$F" || {
+ echo "!! no se pudo reconciliar $F: no apareció el ancla '#define __wasm 1'" >&2
+ exit 1
+ }
+done
+
+# Los dos triples que firefox puede pedir. NO se construye `wasm32-wasip2`: el APKBUILD de Alpine lo
+# hace para rust, que acá no lo consume, y cada triple de más es tiempo de build sin destinatario.
+make CC=clang TARGET_TRIPLE=wasm32-wasip1 BUILTINS_LIB="$BUILTINS"
+make CC=clang TARGET_TRIPLE=wasm32-wasip1-threads THREAD_MODEL=posix BUILTINS_LIB="$BUILTINS"
+
+# `make install` copia `sysroot/{lib,share,include}` en bloque y falla si `share/` no existe.
+mkdir -p sysroot/share
+'''
+install = '''
+set -e
+mkdir -p /out/usr/share/wasi-sysroot
+
+# SE COPIA A MANO EN VEZ DE `make install`, y no es atajo — `make install` NO SIRVE ACÁ.
+#
+# La regla de upstream son dos líneas: `mkdir -p $(INSTALL_DIR)` y
+# `cp -p -r $(SYSROOT)/{lib,share,include} $(INSTALL_DIR)`, con `SYSROOT ?= $(CURDIR)/sysroot`.
+# O sea que copia el sysroot ENTERO, sin mirar el triple. Pero la regla declara `install: finish`
+# y `finish` depende de `libc`, así que invocarla vuelve a entrar al grafo de compilación.
+#
+# El truco de pasarle un triple inexistente (`TARGET_TRIPLE=NOBUILD`, que es lo que hacía la
+# primera versión de esta receta) NO lo evita: make no lo lee como «no construyas», lo lee como
+# «construí para el triple NOBUILD» y muere compilando `build/NOBUILD/.../crt1-command.o` — con
+# un error que habla de un fichero que nadie pidió. Las dos vueltas de arriba ya dejaron el
+# sysroot completo; lo único que falta es moverlo.
+cp -p -r sysroot/lib sysroot/share sysroot/include /out/usr/share/wasi-sysroot/
+
+# GUARDIÁN. Un sysroot con headers pero SIN `libc.a` es exactamente la clase de artefacto que pasa
+# por bueno y revienta 50 minutos después, dentro del build de firefox, con un error que no nombra
+# a wasi-libc. Se exige la libc del triple que firefox va a pedir.
+LIBC=/out/usr/share/wasi-sysroot/lib/wasm32-wasip1/libc.a
+test -s "$LIBC" || {
+ echo "!! el sysroot no trae $LIBC" >&2
+ find /out/usr/share/wasi-sysroot -name '*.a' 2>/dev/null | head -20 >&2
+ exit 1
+}
+test -f /out/usr/share/wasi-sysroot/include/wasm32-wasip1/stdlib.h || {
+ echo "!! el sysroot no trae los headers de wasm32-wasip1" >&2; exit 1; }
+echo "guardián: sysroot wasm32-wasip1 completo (libc.a $(stat -c%s "$LIBC") bytes, $(find /out/usr/share/wasi-sysroot -name '*.a' | wc -l) archivos .a)"
+'''
diff --git a/recipes/wasi-sdk.toml b/recipes/wasi-sdk.toml
new file mode 100644
index 00000000..98cb6e9a
--- /dev/null
+++ b/recipes/wasi-sdk.toml
@@ -0,0 +1,57 @@
+# wasi-sdk — el fichero de configuración que le dice a clang dónde está el sysroot de wasm.
+#
+# Es la pieza más chica de la cadena y la que la vuelve INVISIBLE para quien la usa: sin esto,
+# firefox tendría que enterarse por un flag de cómo llegar al sysroot; con esto, `clang
+# --target=wasm32-wasip1` lo encuentra solo. El clang de Alpine lee `/etc/clang22/<triple>.cfg`
+# —el lab ya trae ahí su propio `x86_64-alpine-linux-musl.cfg`, así que el mecanismo está probado en
+# esta misma máquina— y overlayfs FUSIONA directorios, así que esta dep suma su fichero sin pisar el
+# del lab.
+#
+# NO TIENE FUENTE UPSTREAM porque el contenido es nuestro: una línea. Usa `[source] dir`, el modo
+# que se agregó para `atuq` (SDD 26 §2.ter). Es su segundo consumidor, y el primero fuera del caso
+# que lo motivó.
+#
+# ⚠ `/etc/clang22` lleva la versión del lab en la RUTA. No es una constante que se pueda olvidar: si
+# el lab sube de LLVM, esta ruta deja de existir y el sysroot se vuelve invisible en silencio — por
+# eso el guardián de abajo comprueba que el directorio del lab exista ANTES de escribir dentro.
+name = "wasi-sdk"
+version = "27"
+license = "Apache-2.0"
+
+[source]
+dir = "wasi-sdk"
+
+[build]
+compiler = "clang"
+
+[deps]
+build = ["wasi-libc"]
+
+[build.phases]
+configure = "true"
+compile = "true"
+install = '''
+set -e
+
+# Si el lab cambió de versión de LLVM, esta ruta ya no es la que clang lee. Fallar acá es barato;
+# fallar dentro del build de firefox, cuatro horas después y con un error sobre un header de wasm
+# que no nombra a esta receta, no lo es.
+test -d /etc/clang22 || {
+ echo "!! el lab no tiene /etc/clang22: ¿subió de versión de LLVM?" >&2
+ ls -d /etc/clang* 2>/dev/null >&2
+ exit 1
+}
+
+mkdir -p /out/etc/clang22
+cp /src/wasm32-unknown-wasip1.cfg /out/etc/clang22/
+cp /src/wasm32-unknown-wasip1-threads.cfg /out/etc/clang22/
+
+# GUARDIÁN: que el sysroot al que apunta el .cfg EXISTA. Un `--sysroot` que señala a la nada no da
+# error: clang sigue adelante y falla mucho después por headers que no encuentra.
+S=$(sed -n 's/^--sysroot //p' /out/etc/clang22/wasm32-unknown-wasip1.cfg)
+test -f "$S/lib/wasm32-wasip1/libc.a" || {
+ echo "!! el .cfg apunta a $S y ahí no hay libc.a de wasm32-wasip1" >&2
+ exit 1
+}
+echo "guardián: /etc/clang22 apunta a un sysroot wasm real ($S)"
+'''
diff --git a/recipes/wasi-sdk/wasm32-unknown-wasip1-threads.cfg b/recipes/wasi-sdk/wasm32-unknown-wasip1-threads.cfg
new file mode 100644
index 00000000..1d8f3832
--- /dev/null
+++ b/recipes/wasi-sdk/wasm32-unknown-wasip1-threads.cfg
@@ -0,0 +1 @@
+--sysroot /usr/share/wasi-sysroot
diff --git a/recipes/wasi-sdk/wasm32-unknown-wasip1.cfg b/recipes/wasi-sdk/wasm32-unknown-wasip1.cfg
new file mode 100644
index 00000000..1d8f3832
--- /dev/null
+++ b/recipes/wasi-sdk/wasm32-unknown-wasip1.cfg
@@ -0,0 +1 @@
+--sysroot /usr/share/wasi-sysroot