#!/usr/bin/env bash # bootstrap-devfs.sh — Prepara .dev-fs/ con todo lo que hammer-build necesita: # 1. Rootfs Alpine minirootfs como base hermética del sandbox. # 2. Compilador zig oficial bajo .dev-fs/tools/zig (symlink versionado). # 3. Build tools (make, autoconf, automake, m4, patch, coreutils, libtool, # pkgconf, bash) instaladas dentro del rootfs con apk. # 4. Directorios de caché y work para builds posteriores. # # Idempotente: cada paso se salta si ya hizo. Reinvocar tras un cambio de # versión actualiza in-place. # # Variables (override por entorno): # ALPINE_BRANCH rama de Alpine (default v3.23) # ALPINE_VER versión exacta del tarball (default 3.23.4) # ALPINE_SHA256 sha256 del tarball (verificado obligatorio) # ZIG_VER versión de zig (default 0.16.0) # ZIG_SHA256 sha256 del tarball zig # # Uso: # ./scripts/bootstrap-devfs.sh # bootstrap completo # ./scripts/bootstrap-devfs.sh --skip-apk # sólo rootfs + zig # ./scripts/bootstrap-devfs.sh --force-zig # re-descarga zig set -euo pipefail ALPINE_BRANCH="${ALPINE_BRANCH:-v3.23}" ALPINE_VER="${ALPINE_VER:-3.23.4}" ALPINE_SHA256="${ALPINE_SHA256:-85498865362aa7ebececa0d725a2f2e4db7ac4e4b2850b8df21645afa0d03ee3}" ZIG_VER="${ZIG_VER:-0.16.0}" ZIG_SHA256="${ZIG_SHA256:-70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00}" # Toolchain Go del HOST (para `go mod vendor` en el fetch; cf. recipes/go.toml para el sandbox). GO_VER="${GO_VER:-1.26.4}" GO_SHA256="${GO_SHA256:-1153d3d50e0ac764b447adfe05c2bcf08e889d42a02e0fe0259bd47f6733ad7f}" SKIP_APK=0 FORCE_ZIG=0 FORCE_ROOTFS=0 for arg in "$@"; do case "$arg" in --skip-apk) SKIP_APK=1 ;; --force-zig) FORCE_ZIG=1 ;; --force-rootfs) FORCE_ROOTFS=1 ;; -h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \?//' exit 0 ;; *) echo "argumento desconocido: $arg" >&2 exit 1 ;; esac done REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" DEVFS="$REPO_ROOT/.dev-fs" ROOTFS="$DEVFS/alpine" TOOLS="$DEVFS/tools" ZIG_DIR="$TOOLS/zig-x86_64-linux-$ZIG_VER" ZIG_LINK="$TOOLS/zig" CACHE="$DEVFS/cache" WORK="$REPO_ROOT/work" log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; } mkdir -p "$DEVFS" "$TOOLS" "$CACHE" "$WORK" # --- 1. Rootfs Alpine --------------------------------------------------------- if [[ $FORCE_ROOTFS -eq 1 ]] && [[ -d "$ROOTFS" ]]; then log "rootfs: --force-rootfs ⇒ borro $ROOTFS" rm -rf "$ROOTFS" fi if [[ -f "$ROOTFS/bin/busybox" ]]; then ok "rootfs Alpine ya presente en $ROOTFS" else log "rootfs: descargando alpine-minirootfs $ALPINE_VER" tar="$DEVFS/alpine-minirootfs-$ALPINE_VER.tar.gz" url="https://dl-cdn.alpinelinux.org/alpine/$ALPINE_BRANCH/releases/x86_64/alpine-minirootfs-$ALPINE_VER-x86_64.tar.gz" curl -fsSL "$url" -o "$tar" echo "$ALPINE_SHA256 $tar" | sha256sum -c - mkdir -p "$ROOTFS" tar -xzf "$tar" -C "$ROOTFS" rm -f "$tar" ok "rootfs Alpine $ALPINE_VER extraído" fi # --- 2. Compilador zig -------------------------------------------------------- if [[ $FORCE_ZIG -eq 1 ]] && [[ -e "$ZIG_DIR" ]]; then log "zig: --force-zig ⇒ borro $ZIG_DIR" rm -rf "$ZIG_DIR" fi if [[ -x "$ZIG_LINK/zig" ]]; then ok "zig $ZIG_VER ya presente en $ZIG_LINK" else log "zig: descargando $ZIG_VER" tar="$TOOLS/zig.tar.xz" url="https://ziglang.org/download/$ZIG_VER/zig-x86_64-linux-$ZIG_VER.tar.xz" curl -fsSL "$url" -o "$tar" echo "$ZIG_SHA256 $tar" | sha256sum -c - tar -xJf "$tar" -C "$TOOLS" rm -f "$tar" ln -sfn "zig-x86_64-linux-$ZIG_VER" "$ZIG_LINK" ok "zig $ZIG_VER instalado, symlink $ZIG_LINK" fi # --- 2b. Toolchain Go del host (frente Go, Etapa G) ---------------------------- # El vendoring de módulos Go (`go mod vendor`) corre en el HOST durante el fetch (red # permitida), igual que `cargo vendor`. El binario `go` oficial es estático ⇒ corre en # cualquier host. Lo dejamos en .dev-fs/tools/go (fuente de verdad versionada) y lo # enlazamos a ~/.cargo/bin (que el worker pone en PATH vía `. ~/.cargo/env`). GO_DIR="$TOOLS/go-$GO_VER" GO_LINK="$TOOLS/go" if [[ -x "$GO_LINK/bin/go" ]] && "$GO_LINK/bin/go" version 2>/dev/null | grep -q "go$GO_VER"; then ok "go $GO_VER ya presente en $GO_LINK" else log "go: descargando $GO_VER" tar="$TOOLS/go.tar.gz" url="https://go.dev/dl/go$GO_VER.linux-amd64.tar.gz" curl -fsSL "$url" -o "$tar" echo "$GO_SHA256 $tar" | sha256sum -c - rm -rf "$GO_DIR"; mkdir -p "$GO_DIR" tar -xzf "$tar" -C "$GO_DIR" --strip-components=1 rm -f "$tar" ln -sfn "go-$GO_VER" "$GO_LINK" ok "go $GO_VER instalado en $GO_LINK" fi # Symlink en un dir del PATH del host (junto a cargo) para que el fetch encuentre `go`. if [[ -d "$HOME/.cargo/bin" ]]; then ln -sfn "$GO_LINK/bin/go" "$HOME/.cargo/bin/go" ln -sfn "$GO_LINK/bin/gofmt" "$HOME/.cargo/bin/gofmt" ok "go enlazado en ~/.cargo/bin (PATH del host/worker)" else log "go: ~/.cargo/bin no existe; agregá $GO_LINK/bin al PATH del host a mano" fi # --- 3. Build tools en el rootfs (apk add) ------------------------------------ if [[ $SKIP_APK -eq 1 ]]; then log "apk: --skip-apk ⇒ saltando build tools" else # `binutils` aporta ld/ar/ranlib/nm/strip: configure de autotools los inspecciona # incluso cuando el compilador real es zig cc. # # `rust cargo`: las recetas Cargo (hammerd, arje-zero) corren `cargo build` DENTRO del # sandbox. CAVEAT (primera corrida 2026-06-11): el rust de Alpine tiene host triple # `x86_64-alpine-linux-musl`, pero `BuildSys::Cargo` pide `--target x86_64-unknown-linux-musl` # y Alpine no trae ese std (no hay rustup). Falta resolver el toolchain Rust del lab (plan C.2) # — ver docs/runbooks/stage1-vm-boot.md §8. Hasta entonces, los componentes C (musl, busybox) # sí construyen end-to-end. # BUMP DELIBERADO de toolchain (2026-06-22, [[cargo-recipe-msrv-ceiling]]): Alpine v3.23 trae # rust 1.91.1 ⇒ techo MSRV que bloquea recetas modernas (ouch/uv 1.93, oxlint/ruff 1.94, # zellij 1.92). edge trae rust 1.96.0. Apuntamos los repos del rootfs a EDGE para que `apk add` # resuelva rust/cargo/clang 1.96 (+ arrastra gcc 15.2/musl 1.2.6/llvm22 como deps). El of_tree # del 4/4 NO depende de esto (usa SWAP_RUST con el rust hammer-built); esto es el toolchain del # CATÁLOGO. Para anclar una versión exacta en vez de un edge rodante, pinear un snapshot de edge. if [[ -f "$ROOTFS/etc/apk/repositories" ]]; then printf '%s\n%s\n' \ 'https://dl-cdn.alpinelinux.org/alpine/edge/main' \ 'https://dl-cdn.alpinelinux.org/alpine/edge/community' \ > "$ROOTFS/etc/apk/repositories" fi # `linux-headers`: UAPI del kernel (linux/kd.h, etc.) que zig cc NO bundlea; busybox y otros # paquetes la necesitan. zig cc nativo la encuentra en /usr/include. (Lo destapó la # verificación de reproducibilidad de Stage 2: con el config determinista, los applets de # console-tools la exigen.) # `bwrap git curl`: el rebuild *dentro* del builder rootfs (Stage 2 pleno, SDD 11 §7) corre # hammer-build con su propio toolchain. En el host estas tres las aporta el sistema, pero en la # VM el toolchain ES el único userland capaz, así que deben vivir aquí: `bwrap` anida el sandbox # de build, `git` rematerializa las fuentes git (git archive del mirror), `curl` baja tarballs. # `kmod` aporta insmod/modprobe: en la VM el sandbox de build usa overlay (módulo del kernel, # CONFIG_OVERLAY_FS=m) y el initramfs arranca sin módulos cargados, así que el builder debe poder # cargarlo. El `overlay.ko` en sí es específico del kernel destino (no de Alpine): se inyecta # aparte en el builder (ver docs/runbooks/stage1-vm-boot.md §8c). # `g++ zlib-dev`: bootstrap del frente rust variante (b) — mrustc es C++14 y DEBE compilarse con # g++ (clang/zig rompen sus macros `tagged_union`, ver mrustc issue #388) y enlaza `-lz`. El # toolchain traía libstdc++ runtime pero no cc1plus/headers ni zlib-dev. Son compiladores/libs de # arranque (mismo status que make/zig/gcc); el veto purista es sobre un *rustc* prebuilt, no sobre # un compilador C++. (zlib-dev quedará reemplazado por recipes/zlib.toml cuando el frente avance.) # NOTA: `flex bison` YA NO van aquí — de-Alpinizados a recipes/{flex,bison}.toml (deps.build del # kernel). Sólo el kernel los usa (kconfig); el overlay de deps los provee. (m4, que bison invoca en # runtime, sigue en el base como tool autotools general.) # NOTA: `elfutils-dev` YA NO va aquí — de-Alpinizado a recipes/elfutils.toml (sólo libelf, con shims # musl argp/error/libintl/fts/obstack/rawmemchr), deps.build del kernel. objtool enlaza el libelf # hammer desde la capa overlay. (El runtime libelf.so.1 de Alpine, si algún otro tool lo necesita, # es la misma versión 0.194 ABI-compatible.) # NOTA: `openssl-dev` YA NO va aquí — de-Alpinizado a recipes/openssl.toml (libcrypto estática, # deps.build del kernel). El host-tool certs/extract-cert lo enlaza desde la capa overlay hammer. # El runtime libcrypto3/libssl3 (que curl/git necesitan) lo trae Alpine aparte, no es openssl-dev. # `clang-dev clang-libs`: aportan libclang.so — `bindgen` (que usan varios *-sys: libbzip3-sys, # etc.) lo carga en build para generar bindings de los headers C. No lo trae el base. # `llvm22`: la SUITE binutils de LLVM (llvm-ar/llvm-objdump/llvm-profdata/llvm-nm/llvm-readobj). # `clang`/`clang-libs` NO la arrastran (sólo libLLVM.so + los binarios clang), y mozjs/spidermonkey # con clang exige llvm-ar y llvm-objdump ⇒ sin esto configure aborta "Cannot find ar/llvm-objdump". # Alpine deja los binarios en /usr/lib/llvm22/bin SIN symlinkear a /usr/bin (multi-versión); el paso # 3a-ter de abajo los expone en el PATH estándar, igual que Alpine ya hace con `clang`. NEEDED="make autoconf automake m4 patch coreutils libtool pkgconf bash binutils g++ zlib-dev rust cargo clang-dev clang-libs llvm22 linux-headers bubblewrap git curl kmod" missing="" for pkg in $NEEDED; do # apk info -e devuelve el paquete si está instalado, vacío si no. if ! bwrap --bind "$ROOTFS" / --proc /proc --dev /dev --unshare-all \ --setenv PATH /usr/bin:/usr/sbin:/bin:/sbin \ /sbin/apk info -e "$pkg" >/dev/null 2>&1; then missing="$missing $pkg" fi done if [[ -n "$missing" ]]; then log "apk: instalando paquetes faltantes:$missing" bwrap --bind "$ROOTFS" / --proc /proc --dev /dev \ --ro-bind /etc/resolv.conf /etc/resolv.conf --share-net \ /sbin/apk update >/dev/null bwrap --bind "$ROOTFS" / --proc /proc --dev /dev \ --ro-bind /etc/resolv.conf /etc/resolv.conf --share-net \ /sbin/apk add $missing fi ok "build tools presentes en el rootfs ($NEEDED)" fi # --- 3a-ter. Exponer la suite llvm-* en /usr/bin ----------------------------- # Alpine instala los binutils de LLVM en /usr/lib/llvm22/bin (versionado) sin symlink a /usr/bin, # para permitir varias versiones. El PATH del sandbox es fijo (/opt/zig:/usr/local/bin:/usr/bin:...) # y NO incluye ese dir, así que sin symlinks `llvm-ar`/`llvm-objdump` son invisibles y mozjs no # configura. Los symlinkeamos relativos (idéntico a como el propio Alpine expone `clang`). Idempotente. if [[ -d "$ROOTFS/usr/lib/llvm22/bin" ]]; then for f in "$ROOTFS"/usr/lib/llvm22/bin/llvm-*; do t="$(basename "$f")" [[ -e "$ROOTFS/usr/bin/$t" ]] || ln -s "../lib/llvm22/bin/$t" "$ROOTFS/usr/bin/$t" done ok "suite llvm-* expuesta en /usr/bin" fi # --- 3a-bis. Neutralizar scudo-malloc (frente edge rust) ---------------------- # edge empaqueta rust/cargo con `scudo-malloc` como DEPENDENCIA DURA: los shims rustc/cargo lo # linkean como PRIMER NEEDED ⇒ el allocator endurecido scudo interpone malloc en todo el proceso # (incluido librustc_driver). scudo CRASHEA a rustc en ciertos proc-macros (corrupted-chunk-header / # SIGSEGV: lo cazaron jnv/presenterm/trippy) — choque entre scudo y la carga/descarga de dylibs de # proc-macro. No se puede `apk del scudo-malloc` (rust/cargo dependen). Mitigación: aliasar # libscudo.so → la libc musl ⇒ el NEEDED resuelve pero malloc cae a musl (sin scudo). Los shims no # referencian símbolos scudo-específicos (sólo querían el override de malloc), así que es seguro. # Reversible: el backup .orig-scudo. Destrabó jnv/presenterm/trippy. SCUDO="$ROOTFS/usr/lib/libscudo.so" if [[ $SKIP_APK -eq 0 && -f "$SCUDO" && ! -L "$SCUDO" ]]; then log "scudo: neutralizando (alias libscudo.so → musl libc; evita el crash de rustc en proc-macros)" cp -a "$SCUDO" "$SCUDO.orig-scudo" ln -sf /lib/libc.musl-x86_64.so.1 "$SCUDO" ok "scudo neutralizado" fi # --- 3b. musl del toolchain con PTHREAD_KEYS_MAX=256 (frente rust variante b) -- # El rustc booteado por mrustc (frente rust) agota los pthread keys de musl al correr: # `fatal runtime error: out of TLS keys, aborting` (musl default PTHREAD_KEYS_MAX=128; es el # gotcha #4 de mrustc issue #388). Es el musl del TOOLCHAIN — el loader que toda tool del rootfs # linkea dinámicamente como intérprete (/lib/ld-musl-x86_64.so.1) — NO el musl target del 4/4 # (recipes/musl.toml, estático, intacto). Reconstruimos musl 1.2.5 con 256 keys y reemplazamos el # loader. ABI-compat (mismo musl 1.2.5): gcc/make/etc. siguen corriendo. Seguro para of_tree: el # loader runtime de las tools no afecta los bytes que emiten; el 4/4 es estático contra hammer-musl. # Idempotente: si ya existe el backup .orig128, no rehace nada. # CRÍTICO: el loader reconstruido DEBE ser de la MISMA versión de musl que el rootfs (Alpine edge # es rodante: pasó de 1.2.5 a 1.2.6). Si difieren, los coreutils del rootfs (linkeados contra la # versión de edge) no hallan símbolos nuevos en el loader viejo (p.ej. `renameat2: symbol not # found` con loader 1.2.5 vs coreutils 1.2.6) ⇒ chmod/cat/rm rotos dentro del sandbox y los builds # fallan al ejecutar el wrapper zig-cc (EACCES). Por eso DETECTAMOS la versión del rootfs. MUSL_LOADER="$ROOTFS/lib/ld-musl-x86_64.so.1" if [[ $SKIP_APK -eq 0 && ! -f "$MUSL_LOADER.orig128" ]]; then # versión de musl instalada en el rootfs (apk db): "1.2.6-r2" → "1.2.6" MUSL_VER="$(awk '/^P:musl$/{f=1} f&&/^V:/{sub(/-.*/,"",$0);print substr($0,3);exit}' \ "$ROOTFS/lib/apk/db/installed" 2>/dev/null)" MUSL_VER="${MUSL_VER:-1.2.6}" case "$MUSL_VER" in 1.2.5) MUSL_SHA=a9a118bbe84d8764da0ea0d28b3ab3fae8477fc7e4085d90102b8596fc7c75e4 ;; 1.2.6) MUSL_SHA=d585fd3b613c66151fc3249e8ed44f77020cb5e6c1e635a616d3f9f82460512a ;; *) die "musl $MUSL_VER del rootfs sin sha256 conocido — agregalo al case (bootstrap-devfs.sh §3b)" ;; esac log "musl: reconstruyendo loader del toolchain $MUSL_VER con PTHREAD_KEYS_MAX=256 (frente rust)" MWORK="$WORK/musl256"; rm -rf "$MWORK"; mkdir -p "$MWORK" curl -fsSL -o "$MWORK/musl.tar.gz" "https://musl.libc.org/releases/musl-$MUSL_VER.tar.gz" echo "$MUSL_SHA $MWORK/musl.tar.gz" | sha256sum -c - >/dev/null || die "musl tarball sha256 mismatch" tar -C "$MWORK" -xzf "$MWORK/musl.tar.gz" sed -i 's/#define PTHREAD_KEYS_MAX 128/#define PTHREAD_KEYS_MAX 256/' "$MWORK/musl-$MUSL_VER/include/limits.h" # Build de lib/libc.so (= el loader) dentro del rootfs con gcc (como lo compila Alpine). bwrap --bind "$ROOTFS" / --bind "$MWORK" /musl256 --proc /proc --dev /dev --tmpfs /tmp \ --setenv PATH /usr/bin:/bin --setenv HOME /tmp --chdir "/musl256/musl-$MUSL_VER" \ sh -c 'CC=gcc ./configure --prefix=/usr >/dev/null && make -j"$(nproc)" lib/libc.so >/dev/null' \ || die "build de musl-256 falló" cp -a "$MUSL_LOADER" "$MUSL_LOADER.orig128" # backup del loader Alpine original cp "$MWORK/musl-$MUSL_VER/lib/libc.so" "$MUSL_LOADER" ok "musl del toolchain con 256 TLS keys (loader reemplazado; backup en .orig128)" fi # --- 4. Resumen --------------------------------------------------------------- cat <