use std::collections::BTreeMap;
use std::path::Path;
use cargo_metadata::{Metadata, MetadataCommand, Package};
use super::{METADATA_KEY, Overrides, Project, ProjectError};
use crate::config::Config;
pub struct Workspace {
metadata: Metadata,
}
impl Workspace {
pub fn load(manifest_path: &Path) -> Result<Self, ProjectError> {
let metadata = MetadataCommand::new()
.manifest_path(manifest_path)
.exec()
.map_err(|source| ProjectError::Metadata {
source: Box::new(source),
})?;
Ok(Self { metadata })
}
pub fn projects(&self, overrides: &Overrides) -> Result<Vec<Project>, ProjectError> {
let workspace_root = self.metadata.workspace_root.as_std_path();
let target_directory = self.metadata.target_directory.as_std_path();
let workspace_config = self.workspace_config()?;
let mut projects = Vec::new();
for package in self.selected_packages(overrides)? {
let bins = Self::selected_bins(package, overrides);
if bins.is_empty() {
continue;
}
let config = Self::package_config(package)?.inherit(&workspace_config);
for bin in bins {
projects.push(Project::from_package_bin(
package,
bin,
&config,
overrides,
workspace_root,
target_directory,
)?);
}
}
self.reject_unmatched_bins(overrides, &projects)?;
Self::reject_duplicate_names(&projects)?;
Ok(projects)
}
fn selected_packages(&self, overrides: &Overrides) -> Result<Vec<&Package>, ProjectError> {
let mut packages = if !overrides.packages.is_empty() {
let mut picked = Vec::new();
for name in &overrides.packages {
picked.push(self.package_named(name)?);
}
picked
} else {
let base = if overrides.workspace {
self.metadata.workspace_packages()
} else {
self.default_packages()
};
base.into_iter().filter(|p| Self::publishable(p)).collect()
};
if !overrides.exclude.is_empty() {
packages.retain(|package| {
!overrides
.exclude
.iter()
.any(|name| name == package.name.as_str())
});
}
Ok(packages)
}
fn default_packages(&self) -> Vec<&Package> {
if self.metadata.workspace_default_members.is_available() {
self.metadata.workspace_default_packages()
} else {
self.metadata.workspace_packages()
}
}
fn package_named(&self, name: &str) -> Result<&Package, ProjectError> {
self.metadata
.workspace_packages()
.into_iter()
.find(|package| package.name.as_str() == name)
.ok_or_else(|| ProjectError::PackageNotFound {
name: name.to_owned(),
})
}
fn publishable(package: &Package) -> bool {
!package
.publish
.as_ref()
.is_some_and(|registries| registries.is_empty())
}
fn selected_bins<'a>(package: &'a Package, overrides: &Overrides) -> Vec<&'a str> {
Self::bin_names(package)
.filter(|name| {
overrides.bins.is_empty() || overrides.bins.iter().any(|bin| bin == name)
})
.collect()
}
fn workspace_config(&self) -> Result<Config, ProjectError> {
match self.metadata.workspace_metadata.get(METADATA_KEY) {
Some(value) => Ok(Config::from_metadata(value)?),
None => Ok(Config::default()),
}
}
fn package_config(package: &Package) -> Result<Config, ProjectError> {
match package.metadata.get(METADATA_KEY) {
Some(value) => Ok(Config::from_metadata(value)?),
None => Ok(Config::default()),
}
}
fn reject_unmatched_bins(
&self,
overrides: &Overrides,
projects: &[Project],
) -> Result<(), ProjectError> {
for wanted in &overrides.bins {
if !projects.iter().any(|project| &project.bin == wanted) {
return Err(if overrides.packages.is_empty() {
ProjectError::BinNotInWorkspace {
bin: wanted.clone(),
}
} else {
ProjectError::UnknownBin {
package: overrides.packages.join(", "),
bin: wanted.clone(),
}
});
}
}
Ok(())
}
fn reject_duplicate_names(projects: &[Project]) -> Result<(), ProjectError> {
let mut by_name: BTreeMap<&str, &str> = BTreeMap::new();
for project in projects {
let package = project.package.as_deref().unwrap_or_default();
if let Some(other) = by_name.insert(&project.identity.name, package) {
return Err(ProjectError::AmbiguousBin {
bin: project.identity.name.clone(),
packages: vec![other.to_owned(), package.to_owned()],
});
}
}
Ok(())
}
fn bin_names(package: &Package) -> impl Iterator<Item = &str> {
package
.targets
.iter()
.filter(|target| target.is_bin())
.map(|target| target.name.as_str())
}
}