Skip to content

Commit 12ad300

Browse files
authored
Merge pull request #4 from Point72/tkp/verify
Add a verify command to rederive a lock
2 parents b7650e0 + fbec9a4 commit 12ad300

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),
@@ -278,6 +280,34 @@ struct ManifestArgs {
278280
output: Option<PathBuf>,
279281
}
280282

283+
#[derive(Args)]
284+
struct VerifyArgs {
285+
/// Verify a local lock file's content address (optionally against `--expect`).
286+
#[arg(long, conflicts_with_all = ["env", "registry"])]
287+
lock: Option<PathBuf>,
288+
/// Environment name to resolve from a registry (with `--registry`).
289+
#[arg(long, requires = "registry")]
290+
env: Option<String>,
291+
/// Registry root URL to resolve from (with `--env`).
292+
#[arg(long, requires = "env")]
293+
registry: Option<String>,
294+
/// Target platform (defaults to the current platform).
295+
#[arg(long)]
296+
platform: Option<String>,
297+
/// Python axis value, if the environment fans out over python.
298+
#[arg(long)]
299+
python: Option<String>,
300+
/// Variant axis value (e.g. `cpu`/`gpu`), if any.
301+
#[arg(long)]
302+
variant: Option<String>,
303+
/// Version label to resolve.
304+
#[arg(long, default_value = "latest")]
305+
label: String,
306+
/// For `--lock`: the expected `sha256-<hex>` content address to check.
307+
#[arg(long)]
308+
expect: Option<String>,
309+
}
310+
281311
#[derive(Args)]
282312
struct PublishArgs {
283313
#[command(flatten)]
@@ -706,6 +736,7 @@ async fn run_command(command: Command) -> CliResult {
706736
Command::List(args) => list(args),
707737
Command::DiffVersions(args) => diff_versions(args),
708738
Command::Compose(args) => compose(args).await,
739+
Command::Verify(args) => verify(args),
709740
Command::Image(ImageCommand::Build(args)) => image_build(args).await,
710741
Command::Cache(CacheCommand::Clean { all }) => cache_clean(all),
711742
}
@@ -907,6 +938,72 @@ fn manifest(args: ManifestArgs) -> CliResult {
907938
Ok(())
908939
}
909940

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

0 commit comments

Comments
 (0)