Skip to content

Commit fbec9a4

Browse files
committed
Add a verify command to rederive a lock
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
1 parent e431b65 commit fbec9a4

1 file changed

Lines changed: 97 additions & 0 deletions

File tree

rust/src/cli.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ enum Command {
8686
DiffVersions(DiffVersionsArgs),
8787
/// Compose several published environments into one lock.
8888
Compose(ComposeArgs),
89+
/// Re-derive a lock's content address to verify its integrity.
90+
Verify(VerifyArgs),
8991
/// Build a container image (SIF or OCI) from a published environment.
9092
#[command(subcommand)]
9193
Image(ImageCommand),
@@ -274,6 +276,34 @@ struct ManifestArgs {
274276
output: Option<PathBuf>,
275277
}
276278

279+
#[derive(Args)]
280+
struct VerifyArgs {
281+
/// Verify a local lock file's content address (optionally against `--expect`).
282+
#[arg(long, conflicts_with_all = ["env", "registry"])]
283+
lock: Option<PathBuf>,
284+
/// Environment name to resolve from a registry (with `--registry`).
285+
#[arg(long, requires = "registry")]
286+
env: Option<String>,
287+
/// Registry root URL to resolve from (with `--env`).
288+
#[arg(long, requires = "env")]
289+
registry: Option<String>,
290+
/// Target platform (defaults to the current platform).
291+
#[arg(long)]
292+
platform: Option<String>,
293+
/// Python axis value, if the environment fans out over python.
294+
#[arg(long)]
295+
python: Option<String>,
296+
/// Variant axis value (e.g. `cpu`/`gpu`), if any.
297+
#[arg(long)]
298+
variant: Option<String>,
299+
/// Version label to resolve.
300+
#[arg(long, default_value = "latest")]
301+
label: String,
302+
/// For `--lock`: the expected `sha256-<hex>` content address to check.
303+
#[arg(long)]
304+
expect: Option<String>,
305+
}
306+
277307
#[derive(Args)]
278308
struct PublishArgs {
279309
#[command(flatten)]
@@ -694,6 +724,7 @@ async fn run_command(command: Command) -> CliResult {
694724
Command::List(args) => list(args),
695725
Command::DiffVersions(args) => diff_versions(args),
696726
Command::Compose(args) => compose(args).await,
727+
Command::Verify(args) => verify(args),
697728
Command::Image(ImageCommand::Build(args)) => image_build(args).await,
698729
Command::Cache(CacheCommand::Clean { all }) => cache_clean(all),
699730
}
@@ -892,6 +923,72 @@ fn manifest(args: ManifestArgs) -> CliResult {
892923
Ok(())
893924
}
894925

926+
fn verify(args: VerifyArgs) -> CliResult {
927+
use crate::registry::content_address;
928+
929+
// Local mode: re-derive a lock file's content address, optionally checking
930+
// it against an expected `sha256-<hex>`.
931+
if let Some(lock_path) = &args.lock {
932+
let bytes = std::fs::read(lock_path)?;
933+
let address = content_address(&bytes);
934+
println!("lock {} → {address}", lock_path.display());
935+
if crate::embed::extract_manifest(&bytes)?.is_some() {
936+
println!(" embedded manifest band: present");
937+
}
938+
if let Some(expected) = &args.expect {
939+
if expected == &address {
940+
println!(" content address: OK (matches --expect)");
941+
} else {
942+
return Err(format!(
943+
"content address mismatch: expected {expected}, computed {address}"
944+
)
945+
.into());
946+
}
947+
}
948+
return Ok(());
949+
}
950+
951+
// Registry mode: resolve the release, then re-derive and check the content
952+
// address of its lock (and manifest) against what the index records.
953+
let (Some(env), Some(registry_url)) = (&args.env, &args.registry) else {
954+
return Err("pass --lock <file>, or --env <name> --registry <url>".into());
955+
};
956+
let registry = Registry::new(SpecStore::new(), registry_url.clone());
957+
let platform = args
958+
.platform
959+
.clone()
960+
.unwrap_or_else(|| Platform::current().to_string());
961+
let mut coords = Coordinates::new(env.clone(), platform);
962+
if let Some(py) = &args.python {
963+
coords = coords.with_python(py.clone());
964+
}
965+
if let Some(v) = &args.variant {
966+
coords = coords.with_variant(v.clone());
967+
}
968+
let label = Label::parse(&args.label);
969+
970+
let release = registry.resolve(&coords, &label)?;
971+
println!(
972+
"release {} {} on {}",
973+
release.environment, release.version, release.platform
974+
);
975+
976+
// `pull` re-derives the lock's content address and rejects a mismatch, so a
977+
// successful pull is a verified lock.
978+
registry.pull(&coords, &label)?;
979+
println!(" lock {}: OK", release.lock);
980+
981+
// A published lock relies on a manifest sidecar (the embedded band is
982+
// stripped on publish); verify the sidecar's content address too.
983+
if let Some(manifest_addr) = &release.manifest {
984+
registry.pull_manifest(&coords, &label)?;
985+
println!(" manifest {manifest_addr}: OK");
986+
}
987+
988+
println!("verified");
989+
Ok(())
990+
}
991+
895992
fn publish(args: PublishArgs) -> CliResult {
896993
let registry = args.coords.build_registry();
897994
let coords = args.coords.coordinates();

0 commit comments

Comments
 (0)