use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use cargo_metadata::Metadata;
use cargo_metadata::MetadataCommand;
use cargo_metadata::Package;
use crate::cli::CargoCheckCli;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SelectionScope {
Workspace,
SinglePackage,
}
#[derive(Debug)]
pub(crate) struct Selection {
pub manifest_path: PathBuf,
pub manifest_dir: PathBuf,
pub workspace_root: PathBuf,
pub target_directory: PathBuf,
pub analysis_root: PathBuf,
pub scope: SelectionScope,
pub package_roots: Vec<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CargoCheckPlan {
pub manifest_path: PathBuf,
pub workspace_root: PathBuf,
pub target_directory: PathBuf,
pub analysis_root: PathBuf,
pub cargo_args: Vec<OsString>,
}
pub(crate) fn resolve_cargo_selection(explicit_manifest_path: Option<&Path>) -> Result<Selection> {
let manifest_path = match explicit_manifest_path {
Some(path) => path
.canonicalize()
.with_context(|| format!("failed to canonicalize {}", path.display()))?,
None => find_nearest_manifest(&std::env::current_dir()?)?,
};
let metadata = cargo_metadata_for(&manifest_path)?;
let workspace_root = metadata.workspace_root.clone().into_std_path_buf();
let target_directory = metadata.target_directory.clone().into_std_path_buf();
let manifest_dir = manifest_path
.parent()
.context("manifest path had no parent directory")?
.to_path_buf();
let workspace_manifest = workspace_root.join("Cargo.toml");
let manifest_is_workspace_root = manifest_path == workspace_manifest;
let manifest_matches_package = metadata
.packages
.iter()
.any(|pkg| pkg.manifest_path.as_std_path() == manifest_path);
let scope = if manifest_is_workspace_root
&& (!manifest_matches_package || metadata.workspace_members.len() > 1)
{
SelectionScope::Workspace
} else {
SelectionScope::SinglePackage
};
let package_roots = match scope {
SelectionScope::Workspace => metadata
.workspace_members
.iter()
.filter_map(|id| metadata.packages.iter().find(|pkg| &pkg.id == id))
.map(package_root_from_metadata)
.collect::<Result<Vec<_>>>()?,
SelectionScope::SinglePackage => {
let package = metadata
.packages
.iter()
.find(|pkg| pkg.manifest_path.as_std_path() == manifest_path)
.with_context(|| {
format!(
"manifest {} not found in cargo metadata",
manifest_path.display()
)
})?;
vec![package_root_from_metadata(package)?]
},
};
let analysis_root = match scope {
SelectionScope::Workspace => workspace_root.clone(),
SelectionScope::SinglePackage => manifest_dir.clone(),
};
Ok(Selection {
manifest_path,
manifest_dir,
workspace_root,
target_directory,
analysis_root,
scope,
package_roots,
})
}
pub(crate) fn build_cargo_check_plan(
selection: &Selection,
cargo_cli: &CargoCheckCli,
) -> CargoCheckPlan {
let mut cargo_args = vec![
OsString::from("--manifest-path"),
selection.manifest_path.as_os_str().to_owned(),
];
let default_workspace = selection.scope == SelectionScope::Workspace
&& cargo_cli.package.is_empty()
&& cargo_cli.exclude.is_empty();
let use_workspace = cargo_cli.workspace() || !cargo_cli.exclude.is_empty() || default_workspace;
if use_workspace {
cargo_args.push(OsString::from("--workspace"));
}
append_repeated_flag(&mut cargo_args, "--package", &cargo_cli.package);
append_repeated_flag(&mut cargo_args, "--exclude", &cargo_cli.exclude);
append_bool_flag(&mut cargo_args, "--all-targets", cargo_cli.all_targets());
append_bool_flag(&mut cargo_args, "--lib", cargo_cli.lib());
append_bool_flag(&mut cargo_args, "--bins", cargo_cli.bins());
append_bool_flag(&mut cargo_args, "--examples", cargo_cli.examples());
append_bool_flag(&mut cargo_args, "--tests", cargo_cli.tests());
append_bool_flag(&mut cargo_args, "--benches", cargo_cli.benches());
append_repeated_flag(&mut cargo_args, "--bin", &cargo_cli.bin);
append_repeated_flag(&mut cargo_args, "--example", &cargo_cli.example);
append_repeated_flag(&mut cargo_args, "--test", &cargo_cli.test);
append_repeated_flag(&mut cargo_args, "--bench", &cargo_cli.bench);
CargoCheckPlan {
manifest_path: selection.manifest_path.clone(),
workspace_root: selection.workspace_root.clone(),
target_directory: selection.target_directory.clone(),
analysis_root: selection.analysis_root.clone(),
cargo_args,
}
}
fn append_bool_flag(args: &mut Vec<OsString>, flag: &'static str, enabled: bool) {
if enabled {
args.push(OsString::from(flag));
}
}
fn append_repeated_flag(args: &mut Vec<OsString>, flag: &'static str, values: &[String]) {
for value in values {
args.push(OsString::from(flag));
args.push(OsString::from(value));
}
}
fn package_root_from_metadata(package: &Package) -> Result<PathBuf> {
let package_root = package
.manifest_path
.as_std_path()
.parent()
.context("package manifest path had no parent directory")?;
package_root
.canonicalize()
.with_context(|| format!("failed to canonicalize {}", package_root.display()))
}
fn cargo_metadata_for(manifest_path: &Path) -> Result<Metadata> {
let mut command = MetadataCommand::new();
command.no_deps();
command.manifest_path(manifest_path);
command.exec().context("failed to run cargo metadata")
}
fn find_nearest_manifest(start: &Path) -> Result<PathBuf> {
for dir in start.ancestors() {
let candidate = dir.join("Cargo.toml");
if candidate.is_file() {
return candidate
.canonicalize()
.with_context(|| format!("failed to canonicalize {}", candidate.display()));
}
}
bail!("could not find Cargo.toml in current directory or any parent")
}
#[cfg(test)]
#[allow(clippy::panic, reason = "tests should panic on unexpected values")]
mod tests {
use std::path::PathBuf;
use super::CargoCheckPlan;
use super::Selection;
use super::SelectionScope;
use super::build_cargo_check_plan;
use super::resolve_cargo_selection;
use crate::cli::CargoCheckCli;
use crate::cli::PrimaryTargetCli;
use crate::cli::SecondaryTargetCli;
use crate::cli::WorkspaceCli;
fn fixture_selection(scope: SelectionScope) -> Selection {
Selection {
manifest_path: PathBuf::from("/workspace/Cargo.toml"),
manifest_dir: PathBuf::from("/workspace"),
workspace_root: PathBuf::from("/workspace"),
target_directory: PathBuf::from("/workspace/target"),
analysis_root: PathBuf::from("/workspace"),
scope,
package_roots: vec![PathBuf::from("/workspace/member")],
}
}
fn cargo_args_strings(plan: CargoCheckPlan) -> Vec<String> {
plan.cargo_args
.into_iter()
.map(|arg| arg.to_string_lossy().into_owned())
.collect()
}
#[test]
fn default_workspace_plan_checks_workspace() {
let selection = fixture_selection(SelectionScope::Workspace);
let args = cargo_args_strings(build_cargo_check_plan(
&selection,
&CargoCheckCli::default(),
));
assert_eq!(
args,
vec!["--manifest-path", "/workspace/Cargo.toml", "--workspace"]
);
}
#[test]
fn default_single_package_plan_checks_manifest_only() {
let selection = fixture_selection(SelectionScope::SinglePackage);
let args = cargo_args_strings(build_cargo_check_plan(
&selection,
&CargoCheckCli::default(),
));
assert_eq!(args, vec!["--manifest-path", "/workspace/Cargo.toml"]);
}
#[test]
fn plan_includes_workspace_all_targets() {
let selection = fixture_selection(SelectionScope::Workspace);
let cargo_cli = CargoCheckCli {
workspace: WorkspaceCli { workspace: true },
primary_targets: PrimaryTargetCli {
all_targets: true,
..Default::default()
},
..CargoCheckCli::default()
};
let args = cargo_args_strings(build_cargo_check_plan(&selection, &cargo_cli));
assert_eq!(
args,
vec![
"--manifest-path",
"/workspace/Cargo.toml",
"--workspace",
"--all-targets",
]
);
}
#[test]
fn plan_includes_named_package_and_tests() {
let selection = fixture_selection(SelectionScope::Workspace);
let cargo_cli = CargoCheckCli {
package: vec!["demo".to_string()],
secondary_targets: SecondaryTargetCli {
tests: true,
..Default::default()
},
..CargoCheckCli::default()
};
let args = cargo_args_strings(build_cargo_check_plan(&selection, &cargo_cli));
assert_eq!(
args,
vec![
"--manifest-path",
"/workspace/Cargo.toml",
"--package",
"demo",
"--tests",
]
);
}
#[test]
fn plan_includes_workspace_excludes() {
let selection = fixture_selection(SelectionScope::Workspace);
let cargo_cli = CargoCheckCli {
exclude: vec!["demo".to_string()],
..CargoCheckCli::default()
};
let args = cargo_args_strings(build_cargo_check_plan(&selection, &cargo_cli));
assert_eq!(
args,
vec![
"--manifest-path",
"/workspace/Cargo.toml",
"--workspace",
"--exclude",
"demo",
]
);
}
#[test]
fn plan_includes_specific_named_targets() {
let selection = fixture_selection(SelectionScope::SinglePackage);
let cargo_cli = CargoCheckCli {
bin: vec!["cli".to_string()],
example: vec!["demo".to_string()],
test: vec!["integration".to_string()],
bench: vec!["perf".to_string()],
..CargoCheckCli::default()
};
let args = cargo_args_strings(build_cargo_check_plan(&selection, &cargo_cli));
assert_eq!(
args,
vec![
"--manifest-path",
"/workspace/Cargo.toml",
"--bin",
"cli",
"--example",
"demo",
"--test",
"integration",
"--bench",
"perf",
]
);
}
#[test]
fn resolve_virtual_workspace_root_with_single_member_selects_workspace() {
let temp =
tempfile::tempdir().unwrap_or_else(|error| panic!("create temp fixture dir: {error}"));
std::fs::create_dir_all(temp.path().join("member/src"))
.unwrap_or_else(|error| panic!("create member src dir: {error}"));
std::fs::write(
temp.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"member\"]\nresolver = \"3\"\n",
)
.unwrap_or_else(|error| panic!("write workspace manifest: {error}"));
std::fs::write(
temp.path().join("member/Cargo.toml"),
"[package]\nname = \"member_fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap_or_else(|error| panic!("write member manifest: {error}"));
std::fs::write(temp.path().join("member/src/main.rs"), "fn main() {}\n")
.unwrap_or_else(|error| panic!("write member main: {error}"));
let selection = resolve_cargo_selection(Some(&temp.path().join("Cargo.toml")))
.unwrap_or_else(|error| panic!("resolve workspace selection: {error}"));
assert_eq!(selection.scope, SelectionScope::Workspace);
assert_eq!(selection.package_roots.len(), 1);
assert_eq!(
std::fs::canonicalize(&selection.package_roots[0])
.unwrap_or_else(|error| panic!("canonicalize selected package root: {error}")),
std::fs::canonicalize(temp.path().join("member"))
.unwrap_or_else(|error| panic!("canonicalize expected package root: {error}"))
);
}
}