use crate::error::WorkspaceError;
use cabin_core::{DependencyKind, DependencySource};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use super::canonicalize;
pub(super) struct WorkspaceMembers {
pub(super) included: Vec<PathBuf>,
pub(super) excluded: Vec<PathBuf>,
}
pub(super) fn expand_workspace_members(
workspace_dir: &Path,
members: &[String],
exclude: &[String],
) -> Result<WorkspaceMembers, WorkspaceError> {
let mut included: BTreeSet<PathBuf> = BTreeSet::new();
for pattern in members {
let dirs = expand_member_pattern(workspace_dir, pattern)?;
for dir in dirs {
let manifest = dir.join("cabin.toml");
if !manifest.is_file() {
return Err(WorkspaceError::WorkspaceMemberMissing {
pattern: pattern.clone(),
root: workspace_dir.to_path_buf(),
});
}
let canonical_dir = canonicalize(&dir)?;
included.insert(canonical_dir);
}
}
let mut excluded: BTreeSet<PathBuf> = BTreeSet::new();
let canonical_root = canonicalize(workspace_dir)?;
for pattern in exclude {
if pattern.is_empty() {
return Err(WorkspaceError::UnsupportedWorkspacePattern {
pattern: pattern.clone(),
});
}
let dirs = expand_exclude_pattern(workspace_dir, pattern)?;
let mut hit_any = false;
for dir in dirs {
if !dir.is_dir() {
continue;
}
let Ok(canonical_dir) = canonicalize(&dir) else {
continue;
};
if included.remove(&canonical_dir) {
hit_any = true;
if let Ok(rel) = canonical_dir.strip_prefix(&canonical_root) {
excluded.insert(rel.to_path_buf());
} else {
excluded.insert(canonical_dir.clone());
}
}
}
if !hit_any {
return Err(WorkspaceError::UnusedExcludePattern {
pattern: pattern.clone(),
root: workspace_dir.to_path_buf(),
});
}
}
let mut out: Vec<PathBuf> = Vec::with_capacity(included.len());
for dir in &included {
let manifest = dir.join("cabin.toml");
out.push(canonicalize(&manifest)?);
}
out.sort();
let excluded_paths: Vec<PathBuf> = excluded.into_iter().collect();
Ok(WorkspaceMembers {
included: out,
excluded: excluded_paths,
})
}
pub(super) fn resolve_workspace_dependencies(
mut package: cabin_core::Package,
workspace_deps: &BTreeMap<DependencyKind, BTreeMap<String, DependencySource>>,
) -> Result<cabin_core::Package, WorkspaceError> {
for dep in &mut package.dependencies {
if !matches!(dep.source, DependencySource::Workspace) {
continue;
}
let table = workspace_deps.get(&dep.kind);
let resolved = table
.and_then(|t| t.get(dep.name.as_str()))
.ok_or_else(|| WorkspaceError::UnresolvedWorkspaceDependency {
dep_name: dep.name.as_str().to_owned(),
parent: package.name.as_str().to_owned(),
kind: dep.kind,
})?;
dep.source = resolved.clone();
}
Ok(package)
}
pub(super) fn parse_workspace_dep_source(
name: &str,
req: &str,
) -> Result<DependencySource, WorkspaceError> {
let manifest = format!(
"[package]\nname = \"__workspace_root__\"\nversion = \"0.0.0\"\n[dependencies]\n{name} = \"{}\"\n",
req.replace('"', "\\\""),
);
let parsed = cabin_manifest::parse_manifest_str(&manifest).map_err(|source| {
WorkspaceError::InvalidWorkspaceDependency {
name: name.to_owned(),
source: Box::new(source),
}
})?;
let package = parsed
.package
.expect("inline manifest always has [package]");
let dep = package
.dependencies
.into_iter()
.next()
.expect("inline manifest declared exactly one dependency");
Ok(dep.source)
}
pub(super) fn validate_workspace_pattern(
field: &'static str,
pattern: &str,
) -> Result<(), WorkspaceError> {
if pattern.is_empty() {
return Err(WorkspaceError::UnsupportedWorkspacePattern {
pattern: pattern.to_owned(),
});
}
let p = std::path::Path::new(pattern);
if p.is_absolute() {
return Err(WorkspaceError::WorkspacePatternEscapesRoot {
field,
pattern: pattern.to_owned(),
});
}
for component in p.components() {
if matches!(
component,
std::path::Component::ParentDir | std::path::Component::Prefix(_)
) {
return Err(WorkspaceError::WorkspacePatternEscapesRoot {
field,
pattern: pattern.to_owned(),
});
}
}
Ok(())
}
pub(super) fn expand_member_pattern(
workspace_dir: &Path,
pattern: &str,
) -> Result<Vec<PathBuf>, WorkspaceError> {
validate_workspace_pattern("workspace.members", pattern)?;
if !pattern.contains('*') {
let dir = workspace_dir.join(pattern);
return Ok(vec![dir]);
}
let Some(trimmed) = pattern.strip_suffix("/*") else {
return Err(WorkspaceError::UnsupportedWorkspacePattern {
pattern: pattern.to_owned(),
});
};
if trimmed.contains('*') {
return Err(WorkspaceError::UnsupportedWorkspacePattern {
pattern: pattern.to_owned(),
});
}
let prefix_dir = if trimmed.is_empty() {
workspace_dir.to_path_buf()
} else {
workspace_dir.join(trimmed)
};
if !prefix_dir.is_dir() {
return Err(WorkspaceError::WorkspaceMemberMissing {
pattern: pattern.to_owned(),
root: workspace_dir.to_path_buf(),
});
}
let entries = std::fs::read_dir(&prefix_dir).map_err(|source| WorkspaceError::Io {
path: prefix_dir.clone(),
source,
})?;
let mut out = Vec::new();
for entry in entries {
let entry = entry.map_err(|source| WorkspaceError::Io {
path: prefix_dir.clone(),
source,
})?;
let path = entry.path();
if path.is_dir() && path.join("cabin.toml").is_file() {
out.push(path);
}
}
if out.is_empty() {
return Err(WorkspaceError::WorkspaceMemberMissing {
pattern: pattern.to_owned(),
root: workspace_dir.to_path_buf(),
});
}
out.sort();
Ok(out)
}
pub(super) fn expand_exclude_pattern(
workspace_dir: &Path,
pattern: &str,
) -> Result<Vec<PathBuf>, WorkspaceError> {
validate_workspace_pattern("workspace.exclude", pattern)?;
if !pattern.contains('*') {
return Ok(vec![workspace_dir.join(pattern)]);
}
let Some(trimmed) = pattern.strip_suffix("/*") else {
return Err(WorkspaceError::UnsupportedWorkspacePattern {
pattern: pattern.to_owned(),
});
};
if trimmed.contains('*') {
return Err(WorkspaceError::UnsupportedWorkspacePattern {
pattern: pattern.to_owned(),
});
}
let prefix_dir = if trimmed.is_empty() {
workspace_dir.to_path_buf()
} else {
workspace_dir.join(trimmed)
};
if !prefix_dir.is_dir() {
return Ok(Vec::new());
}
let entries = std::fs::read_dir(&prefix_dir).map_err(|source| WorkspaceError::Io {
path: prefix_dir.clone(),
source,
})?;
let mut out = Vec::new();
for entry in entries {
let entry = entry.map_err(|source| WorkspaceError::Io {
path: prefix_dir.clone(),
source,
})?;
let path = entry.path();
if path.is_dir() {
out.push(path);
}
}
out.sort();
Ok(out)
}