use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf};
use guppy::PackageId;
use guppy::graph::{BuildTargetId, PackageGraph};
use crate::diagnostic::DiagnosticSink;
#[derive(Debug, Clone, Default, clap::Args)]
pub(super) struct PackageSelection {
#[arg(short = 'p', long = "package")]
pub packages: Vec<String>,
#[arg(long = "exclude")]
pub exclude: Vec<String>,
}
pub(super) enum ResolvedInput {
SingleCrate(PathBuf),
Directory(PathBuf),
}
impl ResolvedInput {
pub(super) fn dir(&self) -> &PathBuf {
match self {
ResolvedInput::SingleCrate(p) | ResolvedInput::Directory(p) => p,
}
}
}
pub(super) fn resolve_input(input: &Path) -> anyhow::Result<ResolvedInput> {
if input.file_name() == Some("Cargo.toml".as_ref()) {
anyhow::ensure!(
input.try_exists()?,
"Cargo.toml not found at {}",
input.display()
);
let parent = input.parent().unwrap();
if parent.as_os_str().is_empty() {
Ok(ResolvedInput::SingleCrate(PathBuf::from(".")))
} else {
Ok(ResolvedInput::SingleCrate(parent.to_path_buf()))
}
} else if input.is_dir() {
Ok(ResolvedInput::Directory(input.to_path_buf()))
} else {
anyhow::bail!(
"input must be a directory or a path to a Cargo.toml file, got: {}",
input.display()
);
}
}
pub(super) fn select_packages(
resolved_input: Option<&ResolvedInput>,
selection: &PackageSelection,
workspace: &guppy::graph::Workspace<'_>,
) -> anyhow::Result<Vec<(PackageId, String)>> {
let mut selected: BTreeMap<PackageId, String> = BTreeMap::new();
match resolved_input {
Some(ResolvedInput::SingleCrate(dir)) => {
let dir = dir.canonicalize()?;
let dir = camino::Utf8PathBuf::try_from(dir)?;
let relative = pathdiff::diff_utf8_paths(&dir, workspace.root())
.expect("Failed to compute relative path to target crate");
let pkg = workspace.member_by_path(&relative).map_err(|e| {
anyhow::anyhow!("Could not find workspace member for {relative}: {e}")
})?;
selected.insert(pkg.id().clone(), pkg.name().to_string());
}
Some(ResolvedInput::Directory(dir)) => {
let dir = dir.canonicalize()?;
let dir = camino::Utf8PathBuf::try_from(dir)?;
let relative_dir = pathdiff::diff_utf8_paths(&dir, workspace.root())
.expect("Failed to compute relative path to directory");
for (path, pkg) in workspace.iter_by_path() {
if path.starts_with(&relative_dir) {
selected.insert(pkg.id().clone(), pkg.name().to_string());
}
}
}
None if selection.packages.is_empty() => {
for pkg in workspace.iter() {
selected.insert(pkg.id().clone(), pkg.name().to_string());
}
}
None => {
}
}
for name in &selection.packages {
let pkg = workspace
.member_by_name(name)
.map_err(|e| anyhow::anyhow!("unknown package `{name}`: {e}"))?;
selected.insert(pkg.id().clone(), pkg.name().to_string());
}
for name in &selection.exclude {
let pkg = workspace
.member_by_name(name)
.map_err(|e| anyhow::anyhow!("unknown package in --exclude `{name}`: {e}"))?;
selected.remove(pkg.id());
}
anyhow::ensure!(
!selected.is_empty(),
"No packages selected (after applying --package and --exclude filters)"
);
Ok(selected.into_iter().collect())
}
fn has_library_target(graph: &PackageGraph, package_id: &PackageId) -> bool {
graph
.metadata(package_id)
.map(|meta| {
meta.build_targets()
.any(|t| matches!(t.id(), BuildTargetId::Library))
})
.unwrap_or(false)
}
pub(super) fn filter_library_targets(
packages: Vec<(PackageId, String)>,
graph: &PackageGraph,
explicit_names: &HashSet<String>,
diagnostics: &mut DiagnosticSink,
) -> Vec<(PackageId, String)> {
packages
.into_iter()
.filter(|(id, name)| {
if has_library_target(graph, id) {
return true;
}
let msg = format!(
"package `{name}` has no library target; cheadergen can only generate \
headers from library crates"
);
if explicit_names.contains(name) {
diagnostics.error(msg).emit();
} else {
diagnostics.warning(format!("{msg}; skipping")).emit();
}
false
})
.collect()
}