Skip to content

Commit 49b8806

Browse files
authored
[cargo-verus] feat: check verus and vstd compatibility (#2584)
1 parent b1b65b7 commit 49b8806

7 files changed

Lines changed: 419 additions & 25 deletions

File tree

source/cargo-verus/src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ pub struct VerifyCommand {
7070
#[arg(short, long, action = ArgAction::Count)]
7171
pub verbosity: u8,
7272

73+
/// Check toolchain components, e.g. version compatibility of verus and vstd.
74+
#[arg(long)]
75+
pub check_toolchain: bool,
76+
7377
/// Crates to receive forwarded Verus args
7478
#[arg(
7579
long,

source/cargo-verus/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
mod cli;
2-
mod metadata;
2+
pub mod metadata;
33
mod plan;
44
mod subcommands;
55
pub mod test_utils;

source/cargo-verus/src/metadata.rs

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
};
55

66
use anyhow::{Context, Result};
7-
use cargo_metadata::{Metadata, MetadataCommand, Package, PackageId};
7+
use cargo_metadata::{Metadata, MetadataCommand, Package, PackageId, Source, semver::Version};
88
use serde::Deserialize;
99
use sha2::{Digest, Sha256};
1010

@@ -38,13 +38,13 @@ impl VerusMetadata {
3838
}
3939

4040
pub struct MetadataIndex<'a> {
41-
entries: BTreeMap<&'a PackageId, MetadataIndexEntry<'a>>,
41+
pub entries: BTreeMap<&'a PackageId, MetadataIndexEntry<'a>>,
4242
}
4343

4444
pub struct MetadataIndexEntry<'a> {
45-
package: &'a Package,
46-
verus_metadata: VerusMetadata,
47-
deps: BTreeMap<&'a PackageId, &'a cargo_metadata::NodeDep>,
45+
pub package: &'a Package,
46+
pub verus_metadata: VerusMetadata,
47+
pub deps: BTreeMap<&'a PackageId, &'a cargo_metadata::NodeDep>,
4848
}
4949

5050
impl<'a> MetadataIndex<'a> {
@@ -77,6 +77,10 @@ impl<'a> MetadataIndex<'a> {
7777
Ok(Self { entries })
7878
}
7979

80+
pub fn iter_package_ids(&self) -> impl Iterator<Item = &PackageId> {
81+
self.entries.keys().map(|package_id| *package_id)
82+
}
83+
8084
pub fn get(&self, id: &PackageId) -> &MetadataIndexEntry<'a> {
8185
self.entries.get(id).unwrap()
8286
}
@@ -132,15 +136,92 @@ impl<'a> MetadataIndex<'a> {
132136
}
133137
names
134138
}
139+
140+
/// Collect sources of `vstd` that appear in the verified part of the build.
141+
pub fn collect_vstd_metadata(
142+
&self,
143+
packages_to_verify: &Set<PackageId>,
144+
) -> Set<PackageMetadata> {
145+
// Packages that verification will run on.
146+
let packages_will_verify = Set::from_iter(
147+
packages_to_verify
148+
.iter()
149+
.filter(|package_id| self.get(package_id).verus_metadata.verify)
150+
.cloned(),
151+
);
152+
// Transitive closure of packages that verification will run on.
153+
let tclosure_will_verify = self.get_transitive_closure(packages_will_verify);
154+
155+
// Metadata of all `vstd` instances that appear anywhere in the transitive closure.
156+
let vstd_metadata: Set<PackageMetadata> = tclosure_will_verify
157+
.iter()
158+
.flat_map(|package_id| {
159+
let entry = self.get(package_id);
160+
if entry.verus_metadata.is_vstd {
161+
Some(PackageMetadata::from(entry.package))
162+
} else {
163+
None
164+
}
165+
})
166+
.collect();
167+
168+
assert!(
169+
vstd_metadata.len() <= 1,
170+
"The `vstd` versioning scheme prevents multiple instances in a resolve set.",
171+
// This is a consequence of the current `vstd` versioning scheme.
172+
// In the current scheme, each version matches `0.0.0-*`.
173+
// By definition, *all* such versions are semver-compatible.
174+
// Cargo disallows different *compatible* versions in a resolve set.
175+
// https://doc.rust-lang.org/cargo/reference/resolver.html#semver-compatibility
176+
);
177+
178+
vstd_metadata
179+
}
180+
}
181+
182+
/// Metadata about a package.
183+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
184+
pub struct PackageMetadata {
185+
pub version: Version,
186+
pub source: PackageSource,
135187
}
136188

137-
impl<'a> MetadataIndexEntry<'a> {
138-
pub fn package(&self) -> &'a Package {
139-
self.package
189+
/// Details of a package source.
190+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
191+
pub enum PackageSource {
192+
Registry { url: String },
193+
Git { url: String, rev: Option<String> },
194+
Unsupported,
195+
}
196+
197+
impl From<&Package> for PackageMetadata {
198+
fn from(package: &Package) -> Self {
199+
let version = package.version.clone();
200+
let source =
201+
package.source.as_ref().map(PackageSource::from).unwrap_or(PackageSource::Unsupported);
202+
PackageMetadata { version, source }
140203
}
204+
}
141205

142-
pub fn verus_metadata(&self) -> &VerusMetadata {
143-
&self.verus_metadata
206+
/// NOTE: This code relies on Cargo internals because there's no stable API.
207+
/// The tests in `test_vstd_sources.rs` should be able to detect if these assumptions break.
208+
impl From<&Source> for PackageSource {
209+
fn from(source: &Source) -> Self {
210+
let repr = &source.repr;
211+
if let Some(registry) = repr.strip_prefix("registry+") {
212+
PackageSource::Registry { url: registry.to_string() }
213+
} else if let Some(git_source) = repr.strip_prefix("git+") {
214+
let (url, rev) = if let Some((url, rev)) = git_source.rsplit_once('#') {
215+
(url, Some(rev.to_owned()))
216+
} else {
217+
(git_source, None)
218+
};
219+
// Trim the query part of the URL.
220+
let url = url.split_once('?').map_or(url, |(trimmed_url, _query)| trimmed_url);
221+
PackageSource::Git { url: url.to_string(), rev }
222+
} else {
223+
PackageSource::Unsupported
224+
}
144225
}
145226
}
146227

source/cargo-verus/src/subcommands.rs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use colored::Colorize;
1111

1212
use crate::cli::{CargoOptions, VerifyCommand, VerusArgFwdSelector};
1313
use crate::metadata::{MetadataIndex, fetch_metadata, make_package_id};
14-
use crate::toolchains::TOOLCHAINS;
14+
use crate::toolchains::{self, TOOLCHAINS, is_matching_known_and_used};
1515

1616
pub const CARGO_DEFAULT_LIB_METADATA: &str = "__CARGO_DEFAULT_LIB_METADATA";
1717

@@ -170,6 +170,39 @@ pub fn plan_cargo_run(cfg: VerusConfig) -> Result<CargoRunPlan> {
170170
VerusArgFwdSelector::Deps => &dep_packages,
171171
};
172172

173+
if cfg.options.check_toolchain {
174+
if cfg.options.verbosity > 0 {
175+
println!("Checking toolchain components...");
176+
}
177+
178+
let vstd_metadata = metadata_index.collect_vstd_metadata(packages_to_verify);
179+
let verus_version = get_verus_driver_version()?;
180+
181+
if cfg.options.verbosity > 0 {
182+
println!("verus version: {verus_version:?}");
183+
println!("`vstd` instances:");
184+
for vstd in &vstd_metadata {
185+
println!("version = {:?}", vstd.version.to_string());
186+
println!("source = {:?}", vstd.source);
187+
println!();
188+
}
189+
}
190+
191+
for used_vstd in &vstd_metadata {
192+
let is_compatible = toolchains::TOOLCHAINS.iter().any(|toolchain| {
193+
toolchain.verus == verus_version
194+
&& is_matching_known_and_used(&toolchain.vstd, used_vstd)
195+
});
196+
if !is_compatible {
197+
bail!(
198+
"Components are incompatible:\n\
199+
* verus = {verus_version}\n\
200+
* vstd = {used_vstd:?}\n"
201+
);
202+
}
203+
}
204+
}
205+
173206
/////////////////////////////////////////////////////////
174207
// Phase 2: plan to run Verus via `cargo {subcommand}` //
175208
/////////////////////////////////////////////////////////
@@ -375,12 +408,12 @@ fn make_cargo_plan(
375408
let receives_fwd_verus_args = fwd_verus_args_packages.contains(&pkg_id);
376409

377410
let entry = metadata_index.get(pkg_id);
378-
let package = entry.package();
411+
let package = entry.package;
379412

380413
let package_id =
381414
make_package_id(&package.name, package.version.to_string(), &package.manifest_path);
382415

383-
let verus_metadata = entry.verus_metadata();
416+
let verus_metadata = &entry.verus_metadata;
384417

385418
// The is_builtin, is_builtin_macro, and verify fields are passed as env vars as they
386419
// are relevant for crates which are skipped by Verus. In such cases, the driver avoids
@@ -491,3 +524,31 @@ fn get_verus_driver_path() -> PathBuf {
491524

492525
path
493526
}
527+
528+
/// Run `verus --version` and capture its output.
529+
fn get_verus_driver_version() -> Result<String> {
530+
let command = get_verus_driver_path();
531+
let output = Command::new(&command)
532+
.arg("--version")
533+
.output()
534+
.context(format!("running `{} --version`", command.display()))?;
535+
536+
if !output.status.success() {
537+
bail!(
538+
"`{} --version` failed with status {}.\n\
539+
stdout:\n{}\n\
540+
stderr:\n{}",
541+
command.display(),
542+
output.status,
543+
String::from_utf8_lossy(&output.stdout),
544+
String::from_utf8_lossy(&output.stderr),
545+
);
546+
}
547+
548+
let stdout = String::from_utf8(output.stdout)
549+
.context(format!("`{} --version` produced non-UTF-8 stdout", command.display()))?;
550+
551+
stdout.lines().find_map(|line| line.strip_prefix(" Version: ").map(ToOwned::to_owned)).context(
552+
format!("Failed to parse version from `{}` output:\n{}", command.display(), stdout),
553+
)
554+
}

0 commit comments

Comments
 (0)