use {
serde::Deserialize,
std::{
collections::BTreeSet,
path::{Path, PathBuf},
},
};
#[derive(Deserialize)]
struct CargoLock {
#[serde(default)]
package: Vec<Package>,
}
#[derive(Deserialize)]
struct Package {
name: String,
version: String,
#[serde(default)]
source: Option<String>,
}
pub fn discover_dep_src_roots(workspace_root: &Path) -> Vec<PathBuf> {
let lock_path = workspace_root.join("Cargo.lock");
let Ok(contents) = std::fs::read_to_string(&lock_path) else {
return Vec::new();
};
let Ok(parsed) = toml::from_str::<CargoLock>(&contents) else {
return Vec::new();
};
let Some(cargo_home) = cargo_home() else {
return Vec::new();
};
let registry_roots: Vec<PathBuf> = std::fs::read_dir(cargo_home.join("registry/src"))
.into_iter()
.flatten()
.flatten()
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
let git_checkouts = cargo_home.join("git/checkouts");
let mut roots: Vec<PathBuf> = Vec::new();
let mut seen: BTreeSet<PathBuf> = BTreeSet::new();
for pkg in &parsed.package {
let Some(source) = pkg.source.as_deref() else {
continue;
};
if source.starts_with("registry+") {
let dirname = format!("{}-{}", pkg.name, pkg.version);
for registry in ®istry_roots {
let candidate = registry.join(&dirname);
if candidate.is_dir() && seen.insert(candidate.clone()) {
roots.push(candidate);
break;
}
}
} else if source.starts_with("git+") {
let Some(hash) = source.rsplit('#').next() else {
continue;
};
if hash.len() < 7 {
continue;
}
let rev_prefix = &hash[..7];
let entries = match std::fs::read_dir(&git_checkouts) {
Ok(e) => e,
Err(_) => continue,
};
for outer in entries.flatten() {
let candidate = outer.path().join(rev_prefix);
if let Some(crate_dir) =
find_crate_dir_in_checkout(&candidate, &pkg.name, &pkg.version)
{
if seen.insert(crate_dir.clone()) {
roots.push(crate_dir);
}
break;
}
}
}
}
roots
}
pub fn discover_path_dep_roots(crate_dir: &Path) -> Vec<PathBuf> {
let manifest = crate_dir.join("Cargo.toml");
let Ok(contents) = std::fs::read_to_string(&manifest) else {
return Vec::new();
};
let Ok(parsed) = toml::from_str::<toml::Value>(&contents) else {
return Vec::new();
};
let mut roots = Vec::new();
for section in ["dependencies", "dev-dependencies"] {
let Some(deps) = parsed.get(section).and_then(|v| v.as_table()) else {
continue;
};
for (_name, spec) in deps {
let path_str = match spec {
toml::Value::Table(t) => t.get("path").and_then(|v| v.as_str()),
_ => None,
};
if let Some(p) = path_str {
let resolved = crate_dir.join(p);
if let Ok(canonical) = resolved.canonicalize() {
if canonical.is_dir() {
roots.push(canonical);
}
}
}
}
}
roots
}
fn cargo_home() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("CARGO_HOME") {
return Some(PathBuf::from(p));
}
dirs::home_dir().map(|h| h.join(".cargo"))
}
fn find_crate_dir_in_checkout(root: &Path, name: &str, version: &str) -> Option<PathBuf> {
fn recurse(dir: &Path, name: &str, version: &str, depth: u8) -> Option<PathBuf> {
if depth > 4 {
return None;
}
let manifest = dir.join("Cargo.toml");
if manifest.is_file() {
if let Ok(contents) = std::fs::read_to_string(&manifest) {
if contents.contains(&format!("name = \"{name}\"")) {
if version.is_empty() || contents.contains(&format!("version = \"{version}\""))
{
return Some(dir.to_path_buf());
}
}
}
}
let entries = std::fs::read_dir(dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let Some(fname) = path.file_name().and_then(|s| s.to_str()) else {
continue;
};
if fname.starts_with('.') || fname == "target" || fname == "tests" {
continue;
}
if let Some(hit) = recurse(&path, name, version, depth + 1) {
return Some(hit);
}
}
None
}
recurse(root, name, version, 0)
}