Files
takana/recipes/wasi-libcxx.toml
Sergio cddae46205 wasi-libcxx: el directorio VACÍO que hace visible a libc++ (y el guardián que lo prueba)
Segunda muerte del firefox con RLBox, otra vez a los 6 segundos y con el mismo
mensaje engañoso, ya con wasi-libcxx sellado:

    checking the wasm C compiler can find wasi headers... yes
    checking the wasm C++ compiler can find wasi headers...
    fatal error: 'cstring' file not found

El C pasaba todo y sólo fallaba el C++. Y `cstring` ESTABA en el artefacto, en
include/wasm32-wasip1/c++/v1/cstring. No era un fichero que faltara: era un
fichero que clang no buscaba.

CAUSA. El APKBUILD de Alpine hace un `mkdir -p .../include/c++/v1` que parece
ruido de empaquetado y yo descarté al transcribir. Es la SONDA por la que el
driver de clang decide que el sysroot tiene layout de libc++; sin ese directorio
no añade `include/<triple>/c++/v1` a la lista de búsqueda.

MEDIDO, no deducido. Sysroot fusionado (wasi-libc + wasi-libcxx) y la invocación
literal del configure de Mozilla, dentro del lab:

    clang++ -std=gnu++20 --target=wasm32-wasip1 conftest.cpp --sysroot=$S -c

    sin include/c++/v1  ->  fatal error: 'cstring' file not found
    con include/c++/v1  ->  exit 0

Es el reverso exacto de la regla 3 del CLAUDE.md: allá un directorio vacío es un
artefacto mentiroso, acá un directorio vacío es una declaración dirigida al
compilador. Queda documentado en la receta para que nadie lo borre por limpieza.

Y la lección que deja el guardián nuevo: los tests de presencia que tenía la
receta habrían dado esto por bueno, porque "existe" no es "se encuentra". Así
que ahora la receta corre LA MISMA PRUEBA que el configure de firefox, contra un
sysroot fusionado con el de su dep. Si pasa acá no puede fallar allá; y si falla,
falla en el segundo 20 de esta receta —imprimiendo la lista de búsqueda de
clang— y no en el segundo 6 de un build de cuatro horas que además culpa a otro.

Re-hashea: wasi-libcxx b3:1361deae -> b3:838a9557, y con él firefox.
2026-09-06 01:46:38 +00:00

232 lines
11 KiB
TOML

# wasi-libcxx — libc++ y libc++abi compilados a wasm32. EL ESLABÓN QUE FALTABA.
#
# POR QUÉ EXISTE ESTA RECETA, y no es una suposición: el build de firefox con RLBox encendido murió
# en el configure, a los SEIS SEGUNDOS, con
#
# /tmp/conftest.cpp:1:10: fatal error: 'cstring' file not found
# ERROR: Cannot find wasi headers or problem with the wasm compiler.
#
# El mensaje culpa a los headers de WASI y los headers de WASI estaban perfectos. Lo que falta es
# otra cosa: `<cstring>` es un header de C++, y `wasi-libc` sólo trae la C. Las librerías que RLBox
# enjaula NO son todas C —graphite2 es C++— así que el sandbox necesita una biblioteca estándar de
# C++ para wasm32. Es el mismo patrón que la lección de los headers UAPI: el error nombra lo que
# buscó, no lo que falta.
#
# Contrastado contra Alpine, que es nuestro upstream: su `wasi-sdk` declara
# `depends="wasi-libc wasi-libcxx wasi-compiler-rt"` — TRES, y nosotros teníamos dos. El hueco
# estaba en el grafo antes de que el build lo encontrara.
#
# ⚠ LA VERSIÓN SIGUE A LA DEL LAB, igual que `wasi-compiler-rt` y por la misma razón: es el mismo
# tarball de LLVM (22.1.8, mismo sha256) y libc++ es un contrato con el clang que la va a usar. Si
# el lab sube de LLVM, `TOOLCHAIN_PREFIXES` mueve la huella y esto re-hashea solo.
#
# DÓNDE ATERRIZA: `/usr/share/wasi-sysroot`, el MISMO prefijo que `wasi-libc`. No se pisan: las deps
# se apilan como overlay y overlayfs FUSIONA directorios, así que uno aporta `lib/wasm32-wasip1/
# libc.a` y el otro `libc++.a` en esa misma carpeta. Es exactamente el reparto que hace Alpine con
# dos paquetes sobre un solo sysroot.
name = "wasi-libcxx"
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]
# `wasi-libc` aporta el sysroot contra el que compila libc++ (headers y `libc.a`). NO va
# `wasi-compiler-rt`: acá sólo se producen archivos estáticos (`LIBCXX_ENABLE_SHARED=OFF`), y sin
# paso de enlace los builtins no hacen falta — es también lo que declara Alpine.
build = ["cmake", "samurai", "python3", "wasi-libc"]
[build.phases]
configure = '''
set -e
# El mismo toolchain file que escribe `wasi-compiler-rt`, transcrito por el mismo motivo: son 38
# líneas de declaraciones puras repartidas en dos ficheros del tarball de wasi-sdk, y traerlas
# costaría una fuente de red y una receta enteras. Si se toca uno, se toca el otro.
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})
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
# Los flags del entorno se limpian igual que en `wasi-compiler-rt`: un `-static` o un `-march`
# pensados para el x86_64 nativo son veneno en un enlace a wasm32 y el error no se parece a la causa.
unset CFLAGS CXXFLAGS LDFLAGS
'''
compile = '''
set -e
# `-fno-exceptions`: libc++ para wasm se construye SIN excepciones (`LIBCXX_ENABLE_EXCEPTIONS=OFF`
# abajo), y si los flags no acompañan a la opción de cmake el resultado es una librería a medias.
export CFLAGS="-fno-exceptions --sysroot=/usr/share/wasi-sysroot"
export CXXFLAGS="-fno-exceptions --sysroot=/usr/share/wasi-sysroot"
# Una función y dos llamadas, porque los dos triples se configuran IGUAL salvo por los threads. El
# `-threads` no es opcional: firefox pide el sysroot por el nombre del triple y `wasi-libc` ya
# publica los dos, así que dejar uno sin libc++ sería el hueco de siempre — presente a medias, que
# es peor que ausente.
conf() {
t="$1"; d="$2"; thr=OFF; extra=""
case "$t" in *-threads) thr=ON; extra="-pthread";; esac
cmake -B "$d" -G Ninja -S runtimes -Wno-dev \
-DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi" \
-DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=ON \
-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 \
-DCMAKE_C_FLAGS="$CFLAGS $extra --target=$t" \
-DCMAKE_CXX_FLAGS="$CXXFLAGS $extra --target=$t" \
-DCMAKE_ASM_COMPILER_TARGET="$t" \
-DCMAKE_CXX_COMPILER_TARGET="$t" \
-DCMAKE_C_COMPILER_TARGET="$t" \
-DCMAKE_AR=/usr/bin/llvm-ar \
-DCMAKE_RANLIB=/usr/bin/llvm-ranlib \
-DLLVM_DEFAULT_TARGET_TRIPLE="$t" \
-DCMAKE_STAGING_PREFIX=/usr/share/wasi-sysroot \
-DCXX_SUPPORTS_CXX11=ON \
-DLIBCXX_ABI_VERSION=2 \
-DLIBCXX_BUILD_EXTERNAL_THREAD_LIBRARY=OFF \
-DLIBCXX_CXX_ABI=libcxxabi \
-DLIBCXX_CXX_ABI_INCLUDE_PATHS=libcxxabi/include \
-DLIBCXX_ENABLE_EXCEPTIONS=OFF \
-DLIBCXX_ENABLE_EXPERIMENTAL_LIBRARY=OFF \
-DLIBCXX_ENABLE_FILESYSTEM=OFF \
-DLIBCXX_ENABLE_SHARED=OFF \
-DLIBCXX_ENABLE_THREADS=$thr \
-DLIBCXX_HAS_EXTERNAL_THREAD_API=OFF \
-DLIBCXX_HAS_MUSL_LIBC=ON \
-DLIBCXX_HAS_PTHREAD_API=$thr \
-DLIBCXX_HAS_WIN32_THREAD_API=OFF \
-DLIBCXX_INCLUDE_TESTS=OFF \
-DLIBCXXABI_BUILD_EXTERNAL_THREAD_LIBRARY=OFF \
-DLIBCXXABI_ENABLE_EXCEPTIONS=OFF \
-DLIBCXXABI_ENABLE_PIC=OFF \
-DLIBCXXABI_ENABLE_SHARED=OFF \
-DLIBCXXABI_ENABLE_THREADS=$thr \
-DLIBCXXABI_HAS_EXTERNAL_THREAD_API=OFF \
-DLIBCXXABI_HAS_PTHREAD_API=$thr \
-DLIBCXXABI_HAS_WIN32_THREAD_API=OFF \
-DLIBCXXABI_INCLUDE_TESTS=OFF \
-DLIBCXXABI_LIBCXX_INCLUDES=/src/build-libcxx/include/c++/v1 \
-DLIBCXXABI_LIBCXX_PATH=libcxx \
-DLIBCXXABI_SILENT_TERMINATE:BOOL=ON \
-DLIBCXXABI_USE_LLVM_UNWINDER=OFF \
-DUNIX=ON \
-DWASI_SDK_PREFIX=/usr \
-DLIBCXX_INSTALL_INCLUDE_DIR="include/$t/c++/v1" \
-DLIBCXX_INSTALL_INCLUDE_TARGET_DIR="include/$t/c++/v1" \
-DLIBCXXABI_INSTALL_INCLUDE_DIR="include/$t/c++/v1"
}
conf wasm32-wasip1 build
cmake --build build
conf wasm32-wasip1-threads build-threads
cmake --build build-threads
'''
install = '''
set -e
DESTDIR=/out cmake --install build
DESTDIR=/out cmake --install build-threads
S=/out/usr/share/wasi-sysroot
# El triple con el que cmake NOMBRA los directorios es el canónico (`wasm32-unknown-wasip1`) y el
# que clang BUSCA es el corto (`wasm32-wasip1`) — el mismo que ya publica `wasi-libc`. Sin este
# renombre libc++ quedaría instalada al lado del sitio donde se la busca: presente en el artefacto e
# invisible para el compilador, que es el modo de fallo más caro de todos.
cd "$S/lib"
for t in wasm32-unknown-wasip1 wasm32-unknown-wasip1-threads; do
[ -d "$t" ] && mv -v "$t" "${t/unknown-/}"
done
cd /src
# ⚠ UN DIRECTORIO VACÍO QUE SÍ IMPORTA — y por eso está en su propia línea con su propio párrafo.
#
# El APKBUILD de Alpine hace un `mkdir -p .../include/c++/v1` que parece ruido de empaquetado. NO
# lo es: es la SONDA por la que el driver de clang decide que este sysroot tiene layout de libc++.
# Sin ese directorio, clang NO añade `include/<triple>/c++/v1` a la lista de búsqueda, y los
# headers quedan instalados en el artefacto y a la vez invisibles para el compilador.
#
# MEDIDO, no deducido. Con el sysroot fusionado (wasi-libc + esta receta) y la invocación EXACTA
# que hace el configure de firefox:
#
# clang++ -std=gnu++20 --target=wasm32-wasip1 conftest.cpp --sysroot=$S -c
#
# sin include/c++/v1 -> fatal error: 'cstring' file not found (y `cstring` ESTABA ahí)
# con include/c++/v1 -> exit 0
#
# Es el reverso exacto de la regla 3 del CLAUDE.md: allá un directorio vacío es un artefacto
# mentiroso; acá un directorio vacío es una declaración dirigida al compilador. Borrarlo por
# "limpieza" reintroduce un fallo que aparece a 6 segundos de un build de 4 horas, culpando a los
# headers de WASI, que están perfectos.
mkdir -p "$S/include/c++/v1"
# GUARDIÁN. Se exige exactamente lo que el configure de firefox no encontró: el header de C++ que
# disparó todo esto y la librería que lo respalda, PARA LOS DOS TRIPLES. Un `cmake --install` que no
# instala nada sale con 0, y un sysroot con headers y sin `.a` pasa por bueno y revienta dentro del
# build de firefox, horas después y sin nombrar a esta receta.
for t in wasm32-wasip1 wasm32-wasip1-threads; do
H="$S/include/$t/c++/v1/cstring"
A="$S/lib/$t/libc++.a"
B="$S/lib/$t/libc++abi.a"
for f in "$H" "$A" "$B"; do
test -s "$f" || {
echo "!! falta (o está vacío) $f" >&2
echo " contenido real del sysroot:" >&2
find "$S" -maxdepth 3 \( -name 'libc++*.a' -o -name 'cstring' \) 2>/dev/null | head -20 >&2
exit 1
}
done
echo "guardián: $t ok — libc++.a $(stat -c%s "$A") bytes, libc++abi.a $(stat -c%s "$B") bytes"
done
# ── GUARDIÁN DE VERDAD: CORRER LA MISMA PRUEBA QUE HACE EL CONFIGURE DE FIREFOX ───────────────
# Los tests de arriba comprueban que los ficheros existen, y "existe" no es "se encuentra": el
# fallo que motivó esta receta era exactamente eso —`cstring` presente en el disco e invisible
# para clang—, así que un guardián de presencia lo habría dado por bueno.
#
# Se reproduce la invocación literal del configure de Mozilla contra un sysroot FUSIONADO (el de
# la dep `wasi-libc` más lo que acaba de instalar esta receta), que es la forma en que se va a usar
# de verdad. Si esto pasa acá, no puede fallar allá; y si falla, falla en el segundo 20 de ESTA
# receta y no en el segundo 6 de un build de cuatro horas que además culpa a otro.
P=/tmp/sysroot-fusionado
mkdir -p "$P"
cp -a /usr/share/wasi-sysroot/. "$P"/ # lo que aporta la dep wasi-libc
cp -a "$S"/. "$P"/ # lo que aporta esta receta
echo '#include <cstring>' > /tmp/probe.cpp
for t in wasm32-wasip1 wasm32-wasip1-threads; do
clang++ -std=gnu++20 --target=$t /tmp/probe.cpp --sysroot="$P" -c -o /tmp/probe.o || {
echo "!! libc++ instalada pero INVISIBLE para clang en $t." >&2
echo " Los headers están en el artefacto y el compilador no los encuentra." >&2
echo " Sospechá del directorio-sonda include/c++/v1 antes que de la instalación." >&2
clang++ -std=gnu++20 --target=$t /tmp/probe.cpp --sysroot="$P" -c -o /tmp/probe.o -v 2>&1 \
| sed -n '/search starts/,/End of search/p' >&2
exit 1
}
echo "guardián: clang++ encuentra <cstring> para $t (la prueba exacta del configure de firefox)"
done
'''