use std::sync::Arc;
use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::TargetKind;
use itertools::Itertools;
use serde::Serialize;
use tracing::{debug, debug_span, warn};
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize)]
pub struct Package {
pub name: String,
pub version: String,
pub relative_dir: Utf8PathBuf,
pub top_sources: Vec<Utf8PathBuf>,
}
pub fn packages_from_metadata(metadata: &cargo_metadata::Metadata) -> Vec<Arc<Package>> {
metadata
.workspace_packages()
.into_iter()
.sorted_by_key(|p| &p.name)
.filter_map(|p| Package::from_cargo_metadata(p, &metadata.workspace_root))
.map(Arc::new)
.collect()
}
impl Package {
pub fn from_cargo_metadata(
package_metadata: &cargo_metadata::Package,
workspace_root: &Utf8Path,
) -> Option<Self> {
let name = package_metadata.name.clone();
let _span = debug_span!("package", %name).entered();
let manifest_path = &package_metadata.manifest_path;
debug!(%manifest_path, "walk package");
let Some(relative_dir) = manifest_path
.strip_prefix(workspace_root)
.ok()
.and_then(|p| p.parent())
.map(ToOwned::to_owned)
else {
warn!(
"manifest path {manifest_path:?} for package {name:?} is not within \
the detected source root path {workspace_root:?} or has no parent"
);
return None;
};
Some(Package {
name: name.to_string(),
top_sources: package_top_sources(workspace_root, package_metadata),
version: package_metadata.version.to_string(),
relative_dir,
})
}
pub fn version_qualified_name(&self) -> String {
format!("{}@{}", self.name, self.version)
}
}
fn package_top_sources(
workspace_root: &Utf8Path,
package_metadata: &cargo_metadata::Package,
) -> Vec<Utf8PathBuf> {
let mut found = Vec::new();
let pkg_dir = package_metadata.manifest_path.parent().unwrap();
for target in &package_metadata.targets {
if should_mutate_target(target) {
if let Ok(relpath) = target
.src_path
.strip_prefix(workspace_root)
.map(ToOwned::to_owned)
{
debug!(
"found mutation target {relpath} of kind {kind:?}",
kind = target.kind
);
found.push(relpath);
} else {
warn!("{:?} is not in {:?}", target.src_path, pkg_dir);
}
} else {
debug!(
"skipping target {:?} of kinds {:?}",
target.name, target.kind
);
}
}
found.sort();
found.dedup();
found
}
fn should_mutate_target(target: &cargo_metadata::Target) -> bool {
target.kind.iter().any(|kind| {
matches!(
kind,
TargetKind::Bin
| TargetKind::ProcMacro
| TargetKind::CDyLib
| TargetKind::DyLib
| TargetKind::Lib
| TargetKind::RLib
| TargetKind::StaticLib
)
})
}
#[derive(Debug, Clone)]
#[allow(clippy::module_name_repetitions)]
pub enum PackageSelection {
All,
Explicit(Vec<Arc<Package>>),
}
impl PackageSelection {
#[cfg(test)]
pub fn one<P: Into<Utf8PathBuf>>(
name: &str,
version: &str,
relative_dir: P,
top_source: &str,
) -> Self {
Self::Explicit(vec![Arc::new(Package {
name: name.to_string(),
version: version.to_string(),
relative_dir: relative_dir.into(),
top_sources: vec![top_source.into()],
})])
}
}