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>
This commit is contained in:
2026-06-12 22:13:24 -04:00
co-authored by Claude Opus 4.8
parent 7bc2ee6019
commit d71ef179eb
+55 -18
View File
@@ -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::*;