use std::{collections::BTreeSet as Set, path::PathBuf};
use anyhow::{Context, Result, bail};
use cargo_metadata::{DependencyKind, Node, NodeDep, Package, Target};
use crate::{
buck::{CargoTargetKind, RustRule},
buckal_note, buckal_warn,
buckify::actions::is_third_party,
context::BuckalContext,
platform::{Os, oses_from_platform, platform_is_target_only},
utils::{get_buck2_root, get_vendor_path_relative},
};
pub(super) fn dep_kind_matches(target_kind: CargoTargetKind, dep_kind: DependencyKind) -> bool {
match target_kind {
CargoTargetKind::CustomBuild => dep_kind == DependencyKind::Build,
CargoTargetKind::Test => {
dep_kind == DependencyKind::Development || dep_kind == DependencyKind::Normal
}
_ => dep_kind == DependencyKind::Normal,
}
}
fn get_lib_targets(package: &Package) -> Vec<&Target> {
package
.targets
.iter()
.filter(|t| {
t.kind.contains(&cargo_metadata::TargetKind::Lib)
|| t.kind.contains(&cargo_metadata::TargetKind::CDyLib)
|| t.kind.contains(&cargo_metadata::TargetKind::DyLib)
|| t.kind.contains(&cargo_metadata::TargetKind::RLib)
|| t.kind.contains(&cargo_metadata::TargetKind::StaticLib)
|| t.kind.contains(&cargo_metadata::TargetKind::ProcMacro)
})
.collect()
}
fn resolve_first_party_label(dep_package: &Package) -> Result<String> {
let buck2_root = get_buck2_root().context("failed to get buck2 root")?;
let manifest_path = PathBuf::from(&dep_package.manifest_path);
let manifest_dir = manifest_path
.parent()
.context("manifest_path should always have a parent directory")?;
let relative_path = manifest_dir
.strip_prefix(&buck2_root)
.with_context(|| {
format!(
"dependency manifest dir `{}` is not under Buck2 root `{}`",
manifest_dir.display(),
buck2_root
)
})?
.to_string_lossy()
.replace('\\', "/");
let dep_bin_targets: Vec<_> = dep_package
.targets
.iter()
.filter(|t| t.kind.contains(&cargo_metadata::TargetKind::Bin))
.collect();
let dep_lib_targets = get_lib_targets(dep_package);
if dep_lib_targets.len() != 1 {
bail!(
"Expected exactly one library target for dependency {}, but found {}",
dep_package.name,
dep_lib_targets.len()
);
}
let buckal_name = resolve_buckal_name(&dep_bin_targets, &dep_lib_targets);
Ok(format!("//{relative_path}:{buckal_name}"))
}
fn resolve_buckal_name(dep_bin_targets: &[&Target], dep_lib_targets: &[&Target]) -> String {
if dep_bin_targets
.iter()
.any(|b| b.name == dep_lib_targets[0].name)
{
format!("{}-lib", dep_lib_targets[0].name)
} else {
dep_lib_targets[0].name.to_owned()
}
}
fn resolve_dep_label(dep: &NodeDep, dep_package: &Package) -> Result<(String, Option<String>)> {
let dep_package_name = dep_package.name.to_string();
let is_renamed = dep.name != dep_package_name.replace("-", "_");
let alias = if is_renamed {
Some(dep.name.clone())
} else {
None
};
if !is_third_party(dep_package) {
let label = resolve_first_party_label(dep_package).with_context(|| {
format!(
"failed to resolve first-party label for `{}`",
dep_package.name
)
})?;
Ok((label, alias))
} else {
Ok((
format!(
"//{}:{}",
get_vendor_path_relative(&dep_package.id)?,
dep_package.name
),
alias,
))
}
}
fn insert_dep(
rust_rule: &mut dyn RustRule,
target: &str,
alias: Option<&str>,
platforms: Option<&Set<Os>>,
) -> Result<()> {
if let Some(platforms) = platforms {
for os in platforms {
let os_key = os.key().to_owned();
if let Some(alias) = alias {
let entries = rust_rule
.os_named_deps_mut()
.entry(alias.to_owned())
.or_default();
if let Some(existing) = entries.get(&os_key) {
if existing != target {
bail!(
"os_named_deps alias '{}' had conflicting targets for platform '{}': '{}' vs '{}'",
alias,
os_key,
existing,
target
);
}
} else {
entries.insert(os_key.clone(), target.to_owned());
}
} else {
rust_rule
.os_deps_mut()
.entry(os_key)
.or_default()
.insert(target.to_owned());
}
}
} else if let Some(alias) = alias {
let entry = rust_rule.named_deps_mut().entry(alias.to_owned());
match entry {
std::collections::btree_map::Entry::Vacant(v) => {
v.insert(target.to_owned());
}
std::collections::btree_map::Entry::Occupied(o) => {
if o.get() != target {
buckal_warn!(
"named_deps alias '{}' had conflicting targets: '{}' vs '{}'",
alias,
o.get(),
target
);
}
}
}
} else {
rust_rule.deps_mut().insert(target.to_owned());
}
Ok(())
}
pub(super) fn set_deps(
rust_rule: &mut dyn RustRule,
node: &Node,
kind: CargoTargetKind,
ctx: &BuckalContext,
) -> Result<()> {
for dep in &node.deps {
let Some(dep_package) = ctx.packages_map.get(&dep.pkg) else {
continue;
};
let mut unconditional = false;
let mut platforms = Set::<Os>::new();
let mut has_unsupported_platform = false;
for dk in dep
.dep_kinds
.iter()
.filter(|dk| dep_kind_matches(kind, dk.kind))
{
match &dk.target {
None => unconditional = true,
Some(platform) => {
let oses = oses_from_platform(platform);
if oses.is_empty() {
if platform_is_target_only(platform) {
has_unsupported_platform = true;
continue;
}
unconditional = true;
continue;
}
platforms.extend(oses);
}
}
}
if !unconditional && platforms.is_empty() {
if has_unsupported_platform {
buckal_note!(
"Dependency '{}' (package '{}') targets only unsupported platforms and will be omitted.",
dep.name,
dep_package.name
);
}
continue;
}
let (target_label, alias) = resolve_dep_label(dep, dep_package).with_context(|| {
format!(
"failed to resolve dependency label for '{}' (package '{}')",
dep.name, dep_package.name
)
})?;
if unconditional {
insert_dep(rust_rule, &target_label, alias.as_deref(), None)?;
} else {
insert_dep(rust_rule, &target_label, alias.as_deref(), Some(&platforms))?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use cargo_metadata::TargetKind;
fn mock_target(name: &str, kind: TargetKind) -> Target {
serde_json::from_value(serde_json::json!({
"name": name,
"kind": [kind],
"crate_types": [],
"required_features": [],
"src_path": "/tmp/dummy.rs",
"edition": "2021",
"doctest": true,
"test": true
}))
.unwrap()
}
#[test]
fn test_resolve_buckal_name_with_collision() {
let lib = mock_target("foo", TargetKind::Lib);
let bin = mock_target("foo", TargetKind::Bin);
let lib_targets = vec![&lib];
let bin_targets = vec![&bin];
let name = resolve_buckal_name(&bin_targets, &lib_targets);
assert_eq!(name, "foo-lib");
}
#[test]
fn test_resolve_buckal_name_without_collision() {
let lib = mock_target("foo", TargetKind::Lib);
let bin = mock_target("bar", TargetKind::Bin);
let lib_targets = vec![&lib];
let bin_targets = vec![&bin];
let name = resolve_buckal_name(&bin_targets, &lib_targets);
assert_eq!(name, "foo");
}
}