Skip to content

Commit 831ef76

Browse files
committed
Flesh out first-party feature propagation support
1 parent ccbd8a3 commit 831ef76

13 files changed

Lines changed: 223 additions & 41 deletions

File tree

MODULE.bazel.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rs/extensions.bzl

Lines changed: 122 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ def _add_to_dict(d, k, v):
4949
def _fq_crate(name, version):
5050
return name + "-" + version
5151

52+
_INTERNAL_RUSTC_PLACEHOLDER_CRATES = [
53+
"rustc-std-workspace-alloc",
54+
"rustc-std-workspace-core",
55+
"rustc-std-workspace-std",
56+
]
57+
58+
def _is_internal_rustc_placeholder(crate_name):
59+
return crate_name in _INTERNAL_RUSTC_PLACEHOLDER_CRATES
60+
5261
def _new_feature_resolutions(package_index, possible_deps, possible_features, platform_triples):
5362
return struct(
5463
features_enabled = {triple: set() for triple in platform_triples},
@@ -145,6 +154,53 @@ Make sure you point to the `Cargo.toml` of the workspace, not of `{name}`!”
145154
return inherited
146155
return _spec_to_dep_dict_inner(dep, spec, is_build)
147156

157+
def _cargo_metadata_dep_to_dep_dict(dep):
158+
rename = dep.get("rename")
159+
converted = {
160+
"name": rename or dep["name"],
161+
"optional": dep.get("optional", False),
162+
"default_features": dep.get("uses_default_features", True),
163+
"features": list(dep.get("features", [])),
164+
}
165+
166+
req = dep.get("req")
167+
if req:
168+
converted["req"] = req
169+
170+
kind = dep.get("kind")
171+
if kind and kind != "normal":
172+
converted["kind"] = kind
173+
174+
target = dep.get("target")
175+
if target:
176+
converted["target"] = target
177+
178+
if rename:
179+
converted["package"] = dep["name"]
180+
181+
return converted
182+
183+
def _prepare_possible_deps(dependencies, converter = None):
184+
possible_deps = []
185+
186+
for dep in dependencies:
187+
if converter:
188+
dep = converter(dep)
189+
190+
if dep.get("kind") == "dev":
191+
continue
192+
193+
dep_package = dep.get("package") or dep["name"]
194+
if _is_internal_rustc_placeholder(dep_package):
195+
continue
196+
197+
if dep.get("default_features", True):
198+
_add_to_dict(dep, "features", "default")
199+
200+
possible_deps.append(dep)
201+
202+
return possible_deps
203+
148204
def _generate_hub_and_spokes(
149205
mctx,
150206
hub_name,
@@ -404,27 +460,49 @@ def _generate_hub_and_spokes(
404460
fail("Unknown source %s for crate %s" % (source, name))
405461

406462
possible_features = fact["features"]
407-
possible_deps = [
408-
dep
409-
for dep in fact["dependencies"]
410-
if dep.get("kind") != "dev" and
411-
dep.get("package") not in [
412-
# Internal rustc placeholder crates.
413-
"rustc-std-workspace-alloc",
414-
"rustc-std-workspace-core",
415-
"rustc-std-workspace-std",
416-
]
417-
]
418-
419-
for dep in possible_deps:
420-
if dep.get("default_features", True):
421-
_add_to_dict(dep, "features", "default")
422-
463+
possible_deps = _prepare_possible_deps(fact["dependencies"])
423464
feature_resolutions = _new_feature_resolutions(package_index, possible_deps, possible_features, platform_triples)
424465
package["feature_resolutions"] = feature_resolutions
425466
feature_resolutions_by_fq_crate[_fq_crate(name, version)] = feature_resolutions
426467

427-
for package in packages:
468+
# Keep a resolver-only view that can include workspace members, unlike `versions_by_name`
469+
# which is used for spoke/hub emission.
470+
resolver_versions_by_name = {name: versions[:] for name, versions in versions_by_name.items()}
471+
472+
workspace_members_by_key = {(package["name"], package["version"]): package for package in workspace_members}
473+
resolver_packages = packages[:]
474+
for package in cargo_metadata["packages"]:
475+
name = package["name"]
476+
version = package["version"]
477+
478+
versions = resolver_versions_by_name.get(name, [])
479+
if version not in versions:
480+
if versions:
481+
versions.append(version)
482+
else:
483+
resolver_versions_by_name[name] = [version]
484+
485+
possible_features = package.get("features", {})
486+
possible_deps = _prepare_possible_deps(
487+
package.get("dependencies", []),
488+
converter = _cargo_metadata_dep_to_dep_dict,
489+
)
490+
491+
package_index = len(resolver_packages)
492+
lockfile_pkg = workspace_members_by_key.get((name, version), {})
493+
resolver_package = {
494+
"name": name,
495+
"version": version,
496+
"dependencies": lockfile_pkg.get("dependencies", []),
497+
}
498+
499+
feature_resolutions = _new_feature_resolutions(package_index, possible_deps, possible_features, platform_triples)
500+
resolver_package["feature_resolutions"] = feature_resolutions
501+
feature_resolutions_by_fq_crate[_fq_crate(name, version)] = feature_resolutions
502+
503+
resolver_packages.append(resolver_package)
504+
505+
for package in resolver_packages:
428506
name = package["name"]
429507
deps_by_name = {}
430508
for maybe_fq_dep in package.get("dependencies", []):
@@ -439,24 +517,25 @@ def _generate_hub_and_spokes(
439517
if not dep_package:
440518
dep_package = dep["name"]
441519

442-
versions = versions_by_name.get(dep_package)
520+
versions = resolver_versions_by_name.get(dep_package)
443521
if not versions:
444522
continue
523+
constrained_versions = deps_by_name.get(dep_package)
524+
if constrained_versions:
525+
versions = constrained_versions
526+
445527
if len(versions) == 1:
446528
resolved_version = versions[0]
447529
else:
448-
versions = deps_by_name.get(dep_package)
449-
if not versions:
530+
req = dep.get("req")
531+
if not req:
532+
continue
533+
534+
resolved_version = select_matching_version(req, versions)
535+
if not resolved_version:
536+
if not dep.get("optional"):
537+
print("WARNING: %s: could not resolve %s %s among %s" % (name, dep_package, req, versions))
450538
continue
451-
if len(versions) == 1:
452-
# TODO(zbarsky): validate?
453-
resolved_version = versions[0]
454-
else:
455-
resolved_version = select_matching_version(dep["req"], versions)
456-
if not resolved_version:
457-
if not dep.get("optional"):
458-
print("WARNING: %s: could not resolve %s %s among %s" % (name, dep_package, dep["req"], versions))
459-
continue
460539

461540
dep_fq = _fq_crate(dep_package, resolved_version)
462541
dep["bazel_target"] = "@%s//:%s" % (hub_name, dep_fq)
@@ -473,7 +552,7 @@ def _generate_hub_and_spokes(
473552

474553
_date(mctx, "set up resolutions")
475554

476-
workspace_fq_deps = _compute_workspace_fq_deps(workspace_members, versions_by_name)
555+
workspace_fq_deps = _compute_workspace_fq_deps(workspace_members, resolver_versions_by_name)
477556

478557
workspace_dep_versions_by_name = {}
479558
workspace_dep_labels_by_triple = {triple: set() for triple in platform_triples}
@@ -495,9 +574,7 @@ def _generate_hub_and_spokes(
495574
dep_version = None
496575
if dep_fq:
497576
dep_version = dep_fq[len(dep_name) + 1:]
498-
499-
if not source and dep_version and (dep_name, dep_version) in workspace_member_keys:
500-
continue
577+
is_first_party_dep = not source and dep_version and (dep_name, dep_version) in workspace_member_keys
501578

502579
if validate_lockfile and source and source.startswith("registry+"):
503580
req = dep["req"]
@@ -521,20 +598,24 @@ def _generate_hub_and_spokes(
521598
if not dep_fq:
522599
continue
523600

524-
dep["bazel_target"] = "@%s//:%s" % (hub_name, dep_fq)
601+
if not is_first_party_dep:
602+
dep["bazel_target"] = "@%s//:%s" % (hub_name, dep_fq)
603+
525604
feature_resolutions = feature_resolutions_by_fq_crate[dep_fq]
526605

527-
versions = workspace_dep_versions_by_name.get(dep_name)
528-
if not versions:
529-
versions = set()
530-
workspace_dep_versions_by_name[dep_name] = versions
531-
versions.add(dep_fq)
606+
if not is_first_party_dep:
607+
versions = workspace_dep_versions_by_name.get(dep_name)
608+
if not versions:
609+
versions = set()
610+
workspace_dep_versions_by_name[dep_name] = versions
611+
versions.add(dep_fq)
532612

533613
target = dep.get("target")
534614
match_info = _cfg_match_info_for_target(target, platform_cfg_attrs, cfg_match_cache)
535615

536616
for triple in match_info.matches:
537-
workspace_dep_labels_by_triple[triple].add(":" + dep_name)
617+
if not is_first_party_dep:
618+
workspace_dep_labels_by_triple[triple].add(":" + dep_name)
538619
feature_resolutions.features_enabled[triple].update(features)
539620

540621
# Set initial set of features from annotations
@@ -558,7 +639,7 @@ def _generate_hub_and_spokes(
558639

559640
_date(mctx, "set up initial deps!")
560641

561-
resolve(mctx, packages, feature_resolutions_by_fq_crate, platform_cfg_attrs_by_triple, debug)
642+
resolve(mctx, resolver_packages, feature_resolutions_by_fq_crate, platform_cfg_attrs_by_triple, debug)
562643

563644
# Validate that we aren't trying to enable any `dep:foo` features that were not even in the lockfile.
564645
for package in packages:

test/BUILD.bazel

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,23 @@ genrule(
102102
""",
103103
)
104104

105+
genquery(
106+
name = "first_party_feature_propagation_dep_leaf_deps",
107+
expression = "deps(@first_party_feature_propagation//:dep_leaf-0.1.0)",
108+
scope = ["@first_party_feature_propagation//:dep_leaf-0.1.0"],
109+
)
110+
111+
genrule(
112+
name = "verify_first_party_feature_propagation",
113+
srcs = [":first_party_feature_propagation_dep_leaf_deps"],
114+
outs = ["verify_first_party_feature_propagation.txt"],
115+
cmd = """
116+
grep '@@rules_rs++crate+first_party_feature_propagation//:itoa-1.0.17' $(location :first_party_feature_propagation_dep_leaf_deps)
117+
grep '@@rules_rs++crate+first_party_feature_propagation//:ryu-1.0.23' $(location :first_party_feature_propagation_dep_leaf_deps)
118+
echo ok > $@
119+
""",
120+
)
121+
105122
genrule(
106123
name = "verify_annotation_tags",
107124
srcs = [
@@ -125,6 +142,7 @@ filegroup(
125142
srcs = ["@%s//:_workspace_deps" % test for test in TESTS] + [
126143
":rust_binary",
127144
":verify_cfg_feature_target_dep",
145+
":verify_first_party_feature_propagation",
128146
"@binaries//:protoc-gen-prost__protoc-gen-prost",
129147
] + select({
130148
"@platforms//os:windows": [],

test/MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ TESTS = [
7676
"cfg_feature_target_dep",
7777
"empty_workspace",
7878
"feature_name_overrides_implicit_dep",
79+
"first_party_feature_propagation",
7980
"git_crates",
8081
"git_crates_aliased_deps",
8182
"many_workspace_deps", # Got count 45796 in 20 rounds

test/MODULE.bazel.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "dep_leaf"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
8+
[dependencies]
9+
itoa = { version = "1.0.15", optional = true }
10+
ryu = { version = "1.0.20", optional = true }
11+
12+
[features]
13+
default = []
14+
default_extra = ["dep:itoa"]
15+
with_extra = ["dep:ryu"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Intentionally empty: this fixture exercises dependency feature resolution.

test/first_party_feature_propagation/Cargo.lock

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
members = ["member_foo", "member_bar"]
3+
resolver = "3"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "member_bar"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
8+
[dependencies]
9+
dep_leaf = { path = "../../first_party_feature_propagation.deps/dep_leaf", default-features = false }
10+
11+
[features]
12+
default = ["dep_leaf/default_extra"]
13+
json = ["dep_leaf/with_extra"]

0 commit comments

Comments
 (0)