Skip to content

Commit 8197275

Browse files
committed
cargo-rail: fixing the issue where pinning transitive deps pulls in 'all-feautres' and breaks the graph.
1 parent 97f07d6 commit 8197275

File tree

3 files changed

+26
-6
lines changed

3 files changed

+26
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ rules.md
2626
ARCHITECTURE.md
2727
style.md
2828
demos/
29+
setup
2930

3031
# Cargo-Rail (Testing)
3132
*rail.toml

src/cargo/manifest_ops/entry_builder.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,23 +126,30 @@ pub fn build_transitive_entry(features: &[String]) -> Item {
126126
/// Build a versioned dependency entry for [workspace.dependencies]
127127
///
128128
/// Used when adding transitive dependencies to workspace.dependencies.
129-
/// Creates entries like: `dep = { version = "1.0", features = ["foo"] }`
129+
/// Creates entries like: `dep = { version = "1.0", default-features = false, features = ["foo"] }`
130+
///
131+
/// IMPORTANT: When pinning transitives with explicit features, we MUST set
132+
/// `default-features = false` to prevent cargo from enabling default features
133+
/// that might pull in new dependencies not present in the current Cargo.lock.
130134
///
131135
/// # Arguments
132136
///
133137
/// * `version` - The semver version to use
134-
/// * `features` - Features to enable
138+
/// * `features` - Features to enable (intersection across all targets)
135139
pub fn build_versioned_dep_entry(version: &semver::Version, features: &[String]) -> Item {
136-
// Simple case: just version, no features
140+
// Simple case: just version, no features - let cargo use defaults
141+
// This is safe because we're not changing the feature set
137142
if features.is_empty() {
138143
let mut value = Value::from(format!("^{}", version));
139144
value.decor_mut().set_suffix(" #unified");
140145
return Item::Value(value);
141146
}
142147

143-
// Complex case: version + features
148+
// Complex case: version + explicit features
149+
// MUST disable default-features to avoid pulling in new deps
144150
let mut table = InlineTable::new();
145151
table.insert("version", Value::from(format!("^{}", version)));
152+
table.insert("default-features", Value::from(false));
146153
table.insert("features", build_feature_array(features));
147154

148155
// Add #unified comment marker

src/cargo/multi_target_metadata.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,19 @@ impl MultiTargetMetadata {
316316

317317
if unique_sets.len() > 1 {
318318
// This dep has different features across builds = fragmented
319-
let all_features: HashSet<String> = features.values().flat_map(|s| s.iter().cloned()).collect();
319+
//
320+
// IMPORTANT: Use INTERSECTION of features, not union!
321+
// Using union can enable features that pull in new transitive deps
322+
// that aren't in the current Cargo.lock, breaking resolution.
323+
// The intersection approach is safe - it only pins features that
324+
// are already enabled everywhere, avoiding new dep introduction.
325+
let common_features: HashSet<String> = features
326+
.values()
327+
.fold(None, |acc: Option<HashSet<String>>, set| match acc {
328+
None => Some(set.clone()),
329+
Some(existing) => Some(existing.intersection(set).cloned().collect()),
330+
})
331+
.unwrap_or_default();
320332

321333
// Get the resolved version (use highest across all targets)
322334
let versions = self.all_versions(&dep_name);
@@ -329,7 +341,7 @@ impl MultiTargetMetadata {
329341
name: dep_name.to_string(),
330342
version,
331343
feature_sets: features,
332-
unified_features: all_features.into_iter().collect(),
344+
unified_features: common_features.into_iter().collect(),
333345
});
334346
}
335347
}

0 commit comments

Comments
 (0)