use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use crate::adapter::rust::manifest::CargoToml;
impl CargoToml {
pub fn find_workspace_root(
manifest_path: &Path,
manifest: &CargoToml,
) -> Result<Option<(PathBuf, CargoToml)>> {
let pkg_dir = manifest_path.parent().unwrap_or(Path::new("."));
if let Some(ref pkg) = manifest.package {
if let Some(ref ws_rel) = pkg.workspace {
let candidate = pkg_dir.join(ws_rel).join("Cargo.toml");
if !candidate.is_file() {
anyhow::bail!(
"package.workspace specified at {:?} does not exist or is not a file",
candidate
);
}
let ws_toml = CargoToml::from_file(&candidate).with_context(|| {
format!(
"Failed to parse workspace root specified in package.workspace at {:?}",
candidate
)
})?;
if ws_toml.workspace.is_none() {
anyhow::bail!(
"package.workspace specified at {:?} does not contain a [workspace] section",
candidate
);
}
return Ok(Some((candidate, ws_toml)));
}
}
if manifest.workspace.is_some() {
return Ok(Some((manifest_path.to_path_buf(), manifest.clone())));
}
let mut cur = pkg_dir.parent();
while let Some(dir) = cur {
let candidate = dir.join("Cargo.toml");
if candidate.is_file() {
let ws_toml = CargoToml::from_file(&candidate).with_context(|| {
format!("Failed to parse workspace candidate at {:?}", candidate)
})?;
if let Some(ref ws) = ws_toml.workspace {
for m in &ws.members {
validate_workspace_pattern(m)
.with_context(|| format!("in workspace at {:?}", candidate))?;
}
for ex in &ws.exclude {
validate_workspace_pattern(ex)
.with_context(|| format!("in workspace at {:?}", candidate))?;
}
if let Ok(rel) = pkg_dir.strip_prefix(dir) {
let is_excluded = ws.exclude.iter().any(|ex| matches_path_pattern(ex, rel));
if is_excluded {
cur = dir.parent();
continue;
}
let is_member = if ws.members.is_empty() {
rel.as_os_str().is_empty()
} else {
ws.members.iter().any(|m| matches_path_pattern(m, rel))
};
if is_member {
return Ok(Some((candidate, ws_toml)));
} else {
let pkg_name = manifest
.package
.as_ref()
.map(|p| p.name.as_str())
.unwrap_or("unknown");
anyhow::bail!(
"package `{}` at {:?} is within the workspace at {:?}, but is not listed in `workspace.members` or `workspace.exclude`",
pkg_name,
manifest_path,
candidate
);
}
}
}
}
cur = dir.parent();
}
Ok(None)
}
pub fn effective_resolver_for_manifest(
manifest_path: &Path,
manifest: &CargoToml,
) -> Result<String> {
if manifest.workspace.is_some() {
return Ok(manifest.effective_resolver().to_string());
}
if let Some((_, ws_toml)) = CargoToml::find_workspace_root(manifest_path, manifest)? {
return Ok(ws_toml.effective_resolver().to_string());
}
Ok(manifest.effective_resolver().to_string())
}
pub fn discover_workspace_members(&self, root_dir: &Path) -> Result<Vec<(String, PathBuf)>> {
let mut members = Vec::new();
if let Some(ref ws) = self.workspace {
for ex in &ws.exclude {
validate_workspace_pattern(ex)?;
}
for pattern in &ws.members {
validate_workspace_pattern(pattern)?;
if pattern == "." {
let rel = Path::new(".");
let is_excluded = ws.exclude.iter().any(|ex| matches_path_pattern(ex, rel));
if !is_excluded {
if let Some(ref pkg) = self.package {
members.push((pkg.name.clone(), root_dir.to_path_buf()));
}
}
} else if pattern.contains('*') {
let pattern_path = Path::new(pattern);
let base_dir = pattern_path
.parent()
.map(|p| root_dir.join(p))
.unwrap_or_else(|| root_dir.to_path_buf());
if let Ok(entries) = fs::read_dir(&base_dir) {
for entry in entries.flatten() {
let candidate_dir = entry.path();
if candidate_dir.is_dir() {
if let Ok(rel) = candidate_dir.strip_prefix(root_dir) {
let is_excluded =
ws.exclude.iter().any(|ex| matches_path_pattern(ex, rel));
if !is_excluded && matches_path_pattern(pattern, rel) {
let candidate_manifest = candidate_dir.join("Cargo.toml");
if candidate_manifest.is_file() {
let toml = CargoToml::from_file(&candidate_manifest)
.with_context(|| {
format!(
"Failed to parse workspace member manifest at {:?}",
candidate_manifest
)
})?;
if let Some(pkg) = toml.package {
members.push((pkg.name, candidate_dir));
}
}
}
}
}
}
}
} else {
let rel = Path::new(pattern);
let is_excluded = ws.exclude.iter().any(|ex| matches_path_pattern(ex, rel));
if !is_excluded {
let member_dir = root_dir.join(pattern);
let candidate_manifest = member_dir.join("Cargo.toml");
if candidate_manifest.is_file() {
let toml =
CargoToml::from_file(&candidate_manifest).with_context(|| {
format!(
"Failed to parse workspace member manifest at {:?}",
candidate_manifest
)
})?;
if let Some(pkg) = toml.package {
members.push((pkg.name, member_dir));
}
}
}
}
}
} else if let Some(ref pkg) = self.package {
members.push((pkg.name.clone(), root_dir.to_path_buf()));
}
if let Some(ref pkg) = self.package {
let rel = Path::new(".");
let is_excluded = self
.workspace
.as_ref()
.map(|ws| ws.exclude.iter().any(|ex| matches_path_pattern(ex, rel)))
.unwrap_or(false);
if !is_excluded && !members.iter().any(|(n, _)| n == &pkg.name) {
members.push((pkg.name.clone(), root_dir.to_path_buf()));
}
}
Ok(members)
}
}
pub fn matches_path_pattern(pattern: &str, rel: &Path) -> bool {
let pattern = pattern.trim_end_matches('/');
let rel_str = rel.to_string_lossy();
if pattern == rel_str || (pattern == "." && (rel_str.is_empty() || rel_str == ".")) {
return true;
}
if pattern == "**" {
return !rel.as_os_str().is_empty();
}
if let Some(prefix) = pattern.strip_suffix("/**") {
if let Ok(rest) = rel.strip_prefix(prefix) {
return !rest.as_os_str().is_empty();
}
}
if let Some(prefix) = pattern.strip_suffix("/*") {
if let Ok(rest) = rel.strip_prefix(prefix) {
return rest.components().count() == 1;
}
}
if pattern == "*" {
return rel.components().count() == 1;
}
if let Some(_star_idx) = pattern.find('*') {
let pattern_path = Path::new(pattern);
let pattern_parent = pattern_path.parent();
let rel_parent = rel.parent();
let parents_match = match (pattern_parent, rel_parent) {
(None, None) => true,
(Some(p), None) => p.as_os_str().is_empty(),
(None, Some(r)) => r.as_os_str().is_empty(),
(Some(p), Some(r)) => p == r,
};
if parents_match {
if let (Some(pat_file), Some(rel_file)) = (pattern_path.file_name(), rel.file_name()) {
let pat_name = pat_file.to_string_lossy();
let rel_name = rel_file.to_string_lossy();
if let Some(s_idx) = pat_name.find('*') {
let pre = &pat_name[..s_idx];
let post = &pat_name[s_idx + 1..];
if rel_name.starts_with(pre)
&& rel_name.ends_with(post)
&& rel_name.len() >= pre.len() + post.len()
{
return true;
}
}
}
}
}
false
}
pub fn validate_workspace_pattern(pattern: &str) -> Result<()> {
let p = pattern.trim_end_matches('/');
if p.contains('?') || p.contains('[') || p.contains(']') {
anyhow::bail!(
"unsupported member pattern `{}`: glob character classes and `?` wildcards are not supported",
pattern
);
}
let components: Vec<&str> = p.split('/').collect();
for (idx, comp) in components.iter().enumerate() {
if comp.contains('*') {
if idx + 1 < components.len() {
anyhow::bail!(
"unsupported member pattern `{}`: wildcards in intermediate directories are not supported",
pattern
);
}
if comp.chars().filter(|c| *c == '*').count() > 1 && *comp != "**" {
anyhow::bail!(
"unsupported member pattern `{}`: multiple wildcards in a component are not supported",
pattern
);
}
}
}
Ok(())
}