Skip to content

Commit 1f07ec6

Browse files
krasimirggcopybara-github
authored andcommitted
rust protobuf: use crate name aliases to disambiguate generated dependencies
Previously, whenever a proto_library depends on two proto_library targets with the same name from different packages, there is a possibility for crate name collisions. Collisions caused two different crates to be passed with the same name to a rust compilation action via the rustc --extern flag; result is a rust compilation error. In addition, the aspect compiled the rust code with `force_all_deps_direct`. This is used to handle transitive public imports in protobuf. That has the effect of passing all transitive crate dependencies as-if they were direct via the rustc --extern flag. This compounds the previous issue, rising the risk for crate name collisions. Here we address these two issues. To support transitive public imports without `force_all_deps_direct`, we update the aspect to crawl through the `exports` attribute of proto_library targets and collect the rust providers of transitively exported proto libraries in a new RustProtoInfo.exports_dep_variant_infos field. The generated rust code for a consumer proto_library is then provided the rust providers of the transitively exported dependencies. To address the crate naming collision amongst dependencies, we update the aspect to automatically compute and use disambiguating crate name aliases. These are crate names that encode the full target label of dependencies, in a matter similar to that rules_rust privately uses for its `rename_first_party_crates` functionality. Crucially, this is implemented in protobuf-rust itself and only affects the consumer side of the generated rust code. This avoids the need to synchronize the details between this and rules_rust; uniformly handles first- and third-party dependencies without the need to maintain an explicit distinction, and avoids some issues like running into system filename limits when trying to encode a long target's label into a crate name at crate creation time. PiperOrigin-RevId: 855663626
1 parent 7c67a4c commit 1f07ec6

25 files changed

+1053
-24
lines changed

rust/bazel/aspects.bzl

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ load("@rules_rust//rust/private:rustc.bzl", "rustc_compile_action")
2020
load("//bazel/common:proto_common.bzl", "proto_common")
2121
load("//bazel/common:proto_info.bzl", "ProtoInfo")
2222
load("//bazel/private:cc_proto_aspect.bzl", "cc_proto_aspect")
23+
load(
24+
"encode_raw_string_as_crate_name.bzl",
25+
"encode_raw_string_as_crate_name",
26+
)
2327

2428
visibility(["//rust/...", "//third_party/crubit/rs_bindings_from_cc/..."])
2529

@@ -34,8 +38,13 @@ CrateMappingInfo = provider(
3438
RustProtoInfo = provider(
3539
doc = "Rust protobuf provider info",
3640
fields = {
37-
"dep_variant_infos": "List of DepVariantInfo for the compiled Rust " +
38-
"gencode (also covers its transitive dependencies)",
41+
"dep_variant_infos": "List(DepVariantInfo): infos for the compiled Rust gencode. \
42+
When the target is a regular proto library, this contains a single element -- the info for the top-level generated code. \
43+
When the target is a srcsless proto library, this contains the infos of its dependencies.",
44+
"exports_dep_variant_infos": "List(DepVariantInfo): Transitive infos from targets from the proto_library.exports attribute. \
45+
When the target is a srcsless proto library, this contains the exports infos from all of its dependencies. \
46+
This is a list instead of a depset, since we pass them as direct dependencies when compiling rust code. \
47+
We assume the proto exports feature is not widely used in a way where this will lead to unacceptable analysis-time overhead.",
3948
"crate_mapping": "depset(CrateMappingInfo) containing mappings of all transitive " +
4049
"dependencies of the current proto_library.",
4150
},
@@ -52,6 +61,9 @@ def _rust_version_ge(version):
5261
def label_to_crate_name(ctx, label, toolchain):
5362
return label.name.replace("-", "_")
5463

64+
def encode_label_as_crate_name(label):
65+
return encode_raw_string_as_crate_name(str(label))
66+
5567
def proto_rust_toolchain_label(is_upb):
5668
if is_upb:
5769
return "//rust:proto_rust_upb_toolchain"
@@ -226,7 +238,7 @@ def _compile_cc(
226238
linking_context = linking_context,
227239
)
228240

229-
def _compile_rust(ctx, attr, src, extra_srcs, deps, runtime):
241+
def _compile_rust(ctx, attr, src, extra_srcs, deps, aliases, runtime):
230242
"""Compiles a Rust source file.
231243
232244
Eventually this function could be upstreamed into rules_rust and be made present in rust_common.
@@ -237,6 +249,7 @@ def _compile_rust(ctx, attr, src, extra_srcs, deps, runtime):
237249
src (File): The crate root source file to be compiled.
238250
extra_srcs ([File]): Additional source files to include in the crate.
239251
deps (List[DepVariantInfo]): A list of dependencies needed.
252+
aliases (dict[Target, str]): A mapping from dependency target to its crate name.
240253
runtime: The protobuf runtime target.
241254
242255
Returns:
@@ -292,7 +305,7 @@ def _compile_rust(ctx, attr, src, extra_srcs, deps, runtime):
292305
# generated code to use a consistent name, even though the actual
293306
# name of the runtime crate varies depending on the protobuf kernel
294307
# and build system.
295-
aliases = {runtime: "protobuf"},
308+
aliases = {runtime: "protobuf"} | aliases,
296309
output = lib,
297310
metadata = rmeta,
298311
edition = "2024",
@@ -302,8 +315,6 @@ def _compile_rust(ctx, attr, src, extra_srcs, deps, runtime):
302315
compile_data_targets = depset([]),
303316
owner = ctx.label,
304317
),
305-
# Needed to make transitive public imports not violate layering.
306-
force_all_deps_direct = True,
307318
output_hash = output_hash,
308319
)
309320

@@ -343,18 +354,36 @@ def _rust_proto_aspect_common(target, ctx, is_upb):
343354
transitive_crate_mappings.append(rust_proto_info.crate_mapping)
344355

345356
dep_variant_infos = []
346-
for info in [d[RustProtoInfo].dep_variant_infos for d in proto_deps]:
347-
dep_variant_infos += info
357+
358+
# Infos of exports of dependencies.
359+
dep_exports_dep_variant_infos = []
360+
361+
for dep in proto_deps:
362+
dep_variant_infos += dep[RustProtoInfo].dep_variant_infos
363+
dep_exports_dep_variant_infos += dep[RustProtoInfo].exports_dep_variant_infos
348364

349365
# If there are no srcs, then this is an alias library (which in Rust acts as a middle
350366
# library in a dependency chain). Don't generate any Rust code for it, but do propagate the
351367
# crate mappings.
352368
if not proto_srcs:
353369
return [RustProtoInfo(
354370
dep_variant_infos = dep_variant_infos,
371+
exports_dep_variant_infos = dep_exports_dep_variant_infos,
355372
crate_mapping = depset(transitive = transitive_crate_mappings),
356373
)]
357374

375+
# Add the infos from dependencies' exports, as they are needed to compile the
376+
# generated code of this target.
377+
dep_variant_infos += dep_exports_dep_variant_infos
378+
379+
# Exports of this target are the directly and transitively exported
380+
# dependencies.
381+
exported_proto_deps = getattr(ctx.rule.attr, "exports", [])
382+
exports_dep_variant_infos = []
383+
for d in exported_proto_deps:
384+
exports_dep_variant_infos.extend(d[RustProtoInfo].dep_variant_infos)
385+
exports_dep_variant_infos.extend(d[RustProtoInfo].exports_dep_variant_infos)
386+
358387
proto_lang_toolchain = ctx.attr._proto_lang_toolchain[proto_common.ProtoLangToolchainInfo]
359388
cc_toolchain = find_cpp_toolchain(ctx)
360389
toolchain = ctx.toolchains["@rules_rust//rust:toolchain_type"]
@@ -422,19 +451,35 @@ def _rust_proto_aspect_common(target, ctx, is_upb):
422451
for dep in ctx.attr._extra_deps
423452
]
424453

454+
aliases = {}
455+
456+
for d in dep_variant_infos:
457+
label = Label(d.crate_info.owner)
458+
target = struct(label = label)
459+
qualified_name = encode_label_as_crate_name(label)
460+
aliases[target] = qualified_name
461+
462+
deps = ([dep_variant_info_for_runtime] +
463+
dep_variant_info_for_native_gencode +
464+
dep_variant_infos +
465+
extra_dep_variant_infos +
466+
exports_dep_variant_infos)
467+
425468
dep_variant_info = _compile_rust(
426469
ctx = ctx,
427470
attr = ctx.rule.attr,
428471
src = entry_point_rs_output,
429472
extra_srcs = rs_gencode,
430-
deps = [dep_variant_info_for_runtime] + dep_variant_info_for_native_gencode + dep_variant_infos + extra_dep_variant_infos,
473+
deps = deps,
474+
aliases = aliases,
431475
runtime = runtime,
432476
)
433477
return [RustProtoInfo(
434478
dep_variant_infos = [dep_variant_info],
479+
exports_dep_variant_infos = exports_dep_variant_infos,
435480
crate_mapping = depset(
436481
direct = [CrateMappingInfo(
437-
crate_name = label_to_crate_name(ctx, target.label, toolchain),
482+
crate_name = encode_label_as_crate_name(ctx.label),
438483
import_paths = tuple([get_import_path(f) for f in proto_srcs]),
439484
)],
440485
transitive = transitive_crate_mappings,
@@ -444,7 +489,7 @@ def _rust_proto_aspect_common(target, ctx, is_upb):
444489
def _make_proto_library_aspect(is_upb):
445490
return aspect(
446491
implementation = (_rust_upb_proto_aspect_impl if is_upb else _rust_cc_proto_aspect_impl),
447-
attr_aspects = ["deps"],
492+
attr_aspects = ["deps", "exports"],
448493
requires = ([] if is_upb else [cc_proto_aspect]),
449494
attrs = {
450495
"_collect_cc_coverage": attr.label(
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Protocol Buffers - Google's data interchange format
2+
# Copyright 2026 Google LLC. All rights reserved.
3+
#
4+
# Use of this source code is governed by a BSD-style
5+
# license that can be found in the LICENSE file or at
6+
# https://developers.google.com/open-source/licenses/bsd
7+
8+
"""Implements encode_raw_string_as_crate_name.
9+
10+
Based on an implementation in rules_rust:
11+
* https://github.com/bazelbuild/rules_rust/blob/cdaf15f5796e3e934b074526272823284bbaed01/rust/private/utils.bzl#L643
12+
"""
13+
14+
# This is a list of pairs, where the first element of the pair is a character
15+
# that is allowed in Bazel package or target names but not in crate names; and
16+
# the second element is an encoding of that char suitable for use in a crate
17+
# name.
18+
_encodings = (
19+
(":", "x"),
20+
("!", "excl"),
21+
("%", "prc"),
22+
("@", "ao"),
23+
("^", "caret"),
24+
("`", "bt"),
25+
(" ", "sp"),
26+
("\"", "dq"),
27+
("#", "octo"),
28+
("$", "dllr"),
29+
("&", "amp"),
30+
("'", "sq"),
31+
("(", "lp"),
32+
(")", "rp"),
33+
("*", "astr"),
34+
("-", "d"),
35+
("+", "pl"),
36+
(",", "cm"),
37+
(";", "sm"),
38+
("<", "la"),
39+
("=", "eq"),
40+
(">", "ra"),
41+
("?", "qm"),
42+
("[", "lbk"),
43+
("]", "rbk"),
44+
("{", "lbe"),
45+
("|", "pp"),
46+
("}", "rbe"),
47+
("~", "td"),
48+
("/", "y"),
49+
(".", "pd"),
50+
)
51+
52+
# For each of the above encodings, we generate two substitution rules: one that
53+
# ensures any occurrences of the encodings themselves in the package/target
54+
# aren't clobbered by this translation, and one that does the encoding itself.
55+
# We also include a rule that protects the clobbering-protection rules from
56+
# getting clobbered.
57+
_substitutions = [("_z", "_zz_")] + [
58+
subst
59+
for (pattern, replacement) in _encodings
60+
for subst in (
61+
("_{}_".format(replacement), "_z{}_".format(replacement)),
62+
(pattern, "_{}_".format(replacement)),
63+
)
64+
]
65+
66+
# Expose the substitutions for testing only.
67+
substitutions_for_testing = _substitutions
68+
69+
def _replace_all(string, substitutions):
70+
"""Replaces occurrences of the given patterns in `string`.
71+
72+
There are a few reasons this looks complicated:
73+
* The substitutions are performed with some priority, i.e. patterns that are
74+
listed first in `substitutions` are higher priority than patterns that are
75+
listed later.
76+
* We also take pains to avoid doing replacements that overlap with each
77+
other, since overlaps invalidate pattern matches.
78+
* To avoid hairy offset invalidation, we apply the substitutions
79+
right-to-left.
80+
* To avoid the "_quote" -> "_quotequote_" rule introducing new pattern
81+
matches later in the string during decoding, we take the leftmost
82+
replacement, in cases of overlap. (Note that no rule can induce new
83+
pattern matches *earlier* in the string.) (E.g. "_quotedot_" encodes to
84+
"_quotequote_dot_". Note that "_quotequote_" and "_dot_" both occur in
85+
this string, and overlap.).
86+
87+
Args:
88+
string (string): the string in which the replacements should be performed.
89+
substitutions: the list of patterns and replacements to apply.
90+
91+
Returns:
92+
A string with the appropriate substitutions performed.
93+
"""
94+
95+
# Find the highest-priority pattern matches for each string index, going
96+
# left-to-right and skipping indices that are already involved in a
97+
# pattern match.
98+
plan = {}
99+
matched_indices_set = {}
100+
for pattern_start in range(len(string)):
101+
if pattern_start in matched_indices_set:
102+
continue
103+
for (pattern, replacement) in substitutions:
104+
if not string.startswith(pattern, pattern_start):
105+
continue
106+
length = len(pattern)
107+
plan[pattern_start] = (length, replacement)
108+
matched_indices_set.update([(pattern_start + i, True) for i in range(length)])
109+
break
110+
111+
# Execute the replacement plan, working from right to left.
112+
for pattern_start in sorted(plan.keys(), reverse = True):
113+
length, replacement = plan[pattern_start]
114+
after_pattern = pattern_start + length
115+
string = string[:pattern_start] + replacement + string[after_pattern:]
116+
117+
return string
118+
119+
def encode_raw_string_as_crate_name(str):
120+
"""Encodes a string using the above encoding format.
121+
122+
Args:
123+
str (string): The string to be encoded.
124+
125+
Returns:
126+
An encoded version of the input string.
127+
"""
128+
return _replace_all(str, _substitutions)
129+
130+
def decode_crate_name_as_raw_string_for_testing(crate_name):
131+
"""Decodes a crate_name that was encoded by encode_raw_string_as_crate_name.
132+
133+
This is used to check that the encoding is bijective; it is expected to only
134+
be used in tests.
135+
136+
Args:
137+
crate_name (string): The name of the crate.
138+
139+
Returns:
140+
A string representing the Bazel label (package and target).
141+
"""
142+
return _replace_all(crate_name, [(t[1], t[0]) for t in _substitutions])

rust/test/BUILD

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
# https://developers.google.com/open-source/licenses/bsd
66

77
load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix")
8+
load("@rules_rust//rust:defs.bzl", "rust_library")
89
load("//bazel:proto_library.bzl", "proto_library")
910
load(
1011
"//rust:defs.bzl",
1112
"rust_cc_proto_library",
13+
"rust_proto_library",
1214
"rust_upb_proto_library",
1315
)
1416

@@ -468,3 +470,72 @@ rust_upb_proto_library(
468470
testonly = True,
469471
deps = ["//src/google/protobuf:descriptor_proto"],
470472
)
473+
474+
proto_library(
475+
name = "same_name_direct_deps_proto",
476+
testonly = True,
477+
srcs = ["same_name_direct_deps.proto"],
478+
deps = [
479+
"//rust/test/p:parent_proto",
480+
"//rust/test/q:parent_proto",
481+
],
482+
)
483+
484+
rust_proto_library(
485+
name = "same_name_direct_deps_rust_proto",
486+
testonly = True,
487+
deps = [":same_name_direct_deps_proto"],
488+
)
489+
490+
rust_library(
491+
name = "same_name_direct_deps_rust_consumer",
492+
testonly = True,
493+
srcs = ["same_name_direct_deps_consumer.rs"],
494+
deps = [":same_name_direct_deps_rust_proto"],
495+
)
496+
497+
proto_library(
498+
name = "same_name_exported_deps_proto",
499+
testonly = True,
500+
srcs = ["same_name_exported_deps.proto"],
501+
deps = [
502+
"//rust/test/p:child_proto",
503+
"//rust/test/q:child_proto",
504+
],
505+
)
506+
507+
rust_proto_library(
508+
name = "same_name_exported_deps_rust_proto",
509+
testonly = True,
510+
deps = [":same_name_exported_deps_proto"],
511+
)
512+
513+
rust_library(
514+
name = "same_name_exported_deps_rust_consumer",
515+
testonly = True,
516+
srcs = ["same_name_exported_deps_consumer.rs"],
517+
deps = [":same_name_exported_deps_rust_proto"],
518+
)
519+
520+
proto_library(
521+
name = "same_name_double_alias_exported_deps_proto",
522+
testonly = True,
523+
srcs = ["same_name_double_alias_exported_deps.proto"],
524+
deps = [
525+
"//rust/test/p:grandchild_proto",
526+
"//rust/test/q:grandchild_proto",
527+
],
528+
)
529+
530+
rust_proto_library(
531+
name = "same_name_double_alias_exported_deps_rust_proto",
532+
testonly = True,
533+
deps = [":same_name_double_alias_exported_deps_proto"],
534+
)
535+
536+
rust_library(
537+
name = "same_name_double_alias_exported_deps_rust_consumer",
538+
testonly = True,
539+
srcs = ["same_name_double_alias_exported_deps_consumer.rs"],
540+
deps = [":same_name_double_alias_exported_deps_rust_proto"],
541+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
load("encode_raw_string_as_crate_name_test.bzl", "encode_raw_string_as_crate_name_test_suite")
2+
3+
package(
4+
default_testonly = 1,
5+
)
6+
7+
encode_raw_string_as_crate_name_test_suite(name = "encode_raw_string_as_crate_name_tests")

0 commit comments

Comments
 (0)