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>
This commit is contained in:
Sergio
2026-06-11 11:24:01 +00:00
co-authored by Claude Opus 4.8
parent ce7b570a62
commit 879af17f5c
2 changed files with 22 additions and 17 deletions
+20 -15
View File
@@ -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]
+2 -2
View File
@@ -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"