use std::collections::{BTreeSet, HashSet};
use std::path::{Path, PathBuf};
use cargo_metadata::{MetadataCommand, TargetKind};
use serde_json::Value;
use crate::error::{EachError, LoadMetadataError};
#[derive(Debug, Clone)]
pub(crate) struct Workspace {
pub(crate) members: Vec<Member>,
pub(crate) default_member_names: HashSet<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct Member {
pub(crate) name: String,
pub(crate) version: String,
pub(crate) manifest_path: PathBuf,
pub(crate) publishable: bool,
pub(crate) features: BTreeSet<String>,
pub(crate) targets: Vec<MemberTarget>,
pub(crate) dependencies: BTreeSet<String>,
pub(crate) metadata: Value,
}
#[derive(Debug, Clone)]
pub(crate) struct MemberTarget {
pub(crate) name: String,
pub(crate) kinds: BTreeSet<TargetKind>,
pub(crate) required_features: BTreeSet<String>,
}
impl Member {
#[must_use]
pub(crate) fn spec(&self) -> String {
format!("{}@{}", self.name, self.version)
}
#[must_use]
pub(crate) fn manifest_dir(&self) -> &Path {
self.manifest_path
.parent()
.expect("cargo-metadata always reports a manifest file path with a parent directory")
}
}
impl Workspace {
#[ohno::enrich_err("failed to load cargo workspace metadata")]
pub(crate) fn load(manifest_path: Option<&Path>) -> Result<Self, EachError> {
let mut cmd = MetadataCommand::new();
cmd.no_deps();
if let Some(path) = manifest_path {
cmd.manifest_path(path);
}
let metadata = cmd.exec().map_err(LoadMetadataError::caused_by)?;
let mut members: Vec<Member> = metadata
.workspace_packages()
.iter()
.map(|pkg| {
let dependencies = pkg.dependencies.iter().map(|d| d.name.clone()).collect();
let mut targets: Vec<MemberTarget> = pkg
.targets
.iter()
.map(|target| MemberTarget {
name: target.name.clone(),
kinds: target.kind.iter().cloned().collect(),
required_features: target.required_features.iter().cloned().collect(),
})
.collect();
targets.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.kinds.cmp(&b.kinds)));
Member {
name: pkg.name.to_string(),
version: pkg.version.to_string(),
manifest_path: pkg.manifest_path.clone().into_std_path_buf(),
publishable: pkg.publish.as_ref().is_none_or(|registries| !registries.is_empty()),
features: pkg.features.keys().cloned().collect(),
targets,
dependencies,
metadata: pkg.metadata.clone(),
}
})
.collect();
members.sort_by(|a, b| a.name.cmp(&b.name));
let default_member_names = metadata
.workspace_default_packages()
.iter()
.map(|pkg| pkg.name.to_string())
.collect();
Ok(Self {
members,
default_member_names,
})
}
}
#[must_use]
pub(crate) fn parse_target_kind(kind: &str) -> Option<TargetKind> {
match TargetKind::from(kind) {
TargetKind::Unknown(_) => None,
known => Some(known),
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn parses_every_supported_target_kind() {
for kind in [
"bench",
"bin",
"custom-build",
"cdylib",
"dylib",
"example",
"lib",
"proc-macro",
"rlib",
"staticlib",
"test",
] {
assert!(parse_target_kind(kind).is_some(), "{kind}");
}
}
#[test]
fn rejects_unknown_target_kind() {
assert_eq!(parse_target_kind("future-kind"), None);
}
}