From c5ab7e4502c6a8a81568a506af54bb20cc09a463 Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 11 Jun 2026 00:37:33 +0000 Subject: [PATCH] =?UTF-8?q?feat(build):=20BuildSys::Cargo=20=E2=80=94=20el?= =?UTF-8?q?=20lab=20construye=20crates=20Rust?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Desbloquea integrar el workspace tawasuyu (y cualquier crate Rust) en el lab: - detección por Cargo.toml (gana sobre Makefile huérfano, cede ante CMake de un políglota C+Rust). - traducción de triple zig→rustc (x86_64-linux-musl → x86_64-unknown-linux-musl). - compile: cargo build --release --locked --offline --target ; link por wrapper zig cc (CARGO_TARGET__LINKER) que provee el libgcc_s que el link dinámico musl exige (cierra el 'cannot find -lgcc_s' que destapó M2). link=dynamic añade -C target-feature=-crt-static para dlopen (apps gráficas: Vulkan/Wayland). - install: copia los ejecutables de target//release a /out/usr/bin (busybox-safe). - doc 02-build-lab §Cargo (vendoreo de deps para build offline) + receta de ejemplo recipes/llimphi-counter.toml (caso gráfico dinámico). - 5 tests nuevos (34/34 verdes en hammer-build). Coordina con tawasuyu/03_ukupacha/arje/PLAN-ATESTACION-Y-HAMMER §C (milestone: falta BuildSys::Cargo en el lab). --- crates/hammer-build/src/lib.rs | 112 ++++++++++++++++++++++++++++++++- docs/02-build-lab.md | 22 ++++++- recipes/llimphi-counter.toml | 29 +++++++++ 3 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 recipes/llimphi-counter.toml diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs index a37f9fdf..fa885a8b 100644 --- a/crates/hammer-build/src/lib.rs +++ b/crates/hammer-build/src/lib.rs @@ -173,6 +173,8 @@ enum BuildSys { CMake, /// `meson.build`. Meson, + /// `Cargo.toml`: crate o workspace Rust. Build con `cargo`, link con `zig cc`. + Cargo, /// `Makefile` plano (o `makefile`) sin sistema generador. Make, /// No detectamos nada: la receta debe traer overrides en `[build.phases]`. @@ -199,12 +201,32 @@ fn detect_build_system(src: &Path) -> BuildSys { if has("meson.build") { return BuildSys::Meson; } + // Cargo gana sobre un `Makefile` huérfano (algunos crates traen un wrapper `make`), + // pero cede ante autotools/cmake/meson (un políglota C+Rust suele querer el sistema C). + if has("Cargo.toml") { + return BuildSys::Cargo; + } if has("Makefile") || has("makefile") || has("GNUmakefile") { return BuildSys::Make; } BuildSys::Unknown } +/// 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 +/// `-linux-` se devuelve tal cual: la receta puede dar el triple exacto. +fn rustc_triple(target: &str) -> String { + match target.split('-').collect::>().as_slice() { + [arch, "linux", libc] => format!("{arch}-unknown-linux-{libc}"), + _ => target.to_string(), + } +} + +/// Nombre de la env `CARGO_TARGET__LINKER` para un triple de rustc. +fn cargo_linker_env_var(triple: &str) -> String { + format!("CARGO_TARGET_{}_LINKER", triple.to_uppercase().replace(['-', '.'], "_")) +} + /// Devuelve los comandos a ejecutar para cada fase: overrides explícitos en la receta /// ganan; el resto lo deriva la heurística según el sistema de build detectado. fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result { @@ -223,7 +245,8 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result { "cmake -S . -B _build -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release{flags_suffix}" )), BuildSys::Meson => Some(format!("meson setup _build --prefix=/usr{flags_suffix}")), - BuildSys::Make | BuildSys::Unknown => None, + // Cargo no tiene fase configure separada. + BuildSys::Cargo | BuildSys::Make | BuildSys::Unknown => None, }; } if out.compile.is_none() { @@ -233,6 +256,25 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result { } BuildSys::CMake => Some(r#"cmake --build _build -j "$(nproc)""#.to_string()), BuildSys::Meson => Some("meson compile -C _build".to_string()), + BuildSys::Cargo => { + // Hermético y reproducible: `--offline` (deps vendoreadas en la fuente) + + // `--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}" + )) + } BuildSys::Unknown => None, }; } @@ -243,6 +285,16 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result { } BuildSys::CMake => Some("DESTDIR=/out cmake --install _build".to_string()), BuildSys::Meson => Some("DESTDIR=/out meson install -C _build --no-rebuild".to_string()), + 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); + Some(format!( + "mkdir -p /out/usr/bin && \ + find target/{triple}/release -maxdepth 1 -type f -perm -100 \ + -exec cp {{}} /out/usr/bin/ \\;" + )) + } BuildSys::Unknown => None, }; } @@ -316,6 +368,64 @@ commit = "deadbeef" assert!(p.install.unwrap().contains("meson install")); } + #[test] + fn detect_cargo() { + let d = tempfile::tempdir().unwrap(); + touch(d.path(), "Cargo.toml"); + assert_eq!(detect_build_system(d.path()), BuildSys::Cargo); + 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}"); + let i = p.install.unwrap(); + assert!(i.contains("/out/usr/bin"), "{i}"); + assert!(i.contains("target/x86_64-unknown-linux-musl/release"), "{i}"); + } + + #[test] + fn cargo_wins_over_stray_makefile_but_not_cmake() { + let d = tempfile::tempdir().unwrap(); + touch(d.path(), "Cargo.toml"); + touch(d.path(), "Makefile"); + assert_eq!(detect_build_system(d.path()), BuildSys::Cargo); + // pero un proyecto C+Rust con CMakeLists prefiere el sistema C + touch(d.path(), "CMakeLists.txt"); + assert_eq!(detect_build_system(d.path()), BuildSys::CMake); + } + + #[test] + fn cargo_flags_go_to_compile() { + 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")); + } + + #[test] + fn cargo_dynamic_disables_crt_static() { + let d = tempfile::tempdir().unwrap(); + touch(d.path(), "Cargo.toml"); + let mut r = recipe(&[]); + r.build.link = hammer_core::LinkMode::Dynamic; + let c = resolve_phases(&r, d.path()).unwrap().compile.unwrap(); + assert!(c.contains("target-feature=-crt-static"), "{c}"); + } + + #[test] + fn rustc_triple_translation() { + assert_eq!(rustc_triple("x86_64-linux-musl"), "x86_64-unknown-linux-musl"); + assert_eq!(rustc_triple("aarch64-linux-gnu"), "aarch64-unknown-linux-gnu"); + // un triple ya completo pasa intacto + assert_eq!(rustc_triple("x86_64-unknown-linux-musl"), "x86_64-unknown-linux-musl"); + assert_eq!( + cargo_linker_env_var("x86_64-unknown-linux-musl"), + "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER" + ); + } + #[test] fn detect_make() { let d = tempfile::tempdir().unwrap(); diff --git a/docs/02-build-lab.md b/docs/02-build-lab.md index 49aa10f3..4816c55e 100644 --- a/docs/02-build-lab.md +++ b/docs/02-build-lab.md @@ -87,13 +87,31 @@ mínimas. Sólo `fetch` toca la red, y ocurre **antes** de entrar al sandbox: |---|---|---| | `fetch` | No (red permitida) | Clona/actualiza el repo al commit fijado; verifica hash | | `patch` | Sí | Aplica `source.patches` sobre el árbol read-only (en copia tmpfs) | -| `configure` | Sí | Detecta sistema de build (autotools/cmake/meson/make) y configura | +| `configure` | Sí | Detecta sistema de build (autotools/cmake/meson/cargo/make) y configura | | `compile` | Sí | Compila con el compilador de la receta hacia `target`/`link` | | `install` | Sí | `make install DESTDIR=/out` (o equivalente) | | `seal` | No | Calcula el hash final del árbol `/out`, lo deposita en el store | La detección de sistema de build en `configure` es heurística (presencia de `configure.ac`, -`CMakeLists.txt`, `meson.build`, `Makefile`) y se puede sobreescribir por receta. +`CMakeLists.txt`, `meson.build`, `Cargo.toml`, `Makefile`) y se puede sobreescribir por receta. + +### Cargo (crates Rust) + +Un árbol con `Cargo.toml` se construye con `cargo` (Cargo gana sobre un `Makefile` huérfano, +pero cede ante un `CMakeLists.txt` de un políglota C+Rust). Particularidades: + +- **Triple traducido.** El `target` estilo zig de la receta (`x86_64-linux-musl`) se traduce al + de rustc (`x86_64-unknown-linux-musl`). +- **Link con `zig cc`.** `compile` instala un wrapper `zig cc` como + `CARGO_TARGET__LINKER` — cargo no acepta un linker de dos palabras, y zig provee el + `libgcc_s` que el link **dinámico** musl exige (sin él, `cannot find -lgcc_s`). `link = + "dynamic"` añade `RUSTFLAGS=-C target-feature=-crt-static` (necesario para `dlopen`, p. ej. + apps gráficas que cargan Vulkan/Wayland en runtime). +- **Hermético ⇒ deps vendoreadas.** `compile` corre `cargo build --release --locked --offline`: + el sandbox no tiene red, así que la fuente debe traer `vendor/` + `.cargo/config.toml` + (`cargo vendor`) o las deps pre-pobladas. `--locked` exige `Cargo.lock` fijo (coherente con + ADR 0006). +- **Install.** Copia los ejecutables del top de `target//release/` a `/out/usr/bin`. ## 5. El grafo de dependencias diff --git a/recipes/llimphi-counter.toml b/recipes/llimphi-counter.toml new file mode 100644 index 00000000..0d6153fb --- /dev/null +++ b/recipes/llimphi-counter.toml @@ -0,0 +1,29 @@ +# Ejemplo de receta Rust (BuildSys::Cargo) — un front-door de tawasuyu. +# +# Muestra el caso GRÁFICO (el más exigente): wgpu+winit+vello cargan Vulkan/Wayland por +# `dlopen` en runtime, así que el binario va `link = "dynamic"` (no estático) y el link lo +# cierra el `zig cc` del lab (provee `libgcc_s`). Un crate NO gráfico (CLI, daemon) usa +# `link = "static"` y sale como binario monolítico hidratable por hardlink. +# +# DOS requisitos para que el build hermético funcione (el sandbox no tiene red): +# 1. commit FIJADO del repo público (reproducibilidad, ADR 0006). +# 2. deps VENDOREADAS en la fuente: `cargo vendor` + `.cargo/config.toml`, o el repo ya las +# trae. `cargo build --offline --locked` falla si falta cualquiera. + +name = "llimphi-counter" +version = "0.1.0" + +[source] +repo = "https://git.tawasuyu.net/tawasuyu/llimphi.git" +commit = "0000000000000000000000000000000000000000" # ← fijar al commit real con deps vendoreadas + +[build] +compiler = "zig-cc" # linker del binario (wrapper zig cc, cierra -lgcc_s) +target = "x86_64-linux-musl" # se traduce a x86_64-unknown-linux-musl para rustc +link = "dynamic" # gráfico: necesita dlopen de Vulkan/Wayland en runtime +# Selección del binario a construir dentro del workspace/crate: +flags = ["-p", "llimphi-ui", "--example", "counter"] + +# runtime: en un userland musl coherente (Alpine) el binario dlopea mesa/vulkan/wayland +# musl-built sin fricción; mezclar con gráficos glibc es lo que rompe (ver M2 del plan +# arje↔hammer en tawasuyu).