tawasuyu

sergio/takana

> 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…

TOML 1184Shell 70Rust 62Markdown 501503 Sutichaykuna

build: BuildSys::Cargo nativo para el target del sandbox (Opción A) — hammerd builda

Resuelve el bloqueo Rust de la primera corrida: el rust de Alpine es
x86_64-alpine-linux-musl y --target x86_64-unknown-linux-musl no tiene std.

Cuando recipe.build.target == SANDBOX_NATIVE_TARGET (x86_64-linux-musl, el del
sandbox), se construye NATIVO (sin --target): el std nativo del rust del sandbox
sirve. El wrapper .hammer-zig-cc ahora:
- es un único ejecutable (resuelve el `zig cc` de dos palabras que cargo y cc-rs
  mal-parsean),
- SANEA el triple: cc-rs (build scripts, p.ej. blake3) detecta el host triple de
  Alpine y pasa --target=x86_64-alpine-linux-musl, que zig rechaza
  (UnknownOperatingSystem); lo reescribe a x86_64-linux-musl.
Se usa como CC (cc-rs) y como linker (RUSTFLAGS). El cross real conserva --target.

Validado en VM real: hammerd compila nativo con zig cc y se sella. +1 test (cross
mantiene el triple), detect_cargo actualizado. arje-zero queda bloqueado aparte:
tawasuyu no commitea Cargo.lock (vendor --locked falla) — ver runbook §8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

4895c1c884c481b08b1c10efe2738f782ff51450 · ruwaq Sergio · (4 killa ñawpaq) · tayta 5a1088d5c · kay commitpi sach'ata qhaway

Semántico sayk'uy · 4

crates/hammer-build/src/lib.rs

2 qillqakuna · +89 −18

diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs
index e494c608..8e156e55 100644
--- a/crates/hammer-build/src/lib.rs
+++ b/crates/hammer-build/src/lib.rs
@@ -219,6 +219,12 @@ fn detect_build_system(src: &Path) -> BuildSys {
     BuildSys::Unknown
 }
 
+/// Target nativo del sandbox del lab (Alpine x86_64 musl). Una receta Cargo cuyo `build.target`
+/// sea éste se construye **nativa** (sin `--target`): el rust del sandbox sólo trae su std nativo
+/// (`x86_64-alpine-linux-musl`), no el `x86_64-unknown-linux-musl` que pediría un cross, y no hay
+/// rustup para agregarlo. Ver `docs/runbooks/stage1-vm-boot.md` §8 (primera corrida).
+const SANDBOX_NATIVE_TARGET: &str = "x86_64-linux-musl";
+
 /// Traduce el triple estilo zig/hammer (`x86_64-linux-musl`) al de rustc
 /// (`x86_64-unknown-linux-musl`). Un triple ya completo (con vendor) o ajeno al patrón
 /// `<arch>-linux-<libc>` se devuelve tal cual: la receta puede dar el triple exacto.
@@ -268,19 +274,51 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
                 // `--locked` (Cargo.lock fijo, coherente con ADR 0006). El link va por un
                 // wrapper `zig cc` — cargo no acepta un linker de dos palabras, y zig provee
                 // el `libgcc_s` que el link dinámico musl pide (ver M2 del plan arje↔hammer).
-                let triple = rustc_triple(&recipe.build.target);
-                let linker_var = cargo_linker_env_var(&triple);
-                let rustflags = match recipe.build.link {
-                    // musl es crt-static por defecto; el dinámico lo apaga para poder dlopen.
-                    LinkMode::Dynamic => "RUSTFLAGS='-C target-feature=-crt-static' ",
-                    LinkMode::Static => "",
-                };
-                Some(format!(
-                    "printf '#!/bin/sh\\nexec zig cc \"$@\"\\n' > \"$PWD/.hammer-zig-cc\" && \
-                     chmod +x \"$PWD/.hammer-zig-cc\" && \
-                     {rustflags}{linker_var}=\"$PWD/.hammer-zig-cc\" \
-                     cargo build --release --locked --offline --target {triple}{flags_suffix}"
-                ))
+                if recipe.build.target == SANDBOX_NATIVE_TARGET {
+                    // NATIVO: el sandbox del lab YA es x86_64 musl, así que no cruzamos. El rust del
+                    // sandbox sólo trae su std nativo (`x86_64-alpine-linux-musl`), no
+                    // `x86_64-unknown-linux-musl`; con `--target` fallaría "can't find crate for
+                    // core" (no hay rustup). Ver docs/runbooks/stage1-vm-boot.md §8.
+                    //
+                    // El wrapper `.hammer-zig-cc` hace DOS cosas: (1) es un único ejecutable, lo que
+                    // resuelve el `zig cc` de dos palabras que cargo (linker) y cc-rs (CC)
+                    // mal-parsean; (2) **sanea el triple**: cc-rs (build scripts que compilan C/asm,
+                    // p.ej. blake3) detecta el host triple de Alpine rust y pasa
+                    // `--target=x86_64-alpine-linux-musl`, pero zig no conoce el vendor "alpine"
+                    // (`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 {
+                        LinkMode::Dynamic => " -C target-feature=-crt-static",
+                        LinkMode::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}"
+                    ))
+                } else {
+                    // CROSS real (otro target que zig sí entiende): `--target` + linker por env
+                    // per-triple. Hermético y reproducible: `--offline` (deps vendoreadas) +
+                    // `--locked` (Cargo.lock fijo, ADR 0006). zig provee el `libgcc_s` que el link
+                    // dinámico musl pide (ver M2 del plan arje↔hammer).
+                    let setup = "printf '#!/bin/sh\\nexec zig cc \"$@\"\\n' > \"$PWD/.hammer-zig-cc\" && \
+                                 chmod +x \"$PWD/.hammer-zig-cc\" && ";
+                    let triple = rustc_triple(&recipe.build.target);
+                    let linker_var = cargo_linker_env_var(&triple);
+                    let rustflags = match recipe.build.link {
+                        // musl es crt-static por defecto; el dinámico lo apaga para poder dlopen.
+                        LinkMode::Dynamic => "RUSTFLAGS='-C target-feature=-crt-static' ",
+                        LinkMode::Static => "",
+                    };
+                    Some(format!(
+                        "{setup}{rustflags}{linker_var}=\"$PWD/.hammer-zig-cc\" \
+                         cargo build --release --locked --offline --target {triple}{flags_suffix}"
+                    ))
+                }
             }
             BuildSys::Unknown => None,
         };
@@ -295,10 +333,15 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
             BuildSys::Cargo => {
                 // Copia los ejecutables finales (top del dir release, bit de ejecución) a
                 // /out/usr/bin. `-perm -100` / `-exec cp \;` son busybox-safe (rootfs Alpine).
-                let triple = rustc_triple(&recipe.build.target);
+                // Build nativo ⇒ `target/release`; cross ⇒ `target/<triple>/release`.
+                let rel = if recipe.build.target == SANDBOX_NATIVE_TARGET {
+                    "target/release".to_string()
+                } else {
+                    format!("target/{}/release", rustc_triple(&recipe.build.target))
+                };
                 Some(format!(
                     "mkdir -p /out/usr/bin && \
-                     find target/{triple}/release -maxdepth 1 -type f -perm -100 \
+                     find {rel} -maxdepth 1 -type f -perm -100 \
                      -exec cp {{}} /out/usr/bin/ \\;"
                 ))
             }
@@ -380,16 +423,35 @@ commit = "deadbeef"
         let d = tempfile::tempdir().unwrap();
         touch(d.path(), "Cargo.toml");
         assert_eq!(detect_build_system(d.path()), BuildSys::Cargo);
+        // Target default (x86_64-linux-musl) = nativo del sandbox ⇒ build SIN --target.
         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}");
         assert!(c.contains("--locked") && c.contains("--offline"), "{c}");
-        assert!(c.contains("--target x86_64-unknown-linux-musl"), "{c}");
-        assert!(c.contains("CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="), "{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("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})");
         let i = p.install.unwrap();
         assert!(i.contains("/out/usr/bin"), "{i}");
-        assert!(i.contains("target/x86_64-unknown-linux-musl/release"), "{i}");
+        assert!(i.contains("find target/release"), "nativo instala de target/release ({i})");
+    }
+
+    #[test]
+    fn cargo_cross_target_keeps_triple() {
+        // Un target NO nativo (cross real) sí pasa --target + el linker per-triple.
+        let d = tempfile::tempdir().unwrap();
+        touch(d.path(), "Cargo.toml");
+        let mut r = recipe(&[]);
+        r.build.target = "aarch64-linux-musl".into();
+        let p = resolve_phases(&r, d.path()).unwrap();
+        let c = p.compile.unwrap();
+        assert!(c.contains("--target aarch64-unknown-linux-musl"), "{c}");
+        assert!(c.contains("CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="), "{c}");
+        assert!(p.install.unwrap().contains("target/aarch64-unknown-linux-musl/release"));
     }
 
     #[test]
diff --git a/docs/runbooks/stage1-vm-boot.md b/docs/runbooks/stage1-vm-boot.md
index d36ac3be..686be694 100644
--- a/docs/runbooks/stage1-vm-boot.md
+++ b/docs/runbooks/stage1-vm-boot.md
@@ -155,6 +155,15 @@ Rust está bloqueado por una decisión de toolchain.** Hallazgos, en orden:
 **Conclusión:** el bootstrap C (musl+busybox+zig cc) está **validado**. Completar el rootfs y el boot
 en QEMU requiere resolver el toolchain Rust (hammerd/arje-zero) y más disco (vendor del monorepo).
 
+**Actualización (Opción A implementada).** `BuildSys::Cargo` ahora construye **nativo** sin `--target`
+cuando el target es el del sandbox (`SANDBOX_NATIVE_TARGET`), con un wrapper `.hammer-zig-cc` que
+(a) resuelve el `zig cc` de dos palabras para cargo/cc-rs y (b) **sanea el triple** que cc-rs pasa
+(`--target=x86_64-alpine-linux-musl` → `x86_64-linux-musl`, que zig sí entiende). Resultado:
+**hammerd compila nativo y se sella ✓** — el camino Rust funciona. **arje-zero queda bloqueado** por
+otra razón: **tawasuyu no commitea `Cargo.lock`** (`git ls-files Cargo.lock` vacío), así que el
+`git archive` no lo trae y `cargo vendor --locked` no puede crearlo. Decisión pendiente: commitear
+el lock en tawasuyu (reproducible, plan C.2 #5) vs relajar `--locked` en el vendoring de hammer.
+
 ## 9. Cross-check opcional — `arje-packager`
 
 arje trae su propio empaquetador (`03_ukupacha/arje/init/arje-packager`):