diff --git a/crates/cli/src/common.rs b/crates/cli/src/common.rs index fb819c3..9e5ae52 100644 --- a/crates/cli/src/common.rs +++ b/crates/cli/src/common.rs @@ -5,7 +5,7 @@ use module_parser::{ CargoToml, CargoTomlDependencies, CargoTomlDependency, Config, ConfigModuleMetadata, Package, get_dependencies, get_module_name_from_crate, }; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::fmt::{self, Display}; use std::fs; use std::path::{Path, PathBuf}; @@ -217,24 +217,24 @@ static CARGO_DEPS: LazyLock> = LazyLock::new(|| { fn create_required_deps(path: &Path) -> anyhow::Result { let mut deps = get_dependencies(path, &CARGO_DEPS)?; if let Some(modkit) = deps.get_mut("modkit") { - modkit.features = vec!["bootstrap".to_owned()]; + modkit.features.insert("bootstrap".to_owned()); } else { deps.insert( "modkit".to_owned(), CargoTomlDependency { package: Some("cf-modkit".to_owned()), - features: vec!["bootstrap".to_owned()], + features: BTreeSet::from(["bootstrap".to_owned()]), ..Default::default() }, ); } if let Some(tokio) = deps.get_mut("tokio") { - tokio.features = vec!["full".to_owned()]; + tokio.features.insert("full".to_owned()); } else { deps.insert( "tokio".to_owned(), CargoTomlDependency { - features: vec!["full".to_owned()], + features: BTreeSet::from(["full".to_owned()]), version: Some("1".to_owned()), ..Default::default() }, diff --git a/crates/cli/src/mod/add.rs b/crates/cli/src/mod/add.rs index aef1d6f..0f82ce0 100644 --- a/crates/cli/src/mod/add.rs +++ b/crates/cli/src/mod/add.rs @@ -188,11 +188,7 @@ fn merge_dependency_metadata(existing: &mut CargoTomlDependency, incoming: &Carg _ => {} } - for feat in &incoming.features { - if !existing.features.contains(feat) { - existing.features.push(feat.clone()); - } - } + existing.features.extend(incoming.features.iter().cloned()); if existing.path.is_none() && incoming.path.is_some() { existing.path.clone_from(&incoming.path); @@ -618,6 +614,7 @@ mod tests { should_replace_with_newer_semver, }; use module_parser::{CargoTomlDependencies, CargoTomlDependency}; + use std::collections::BTreeSet; #[test] fn replaces_workspace_dep_version_with_newer_semver() { @@ -634,7 +631,7 @@ mod tests { "reqwest".to_owned(), CargoTomlDependency { version: Some("0.13".to_owned()), - features: vec!["stream".to_owned()], + features: BTreeSet::from(["stream".to_owned()]), ..CargoTomlDependency::default() }, ); @@ -670,7 +667,7 @@ mod tests { CargoTomlDependency { version: Some("0.13".to_owned()), default_features: Some(false), - features: vec!["json".to_owned()], + features: BTreeSet::from(["json".to_owned()]), ..CargoTomlDependency::default() }, ); diff --git a/crates/module-parser/src/config.rs b/crates/module-parser/src/config.rs index 09945d3..d482fee 100644 --- a/crates/module-parser/src/config.rs +++ b/crates/module-parser/src/config.rs @@ -1,6 +1,6 @@ use anyhow::bail; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::fmt; #[derive(Deserialize)] @@ -25,7 +25,7 @@ impl Config { CargoTomlDependency { package: module.metadata.package, version: module.metadata.version, - features: module.metadata.features, + features: module.metadata.features.into_iter().collect(), default_features: module.metadata.default_features, path: module.metadata.path, }, @@ -112,8 +112,8 @@ pub struct CargoTomlDependency { deserialize_with = "opt_string_none_as_star::deserialize" )] pub version: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub features: Vec, + #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] + pub features: BTreeSet, #[serde(default, skip_serializing_if = "Option::is_none")] pub default_features: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/module-parser/src/metadata.rs b/crates/module-parser/src/metadata.rs index 72a63ef..d18f149 100644 --- a/crates/module-parser/src/metadata.rs +++ b/crates/module-parser/src/metadata.rs @@ -3,7 +3,7 @@ use super::source::{NotFoundError, resolve_rust_path}; use crate::{CargoTomlDependencies, CargoTomlDependency}; use anyhow::Context; use cargo_metadata::{DependencyKind, Package, PackageId, Target}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -86,13 +86,32 @@ pub fn get_dependencies( path: &Path, deps: &HashMap, ) -> anyhow::Result { - let meta = cargo_metadata::MetadataCommand::new() + let cargo_metadata::Metadata { + packages, resolve, .. + } = cargo_metadata::MetadataCommand::new() .current_dir(path) .exec() .context("failed to run cargo metadata")?; + let resolve_nodes = resolve.as_ref().map(|resolve| { + resolve + .nodes + .iter() + .map(|node| (node.id.clone(), node)) + .collect::>() + }); let mut res = CargoTomlDependencies::with_capacity(deps.len()); - for pkg in meta.packages { + for pkg in packages { if let Some(name) = deps.get(pkg.name.as_str()) { + let features = resolve_nodes + .as_ref() + .and_then(|nodes| nodes.get(&pkg.id)) + .map(|node| { + node.features + .iter() + .map(ToString::to_string) + .collect::>() + }) + .unwrap_or_default(); res.insert( name.clone(), CargoTomlDependency { @@ -102,6 +121,7 @@ pub fn get_dependencies( Some(pkg.name.to_string()) }, version: Some(pkg.version.to_string()), + features, ..Default::default() }, ); @@ -271,8 +291,11 @@ fn is_normal_dependency(dep: &cargo_metadata::NodeDep) -> bool { #[cfg(test)] mod tests { - use super::{list_library_mappings_from_metadata, resolve_source_from_metadata}; + use super::{ + get_dependencies, list_library_mappings_from_metadata, resolve_source_from_metadata, + }; use crate::test_utils::TempDirExt; + use std::collections::{BTreeSet, HashMap}; use tempfile::TempDir; #[test] @@ -505,4 +528,63 @@ mod tests { ] ); } + + #[test] + fn gets_enabled_features_for_located_packages() { + let temp_dir = TempDir::new().expect("failed to create temp dir"); + temp_dir.write( + "Cargo.toml", + r#" + [package] + name = "cf-app" + version = "0.1.0" + edition = "2024" + + [dependencies] + helper_alias = { package = "cf-helper", path = "cf-helper", default-features = false, features = ["grpc", "otel"] } + "#, + ); + temp_dir.write( + "src/lib.rs", + r#" + pub fn app() {} + "#, + ); + temp_dir.write( + "cf-helper/Cargo.toml", + r#" + [package] + name = "cf-helper" + version = "0.3.0" + edition = "2024" + + [features] + default = ["base"] + base = [] + grpc = [] + otel = [] + "#, + ); + temp_dir.write( + "cf-helper/src/lib.rs", + r#" + pub fn helper() {} + "#, + ); + + let dependency_aliases = + HashMap::from([("cf-helper".to_owned(), "helper_alias".to_owned())]); + let dependencies = get_dependencies(temp_dir.path(), &dependency_aliases) + .expect("metadata should load dependencies"); + let helper = dependencies + .get("helper_alias") + .expect("dependency should be present"); + + assert_eq!(helper.package.as_deref(), Some("cf-helper")); + assert_eq!(helper.version.as_deref(), Some("0.3.0")); + assert_eq!( + helper.features, + BTreeSet::from(["grpc".to_owned(), "otel".to_owned()]) + ); + } }