feat(build): BuildSys::Cargo — el lab construye crates Rust
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 <triple>; link por wrapper zig cc (CARGO_TARGET_<T>_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/<triple>/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).
This commit is contained in:
@@ -173,6 +173,8 @@ enum BuildSys {
|
|||||||
CMake,
|
CMake,
|
||||||
/// `meson.build`.
|
/// `meson.build`.
|
||||||
Meson,
|
Meson,
|
||||||
|
/// `Cargo.toml`: crate o workspace Rust. Build con `cargo`, link con `zig cc`.
|
||||||
|
Cargo,
|
||||||
/// `Makefile` plano (o `makefile`) sin sistema generador.
|
/// `Makefile` plano (o `makefile`) sin sistema generador.
|
||||||
Make,
|
Make,
|
||||||
/// No detectamos nada: la receta debe traer overrides en `[build.phases]`.
|
/// 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") {
|
if has("meson.build") {
|
||||||
return BuildSys::Meson;
|
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") {
|
if has("Makefile") || has("makefile") || has("GNUmakefile") {
|
||||||
return BuildSys::Make;
|
return BuildSys::Make;
|
||||||
}
|
}
|
||||||
BuildSys::Unknown
|
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
|
||||||
|
/// `<arch>-linux-<libc>` se devuelve tal cual: la receta puede dar el triple exacto.
|
||||||
|
fn rustc_triple(target: &str) -> String {
|
||||||
|
match target.split('-').collect::<Vec<_>>().as_slice() {
|
||||||
|
[arch, "linux", libc] => format!("{arch}-unknown-linux-{libc}"),
|
||||||
|
_ => target.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nombre de la env `CARGO_TARGET_<TRIPLE>_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
|
/// 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.
|
/// 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<Phases> {
|
fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
|
||||||
@@ -223,7 +245,8 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
|
|||||||
"cmake -S . -B _build -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release{flags_suffix}"
|
"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::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() {
|
if out.compile.is_none() {
|
||||||
@@ -233,6 +256,25 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
|
|||||||
}
|
}
|
||||||
BuildSys::CMake => Some(r#"cmake --build _build -j "$(nproc)""#.to_string()),
|
BuildSys::CMake => Some(r#"cmake --build _build -j "$(nproc)""#.to_string()),
|
||||||
BuildSys::Meson => Some("meson compile -C _build".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,
|
BuildSys::Unknown => None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -243,6 +285,16 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
|
|||||||
}
|
}
|
||||||
BuildSys::CMake => Some("DESTDIR=/out cmake --install _build".to_string()),
|
BuildSys::CMake => Some("DESTDIR=/out cmake --install _build".to_string()),
|
||||||
BuildSys::Meson => Some("DESTDIR=/out meson install -C _build --no-rebuild".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,
|
BuildSys::Unknown => None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -316,6 +368,64 @@ commit = "deadbeef"
|
|||||||
assert!(p.install.unwrap().contains("meson install"));
|
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]
|
#[test]
|
||||||
fn detect_make() {
|
fn detect_make() {
|
||||||
let d = tempfile::tempdir().unwrap();
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
+20
-2
@@ -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 |
|
| `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) |
|
| `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` |
|
| `compile` | Sí | Compila con el compilador de la receta hacia `target`/`link` |
|
||||||
| `install` | Sí | `make install DESTDIR=/out` (o equivalente) |
|
| `install` | Sí | `make install DESTDIR=/out` (o equivalente) |
|
||||||
| `seal` | No | Calcula el hash final del árbol `/out`, lo deposita en el store |
|
| `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`,
|
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_<triple>_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/<triple>/release/` a `/out/usr/bin`.
|
||||||
|
|
||||||
## 5. El grafo de dependencias
|
## 5. El grafo de dependencias
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
Reference in New Issue
Block a user