use std::collections::{BTreeMap, BTreeSet, HashMap};
use camino::Utf8PathBuf;
use anyhow::{Result, bail};
use cabin_core::{ResolvedProfileFlags, SystemDependency, TargetPlatform, Verbosity};
use cabin_system_deps::{
PkgConfigError, PkgConfigTool, SystemDependencyFlags, SystemDependencyProbeRequest,
SystemDependencyResolution, probe_system_dependency,
};
use cabin_workspace::PackageGraph;
use crate::cli::term_verbosity::Reporter;
type SystemDependencyReports = BTreeMap<usize, Vec<SystemDependencyResolution>>;
type AugmentedBuildFlags = (
HashMap<usize, ResolvedProfileFlags>,
SystemDependencyReports,
);
pub(crate) fn augment_build_flags_with_system_deps(
graph: &PackageGraph,
host_platform: &TargetPlatform,
dev_for: &BTreeSet<String>,
mut build_flags: HashMap<usize, ResolvedProfileFlags>,
reporter: Reporter,
) -> Result<AugmentedBuildFlags> {
let active = collect_active_system_deps(graph, host_platform, dev_for);
if active.is_empty() {
return Ok((build_flags, BTreeMap::new()));
}
let tool = PkgConfigTool::from_env(|key| std::env::var_os(key));
let count = total_count(&active);
let noun = if count == 1 {
"dependency"
} else {
"dependencies"
};
reporter.aux_verbose(format_args!(
"cabin: probing {count} system {noun} via {}",
tool.executable().to_string_lossy(),
));
if let Err(err) = tool.check_available() {
return Err(err.into());
}
let mut reports: BTreeMap<usize, Vec<SystemDependencyResolution>> = BTreeMap::new();
for (pkg_idx, deps) in active {
let pkg_name = graph.packages[pkg_idx].package.name.as_str();
let entry = build_flags.entry(pkg_idx).or_default();
let mut pkg_reports: Vec<SystemDependencyResolution> = Vec::with_capacity(deps.len());
for dep in deps {
let resolved = probe_dep(&tool, pkg_name, dep, reporter)?;
merge_flags(entry, &resolved.flags);
pkg_reports.push(resolved);
}
reports.insert(pkg_idx, pkg_reports);
}
Ok((build_flags, reports))
}
pub(crate) fn has_active_system_deps(
graph: &PackageGraph,
host_platform: &TargetPlatform,
dev_for: &BTreeSet<String>,
selected: &BTreeSet<usize>,
) -> bool {
collect_active_system_deps(graph, host_platform, dev_for)
.iter()
.any(|(idx, _)| selected.contains(idx))
}
pub(crate) fn ensure_dialect_supports_system_deps(
graph: &PackageGraph,
host_platform: &TargetPlatform,
dev_for: &BTreeSet<String>,
dialect: cabin_build::Dialect,
selected: &BTreeSet<usize>,
) -> Result<()> {
if dialect == cabin_build::Dialect::Msvc
&& has_active_system_deps(graph, host_platform, dev_for, selected)
{
bail!(
"`system = true` dependencies are resolved with pkg-config, whose GNU-style \
flags the MSVC backend cannot consume; system dependencies are not supported \
with an MSVC toolchain (build with a GCC/Clang toolchain, or remove the \
system dependency)"
);
}
Ok(())
}
fn probe_dep(
tool: &PkgConfigTool,
pkg_name: &str,
dep: &SystemDependency,
reporter: Reporter,
) -> Result<SystemDependencyResolution> {
let verbosity = reporter.verbosity();
if verbosity == Verbosity::VeryVerbose {
reporter.aux_very_verbose(format_args!(
"cabin: probing `{}` for package `{}` (version = {:?})",
dep.name.as_str(),
pkg_name,
dep.version,
));
}
let request = SystemDependencyProbeRequest {
name: dep.name.as_str(),
version_requirement: &dep.version,
tool,
};
match probe_system_dependency(&request) {
Ok(resolved) => {
if verbosity.shows_verbose() {
let version_suffix = match resolved.version.as_deref() {
Some(v) => format!(" (version {v})"),
None => String::new(),
};
reporter.aux_verbose(format_args!(
"cabin: system dependency `{}` ok{}",
resolved.name, version_suffix,
));
}
Ok(resolved)
}
Err(err) => bail!(format_probe_error(pkg_name, dep, err)),
}
}
fn format_probe_error(
pkg_name: &str,
dep: &SystemDependency,
err: PkgConfigError,
) -> anyhow::Error {
let message = format!(
"package `{}` failed to probe system dependency `{}`: {}",
pkg_name,
dep.name.as_str(),
err,
);
anyhow::Error::new(err).context(message)
}
fn merge_flags(flags: &mut ResolvedProfileFlags, contrib: &SystemDependencyFlags) {
let mut seen: BTreeSet<Utf8PathBuf> = flags
.include_dirs
.iter()
.chain(flags.system_include_dirs.iter())
.cloned()
.collect();
for dir in &contrib.include_dirs {
if seen.insert(dir.clone()) {
flags.include_dirs.push(dir.clone());
}
}
for dir in &contrib.system_include_dirs {
if seen.insert(dir.clone()) {
flags.system_include_dirs.push(dir.clone());
}
}
flags
.extra_compile_args
.extend(contrib.extra_compile_args.iter().cloned());
flags.ldflags.extend(contrib.ldflags.iter().cloned());
}
fn collect_active_system_deps<'a>(
graph: &'a PackageGraph,
host_platform: &TargetPlatform,
dev_for: &BTreeSet<String>,
) -> Vec<(usize, Vec<&'a SystemDependency>)> {
use cabin_core::DependencyKind;
let mut out: Vec<(usize, Vec<&'a SystemDependency>)> = Vec::new();
for &idx in &graph.primary_packages {
let package = &graph.packages[idx].package;
if package.system_dependencies.is_empty() {
continue;
}
let pkg_name = package.name.as_str();
let mut deps: Vec<&SystemDependency> = Vec::new();
for dep in &package.system_dependencies {
match dep.kind {
DependencyKind::Normal => {}
DependencyKind::Dev if dev_for.contains(pkg_name) => {}
DependencyKind::Dev => continue,
}
if let Some(cond) = &dep.condition
&& !cond.evaluate(&cabin_core::ConditionContext::platform_only(host_platform))
{
continue;
}
deps.push(dep);
}
if !deps.is_empty() {
deps.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
out.push((idx, deps));
}
}
out.sort_by_key(|(idx, _)| *idx);
out
}
fn total_count(active: &[(usize, Vec<&SystemDependency>)]) -> usize {
active.iter().map(|(_, v)| v.len()).sum()
}
#[cfg(test)]
mod tests {
use super::*;
use cabin_core::ResolvedProfileFlags;
fn flags() -> ResolvedProfileFlags {
ResolvedProfileFlags::default()
}
#[test]
fn merge_appends_include_paths_uniquely() {
let mut f = flags();
f.include_dirs.push(Utf8PathBuf::from("/already"));
let contrib = SystemDependencyFlags {
include_dirs: vec![Utf8PathBuf::from("/already"), Utf8PathBuf::from("/added")],
..Default::default()
};
merge_flags(&mut f, &contrib);
assert_eq!(
f.include_dirs,
vec![Utf8PathBuf::from("/already"), Utf8PathBuf::from("/added")],
);
}
#[test]
fn merge_routes_system_include_paths_to_system_bucket() {
let mut f = flags();
let contrib = SystemDependencyFlags {
system_include_dirs: vec![Utf8PathBuf::from("/opt/zlib/include")],
..Default::default()
};
merge_flags(&mut f, &contrib);
assert_eq!(
f.system_include_dirs,
vec![Utf8PathBuf::from("/opt/zlib/include")],
);
assert!(f.include_dirs.is_empty());
}
#[test]
fn merge_dedups_include_paths_across_buckets() {
let mut f = flags();
f.include_dirs.push(Utf8PathBuf::from("/already"));
let contrib = SystemDependencyFlags {
system_include_dirs: vec![Utf8PathBuf::from("/already"), Utf8PathBuf::from("/added")],
..Default::default()
};
merge_flags(&mut f, &contrib);
assert_eq!(f.include_dirs, vec![Utf8PathBuf::from("/already")]);
assert_eq!(f.system_include_dirs, vec![Utf8PathBuf::from("/added")]);
}
#[test]
fn merge_preserves_link_args_order() {
let mut f = flags();
f.ldflags.push("-lpriv".into());
let contrib = SystemDependencyFlags {
ldflags: vec![
"-L/lib".into(),
"-lssl".into(),
"-lcrypto".into(),
"-lssl".into(),
],
..Default::default()
};
merge_flags(&mut f, &contrib);
assert_eq!(
f.ldflags,
vec![
"-lpriv".to_owned(),
"-L/lib".to_owned(),
"-lssl".to_owned(),
"-lcrypto".to_owned(),
"-lssl".to_owned(),
],
);
}
#[test]
fn merge_appends_compile_args_in_order() {
let mut f = flags();
let contrib = SystemDependencyFlags {
extra_compile_args: vec!["-pthread".into(), "-fPIC".into()],
..Default::default()
};
merge_flags(&mut f, &contrib);
assert_eq!(
f.extra_compile_args,
vec!["-pthread".to_owned(), "-fPIC".to_owned()],
);
}
fn graph_with_system_deps(deps: Vec<SystemDependency>) -> PackageGraph {
use cabin_workspace::{PackageKind, WorkspacePackage};
use std::path::PathBuf;
let package = cabin_core::Package::with_config(cabin_core::PackageConfigInput {
name: cabin_core::PackageName::new("root").unwrap(),
version: semver::Version::parse("0.1.0").unwrap(),
targets: Vec::new(),
dependencies: Vec::new(),
system_dependencies: deps,
features: cabin_core::Features::default(),
})
.unwrap();
PackageGraph {
root_manifest_path: PathBuf::from("/tmp/cabin.toml"),
root_dir: PathBuf::from("/tmp"),
is_workspace_root: false,
root_package: Some(0),
root_settings: Default::default(),
primary_packages: vec![0],
default_members: vec![0],
excluded_members: Vec::new(),
packages: vec![WorkspacePackage {
package,
manifest_dir: PathBuf::from("/tmp"),
manifest_path: PathBuf::from("/tmp/cabin.toml"),
kind: PackageKind::Local,
deps: Vec::new(),
is_port: false,
}],
}
}
fn zlib_dep() -> SystemDependency {
SystemDependency {
name: cabin_core::PackageName::new("zlib").unwrap(),
version: String::new(),
kind: cabin_core::DependencyKind::Normal,
condition: None,
}
}
#[test]
fn msvc_with_selected_active_system_dep_is_rejected_but_gnu_is_not() {
let graph = graph_with_system_deps(vec![zlib_dep()]);
let host = TargetPlatform::current();
let dev_for = BTreeSet::new();
let selected = BTreeSet::from([0usize]);
let err = ensure_dialect_supports_system_deps(
&graph,
&host,
&dev_for,
cabin_build::Dialect::Msvc,
&selected,
)
.unwrap_err();
assert!(
err.to_string().contains("MSVC"),
"expected an MSVC-specific rejection, got: {err}"
);
ensure_dialect_supports_system_deps(
&graph,
&host,
&dev_for,
cabin_build::Dialect::GnuLike,
&selected,
)
.unwrap();
}
#[test]
fn msvc_without_system_deps_is_accepted() {
let graph = graph_with_system_deps(vec![]);
let host = TargetPlatform::current();
let dev_for = BTreeSet::new();
let selected = BTreeSet::from([0usize]);
ensure_dialect_supports_system_deps(
&graph,
&host,
&dev_for,
cabin_build::Dialect::Msvc,
&selected,
)
.unwrap();
}
#[test]
fn msvc_with_unselected_system_dep_is_accepted() {
let graph = graph_with_system_deps(vec![zlib_dep()]);
let host = TargetPlatform::current();
let dev_for = BTreeSet::new();
let selected = BTreeSet::new(); ensure_dialect_supports_system_deps(
&graph,
&host,
&dev_for,
cabin_build::Dialect::Msvc,
&selected,
)
.unwrap();
}
}