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 1181Shell 83Rust 62Markdown 521515 Sutichaykuna

⎇ 09d50f9e60c9f523e99aff3d6c8dcf2e5b3cd1a5 crates/hammer-build/src/swm_bridge.rs Rust · 260 siq'ikuna · 8.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

Fase 4 — formato .swm: verify, apply (config_edit/file_drop/source_patch) y export 09d50f9e6 · Sergio ·

1//! Puente entre `hammer_core::Mutation::SourcePatch` y el laboratorio.
2//!
3//! Convierte la mutación serializada en una `Recipe` efímera + un archivo de patch
4//! temporal, y delega en `build` para sellar el artefacto en el store. Luego se hidrata.
5//!
6//! Este módulo vive en `hammer-build` (no en `hammer-core`) porque depende del store y del
7//! sandbox. `hammer-core` debe permanecer puro (sin red ni mounts) para que el watcher y los
8//! tests no arrastren toda la cadena del lab.
9
10use std::path::{Path, PathBuf};
11
12use hammer_core::{
13    swm::{Mutation, SwmBuild},
14    ArtifactHash, Compiler, LinkMode, Recipe, Store,
15};
16
17use crate::{build, BuildConfig};
18
19/// Construye una `SourcePatch` y devuelve el hash sellado en el store. El llamador hidrata
20/// luego (típicamente al overlay activo).
21///
22/// `scratch_root` sirve para escribir el patch inline a disco — usamos `cfg.work_root` por
23/// defecto si el caller no quiere gestionarlo a mano.
24pub fn build_source_patch(
25    mutation: &Mutation,
26    cfg: &BuildConfig,
27    store: &Store,
28    scratch_root: Option<&Path>,
29) -> hammer_core::Result<ArtifactHash> {
30    let (repo, commit, patch, patch_url, build_cfg, target_bin, expected) = match mutation {
31        Mutation::SourcePatch {
32            repo,
33            commit,
34            patch,
35            patch_url,
36            build,
37            target_bin,
38            expected_hash,
39        } => (repo, commit, patch, patch_url, build, target_bin, expected_hash),
40        _ => {
41            return Err(hammer_core::Error::Recipe(
42                "build_source_patch: la mutación no es 'source_patch'".into(),
43            ));
44        }
45    };
46
47    if patch_url.is_some() {
48        return Err(hammer_core::Error::Recipe(
49            "patch_url remoto no soportado todavía: usa 'patch' inline".into(),
50        ));
51    }
52
53    let scratch = scratch_root.unwrap_or(&cfg.work_root).to_path_buf();
54    let recipe_dir = scratch.join("swm-recipes");
55    std::fs::create_dir_all(&recipe_dir)?;
56
57    // Si trae patch inline, lo materializamos a disco bajo un nombre derivado del commit
58    // (estable: si el .swm se reaplica, reusamos el mismo archivo y el hash de la receta no
59    // depende de aleatoriedad).
60    let mut patches: Vec<String> = Vec::new();
61    if let Some(text) = patch {
62        let patch_path = recipe_dir.join(format!("{commit}.patch"));
63        std::fs::write(&patch_path, text.as_bytes())?;
64        patches.push(patch_path.to_string_lossy().into_owned());
65    }
66
67    let name = derive_name(target_bin);
68    let recipe = synthesize_recipe(
69        &name,
70        commit,
71        repo,
72        build_cfg,
73        patches,
74        &recipe_dir,
75    )?;
76
77    let hash = build(&recipe, cfg, store)?;
78    if let Some(want) = expected {
79        let want_hex = want.strip_prefix("b3:").unwrap_or(want);
80        let got_hex = hash
81            .as_str()
82            .strip_prefix("b3:")
83            .expect("of_inputs siempre devuelve 'b3:'");
84        if want_hex != got_hex {
85            return Err(hammer_core::Error::Recipe(format!(
86                "expected_hash no coincide: declarado=b3:{want_hex}, obtenido={hash}"
87            )));
88        }
89    }
90    Ok(hash)
91}
92
93/// Nombre legible para el directorio del store. `/usr/bin/grep` → `grep`. Si el path no
94/// tiene basename (raro), usamos `swm-bin`. El nombre afecta SÓLO al sufijo legible
95/// (`<hash>-<name>`), no al hash; cambiarlo no rompe la caché del store.
96fn derive_name(target_bin: &str) -> String {
97    Path::new(target_bin)
98        .file_name()
99        .and_then(|s| s.to_str())
100        .unwrap_or("swm-bin")
101        .to_string()
102}
103
104fn synthesize_recipe(
105    name: &str,
106    commit: &str,
107    repo: &str,
108    build_cfg: &SwmBuild,
109    patches: Vec<String>,
110    base_dir: &Path,
111) -> hammer_core::Result<Recipe> {
112    let compiler = parse_compiler(&build_cfg.compiler)?;
113    let link = parse_link(&build_cfg.link)?;
114    // Construimos la receta vía TOML para reusar las defaults y el validador de `Source`.
115    let toml_text = format!(
116        r#"
117name = "{name}"
118version = "swm-{commit_short}"
119
120[source]
121repo   = "{repo}"
122commit = "{commit}"
123
124[build]
125compiler = "{compiler}"
126target   = "{target}"
127link     = "{link}"
128flags    = []
129"#,
130        name = name,
131        commit_short = &commit[..commit.len().min(12)],
132        repo = repo,
133        commit = commit,
134        compiler = compiler.as_str(),
135        target = build_cfg.target,
136        link = link.as_str(),
137    );
138    let mut recipe = Recipe::from_toml(&toml_text)?;
139    recipe.build.flags = build_cfg.flags.clone();
140    // Resolución de patches relativos al recipe_dir donde acabamos de escribir los inline.
141    // Aceptamos rutas absolutas tal cual: las hemos generado nosotros.
142    recipe.source.patches = patches
143        .into_iter()
144        .map(|p| {
145            let path = Path::new(&p);
146            if path.is_absolute() {
147                p
148            } else {
149                base_dir.join(&p).to_string_lossy().into_owned()
150            }
151        })
152        .collect();
153    recipe.base_dir = PathBuf::from("/"); // patches absolutos: base_dir es irrelevante
154    Ok(recipe)
155}
156
157fn parse_compiler(s: &str) -> hammer_core::Result<Compiler> {
158    match s {
159        "zig-cc" => Ok(Compiler::ZigCc),
160        "clang" => Ok(Compiler::Clang),
161        "gcc" => Ok(Compiler::Gcc),
162        other => Err(hammer_core::Error::Recipe(format!(
163            "compiler desconocido: '{other}' (esperado zig-cc/clang/gcc)"
164        ))),
165    }
166}
167
168fn parse_link(s: &str) -> hammer_core::Result<LinkMode> {
169    match s {
170        "static" => Ok(LinkMode::Static),
171        "dynamic" => Ok(LinkMode::Dynamic),
172        other => Err(hammer_core::Error::Recipe(format!(
173            "link desconocido: '{other}' (esperado static/dynamic)"
174        ))),
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    fn fake_swm_build() -> SwmBuild {
183        SwmBuild {
184            compiler: "zig-cc".into(),
185            target: "x86_64-linux-musl".into(),
186            link: "static".into(),
187            flags: vec!["--enable-foo".into()],
188        }
189    }
190
191    #[test]
192    fn derive_name_from_target() {
193        assert_eq!(derive_name("/usr/bin/grep"), "grep");
194        assert_eq!(derive_name("/bin/ls"), "ls");
195        assert_eq!(derive_name("/"), "swm-bin");
196    }
197
198    #[test]
199    fn synthesize_recipe_basic() {
200        let d = tempfile::tempdir().unwrap();
201        let r = synthesize_recipe(
202            "grep",
203            "a1b2c3d4e5f6a7b8c9",
204            "git://example/grep.git",
205            &fake_swm_build(),
206            vec![],
207            d.path(),
208        )
209        .unwrap();
210        assert_eq!(r.name, "grep");
211        assert!(r.version.starts_with("swm-a1b2c3d4"));
212        assert_eq!(r.build.flags, vec!["--enable-foo"]);
213        match r.source.kind().unwrap() {
214            hammer_core::SourceKind::Git { repo, commit } => {
215                assert_eq!(repo, "git://example/grep.git");
216                assert_eq!(commit, "a1b2c3d4e5f6a7b8c9");
217            }
218            _ => panic!("modo git esperado"),
219        }
220    }
221
222    #[test]
223    fn parse_compiler_and_link() {
224        assert_eq!(parse_compiler("zig-cc").unwrap(), Compiler::ZigCc);
225        assert!(parse_compiler("xlc").is_err());
226        assert_eq!(parse_link("static").unwrap(), LinkMode::Static);
227        assert!(parse_link("partial").is_err());
228    }
229
230    #[test]
231    fn rejects_non_source_patch_mutation() {
232        let m = Mutation::ConfigEdit {
233            file: "/etc/x".into(),
234            inline_diff: "- a\n+ b\n".into(),
235        };
236        let d = tempfile::tempdir().unwrap();
237        let store = Store::open(d.path().join("store")).unwrap();
238        let cfg = BuildConfig::defaults_for_store(store.root());
239        let err = build_source_patch(&m, &cfg, &store, None).unwrap_err().to_string();
240        assert!(err.contains("source_patch"), "{err}");
241    }
242
243    #[test]
244    fn rejects_patch_url() {
245        let m = Mutation::SourcePatch {
246            repo: "git://x".into(),
247            commit: "abc".into(),
248            patch: None,
249            patch_url: Some("https://example/foo.patch".into()),
250            build: fake_swm_build(),
251            target_bin: "/bin/x".into(),
252            expected_hash: None,
253        };
254        let d = tempfile::tempdir().unwrap();
255        let store = Store::open(d.path().join("store")).unwrap();
256        let cfg = BuildConfig::defaults_for_store(store.root());
257        let err = build_source_patch(&m, &cfg, &store, None).unwrap_err().to_string();
258        assert!(err.contains("patch_url"), "{err}");
259    }
260}