> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
build: flags estáticos vía `cargo rustc -- …` (no RUSTFLAGS) para no romper proc-macros
Tercera iteración del fix de la corrida real (runbook §8). +crt-static (y
relocation-model=static) en RUSTFLAGS rompen los proc-macros en build nativo:
RUSTFLAGS alcanza a las deps, y un proc-macro es un dylib PIC que no puede ser
estático ("cannot produce proc-macro for clap_derive ... crate types").
Fix: pasar los flags de codegen del binario por `cargo rustc --release … -- <flags>`,
que los aplica SÓLO a la crate top-level, dejando proc-macros/deps intactos. Así el
binario final es crt-static + relocation-model=static (AUTOCONTENIDO y ET_EXEC sin
interpreter, como busybox: sin libc.so, sin loader, sin soname). El linker (zig cc)
sigue por RUSTFLAGS. musl queda --disable-shared (no se necesita loader).
Validado por unit tests; la corrida real rebuildea hammerd/arje-zero crt-static.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs
index 1a2aabcb..28fb1877 100644
--- a/crates/hammer-build/src/lib.rs
+++ b/crates/hammer-build/src/lib.rs
@@ -288,23 +288,27 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
// (`UnknownOperatingSystem`); lo reescribimos a `x86_64-linux-musl`. Se usa como
// CC (cc-rs) y como linker (RUSTFLAGS, target-agnóstico — el default sería
// cc/gcc, ausente en el sandbox zig-only).
- let crt = match recipe.build.link {
- // Estático = AUTOCONTENIDO: el rust de Alpine enlaza musl dinámico por
- // defecto (deja `DT_NEEDED libc.musl-x86_64.so.1` y rompe el boot — el
- // loader no resuelve memcpy/…; ver runbook §8). `+crt-static` mete musl
- // dentro; `relocation-model=static` lo hace ET_EXEC sin interpreter (vía de
- // oro de hammer, como busybox: sin libc.so, sin loader, sin soname).
- LinkMode::Static => " -C target-feature=+crt-static -C relocation-model=static",
+ // Flags de codegen del BINARIO FINAL, vía `cargo rustc -- …` (NO RUSTFLAGS): en
+ // build nativo RUSTFLAGS también alcanza a los proc-macros (deps), y
+ // `+crt-static`/`relocation-model=static` los rompe ("cannot produce
+ // proc-macro" — un proc-macro es un dylib PIC). `cargo rustc -- …` los aplica
+ // sólo a la crate top-level, dejando los proc-macros intactos.
+ let bin_flags = match recipe.build.link {
+ // Estático AUTOCONTENIDO y no-PIE: `+crt-static` mete musl dentro (sin
+ // DT_NEEDED — el rust de Alpine enlaza musl dinámico por defecto, lo que deja
+ // `libc.musl-x86_64.so.1` y rompe el boot, runbook §8); `relocation-model=static`
+ // ⇒ ET_EXEC sin interpreter (vía de oro: como busybox, sin libc.so/loader/soname).
+ LinkMode::Static => "-C target-feature=+crt-static -C relocation-model=static",
// Dinámico: apaga crt-static para poder dlopen (necesita el loader/musl en el rootfs).
- LinkMode::Dynamic => " -C target-feature=-crt-static",
+ LinkMode::Dynamic => "-C target-feature=-crt-static",
};
Some(format!(
"printf '%s\\n' '#!/bin/sh' 'for a do' \
'case \"$a\" in --target=*) a=--target=x86_64-linux-musl ;; esac' \
'set -- \"$@\" \"$a\"' 'shift' 'done' 'exec zig cc \"$@\"' \
> \"$PWD/.hammer-zig-cc\" && chmod +x \"$PWD/.hammer-zig-cc\" && \
- CC=\"$PWD/.hammer-zig-cc\" RUSTFLAGS=\"-C linker=$PWD/.hammer-zig-cc{crt}\" \
- cargo build --release --locked --offline{flags_suffix}"
+ CC=\"$PWD/.hammer-zig-cc\" RUSTFLAGS=\"-C linker=$PWD/.hammer-zig-cc\" \
+ cargo rustc --release --locked --offline{flags_suffix} -- {bin_flags}"
))
} else {
// CROSS real (otro target que zig sí entiende): `--target` + linker por env
@@ -433,13 +437,13 @@ commit = "deadbeef"
let p = resolve_phases(&recipe(&[]), d.path()).unwrap();
assert!(p.configure.is_none(), "cargo no usa configure");
let c = p.compile.unwrap();
- assert!(c.contains("cargo build --release"), "{c}");
+ // Nativo: `cargo rustc` (no `cargo build`) para pasar los flags de codegen tras `--` sólo
+ // a la crate final; sin `--target` (lo cross sí; el único --target es el del wrapper saneador).
+ assert!(c.contains("cargo rustc --release"), "{c}");
assert!(c.contains("--locked") && c.contains("--offline"), "{c}");
- // Nativo: `cargo build` NO lleva --target (lo cross sí); el único --target del comando es
- // el del wrapper saneador del triple alpine.
assert!(!c.contains("--offline --target"), "nativo: cargo sin --target ({c})");
assert!(c.contains("a=--target=x86_64-linux-musl"), "wrapper sanea el triple ({c})");
- assert!(c.contains("target-feature=+crt-static"), "estático = autocontenido ({c})");
+ assert!(c.contains("-- -C target-feature=+crt-static"), "crt-static scoped tras -- ({c})");
assert!(c.contains("relocation-model=static"), "no-PIE: ET_EXEC sin interpreter ({c})");
assert!(c.contains("CC=\"$PWD/.hammer-zig-cc\""), "CC = wrapper para cc-rs ({c})");
assert!(c.contains("RUSTFLAGS=\"-C linker=$PWD/.hammer-zig-cc"), "linker por RUSTFLAGS ({c})");
@@ -478,7 +482,8 @@ commit = "deadbeef"
let d = tempfile::tempdir().unwrap();
touch(d.path(), "Cargo.toml");
let p = resolve_phases(&recipe(&["--features", "gpu"]), d.path()).unwrap();
- assert!(p.compile.unwrap().ends_with("--features gpu"));
+ // Los flags de la receta van como args de cargo (antes del `--` que separa los de rustc).
+ assert!(p.compile.unwrap().contains("--features gpu -- "));
}
#[test]
diff --git a/recipes/musl.toml b/recipes/musl.toml
index b3c59692..e46ef5a3 100644
--- a/recipes/musl.toml
+++ b/recipes/musl.toml
@@ -18,8 +18,8 @@ link = "static"
[build.phases]
# El sandbox exporta CC="zig cc", DESTDIR=/out y PREFIX=/usr. musl trae su propio `configure`.
-# Sólo estático: los componentes Rust se compilan crt-static (autocontenidos, sin libc.so), así que
-# el rootfs no necesita el loader/shared musl. Ver runbooks/stage1-vm-boot.md §8.
+# Sólo estático: los componentes Rust se compilan crt-static + no-PIE (autocontenidos, ET_EXEC sin
+# interpreter), así que el rootfs no necesita el loader/shared de musl. Ver runbooks §8.
configure = "./configure --prefix=/usr --disable-shared --enable-static"
compile = "make -j\"$(nproc)\""
install = "make install"