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/. - 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::>>()?; + let swaps = swaps.iter().map(|s| parse_swap(s)).collect::>>()?; 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/`. 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 { + 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::*;