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 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

fix(cli): --swap no debe confundir el `:` de `b3:` con el separador del rel_path

`--swap make=b3:fbad…` se parseaba como hash=`b3`, rel_path=`fbad…` porque el
split del rel_path opcional (`name=hash[:rel_path]`) corría ANTES de quitar el
prefijo `b3:`. Resultado: el assemble del builder fallaba con "no existe en el
artefacto sellado b3:b3" (lo cazó el primer intento de SWAP_MAKE=1 in-VM, antes
de bootear la VM ⇒ cero tiempo de máquina perdido).

Fix: quitar `b3:` del hash antes de buscar el `:` del rel_path. Extraje el parseo
inline a `parse_swap()` y le puse 4 tests de regresión (b3:+default, hash pelado+
rel explícito, b3:+rel explícito, falta '=').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

d71ef179ebd8c356fbfabc1dce81011dc97ebec4 · ruwaq sergio · (4 killa ñawpaq) · tayta 7bc2ee601 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 7

crates/hammer-cli/src/main.rs

1 qillqakuna · +55 −18

diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs
index 3c1ea22c..7f69d9fb 100644
--- a/crates/hammer-cli/src/main.rs
+++ b/crates/hammer-cli/src/main.rs
@@ -590,24 +590,7 @@ fn main() -> anyhow::Result<()> {
                     other => anyhow::bail!("--seed debe ser 'zig' o 'musl-cross-make', no '{other}'"),
                 };
                 let store = hammer_core::Store::open(&cli.store)?;
-                let swaps = swaps
-                    .iter()
-                    .map(|s| {
-                        // name=hash[:rel_path]; rel_path por defecto usr/bin/<name>.
-                        let (name, rest) = s
-                            .split_once('=')
-                            .ok_or_else(|| anyhow::anyhow!("--swap '{s}': falta '=' (name=hash[:rel_path])"))?;
-                        let (hash, rel_path) = match rest.split_once(':') {
-                            Some((h, p)) => (h, p.to_string()),
-                            None => (rest, format!("usr/bin/{name}")),
-                        };
-                        Ok::<_, anyhow::Error>(hammer_bootstrap::ToolchainSwap {
-                            name: name.to_string(),
-                            artifact: hammer_core::ArtifactHash::from_hex(hash.trim_start_matches("b3:")),
-                            rel_path,
-                        })
-                    })
-                    .collect::<anyhow::Result<Vec<_>>>()?;
+                let swaps = swaps.iter().map(|s| parse_swap(s)).collect::<anyhow::Result<Vec<_>>>()?;
                 let spec = hammer_bootstrap::BuilderSpec {
                     stage1_rootfs: hammer_core::ArtifactHash::from_hex(stage1.trim_start_matches("b3:")),
                     seed_hash: hammer_core::ArtifactHash::from_hex(seed_hash.trim_start_matches("b3:")),
@@ -1463,6 +1446,60 @@ fn run_ctl(line: &str, fifo: &std::path::Path) -> anyhow::Result<()> {
     Ok(())
 }
 
+/// Parsea un `--swap name=hash[:rel_path]` (variante b del builder, SDD 11 §7.2b). El `rel_path`
+/// por defecto es `usr/bin/<name>`. Quitamos el prefijo `b3:` del hash ANTES de buscar el `:` del
+/// rel_path opcional — si no, `make=b3:abc` se parsearía como hash=`b3`, rel_path=`abc`.
+fn parse_swap(s: &str) -> anyhow::Result<hammer_bootstrap::ToolchainSwap> {
+    let (name, rest) = s
+        .split_once('=')
+        .ok_or_else(|| anyhow::anyhow!("--swap '{s}': falta '=' (name=hash[:rel_path])"))?;
+    let rest = rest.trim_start_matches("b3:");
+    let (hash, rel_path) = match rest.split_once(':') {
+        Some((h, p)) => (h, p.to_string()),
+        None => (rest, format!("usr/bin/{name}")),
+    };
+    Ok(hammer_bootstrap::ToolchainSwap {
+        name: name.to_string(),
+        artifact: hammer_core::ArtifactHash::from_hex(hash),
+        rel_path,
+    })
+}
+
+#[cfg(test)]
+mod swap_tests {
+    use super::*;
+
+    #[test]
+    fn swap_with_b3_prefix_keeps_full_hash() {
+        // Regresión: el `:` del prefijo `b3:` no debe confundirse con el separador del rel_path.
+        let s = parse_swap("make=b3:fbad44ac").unwrap();
+        assert_eq!(s.name, "make");
+        assert_eq!(s.artifact.as_str(), "b3:fbad44ac");
+        assert_eq!(s.rel_path, "usr/bin/make", "rel_path por defecto");
+    }
+
+    #[test]
+    fn swap_bare_hash_and_explicit_rel_path() {
+        let s = parse_swap("bwrap=abcd:usr/bin/bwrap").unwrap();
+        assert_eq!(s.name, "bwrap");
+        assert_eq!(s.artifact.as_str(), "b3:abcd");
+        assert_eq!(s.rel_path, "usr/bin/bwrap");
+    }
+
+    #[test]
+    fn swap_b3_prefix_with_explicit_rel_path() {
+        let s = parse_swap("make=b3:fbad:usr/local/bin/make").unwrap();
+        assert_eq!(s.artifact.as_str(), "b3:fbad");
+        assert_eq!(s.rel_path, "usr/local/bin/make");
+    }
+
+    #[test]
+    fn swap_missing_equals_errors() {
+        let err = parse_swap("makeb3:fbad").unwrap_err().to_string();
+        assert!(err.contains("falta '='"), "{err}");
+    }
+}
+
 #[cfg(test)]
 mod export_tests {
     use super::*;