gioser

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 1532Shell 196Rust 108Markdown 993686 Sutichaykuna

⎇ de036d366605e338a6f604890433c11356101fb8 crates/hammer-build/src/lib.rs Rust · 325 siq'ikuna · 11.7 KB Ñawpa willay Pi imata llamkarqan ch'uya

Fase 4 — provenance en `hammer export` (sidecar de receta en el store) de036d366 · Sergio ·

1//! El laboratorio: compila una receta de forma hermética y la sella en el store; e hidrata
2//! artefactos al FHS. Ver `docs/02-build-lab.md` y `docs/03-hydration.md`.
3
4use std::path::{Path, PathBuf};
5
6use hammer_core::{ArtifactHash, LinkMode, Phases, Recipe, Store};
7
8pub mod config;
9pub mod fetch;
10pub mod hydrate;
11pub mod sandbox;
12pub mod swm_bridge;
13
14pub use config::BuildConfig;
15pub use hydrate::{hydrate as run_hydrate, HydrateReport, HydratedFile};
16pub use sandbox::Sandbox;
17pub use swm_bridge::build_source_patch;
18
19/// Calcula el `ArtifactHash` de una receta, resolviendo recursivamente sus deps de build.
20/// Ver `docs/02-build-lab.md` §2 y §5.
21pub fn artifact_hash(recipe: &Recipe, _store: &Store) -> hammer_core::Result<ArtifactHash> {
22    let dep_hashes: Vec<ArtifactHash> = Vec::new();
23    for _dep in &recipe.deps.build {
24        // TODO(fase-0): cargar la receta de la dep y recursar.
25    }
26    let inputs = recipe.hash_inputs(&dep_hashes)?;
27    let refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect();
28    Ok(ArtifactHash::of_inputs(&refs))
29}
30
31/// Compila una receta y devuelve el hash del artefacto sellado en el store.
32/// Si el hash ya existe en el store, devuelve sin recompilar (caché).
33pub fn build(
34    recipe: &Recipe,
35    cfg: &BuildConfig,
36    store: &Store,
37) -> hammer_core::Result<ArtifactHash> {
38    let h = artifact_hash(recipe, store)?;
39    if store.has(&h, &recipe.name) {
40        tracing::info!(hash = %h, name = %recipe.name, "caché: artefacto ya en el store");
41        return Ok(h);
42    }
43    sandbox::ensure_layout(&cfg.rootfs, &cfg.zig_dir)?;
44
45    tracing::info!(hash = %h, name = %recipe.name, "fetch");
46    let src_tree = fetch::fetch(recipe, &cfg.work_root)?;
47
48    if !recipe.source.patches.is_empty() {
49        tracing::info!(n = recipe.source.patches.len(), "aplicando patches");
50        fetch::apply_patches(recipe, &src_tree)?;
51    }
52
53    let out_dir = unique_out_dir(&cfg.work_root, &recipe.name, &h)?;
54    let mut env: Vec<(String, String)> = Vec::new();
55    if matches!(recipe.build.link, LinkMode::Static) {
56        // autotools/cmake leen LDFLAGS para el link final; -static fuerza el binario
57        // monolítico que pide nuestra estrategia de hidratación primaria.
58        env.push(("LDFLAGS".into(), "-static".into()));
59    }
60    if let Some(cache) = &cfg.cache_root {
61        std::fs::create_dir_all(cache.join("zig"))?;
62    }
63    let sb = Sandbox {
64        rootfs: cfg.rootfs.clone(),
65        zig_dir: cfg.zig_dir.clone(),
66        src_dir: src_tree.clone(),
67        out_dir: out_dir.clone(),
68        cache_dir: cfg.cache_root.clone(),
69        env,
70    };
71
72    let phases = resolve_phases(recipe, &src_tree)?;
73    if let Some(cmd) = &phases.configure {
74        tracing::info!(cmd, "phase: configure");
75        sb.run(cmd)?;
76    }
77    if let Some(cmd) = &phases.compile {
78        tracing::info!(cmd, "phase: compile");
79        sb.run(cmd)?;
80    } else {
81        return Err(hammer_core::Error::Recipe(
82            "receta sin fase 'compile' y heurística no encontró ninguna".into(),
83        ));
84    }
85    if let Some(cmd) = &phases.install {
86        tracing::info!(cmd, "phase: install");
87        sb.run(cmd)?;
88    } else {
89        return Err(hammer_core::Error::Recipe(
90            "receta sin fase 'install' y heurística no encontró ninguna".into(),
91        ));
92    }
93
94    // Sidecar de provenance: escribimos la receta serializada DENTRO de out_dir antes del
95    // seal, para que quede congelada en el árbol read-only del store. `hammer export` la
96    // lee de vuelta para emitir `source_patch` con la receta original en vez de un
97    // `file_drop` opaco. Ver `docs/04-overlay.md` §provenance (Fase 4).
98    let sidecar_path = out_dir.join(hammer_core::store::RECIPE_SIDECAR_REL);
99    if let Some(parent) = sidecar_path.parent() {
100        std::fs::create_dir_all(parent)?;
101    }
102    let toml_text = recipe.to_toml()?;
103    std::fs::write(&sidecar_path, toml_text.as_bytes())?;
104
105    let sealed = store.seal(&out_dir, &h, &recipe.name)?;
106    tracing::info!(path = %sealed.display(), "sealed");
107    Ok(h)
108}
109
110/// Sistemas de build que reconoce la heurística por inspección del árbol del source.
111/// Ver `docs/02-build-lab.md` §4.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113enum BuildSys {
114    /// `configure.ac` (o `.in`) sin `configure` generado: hay que correr `autoreconf` primero.
115    AutoconfRaw,
116    /// `configure` ya está en el tarball release: vamos directo a `./configure`.
117    AutoconfReady,
118    /// `CMakeLists.txt`.
119    CMake,
120    /// `meson.build`.
121    Meson,
122    /// `Makefile` plano (o `makefile`) sin sistema generador.
123    Make,
124    /// No detectamos nada: la receta debe traer overrides en `[build.phases]`.
125    Unknown,
126}
127
128fn detect_build_system(src: &Path) -> BuildSys {
129    let has = |p: &str| src.join(p).exists();
130    // El orden importa: cuando coexisten varios (un tarball release de autotools trae el
131    // `configure` regenerado y un `Makefile` huérfano de un build previo), preferimos el
132    // sistema "más alto" que sí sabe regenerar todo.
133    if has("configure.ac") || has("configure.in") {
134        if has("configure") {
135            return BuildSys::AutoconfReady;
136        }
137        return BuildSys::AutoconfRaw;
138    }
139    if has("configure") {
140        return BuildSys::AutoconfReady;
141    }
142    if has("CMakeLists.txt") {
143        return BuildSys::CMake;
144    }
145    if has("meson.build") {
146        return BuildSys::Meson;
147    }
148    if has("Makefile") || has("makefile") || has("GNUmakefile") {
149        return BuildSys::Make;
150    }
151    BuildSys::Unknown
152}
153
154/// Devuelve los comandos a ejecutar para cada fase: overrides explícitos en la receta
155/// ganan; el resto lo deriva la heurística según el sistema de build detectado.
156fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
157    let mut out = recipe.build.phases.clone();
158    let bs = detect_build_system(src);
159    let flags = recipe.build.flags.join(" ");
160    let flags_suffix = if flags.is_empty() { String::new() } else { format!(" {flags}") };
161
162    if out.configure.is_none() {
163        out.configure = match bs {
164            BuildSys::AutoconfRaw => Some(format!(
165                "autoreconf -fi && ./configure --prefix=/usr{flags_suffix}"
166            )),
167            BuildSys::AutoconfReady => Some(format!("./configure --prefix=/usr{flags_suffix}")),
168            BuildSys::CMake => Some(format!(
169                "cmake -S . -B _build -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release{flags_suffix}"
170            )),
171            BuildSys::Meson => Some(format!("meson setup _build --prefix=/usr{flags_suffix}")),
172            BuildSys::Make | BuildSys::Unknown => None,
173        };
174    }
175    if out.compile.is_none() {
176        out.compile = match bs {
177            BuildSys::AutoconfRaw | BuildSys::AutoconfReady | BuildSys::Make => {
178                Some(r#"make -j"$(nproc)""#.to_string())
179            }
180            BuildSys::CMake => Some(r#"cmake --build _build -j "$(nproc)""#.to_string()),
181            BuildSys::Meson => Some("meson compile -C _build".to_string()),
182            BuildSys::Unknown => None,
183        };
184    }
185    if out.install.is_none() {
186        out.install = match bs {
187            BuildSys::AutoconfRaw | BuildSys::AutoconfReady | BuildSys::Make => {
188                Some("make install DESTDIR=/out PREFIX=/usr".to_string())
189            }
190            BuildSys::CMake => Some("DESTDIR=/out cmake --install _build".to_string()),
191            BuildSys::Meson => Some("DESTDIR=/out meson install -C _build --no-rebuild".to_string()),
192            BuildSys::Unknown => None,
193        };
194    }
195    Ok(out)
196}
197
198#[cfg(test)]
199mod heuristic_tests {
200    use super::*;
201
202    fn touch(dir: &Path, name: &str) {
203        std::fs::write(dir.join(name), b"").unwrap();
204    }
205
206    fn recipe(flags: &[&str]) -> Recipe {
207        let mut r = Recipe::from_toml(
208            r#"
209name = "x"
210version = "0"
211[source]
212repo = "git://x"
213commit = "deadbeef"
214[build]
215"#,
216        )
217        .unwrap();
218        r.build.flags = flags.iter().map(|s| (*s).to_string()).collect();
219        r
220    }
221
222    #[test]
223    fn detect_autoconf_raw_when_only_configure_ac() {
224        let d = tempfile::tempdir().unwrap();
225        touch(d.path(), "configure.ac");
226        assert_eq!(detect_build_system(d.path()), BuildSys::AutoconfRaw);
227        let p = resolve_phases(&recipe(&[]), d.path()).unwrap();
228        assert!(p.configure.as_deref().unwrap().starts_with("autoreconf -fi &&"));
229        assert!(p.install.as_deref().unwrap().contains("DESTDIR=/out"));
230    }
231
232    #[test]
233    fn detect_autoconf_ready_when_configure_exists() {
234        let d = tempfile::tempdir().unwrap();
235        touch(d.path(), "configure.ac");
236        touch(d.path(), "configure");
237        assert_eq!(detect_build_system(d.path()), BuildSys::AutoconfReady);
238        let p = resolve_phases(&recipe(&["--enable-foo"]), d.path()).unwrap();
239        let c = p.configure.unwrap();
240        assert!(c.starts_with("./configure --prefix=/usr"), "{c}");
241        assert!(c.ends_with("--enable-foo"), "{c}");
242    }
243
244    #[test]
245    fn detect_cmake() {
246        let d = tempfile::tempdir().unwrap();
247        touch(d.path(), "CMakeLists.txt");
248        assert_eq!(detect_build_system(d.path()), BuildSys::CMake);
249        let p = resolve_phases(&recipe(&[]), d.path()).unwrap();
250        assert!(p.configure.unwrap().contains("-DCMAKE_INSTALL_PREFIX=/usr"));
251        assert!(p.compile.unwrap().contains("cmake --build _build"));
252        assert!(p.install.unwrap().contains("DESTDIR=/out"));
253    }
254
255    #[test]
256    fn detect_meson() {
257        let d = tempfile::tempdir().unwrap();
258        touch(d.path(), "meson.build");
259        assert_eq!(detect_build_system(d.path()), BuildSys::Meson);
260        let p = resolve_phases(&recipe(&[]), d.path()).unwrap();
261        assert!(p.configure.unwrap().contains("meson setup _build"));
262        assert!(p.install.unwrap().contains("meson install"));
263    }
264
265    #[test]
266    fn detect_make() {
267        let d = tempfile::tempdir().unwrap();
268        touch(d.path(), "Makefile");
269        assert_eq!(detect_build_system(d.path()), BuildSys::Make);
270        let p = resolve_phases(&recipe(&[]), d.path()).unwrap();
271        assert!(p.configure.is_none(), "make plano no necesita configure");
272        assert!(p.compile.unwrap().starts_with("make"));
273    }
274
275    #[test]
276    fn unknown_returns_no_phases() {
277        let d = tempfile::tempdir().unwrap();
278        // sólo un README, ningún archivo característico.
279        std::fs::write(d.path().join("README"), b"hi").unwrap();
280        assert_eq!(detect_build_system(d.path()), BuildSys::Unknown);
281        let p = resolve_phases(&recipe(&[]), d.path()).unwrap();
282        assert!(p.compile.is_none());
283        assert!(p.install.is_none());
284    }
285
286    #[test]
287    fn recipe_override_wins_over_heuristic() {
288        let d = tempfile::tempdir().unwrap();
289        touch(d.path(), "Makefile");
290        let mut r = recipe(&[]);
291        r.build.phases.compile = Some("zig cc -static -o foo foo.c".to_string());
292        r.build.phases.install = Some("cp foo /out/bin/foo".to_string());
293        let p = resolve_phases(&r, d.path()).unwrap();
294        assert_eq!(p.compile.unwrap(), "zig cc -static -o foo foo.c");
295        assert_eq!(p.install.unwrap(), "cp foo /out/bin/foo");
296    }
297}
298
299
300fn unique_out_dir(
301    work_root: &Path,
302    name: &str,
303    h: &ArtifactHash,
304) -> hammer_core::Result<PathBuf> {
305    let dir = work_root
306        .join("out")
307        .join(h.store_dir_name(name));
308    if dir.exists() {
309        std::fs::remove_dir_all(&dir)?;
310    }
311    std::fs::create_dir_all(&dir)?;
312    Ok(dir)
313}
314
315/// Proyecta un artefacto del store al FHS (real o de overlay). Resuelve el directorio del
316/// artefacto a partir del hash y delega en [`hydrate::hydrate`].
317pub fn hydrate(
318    h: &ArtifactHash,
319    store: &Store,
320    target_fhs: &Path,
321    mode: LinkMode,
322) -> hammer_core::Result<HydrateReport> {
323    let artifact_dir = store.find_by_hash(h.as_str())?;
324    hydrate::hydrate(&artifact_dir, target_fhs, mode)
325}