use crate::error::WorkspaceError;
use crate::graph::{DependencyEdge, PackageGraph, PackageKind, WorkspacePackage};
use cabin_core::{DependencyKind, DependencySource, PackageName, PortDepSource};
use cabin_manifest::ParsedManifest;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::path::{Path, PathBuf};
mod members;
#[cfg(test)]
mod tests;
mod topo;
use self::members::{
WorkspaceMembers, expand_workspace_members, parse_workspace_dep_source,
resolve_workspace_dependencies, resolve_workspace_standards, validate_workspace_pattern,
};
use self::topo::topo_sort;
#[derive(Debug, Clone)]
pub struct RegistryPackageSource {
pub name: PackageName,
pub version: semver::Version,
pub manifest_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct PatchedPackageSource {
pub name: PackageName,
pub version: semver::Version,
pub manifest_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct PortPackageSource {
pub name: PackageName,
pub version: semver::Version,
pub manifest_path: PathBuf,
pub origin: cabin_port::PortOrigin,
}
pub fn load_workspace(manifest_path: impl AsRef<Path>) -> Result<PackageGraph, WorkspaceError> {
load_workspace_inner(
manifest_path,
&[],
&[],
&[],
&RegistryEnforcement::strict(),
&BTreeSet::new(),
&PortMode::Strict,
)
}
pub fn load_workspace_skip_ports(
manifest_path: impl AsRef<Path>,
) -> Result<PackageGraph, WorkspaceError> {
load_workspace_inner(
manifest_path,
&[],
&[],
&[],
&RegistryEnforcement::strict(),
&BTreeSet::new(),
&PortMode::SkipAll,
)
}
#[derive(Debug, Clone)]
pub struct WorkspaceLoadOptions<'a> {
pub registry: &'a [RegistryPackageSource],
pub patches: &'a [PatchedPackageSource],
pub ports: &'a [PortPackageSource],
pub registry_policy: RegistryPolicy<'a>,
pub include_dev_for: &'a BTreeSet<String>,
pub port_policy: PortPolicy<'a>,
}
#[derive(Debug, Clone, Default)]
pub enum PortPolicy<'a> {
#[default]
Strict,
TolerateExcept(&'a BTreeSet<String>),
}
#[derive(Debug, Clone, Default)]
pub enum RegistryPolicy<'a> {
#[default]
Strict,
StrictFor(&'a BTreeSet<String>),
}
pub fn load_workspace_with_options(
manifest_path: impl AsRef<Path>,
options: &WorkspaceLoadOptions<'_>,
) -> Result<PackageGraph, WorkspaceError> {
let policy = match &options.registry_policy {
RegistryPolicy::Strict => RegistryEnforcement::strict(),
RegistryPolicy::StrictFor(set) => RegistryEnforcement::scoped((*set).clone()),
};
let port_mode = match &options.port_policy {
PortPolicy::Strict => PortMode::Strict,
PortPolicy::TolerateExcept(strict) => PortMode::TolerateExcept((*strict).clone()),
};
load_workspace_inner(
manifest_path,
options.registry,
options.patches,
options.ports,
&policy,
options.include_dev_for,
&port_mode,
)
}
#[derive(Debug, Clone)]
struct RegistryEnforcement {
strict_packages: Option<BTreeSet<String>>,
}
impl RegistryEnforcement {
fn strict() -> Self {
Self {
strict_packages: None,
}
}
fn scoped(strict_packages: BTreeSet<String>) -> Self {
Self {
strict_packages: Some(strict_packages),
}
}
fn requires_registry_for(&self, parent_name: &str) -> bool {
match &self.strict_packages {
None => true,
Some(set) => set.contains(parent_name),
}
}
}
#[derive(Debug, Clone)]
enum PortMode {
Strict,
SkipAll,
TolerateExcept(BTreeSet<String>),
}
#[allow(clippy::too_many_lines)] fn load_workspace_inner(
manifest_path: impl AsRef<Path>,
registry: &[RegistryPackageSource],
patches: &[PatchedPackageSource],
ports: &[PortPackageSource],
policy: &RegistryEnforcement,
include_dev_for: &BTreeSet<String>,
port_mode: &PortMode,
) -> Result<PackageGraph, WorkspaceError> {
let manifest_path = canonicalize(manifest_path.as_ref())?;
let root_dir = manifest_path
.parent()
.ok_or_else(|| WorkspaceError::Io {
path: manifest_path.clone(),
source: std::io::Error::other("manifest path has no parent directory"),
})?
.to_path_buf();
let root_manifest = parse_manifest(&manifest_path)?;
if root_manifest.package.is_none() && root_manifest.workspace.is_none() {
return Err(WorkspaceError::EmptyManifest {
path: manifest_path,
});
}
let host_platform = cabin_core::TargetPlatform::current();
let is_workspace_root = root_manifest.workspace.is_some();
let mut loader = Loader::default();
let WorkspaceRootScan {
primary_manifest_paths,
excluded_member_paths,
default_members: workspace_default_members,
workspace_deps,
workspace_standards,
} = scan_workspace_root(&root_manifest, &root_dir, &manifest_path)?;
let port_lookup = build_port_lookup(ports)?;
let registry_lookup = build_registry_lookup(registry, patches)?;
let mut to_load = initial_load_queue(&primary_manifest_paths, registry, patches, ports)?;
let root_manifest_path = manifest_path.clone();
let ctx = DepResolutionContext {
host_platform: &host_platform,
skip_port_edges: matches!(port_mode, PortMode::SkipAll),
tolerate_strict_set: match port_mode {
PortMode::TolerateExcept(set) => Some(set),
_ => None,
},
ports: &port_lookup,
registry: ®istry_lookup,
policy,
include_dev_for,
};
while let Some(manifest_path) = to_load.pop() {
if loader.manifest_index.contains_key(&manifest_path) {
continue;
}
let parsed = parse_manifest(&manifest_path)?;
let package = parsed.package.ok_or_else(|| {
WorkspaceError::LocalDependencyIsWorkspace {
dep_name: project_alias_for(&loader, &manifest_path),
path: manifest_path.clone(),
}
})?;
reject_member_manifest_overrides(&package, &manifest_path, &root_manifest_path)?;
validate_registry_pin(&package, &manifest_path, ®istry_lookup)?;
let manifest_dir = manifest_path
.parent()
.expect("canonicalized manifest path has a parent")
.to_path_buf();
reject_external_workspace_markers(
&package,
&manifest_path,
®istry_lookup,
&port_lookup,
)?;
let package = resolve_workspace_dependencies(package, &workspace_deps)?;
let package = resolve_workspace_standards(package, workspace_standards, &manifest_path)?;
let dep_paths = resolve_dep_paths(&ctx, &package, &manifest_path, &manifest_dir)?;
verify_dep_path_names(&dep_paths)?;
let index = loader.packages.len();
loader.manifest_index.insert(manifest_path.clone(), index);
loader.packages.push(LoadedPackage {
package,
manifest_path,
manifest_dir,
dep_paths,
});
for dep in &loader.packages[index].dep_paths {
to_load.push(dep.path.clone());
}
}
reject_duplicate_package_names(&loader.packages)?;
let packages = link_workspace_packages(&loader, ®istry_lookup, &port_lookup);
let (sorted, new_position) = apply_topo_order(&packages)?;
let primary_packages: Vec<usize> = primary_manifest_paths
.iter()
.map(|p| new_position[&loader.manifest_index[p]])
.collect();
let root_package = root_manifest
.package
.is_some()
.then(|| new_position[&loader.manifest_index[&manifest_path]]);
let default_members = resolve_default_members(
&workspace_default_members,
&root_dir,
&loader,
&new_position,
&primary_packages,
)?;
Ok(PackageGraph {
root_manifest_path: manifest_path,
root_dir,
is_workspace_root,
root_package,
root_settings: root_manifest.root_settings.into(),
primary_packages,
default_members,
excluded_members: excluded_member_paths,
packages: sorted,
})
}
struct WorkspaceRootScan {
primary_manifest_paths: Vec<PathBuf>,
excluded_member_paths: Vec<PathBuf>,
default_members: Vec<String>,
workspace_deps: BTreeMap<DependencyKind, BTreeMap<String, DependencySource>>,
workspace_standards: cabin_core::WorkspaceStandardDefaults,
}
fn scan_workspace_root(
root_manifest: &ParsedManifest,
root_dir: &Path,
manifest_path: &Path,
) -> Result<WorkspaceRootScan, WorkspaceError> {
let mut primary_manifest_paths: Vec<PathBuf> = Vec::new();
if root_manifest.package.is_some() {
primary_manifest_paths.push(manifest_path.to_path_buf());
}
let mut default_members: Vec<String> = Vec::new();
let mut workspace_deps: BTreeMap<DependencyKind, BTreeMap<String, DependencySource>> =
BTreeMap::new();
let mut workspace_standards = cabin_core::WorkspaceStandardDefaults::default();
let mut excluded_member_paths: Vec<PathBuf> = Vec::new();
if let Some(workspace) = &root_manifest.workspace {
let WorkspaceMembers { included, excluded } =
expand_workspace_members(root_dir, &workspace.members, &workspace.exclude)?;
for canonical in included {
let parsed = parse_manifest(&canonical)?;
if parsed.workspace.is_some() {
return Err(WorkspaceError::NestedWorkspace { path: canonical });
}
primary_manifest_paths.push(canonical);
}
excluded_member_paths = excluded;
default_members.clone_from(&workspace.default_members);
workspace_standards = workspace.standards;
for (kind, table) in [
(DependencyKind::Normal, &workspace.dependencies),
(DependencyKind::Dev, &workspace.dev_dependencies),
] {
if table.is_empty() {
continue;
}
let entry = workspace_deps.entry(kind).or_default();
for (name, req) in table {
entry.insert(name.clone(), parse_workspace_dep_source(name, req)?);
}
}
}
Ok(WorkspaceRootScan {
primary_manifest_paths,
excluded_member_paths,
default_members,
workspace_deps,
workspace_standards,
})
}
struct PortLookup {
by_canonical_dir: HashMap<PathBuf, PathBuf>,
by_name: HashMap<String, PathBuf>,
canonical_paths: HashSet<PathBuf>,
}
fn build_port_lookup(ports: &[PortPackageSource]) -> Result<PortLookup, WorkspaceError> {
let mut by_canonical_dir: HashMap<PathBuf, PathBuf> = HashMap::new();
let mut by_name: HashMap<String, PathBuf> = HashMap::new();
let mut canonical_paths: HashSet<PathBuf> = HashSet::new();
for entry in ports {
match &entry.origin {
cabin_port::PortOrigin::PortDir(port_dir) => {
let port_dir_canonical = canonicalize(port_dir)?;
if let Some(previous) =
by_canonical_dir.insert(port_dir_canonical, entry.manifest_path.clone())
{
return Err(WorkspaceError::DuplicatePackageName {
name: entry.name.as_str().to_owned(),
first: previous,
second: entry.manifest_path.clone(),
});
}
}
cabin_port::PortOrigin::Builtin(name) => {
if let Some(previous) =
by_name.insert((*name).to_owned(), entry.manifest_path.clone())
{
return Err(WorkspaceError::DuplicatePackageName {
name: entry.name.as_str().to_owned(),
first: previous,
second: entry.manifest_path.clone(),
});
}
}
}
canonical_paths.insert(canonicalize(&entry.manifest_path)?);
}
Ok(PortLookup {
by_canonical_dir,
by_name,
canonical_paths,
})
}
struct RegistryLookup<'a> {
by_name: HashMap<&'a str, PathBuf>,
canonical_names: HashMap<PathBuf, &'a PackageName>,
canonical_versions: HashMap<PathBuf, &'a semver::Version>,
canonical_paths: HashSet<PathBuf>,
patch_canonical_paths: HashSet<PathBuf>,
}
fn build_registry_lookup<'a>(
registry: &'a [RegistryPackageSource],
patches: &'a [PatchedPackageSource],
) -> Result<RegistryLookup<'a>, WorkspaceError> {
let mut by_name: HashMap<&str, PathBuf> = HashMap::new();
let mut canonical_names: HashMap<PathBuf, &PackageName> = HashMap::new();
let mut canonical_versions: HashMap<PathBuf, &semver::Version> = HashMap::new();
let mut canonical_paths: HashSet<PathBuf> = HashSet::new();
let mut patch_canonical_paths: HashSet<PathBuf> = HashSet::new();
for entry in registry {
let canonical = canonicalize(&entry.manifest_path)?;
by_name.insert(entry.name.as_str(), canonical.clone());
canonical_names.insert(canonical.clone(), &entry.name);
canonical_versions.insert(canonical.clone(), &entry.version);
canonical_paths.insert(canonical);
}
for entry in patches {
let canonical = canonicalize(&entry.manifest_path)?;
if canonical_paths.contains(&canonical) {
return Err(WorkspaceError::PatchConflictsWithRegistry {
package: entry.name.as_str().to_owned(),
path: canonical,
});
}
if let Some(existing) = by_name.insert(entry.name.as_str(), canonical.clone()) {
return Err(WorkspaceError::DuplicatePackageName {
name: entry.name.as_str().to_owned(),
first: existing,
second: canonical,
});
}
canonical_names.insert(canonical.clone(), &entry.name);
canonical_versions.insert(canonical.clone(), &entry.version);
canonical_paths.insert(canonical.clone());
patch_canonical_paths.insert(canonical);
}
Ok(RegistryLookup {
by_name,
canonical_names,
canonical_versions,
canonical_paths,
patch_canonical_paths,
})
}
fn initial_load_queue(
primary_manifest_paths: &[PathBuf],
registry: &[RegistryPackageSource],
patches: &[PatchedPackageSource],
ports: &[PortPackageSource],
) -> Result<Vec<PathBuf>, WorkspaceError> {
let mut to_load: Vec<PathBuf> = primary_manifest_paths.to_vec();
for entry in registry {
to_load.push(canonicalize(&entry.manifest_path)?);
}
for entry in patches {
to_load.push(canonicalize(&entry.manifest_path)?);
}
for entry in ports {
to_load.push(canonicalize(&entry.manifest_path)?);
}
Ok(to_load)
}
fn reject_member_manifest_overrides(
package: &cabin_core::Package,
manifest_path: &Path,
root_manifest_path: &Path,
) -> Result<(), WorkspaceError> {
if manifest_path == root_manifest_path {
return Ok(());
}
if !package.profiles.is_empty() {
return Err(WorkspaceError::MemberDeclaresProfiles {
package: package.name.as_str().to_owned(),
path: manifest_path.to_path_buf(),
});
}
if !package.toolchain.is_empty() {
return Err(WorkspaceError::MemberDeclaresToolchain {
package: package.name.as_str().to_owned(),
path: manifest_path.to_path_buf(),
});
}
if !package.compiler_wrapper.is_empty() {
return Err(WorkspaceError::MemberDeclaresCompilerWrapper {
package: package.name.as_str().to_owned(),
path: manifest_path.to_path_buf(),
});
}
if !package.patches.is_empty() {
return Err(WorkspaceError::MemberDeclaresPatches {
package: package.name.as_str().to_owned(),
path: manifest_path.to_path_buf(),
});
}
Ok(())
}
fn validate_registry_pin(
package: &cabin_core::Package,
manifest_path: &Path,
registry: &RegistryLookup<'_>,
) -> Result<(), WorkspaceError> {
let Some(expected_version) = registry.canonical_versions.get(manifest_path) else {
return Ok(());
};
let expected_name = registry.canonical_names.get(manifest_path).copied();
let version_ok = &package.version == *expected_version;
let name_ok = expected_name.is_none_or(|n| n.as_str() == package.name.as_str());
if !name_ok {
return Err(WorkspaceError::RegistryPackageNameMismatch {
name: expected_name
.map(|n| n.as_str().to_owned())
.unwrap_or_default(),
actual_name: package.name.as_str().to_owned(),
path: manifest_path.to_path_buf(),
});
}
if !version_ok {
return Err(WorkspaceError::RegistryPackageMismatch {
name: expected_name
.map(|n| n.as_str().to_owned())
.unwrap_or_default(),
version: expected_version.to_string(),
actual_name: package.name.as_str().to_owned(),
actual_version: package.version.to_string(),
path: manifest_path.to_path_buf(),
});
}
Ok(())
}
fn reject_external_workspace_markers(
package: &cabin_core::Package,
manifest_path: &Path,
registry: &RegistryLookup<'_>,
ports: &PortLookup,
) -> Result<(), WorkspaceError> {
if registry.patch_canonical_paths.contains(manifest_path) {
return Ok(());
}
let origin = if ports.canonical_paths.contains(manifest_path) {
"foundation-port"
} else if registry.canonical_paths.contains(manifest_path) {
"registry"
} else {
return Ok(());
};
if let Some(field) = package.language.workspace_marker_field() {
return Err(WorkspaceError::ExternalPackageDeclaresWorkspaceStandard {
origin,
package: package.name.as_str().to_owned(),
field,
path: manifest_path.to_path_buf(),
});
}
if let Some(dep) = package
.dependencies
.iter()
.find(|dep| matches!(dep.source, DependencySource::Workspace))
{
return Err(WorkspaceError::ExternalPackageDeclaresWorkspaceDependency {
origin,
package: package.name.as_str().to_owned(),
dep_name: dep.name.as_str().to_owned(),
path: manifest_path.to_path_buf(),
});
}
Ok(())
}
struct DepResolutionContext<'a> {
host_platform: &'a cabin_core::TargetPlatform,
skip_port_edges: bool,
tolerate_strict_set: Option<&'a BTreeSet<String>>,
ports: &'a PortLookup,
registry: &'a RegistryLookup<'a>,
policy: &'a RegistryEnforcement,
include_dev_for: &'a BTreeSet<String>,
}
fn resolve_dep_paths(
ctx: &DepResolutionContext<'_>,
package: &cabin_core::Package,
manifest_path: &Path,
manifest_dir: &Path,
) -> Result<Vec<DepPath>, WorkspaceError> {
let dev_active_for_this_pkg = ctx.include_dev_for.contains(package.name.as_str());
let parent_is_registry = ctx.registry.canonical_paths.contains(manifest_path)
&& !ctx.registry.patch_canonical_paths.contains(manifest_path)
&& !ctx.ports.canonical_paths.contains(manifest_path);
let mut dep_paths: Vec<DepPath> = Vec::with_capacity(package.dependencies.len());
for dep in &package.dependencies {
let kind_active = dep.kind.is_resolved_by_default()
|| (dev_active_for_this_pkg && dep.kind == DependencyKind::Dev);
if !kind_active {
continue;
}
if !dep.matches_platform(ctx.host_platform) {
continue;
}
if parent_is_registry {
match &dep.source {
DependencySource::Path(_) => {
return Err(WorkspaceError::RegistryPackageDeclaresPathDependency {
package: package.name.as_str().to_owned(),
dep_name: dep.name.as_str().to_owned(),
path: manifest_path.to_path_buf(),
});
}
DependencySource::Port(_) => {
return Err(WorkspaceError::RegistryPackageDeclaresPortDependency {
package: package.name.as_str().to_owned(),
dep_name: dep.name.as_str().to_owned(),
path: manifest_path.to_path_buf(),
});
}
DependencySource::Version(_) | DependencySource::Workspace => {}
}
}
let Some(canonical) =
resolve_dep_manifest_path(ctx, dep, package.name.as_str(), manifest_dir)?
else {
continue;
};
dep_paths.push(DepPath {
name: dep.name.as_str().to_owned(),
path: canonical,
kind: dep.kind,
condition: dep.condition.clone(),
});
}
Ok(dep_paths)
}
fn resolve_dep_manifest_path(
ctx: &DepResolutionContext<'_>,
dep: &cabin_core::Dependency,
parent_name: &str,
manifest_dir: &Path,
) -> Result<Option<PathBuf>, WorkspaceError> {
let canonical = match &dep.source {
DependencySource::Path(rel) => {
let candidate = manifest_dir.join(rel).join("cabin.toml");
if !candidate.is_file() {
return Err(WorkspaceError::LocalDependencyManifestMissing {
dep_name: dep.name.as_str().to_owned(),
expected: candidate,
});
}
canonicalize(&candidate)?
}
DependencySource::Port(PortDepSource::Path(rel)) => {
if ctx.skip_port_edges {
return Ok(None);
}
let tolerate = ctx
.tolerate_strict_set
.is_some_and(|set| !set.contains(parent_name));
let port_dir = manifest_dir.join(rel);
if !port_dir.is_dir() {
if tolerate {
return Ok(None);
}
return Err(WorkspaceError::PortDirectoryMissing {
dep_name: dep.name.as_str().to_owned(),
parent: parent_name.to_owned(),
port_dir,
});
}
let port_dir_canonical = canonicalize(&port_dir)?;
if let Some(manifest_path) = ctx.ports.by_canonical_dir.get(&port_dir_canonical) {
canonicalize(manifest_path)?
} else {
if tolerate {
return Ok(None);
}
return Err(WorkspaceError::PortDependencyNotPrepared {
dep_name: dep.name.as_str().to_owned(),
parent: parent_name.to_owned(),
port_dir: port_dir_canonical,
});
}
}
DependencySource::Port(PortDepSource::Builtin { name, .. }) => {
if ctx.skip_port_edges {
return Ok(None);
}
let tolerate = ctx
.tolerate_strict_set
.is_some_and(|set| !set.contains(parent_name));
if let Some(manifest_path) = ctx.ports.by_name.get(name.as_str()) {
canonicalize(manifest_path)?
} else {
if tolerate {
return Ok(None);
}
return Err(WorkspaceError::BuiltinPortDependencyNotPrepared {
dep_name: dep.name.as_str().to_owned(),
parent: parent_name.to_owned(),
});
}
}
DependencySource::Version(_) => {
if ctx.registry.by_name.is_empty() {
return Ok(None);
}
if let Some(path) = ctx.registry.by_name.get(dep.name.as_str()) {
path.clone()
} else {
if !ctx.policy.requires_registry_for(parent_name) {
return Ok(None);
}
return Err(WorkspaceError::UnresolvedRegistryDependency {
dep_name: dep.name.as_str().to_owned(),
parent: parent_name.to_owned(),
});
}
}
DependencySource::Workspace => {
return Err(WorkspaceError::UnresolvedWorkspaceDependency {
dep_name: dep.name.as_str().to_owned(),
parent: parent_name.to_owned(),
kind: dep.kind,
});
}
};
Ok(Some(canonical))
}
fn verify_dep_path_names(dep_paths: &[DepPath]) -> Result<(), WorkspaceError> {
for DepPath {
name: dep_name,
path: dep_manifest_path,
..
} in dep_paths
{
let dep_parsed = parse_manifest(dep_manifest_path)?;
let actual = dep_parsed.package.as_ref().ok_or_else(|| {
WorkspaceError::LocalDependencyIsWorkspace {
dep_name: dep_name.clone(),
path: dep_manifest_path.clone(),
}
})?;
if actual.name.as_str() != dep_name {
return Err(WorkspaceError::DependencyNameMismatch {
dep_name: dep_name.clone(),
actual_name: actual.name.as_str().to_owned(),
path: dep_manifest_path.clone(),
});
}
}
Ok(())
}
fn reject_duplicate_package_names(packages: &[LoadedPackage]) -> Result<(), WorkspaceError> {
let mut seen: HashMap<&str, &PathBuf> = HashMap::new();
for pkg in packages {
let name = pkg.package.name.as_str();
if let Some(prev) = seen.insert(name, &pkg.manifest_path) {
return Err(WorkspaceError::DuplicatePackageName {
name: name.to_owned(),
first: prev.clone(),
second: pkg.manifest_path.clone(),
});
}
}
Ok(())
}
fn link_workspace_packages(
loader: &Loader,
registry: &RegistryLookup<'_>,
ports: &PortLookup,
) -> Vec<WorkspacePackage> {
let mut packages: Vec<WorkspacePackage> = Vec::with_capacity(loader.packages.len());
for pkg in &loader.packages {
let mut deps = Vec::with_capacity(pkg.dep_paths.len());
for dep in &pkg.dep_paths {
let idx = *loader
.manifest_index
.get(&dep.path)
.expect("dep manifest should have been loaded");
deps.push(DependencyEdge {
index: idx,
kind: dep.kind,
condition: dep.condition.clone(),
});
}
let kind = if registry.patch_canonical_paths.contains(&pkg.manifest_path) {
PackageKind::Local
} else if ports.canonical_paths.contains(&pkg.manifest_path) {
PackageKind::Local
} else if registry.canonical_paths.contains(&pkg.manifest_path) {
PackageKind::Registry
} else {
PackageKind::Local
};
let is_port = ports.canonical_paths.contains(&pkg.manifest_path);
packages.push(WorkspacePackage {
package: pkg.package.clone(),
manifest_path: pkg.manifest_path.clone(),
manifest_dir: pkg.manifest_dir.clone(),
deps,
kind,
is_port,
});
}
packages
}
fn apply_topo_order(
packages: &[WorkspacePackage],
) -> Result<(Vec<WorkspacePackage>, HashMap<usize, usize>), WorkspaceError> {
let topo = topo_sort(packages)?;
let new_position: HashMap<usize, usize> = topo
.iter()
.enumerate()
.map(|(new_idx, &old_idx)| (old_idx, new_idx))
.collect();
let mut sorted: Vec<WorkspacePackage> = topo
.iter()
.map(|&old_idx| packages[old_idx].clone())
.collect();
for pkg in &mut sorted {
for edge in &mut pkg.deps {
edge.index = new_position[&edge.index];
}
}
Ok((sorted, new_position))
}
fn resolve_default_members(
entries: &[String],
root_dir: &Path,
loader: &Loader,
new_position: &HashMap<usize, usize>,
primary_packages: &[usize],
) -> Result<Vec<usize>, WorkspaceError> {
let mut default_members: Vec<usize> = Vec::new();
let mut seen_default: HashSet<usize> = HashSet::new();
for entry in entries {
validate_workspace_pattern("workspace.default-members", entry)?;
let dir = root_dir.join(entry);
let canonical_dir =
canonicalize(&dir).map_err(|_| WorkspaceError::DefaultMemberNotInMembers {
member: entry.clone(),
})?;
let manifest = canonical_dir.join("cabin.toml");
let idx = loader
.manifest_index
.get(&manifest)
.copied()
.ok_or_else(|| WorkspaceError::DefaultMemberNotInMembers {
member: entry.clone(),
})?;
let new_idx = new_position[&idx];
if !primary_packages.contains(&new_idx) {
return Err(WorkspaceError::DefaultMemberNotInMembers {
member: entry.clone(),
});
}
if seen_default.insert(new_idx) {
default_members.push(new_idx);
}
}
Ok(default_members)
}
#[derive(Default)]
struct Loader {
packages: Vec<LoadedPackage>,
manifest_index: HashMap<PathBuf, usize>,
}
struct LoadedPackage {
package: cabin_core::Package,
manifest_path: PathBuf,
manifest_dir: PathBuf,
dep_paths: Vec<DepPath>,
}
#[derive(Debug, Clone)]
struct DepPath {
name: String,
path: PathBuf,
kind: cabin_core::DependencyKind,
condition: Option<cabin_core::Condition>,
}
fn project_alias_for(loader: &Loader, manifest_path: &Path) -> String {
for pkg in &loader.packages {
for dep in &pkg.dep_paths {
if dep.path == manifest_path {
return dep.name.clone();
}
}
}
manifest_path.display().to_string()
}
fn parse_manifest(path: &Path) -> Result<ParsedManifest, WorkspaceError> {
cabin_manifest::load_manifest(path).map_err(|source| WorkspaceError::Manifest {
path: path.to_path_buf(),
source: Box::new(source),
})
}
pub(super) fn canonicalize(path: &Path) -> Result<PathBuf, WorkspaceError> {
cabin_fs::canonicalize(path).map_err(|source| classify_manifest_io(path, source))
}
fn classify_manifest_io(path: &Path, source: std::io::Error) -> WorkspaceError {
match source.kind() {
std::io::ErrorKind::NotFound => WorkspaceError::ManifestNotFound {
path: path.to_path_buf(),
},
_ => WorkspaceError::ManifestUnreadable {
path: path.to_path_buf(),
source,
},
}
}