Skip to content

Commit 99220c7

Browse files
committed
cargo-rail: updating the 'unify' process and transitive nightmare; cleaning up the split/sync commands and rail.toml configs.
1 parent 9b98402 commit 99220c7

File tree

19 files changed

+1175
-620
lines changed

19 files changed

+1175
-620
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ components = ["clippy", "rustfmt"]
179179

180180
# Dependency unification
181181
[unify]
182-
pin_transitives = false
182+
consolidate_transitive_features = false # Consolidate transitive deps
183+
transitive_feature_host = "auto" # Smart auto-selection
183184
validate_targets = []
184185

185186
# Split/sync (optional)

src/cargo/manifest.rs

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -287,21 +287,48 @@ impl CargoTransform {
287287
doc["workspace"] = table();
288288
}
289289

290-
// Ensure [workspace.dependencies] section exists
291-
let workspace = doc["workspace"]
292-
.as_table_mut()
293-
.ok_or_else(|| RailError::message("[workspace] is not a table"))?;
290+
// Write each unified dependency
291+
// Group by target (None for regular deps, Some(target) for platform-specific)
292+
for unified in unified_deps {
293+
// Determine which table to write to based on target
294+
let deps_table = if let Some(ref target) = unified.target {
295+
// Platform-specific dependency: write to [target.'<target>'.dependencies]
296+
let target_key = format!("target.'{}'", target);
297+
298+
// Ensure target section exists in workspace
299+
let workspace = doc["workspace"]
300+
.as_table_mut()
301+
.ok_or_else(|| RailError::message("[workspace] is not a table"))?;
302+
303+
if !workspace.contains_key(&target_key) {
304+
workspace[&target_key] = table();
305+
}
294306

295-
if !workspace.contains_key("dependencies") {
296-
workspace["dependencies"] = table();
297-
}
307+
let target_section = workspace[&target_key]
308+
.as_table_mut()
309+
.ok_or_else(|| RailError::message(format!("[workspace.{}] is not a table", target_key)))?;
298310

299-
let workspace_deps = workspace["dependencies"]
300-
.as_table_mut()
301-
.ok_or_else(|| RailError::message("[workspace.dependencies] is not a table"))?;
311+
if !target_section.contains_key("dependencies") {
312+
target_section["dependencies"] = table();
313+
}
302314

303-
// Write each unified dependency
304-
for unified in unified_deps {
315+
target_section["dependencies"]
316+
.as_table_mut()
317+
.ok_or_else(|| RailError::message(format!("[workspace.{}.dependencies] is not a table", target_key)))?
318+
} else {
319+
// Regular dependency: write to [workspace.dependencies]
320+
let workspace = doc["workspace"]
321+
.as_table_mut()
322+
.ok_or_else(|| RailError::message("[workspace] is not a table"))?;
323+
324+
if !workspace.contains_key("dependencies") {
325+
workspace["dependencies"] = table();
326+
}
327+
328+
workspace["dependencies"]
329+
.as_table_mut()
330+
.ok_or_else(|| RailError::message("[workspace.dependencies] is not a table"))?
331+
};
305332
// Use inline table for simple deps and deps with few features
306333
// Use regular table format only for deps with many features (>10) to avoid long lines
307334
let use_inline_table = unified.features.len() <= 10;
@@ -335,12 +362,12 @@ impl CargoTransform {
335362
}
336363

337364
// Insert the dependency
338-
workspace_deps.insert(&unified.name, toml_edit::Item::Value(dep_table.into()));
365+
deps_table.insert(&unified.name, toml_edit::Item::Value(dep_table.into()));
339366

340367
// Add comments if enabled and any exist
341368
if add_comments
342369
&& !unified.comments.is_empty()
343-
&& let Some(item) = workspace_deps.get_mut(&unified.name)
370+
&& let Some(item) = deps_table.get_mut(&unified.name)
344371
{
345372
let comment_str = unified.comments.join(", ");
346373
item
@@ -373,7 +400,7 @@ impl CargoTransform {
373400
dep_table["features"] = toml_edit::value(Value::Array(features_array));
374401

375402
// Insert the dependency
376-
workspace_deps.insert(&unified.name, dep_table);
403+
deps_table.insert(&unified.name, dep_table);
377404

378405
// Add comments if enabled and any exist
379406
// Note: Comments for regular tables are more complex - for now we skip them
@@ -671,12 +698,15 @@ members = ["crate-a", "crate-b"]
671698
name: "serde".to_string(),
672699
version_req: VersionReq::parse("1.0").unwrap(),
673700
features: vec!["derive".to_string()],
701+
feature_provenance: HashMap::new(),
674702
default_features: true,
675703
used_by: vec!["crate-a".to_string(), "crate-b".to_string()],
676704
dep_kinds: HashSet::new(),
677705
fragmentation_count: 2,
678706
path: None,
707+
target: None,
679708
comments: Vec::new(),
709+
is_proc_macro: false,
680710
}];
681711

682712
// Write workspace dependencies
@@ -728,12 +758,15 @@ members = ["crate-a"]
728758
name: "anyhow".to_string(),
729759
version_req: VersionReq::parse("1.0").unwrap(),
730760
features: vec![], // No features
761+
feature_provenance: HashMap::new(),
731762
default_features: true,
732763
used_by: vec!["crate-a".to_string()],
733764
dep_kinds: HashSet::new(),
734765
fragmentation_count: 1,
735766
path: None,
767+
target: None,
736768
comments: Vec::new(),
769+
is_proc_macro: false,
737770
}];
738771

739772
transformer
@@ -766,12 +799,15 @@ members = ["crate-a"]
766799
name: "tokio".to_string(),
767800
version_req: VersionReq::parse("1.0").unwrap(),
768801
features: vec!["fs".to_string(), "net".to_string()],
802+
feature_provenance: HashMap::new(),
769803
default_features: false, // Explicitly disabled
770804
used_by: vec!["crate-a".to_string()],
771805
dep_kinds: HashSet::new(),
772806
fragmentation_count: 1,
773807
path: None,
808+
target: None,
774809
comments: Vec::new(),
810+
is_proc_macro: false,
775811
}];
776812

777813
transformer

src/cargo/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ pub mod validate;
2424

2525
pub use manifest::{CargoTransform, TransformContext};
2626
pub use metadata::WorkspaceMetadata;
27-
pub use unify::{IssueSeverity, UnifiedDep, UnifyConfig, UnifyReport, UnifyStrategy, WorkspaceUnifier};
27+
pub use unify::{IssueSeverity, UnifiedDep, UnifyConfig, UnifyReport, WorkspaceUnifier};
2828
pub use validate::validate_targets;

src/cargo/unify/collector.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Dependency collection from workspace members
22
33
use super::path_handling::is_workspace_member_path;
4-
use super::types::DependencyInstance;
4+
use super::types::{DependencyInstance, FeatureSource};
55
use crate::cargo::WorkspaceMetadata;
66
use std::collections::{HashMap, HashSet};
77

@@ -39,17 +39,25 @@ pub fn collect_dependencies(metadata: &WorkspaceMetadata) -> Vec<DependencyInsta
3939
dep.features.clone()
4040
};
4141

42+
// Track feature provenance: WHY is each feature enabled?
43+
let feature_provenance = determine_feature_provenance(&features, dep, &pkg.name, metadata);
44+
45+
// Detect if this is a proc-macro crate
46+
// Proc-macros are build-time only and have different optimization strategies
47+
let is_proc_macro = metadata.is_proc_macro_crate(&dep.name);
48+
4249
instances.push(DependencyInstance {
4350
member: pkg.name.to_string(),
4451
name: dep.name.clone(),
4552
version_req: dep.req.clone(),
4653
features,
54+
feature_provenance,
4755
default_features: dep.uses_default_features,
48-
optional: dep.optional,
4956
kind: dep.kind,
5057
target: dep.target.as_ref().map(|t| t.to_string()),
5158
rename: dep.rename.clone(),
5259
path: dep.path.clone(),
60+
is_proc_macro,
5361
});
5462
}
5563
}
@@ -111,3 +119,68 @@ fn get_member_workspace_deps(manifest_path: &cargo_metadata::camino::Utf8Path) -
111119

112120
workspace_deps
113121
}
122+
123+
/// Determine WHY each feature is enabled for a dependency
124+
///
125+
/// This is the key to showing users Cargo.toml-level information about their dependencies.
126+
/// We check multiple sources in order of specificity:
127+
/// 1. Direct declaration in member's Cargo.toml
128+
/// 2. Default features
129+
/// 3. Target-specific
130+
/// 4. Transitive or --all-features
131+
fn determine_feature_provenance(
132+
resolved_features: &[String],
133+
dep: &cargo_metadata::Dependency,
134+
member_name: &str,
135+
metadata: &WorkspaceMetadata,
136+
) -> HashMap<String, FeatureSource> {
137+
let mut provenance = HashMap::new();
138+
139+
// Get the actual package metadata for this dependency
140+
let dep_pkg = metadata.get_package(&dep.name);
141+
142+
for feature in resolved_features {
143+
let source = determine_single_feature_source(feature, dep, member_name, dep_pkg);
144+
provenance.insert(feature.clone(), source);
145+
}
146+
147+
provenance
148+
}
149+
150+
/// Determine the source of a single feature
151+
fn determine_single_feature_source(
152+
feature: &str,
153+
dep: &cargo_metadata::Dependency,
154+
member_name: &str,
155+
dep_pkg: Option<&cargo_metadata::Package>,
156+
) -> FeatureSource {
157+
// 1. Check if declared directly in this member's Cargo.toml
158+
if dep.features.contains(&feature.to_string()) {
159+
return FeatureSource::Direct {
160+
member: member_name.to_string(),
161+
};
162+
}
163+
164+
// 2. Check if it's a default feature (and default features are enabled)
165+
if dep.uses_default_features
166+
&& let Some(pkg) = dep_pkg
167+
&& let Some(default_features) = pkg.features.get("default")
168+
{
169+
// Default features can reference other features
170+
if default_features.iter().any(|f| f.trim_start_matches("dep:") == feature) {
171+
return FeatureSource::Default;
172+
}
173+
}
174+
175+
// 3. Check if target-specific
176+
if let Some(ref target) = dep.target {
177+
return FeatureSource::TargetSpecific {
178+
target: target.to_string(),
179+
};
180+
}
181+
182+
// 4. Otherwise it's either transitive or from --all-features
183+
// For now, we classify it as AllFeatures since we're using --all-features metadata
184+
// In a future enhancement, we could trace the actual dependency chain
185+
FeatureSource::AllFeatures
186+
}

src/cargo/unify/config.rs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
//! Configuration for workspace dependency unification
22
3+
use crate::config::RailConfig;
34
use std::collections::HashSet;
45

56
/// Configuration for workspace unification
67
#[derive(Debug, Clone)]
78
pub struct UnifyConfig {
8-
/// Strategy for selecting dependencies to unify
9-
///
10-
/// Currently only UnifyStrategy::All is supported. This field is kept for
11-
/// future extensibility (e.g., threshold-based unification, fragmentation-only).
12-
#[allow(dead_code)]
13-
pub strategy: UnifyStrategy,
14-
159
/// Allow renamed dependencies to be unified (default: false)
1610
pub allow_renamed: bool,
1711

@@ -31,17 +25,9 @@ pub struct UnifyConfig {
3125
pub generate_report: bool,
3226
}
3327

34-
/// Strategy for selecting which dependencies to unify
35-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36-
pub enum UnifyStrategy {
37-
/// Unify all possible dependencies
38-
All,
39-
}
40-
4128
impl Default for UnifyConfig {
4229
fn default() -> Self {
4330
Self {
44-
strategy: UnifyStrategy::All,
4531
allow_renamed: false,
4632
exclude: HashSet::new(),
4733
include: HashSet::new(),
@@ -51,3 +37,16 @@ impl Default for UnifyConfig {
5137
}
5238
}
5339
}
40+
41+
impl From<&RailConfig> for UnifyConfig {
42+
fn from(rail_config: &RailConfig) -> Self {
43+
Self {
44+
allow_renamed: rail_config.unify.allow_renamed,
45+
exclude: rail_config.unify.exclude.iter().cloned().collect(),
46+
include: rail_config.unify.include.iter().cloned().collect(),
47+
auto_resolve_version_conflicts: rail_config.unify.conflicts.auto_resolve,
48+
add_conflict_comments: rail_config.unify.conflicts.add_markers,
49+
generate_report: rail_config.unify.output.generate_report,
50+
}
51+
}
52+
}

src/cargo/unify/issue_detection.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::path_handling::are_all_identical_workspace_paths;
44
use super::types::{DependencyInstance, IssueSeverity, IssueType, UnificationIssue};
55
use crate::cargo::WorkspaceMetadata;
66
use crate::error::RailResult;
7-
use std::collections::HashMap;
7+
use std::collections::{HashMap, HashSet};
88

99
/// Detect dependencies that exist in multiple resolved versions
1010
///
@@ -138,14 +138,34 @@ pub fn detect_issues(
138138
// Check if all are target-specific
139139
if instances.iter().all(|i| i.target.is_some()) {
140140
let targets: Vec<_> = instances.iter().filter_map(|i| i.target.clone()).collect();
141+
142+
// Check if all instances use the SAME target
143+
let unique_targets: HashSet<_> = targets.iter().collect();
144+
145+
if unique_targets.len() == 1 {
146+
// All instances use the same target - we CAN unify this!
147+
// This is NOT an issue, it's a unification opportunity
148+
// Don't return an issue here - let the normal unification flow handle it
149+
// The unified dep can use [target.'cfg(...)'.dependencies]
150+
return None;
151+
}
152+
153+
// Multiple different targets - this is more complex
141154
return Some(UnificationIssue {
142155
dep_name: dep_name.to_string(),
143156
issue_type: IssueType::AllTargetSpecific {
144157
targets: targets.clone(),
145158
},
146-
severity: IssueSeverity::Info, // Just informational, not a blocker
159+
severity: IssueSeverity::Info, // Just informational
147160
affected_members: instances.iter().map(|i| i.member.clone()).collect(),
148-
suggestion: "Target-specific dependencies cannot be unified at workspace level".to_string(),
161+
suggestion: format!(
162+
"Multiple platform-specific targets detected: {}. Consider per-platform unification.",
163+
unique_targets
164+
.iter()
165+
.map(|t| format!("'{}'", t))
166+
.collect::<Vec<_>>()
167+
.join(", ")
168+
),
149169
});
150170
}
151171

@@ -194,12 +214,13 @@ mod tests {
194214
name: "test-dep".to_string(),
195215
version_req: VersionReq::parse(version).unwrap(),
196216
features: vec![],
217+
feature_provenance: std::collections::HashMap::new(),
197218
default_features: true,
198-
optional: false,
199219
kind: DependencyKind::Normal,
200220
target: None,
201221
rename: None,
202222
path: None,
223+
is_proc_macro: false,
203224
}
204225
}
205226

src/cargo/unify/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ mod unifier;
5050
mod version_merge;
5151

5252
// Re-export public types
53-
pub use config::{UnifyConfig, UnifyStrategy};
53+
pub use config::UnifyConfig;
5454
pub use report::UnifyReport;
5555
pub use types::{IssueSeverity, MemberEdit, UnificationPlan, UnificationStats, UnifiedDep};
5656

0 commit comments

Comments
 (0)