use anyhow::{Context, Result};
use cargo_metadata::MetadataCommand;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathDepCrate {
pub crate_name: String,
pub src_dir: PathBuf,
}
pub fn discover_path_deps(manifest_path: &Path, app_package: &str) -> Result<Vec<PathDepCrate>> {
let metadata = MetadataCommand::new()
.manifest_path(manifest_path)
.exec()
.with_context(|| {
format!(
"cargo metadata failed for {} (package: {app_package})",
manifest_path.display(),
)
})?;
let resolve = metadata
.resolve
.as_ref()
.context("cargo metadata returned no resolve graph")?;
let root_id = resolve
.root
.as_ref()
.cloned()
.or_else(|| {
metadata
.packages
.iter()
.find(|p| p.name == app_package)
.map(|p| p.id.clone())
})
.with_context(|| format!("cargo package `{app_package}` not found in the workspace"))?;
let mut out: Vec<PathDepCrate> = Vec::new();
let mut visit: Vec<&cargo_metadata::PackageId> = vec![&root_id];
let mut seen: HashSet<&cargo_metadata::PackageId> = HashSet::new();
while let Some(pkg_id) = visit.pop() {
if !seen.insert(pkg_id) {
continue;
}
let Some(pkg) = metadata.packages.iter().find(|p| &p.id == pkg_id) else {
continue;
};
if pkg.source.is_some() {
continue;
}
if let Some(manifest_dir) = pkg.manifest_path.parent() {
let src_dir = manifest_dir.join("src");
out.push(PathDepCrate {
crate_name: pkg.name.replace('-', "_"),
src_dir: src_dir.into(),
});
}
if let Some(node) = resolve.nodes.iter().find(|n| &n.id == pkg_id) {
for dep in &node.deps {
visit.push(&dep.pkg);
}
}
}
Ok(out)
}
pub fn identify_crate_for_paths(paths: &[PathBuf], crates: &[PathDepCrate]) -> Option<String> {
let mut found: Option<&str> = None;
for p in paths {
let hit = best_crate_for(p, crates)?;
match found {
None => found = Some(hit),
Some(prev) if prev != hit => return None,
_ => {}
}
}
found.map(str::to_owned)
}
fn best_crate_for<'a>(path: &Path, crates: &'a [PathDepCrate]) -> Option<&'a str> {
let mut best: Option<(&str, usize)> = None;
for c in crates {
if path.starts_with(&c.src_dir) {
let depth = c.src_dir.components().count();
if best.map(|(_, d)| depth > d).unwrap_or(true) {
best = Some((&c.crate_name, depth));
}
}
}
best.map(|(n, _)| n)
}
#[cfg(test)]
mod tests {
use super::*;
fn cr(name: &str, dir: &str) -> PathDepCrate {
PathDepCrate {
crate_name: name.into(),
src_dir: PathBuf::from(dir),
}
}
#[test]
fn identify_returns_the_matching_crate() {
let crates = vec![
cr("podcast", "/ws/examples/podcast/src"),
cr(
"podcast_ui_kit",
"/ws/examples/podcast/crates/podcast-ui-kit/src",
),
];
let paths = vec![PathBuf::from(
"/ws/examples/podcast/crates/podcast-ui-kit/src/top_nav.rs",
)];
assert_eq!(
identify_crate_for_paths(&paths, &crates),
Some("podcast_ui_kit".into())
);
}
#[test]
fn identify_returns_none_when_paths_span_multiple_crates() {
let crates = vec![
cr("podcast", "/ws/examples/podcast/src"),
cr(
"podcast_ui_kit",
"/ws/examples/podcast/crates/podcast-ui-kit/src",
),
];
let paths = vec![
PathBuf::from("/ws/examples/podcast/src/lib.rs"),
PathBuf::from("/ws/examples/podcast/crates/podcast-ui-kit/src/top_nav.rs"),
];
assert_eq!(identify_crate_for_paths(&paths, &crates), None);
}
#[test]
fn identify_returns_none_when_no_crate_matches() {
let crates = vec![cr("podcast", "/ws/examples/podcast/src")];
let paths = vec![PathBuf::from("/some/unrelated/path/foo.rs")];
assert_eq!(identify_crate_for_paths(&paths, &crates), None);
}
#[test]
fn identify_picks_the_deeper_match_when_src_dirs_nest() {
let crates = vec![
cr("outer", "/ws/foo/src"),
cr("inner", "/ws/foo/src/inner_pkg/src"),
];
let paths = vec![PathBuf::from("/ws/foo/src/inner_pkg/src/lib.rs")];
assert_eq!(
identify_crate_for_paths(&paths, &crates),
Some("inner".into())
);
}
#[test]
fn identify_handles_single_crate_batch() {
let crates = vec![cr("podcast", "/ws/podcast/src")];
let paths = vec![
PathBuf::from("/ws/podcast/src/lib.rs"),
PathBuf::from("/ws/podcast/src/main.rs"),
];
assert_eq!(
identify_crate_for_paths(&paths, &crates),
Some("podcast".into())
);
}
}