Skip to content

Commit f98211f

Browse files
committed
Add per-platform crate_universe feature and dependency exclusions
`crate_universe` resolves crate features once over the whole Cargo workspace, so Cargo's workspace-wide feature unification activates a feature requested by any member on the single shared target generated for that crate, for every supported platform triple. This forces features onto platforms that cannot build them: an unconditional `tokio = { features = ["net"] }` anywhere in the workspace pulls in the `net` feature for Tokio (and thus the transitive `mio` and `socket2` dependencies). This happens even on `wasm32-unknown-unknown`, where they do not compile. This MR adds new `disabled_features` and `excluded_deps` attributes to `crate.annotation` and `crate.annotation_select`. They allow a user to trim the resolved feature and dependency sets of a crate. The removal is scoped to a triple and it demotes a formerly unconditional value onto the other supported triples. This way, only the named platform loses it. Because the trimming operates on the shared target's own resolved attributes, it fixes the crate for every consumer at once, regardless of which workspace member (or dev-dependency) unioned the feature in. New unit tests have been added, as well as an integration test that demonstrates the issue for Tokio compiled both with and without the `net` feature. Fixes #4163.
1 parent e65f64b commit f98211f

24 files changed

Lines changed: 1263 additions & 3 deletions

File tree

.bazelci/presubmit.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,16 @@ tasks:
773773
run_targets:
774774
- "//:vendor_edit_test_in_tree"
775775

776+
# --- wasm_cfg_features (linux only; the wasm32 build is host-independent) ---
777+
cu_integ_wasm_cfg_features_ubuntu2204:
778+
name: Crate Universe - wasm_cfg_features
779+
platform: ubuntu2204
780+
working_directory: crate_universe/tests/integration/wasm_cfg_features
781+
build_targets:
782+
- "//..."
783+
test_targets:
784+
- "//..."
785+
776786
example_bindgen_toolchain:
777787
name: Example custom bindgen toolchain registration
778788
platform: ubuntu2204

crate_universe/extensions.bzl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,12 @@ _ANNOTATION_SELECT_ATTRS = {
14171417
"deps": _relative_label_list(
14181418
doc = "A list of labels to add to a crate's `rust_library::deps` attribute.",
14191419
),
1420+
"disabled_features": attr.string_list(
1421+
doc = "A list of features to remove from a crate's resolved `rust_library::crate_features` attribute. Applied per-triple when set via `annotation_select`, preserving the feature on the other supported triples.",
1422+
),
1423+
"excluded_deps": attr.string_list(
1424+
doc = "A list of dependency crate names to remove from a crate's resolved `rust_library::deps` attribute. Typically paired with `disabled_features` to also drop the optional dependencies a disabled feature would have activated.",
1425+
),
14201426
"link_deps": _relative_label_list(
14211427
doc = "A list of labels to add to a crate's `rust_library::link_deps` attribute.",
14221428
),
@@ -1624,6 +1630,34 @@ crate = module_extension(
16241630
doc = """\
16251631
Crate universe module extensions.
16261632
1633+
## Removing features and dependencies on specific platforms
1634+
1635+
Cargo enables features additively. When one crate in the build turns on a
1636+
feature of a shared dependency, that feature is enabled for every target that
1637+
uses the dependency. This is usually fine, but sometimes it enables a feature on
1638+
a platform that cannot build it.
1639+
1640+
Take the `net` feature of `tokio`. It requires the `mio` and `socket2` crates,
1641+
which do not compile for `wasm32-unknown-unknown`. If any crate in the build
1642+
enables `net`, then `net` is enabled for `tokio` on every platform, and the Wasm
1643+
build fails.
1644+
1645+
The `disabled_features` and `excluded_deps` fields on an annotation remove
1646+
features and dependencies from a crate. Pair them with `crate.annotation_select`
1647+
to limit the removal to specific target triples. The feature or dependency stays
1648+
enabled on the other triples, so only the platform you name loses it:
1649+
1650+
```python
1651+
crate.annotation_select(
1652+
crate = "tokio",
1653+
triples = ["wasm32-unknown-unknown"],
1654+
# For wasm32 only: drop `net` and the implicit `mio` and `socket2` features,
1655+
# and exclude the `mio` and `socket2` crates. `net` stays enabled elsewhere.
1656+
disabled_features = ["net", "mio", "socket2"],
1657+
excluded_deps = ["mio", "socket2"],
1658+
)
1659+
```
1660+
16271661
Environment Variables:
16281662
16291663
| variable | usage |

crate_universe/private/crate.bzl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ def _annotation(
107107
compile_data_glob = None,
108108
compile_data_glob_excludes = None,
109109
crate_features = None,
110+
disabled_features = None,
111+
excluded_deps = None,
110112
data = None,
111113
data_glob = None,
112114
deps = None,
@@ -158,6 +160,14 @@ def _annotation(
158160
attribute.
159161
crate_features (optional): A list of strings to add to a crate's `rust_library::crate_features`
160162
attribute.
163+
disabled_features (optional): Features to remove from a crate's resolved `crate_features`. Accepts a
164+
list (applies to all platforms) or a `crate.select()` keyed by target triple (applies per-platform,
165+
preserving the feature on the other supported triples). Use to remove a feature that ends up
166+
activated on a platform where it cannot be built (e.g. tokio's `net` on `wasm32-unknown-unknown`).
167+
excluded_deps (optional): Dependencies (by crate name) to remove from a crate's resolved `deps`. Follows
168+
the same list / `crate.select()` semantics as `disabled_features` and is typically paired with it to
169+
also drop the optional dependencies a disabled feature would have activated (e.g. `mio` and `socket2`
170+
alongside tokio's `net`).
161171
data (list, optional): A list of labels to add to a crate's `rust_library::data` attribute.
162172
data_glob (list, optional): A list of glob patterns to add to a crate's `rust_library::data` attribute.
163173
deps (list, optional): A list of labels to add to a crate's `rust_library::deps` attribute.
@@ -221,6 +231,8 @@ def _annotation(
221231
compile_data_glob = compile_data_glob,
222232
compile_data_glob_excludes = compile_data_glob_excludes,
223233
crate_features = crate_features,
234+
disabled_features = disabled_features,
235+
excluded_deps = excluded_deps,
224236
data = _stringify_list(data),
225237
data_glob = data_glob,
226238
deps = _stringify_list(deps),

crate_universe/src/config.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,22 @@ pub(crate) struct CrateAnnotations {
290290
/// [crate_features](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-crate_features) attribute.
291291
pub(crate) crate_features: Option<Select<BTreeSet<String>>>,
292292

293+
/// Features to remove from the target's `crate_features`. Values under a
294+
/// specific target triple are dropped for that triple only (a feature that
295+
/// was unconditional is preserved on the other supported triples); values
296+
/// under the common configuration are dropped everywhere. Used to undo
297+
/// features that whole-workspace feature unification forces onto a platform
298+
/// that cannot support them (e.g. tokio's `net` on `wasm32-unknown-unknown`).
299+
#[serde(default, skip_serializing_if = "Option::is_none")]
300+
pub(crate) disabled_features: Option<Select<BTreeSet<String>>>,
301+
302+
/// Dependencies (by crate name) to remove from the target's `deps`. Follows
303+
/// the same per-triple semantics as `disabled_features` and is typically
304+
/// paired with it to drop the optional dependencies a disabled feature would
305+
/// have activated (e.g. `mio`/`socket2` alongside tokio's `net`).
306+
#[serde(default, skip_serializing_if = "Option::is_none")]
307+
pub(crate) excluded_deps: Option<Select<BTreeSet<String>>>,
308+
293309
/// Additional data to pass to the target's
294310
/// [data](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-data) attribute.
295311
pub(crate) data: Option<Select<BTreeSet<Label>>>,
@@ -452,6 +468,8 @@ impl Add for CrateAnnotations {
452468
proc_macro_deps: select_merge(self.proc_macro_deps, rhs.proc_macro_deps),
453469
link_deps: select_merge(self.link_deps, rhs.link_deps),
454470
crate_features: select_merge(self.crate_features, rhs.crate_features),
471+
disabled_features: select_merge(self.disabled_features, rhs.disabled_features),
472+
excluded_deps: select_merge(self.excluded_deps, rhs.excluded_deps),
455473
data: select_merge(self.data, rhs.data),
456474
data_glob: joined_extra_member!(self.data_glob, rhs.data_glob, BTreeSet::new, BTreeSet::extend),
457475
disable_pipelining: self.disable_pipelining || rhs.disable_pipelining,

crate_universe/src/context.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ impl Context {
6464

6565
pub(crate) fn new(annotations: Annotations, sources_are_present: bool) -> anyhow::Result<Self> {
6666
// Build a map of crate contexts
67-
let crates: BTreeMap<CrateId, CrateContext> = annotations
67+
let mut crates: BTreeMap<CrateId, CrateContext> = annotations
6868
.metadata
6969
.crates
7070
.values()
@@ -84,6 +84,21 @@ impl Context {
8484
})
8585
.collect::<Result<_, _>>()?;
8686

87+
// Apply per-platform `disabled_features` / `excluded_deps` annotations.
88+
// These trim the resolver's whole-workspace feature/dependency output
89+
// for specific target triples (e.g. dropping tokio's `net` feature and
90+
// its `mio`/`socket2` deps on `wasm32-unknown-unknown`). Done here, after
91+
// every crate context exists, because the re-pinning of a formerly
92+
// unconditional value needs the full set of supported platform triples.
93+
for (crate_id, context) in crates.iter_mut() {
94+
if let Some(paired_extras) = annotations.pairred_extras.get(crate_id) {
95+
context.apply_exclusions(
96+
&paired_extras.crate_extra,
97+
&annotations.config.supported_platform_triples,
98+
);
99+
}
100+
}
101+
87102
// Filter for any crate that contains a binary
88103
let binary_crates: BTreeSet<CrateId> = crates
89104
.iter()

crate_universe/src/context/crate_context.rs

Lines changed: 193 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ use camino::Utf8PathBuf;
66
use cargo_metadata::{Node, Package, PackageId};
77
use serde::{Deserialize, Serialize};
88

9-
use crate::config::{AliasRule, CrateId, GenBinaries};
9+
use crate::config::{AliasRule, CrateAnnotations, CrateId, GenBinaries};
1010
use crate::metadata::{
1111
CrateAnnotation, Dependency, PairedExtras, SourceAnnotation, TreeResolverMetadata,
1212
};
13-
use crate::select::Select;
13+
use crate::select::{Select, SelectableOrderedValue};
1414
use crate::utils::sanitize_module_name;
1515
use crate::utils::starlark::{Glob, Label};
16+
use crate::utils::target_triple::TargetTriple;
1617

1718
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1819
pub struct CrateDependency {
@@ -377,6 +378,93 @@ pub(crate) struct CrateContext {
377378
pub(crate) override_targets: BTreeMap<String, Label>,
378379
}
379380

381+
/// Remove values from a `Select` according to a removal spec keyed by
382+
/// configuration.
383+
///
384+
/// * A name listed under the common configuration (`None`) removes every
385+
/// matching value from every configuration.
386+
/// * A name listed under a specific target triple removes matching values for
387+
/// that triple only. A matching value that was unconditional (`common`) is
388+
/// first demoted onto every *other* supported triple so those platforms keep
389+
/// it; only the named triple loses it.
390+
///
391+
/// `matches` decides whether a value in the `Select` corresponds to a name in
392+
/// the removal spec (feature-name equality for `crate_features`, crate-name
393+
/// equality for `deps`).
394+
fn remove_selected<T, F>(
395+
select: Select<BTreeSet<T>>,
396+
removals: &Select<BTreeSet<String>>,
397+
supported_platform_triples: &BTreeSet<String>,
398+
matches: F,
399+
) -> Select<BTreeSet<T>>
400+
where
401+
T: SelectableOrderedValue,
402+
F: Fn(&T, &str) -> bool,
403+
{
404+
let (mut common, mut selects) = select.into_parts();
405+
406+
// Partition the removal spec into names to drop everywhere (common) and
407+
// names to drop for a specific triple.
408+
let mut global: BTreeSet<String> = BTreeSet::new();
409+
let mut per_triple: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
410+
for (config, name) in removals.items() {
411+
match config {
412+
None => {
413+
global.insert(name);
414+
}
415+
Some(triple) => {
416+
per_triple.entry(triple).or_default().insert(name);
417+
}
418+
}
419+
}
420+
421+
let matches_any = |value: &T, names: &BTreeSet<String>| names.iter().any(|n| matches(value, n));
422+
423+
// Global removals: drop from the common set and every configuration.
424+
if !global.is_empty() {
425+
common.retain(|value| !matches_any(value, &global));
426+
for set in selects.values_mut() {
427+
set.retain(|value| !matches_any(value, &global));
428+
}
429+
}
430+
431+
if !per_triple.is_empty() {
432+
// Values in `common` that at least one triple wants removed must be
433+
// demoted: dropped from `common` and re-pinned onto every supported
434+
// triple that still wants them.
435+
let demote: BTreeSet<T> = common
436+
.iter()
437+
.filter(|value| per_triple.values().any(|names| matches_any(value, names)))
438+
.cloned()
439+
.collect();
440+
for value in &demote {
441+
common.remove(value);
442+
for triple in supported_platform_triples {
443+
let removed_here = per_triple
444+
.get(triple)
445+
.is_some_and(|names| matches_any(value, names));
446+
if !removed_here {
447+
selects
448+
.entry(triple.clone())
449+
.or_default()
450+
.insert(value.clone());
451+
}
452+
}
453+
}
454+
455+
// Drop values that were pinned to a specific triple's set.
456+
for (triple, names) in &per_triple {
457+
if let Some(set) = selects.get_mut(triple) {
458+
set.retain(|value| !matches_any(value, names));
459+
}
460+
}
461+
}
462+
463+
selects.retain(|_, set| !set.is_empty());
464+
465+
Select::from_parts(common, selects)
466+
}
467+
380468
impl CrateContext {
381469
#[allow(clippy::too_many_arguments)]
382470
pub(crate) fn new(
@@ -576,6 +664,46 @@ impl CrateContext {
576664
.with_overrides(extras))
577665
}
578666

667+
/// Apply the `disabled_features` / `excluded_deps` annotations, trimming the
668+
/// resolver-provided `crate_features` and `deps` for the requested target
669+
/// triples. This is the escape hatch for whole-workspace feature unification
670+
/// forcing a feature (and its optional deps) onto a platform that cannot
671+
/// build it — see [`crate::config::CrateAnnotations::disabled_features`].
672+
pub(crate) fn apply_exclusions(
673+
&mut self,
674+
extra: &CrateAnnotations,
675+
supported_platform_triples: &BTreeSet<TargetTriple>,
676+
) {
677+
if extra.disabled_features.is_none() && extra.excluded_deps.is_none() {
678+
return;
679+
}
680+
681+
let triples: BTreeSet<String> = supported_platform_triples
682+
.iter()
683+
.map(TargetTriple::to_bazel)
684+
.collect();
685+
686+
if let Some(disabled) = &extra.disabled_features {
687+
let features = std::mem::take(&mut self.common_attrs.crate_features);
688+
self.common_attrs.crate_features =
689+
remove_selected(features, disabled, &triples, |feature, name| feature == name);
690+
}
691+
692+
if let Some(excluded) = &extra.excluded_deps {
693+
let deps = std::mem::take(&mut self.common_attrs.deps);
694+
self.common_attrs.deps =
695+
remove_selected(deps, excluded, &triples, |dep, name| dep.id.name == name);
696+
697+
// Also trim dev-dependencies: a feature unioned onto the shared
698+
// target from a workspace member's dev-dependencies (e.g. a test
699+
// HTTP server pulling tokio `net`) can drag the same optional deps
700+
// into `deps_dev`.
701+
let deps_dev = std::mem::take(&mut self.common_attrs.deps_dev);
702+
self.common_attrs.deps_dev =
703+
remove_selected(deps_dev, excluded, &triples, |dep, name| dep.id.name == name);
704+
}
705+
}
706+
579707
fn with_overrides(mut self, extras: &BTreeMap<CrateId, PairedExtras>) -> Self {
580708
let id = CrateId::new(self.name.clone(), self.version.clone());
581709

@@ -935,6 +1063,69 @@ mod test {
9351063
.unwrap()
9361064
}
9371065

1066+
fn triples(names: &[&str]) -> BTreeSet<String> {
1067+
names.iter().map(|n| n.to_string()).collect()
1068+
}
1069+
1070+
fn features(values: &[(Option<&str>, &str)]) -> Select<BTreeSet<String>> {
1071+
let mut select = Select::<BTreeSet<String>>::new();
1072+
for (config, value) in values {
1073+
select.insert(value.to_string(), config.map(str::to_string));
1074+
}
1075+
select
1076+
}
1077+
1078+
#[test]
1079+
fn remove_selected_demotes_unconditional_feature_off_one_triple() {
1080+
// `net` is unconditional (forced on by whole-workspace unification);
1081+
// disabling it on wasm32 must keep it active on the other triples.
1082+
let input = features(&[(None, "sync"), (None, "net")]);
1083+
let disabled = features(&[(Some("wasm32-unknown-unknown"), "net")]);
1084+
let supported = triples(&[
1085+
"wasm32-unknown-unknown",
1086+
"x86_64-unknown-linux-gnu",
1087+
"aarch64-apple-darwin",
1088+
]);
1089+
1090+
let (common, selects) =
1091+
remove_selected(input, &disabled, &supported, |f, n| f == n).into_parts();
1092+
1093+
assert_eq!(common, triples(&["sync"]));
1094+
assert_eq!(selects.get("wasm32-unknown-unknown"), None);
1095+
assert_eq!(selects["x86_64-unknown-linux-gnu"], triples(&["net"]));
1096+
assert_eq!(selects["aarch64-apple-darwin"], triples(&["net"]));
1097+
}
1098+
1099+
#[test]
1100+
fn remove_selected_drops_triple_specific_value() {
1101+
// `net` is only present on wasm32 already; disabling it there removes it
1102+
// entirely without touching other triples.
1103+
let input = features(&[(None, "sync"), (Some("wasm32-unknown-unknown"), "net")]);
1104+
let disabled = features(&[(Some("wasm32-unknown-unknown"), "net")]);
1105+
let supported = triples(&["wasm32-unknown-unknown", "x86_64-unknown-linux-gnu"]);
1106+
1107+
let (common, selects) =
1108+
remove_selected(input, &disabled, &supported, |f, n| f == n).into_parts();
1109+
1110+
assert_eq!(common, triples(&["sync"]));
1111+
assert!(selects.is_empty());
1112+
}
1113+
1114+
#[test]
1115+
fn remove_selected_common_config_removes_everywhere() {
1116+
// A removal keyed to the common config drops the value from every
1117+
// configuration.
1118+
let input = features(&[(None, "net"), (Some("x86_64-unknown-linux-gnu"), "net")]);
1119+
let disabled = features(&[(None, "net")]);
1120+
let supported = triples(&["wasm32-unknown-unknown", "x86_64-unknown-linux-gnu"]);
1121+
1122+
let (common, selects) =
1123+
remove_selected(input, &disabled, &supported, |f, n| f == n).into_parts();
1124+
1125+
assert!(common.is_empty());
1126+
assert!(selects.is_empty());
1127+
}
1128+
9381129
#[test]
9391130
fn new_context() {
9401131
let annotations = common_annotations();

0 commit comments

Comments
 (0)