Skip to content

Commit b19fbe6

Browse files
Move proto_common implementation from Bazel binary
Move toolchain helpers into a separate private file. After the migration those should be removed, so let's not make it a new public APIs. Copybara also handles differences in toolchain type labels. Remove ProtoLangToolchainInfo from proto_common. Providers need special care when moving and will be moved last. Read INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION from native proto_common. This is a BuildLanguageConfiguration flag (load time flag) and may only be read through native support. Add a dependency to the latest bazel_features that supports this check. On older Bazel versions fail if ALLOWLIST is configured. Implement version check for PackageSpecificationInfo. It's only available after Bazel 6.4.0. The rest of the implementation matches Bazel version. Tests are quite extensive and will be submitted in separate PRs. PiperOrigin-RevId: 651699412
1 parent 4b9a76c commit b19fbe6

File tree

8 files changed

+488
-8
lines changed

8 files changed

+488
-8
lines changed

MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ bazel_dep(name = "rules_python", version = "0.28.0")
2424
bazel_dep(name = "rules_rust", version = "0.45.1")
2525
bazel_dep(name = "platforms", version = "0.0.8")
2626
bazel_dep(name = "zlib", version = "1.3.1")
27+
bazel_dep(name = "bazel_features", version = "1.13.0", repo_name = "proto_bazel_features")
2728

2829
# TODO: remove after toolchain types are moved to protobuf
2930
bazel_dep(name = "rules_proto", version = "4.0.0")

bazel/common/BUILD.bazel

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ bzl_library(
77
],
88
visibility = ["//visibility:public"],
99
deps = [
10-
"//bazel/private:native_bzl",
10+
":proto_lang_toolchain_info_bzl",
11+
"//bazel/private:toolchain_helpers_bzl",
12+
"@proto_bazel_features//:features",
1113
],
1214
)
1315

@@ -29,6 +31,6 @@ bzl_library(
2931
],
3032
visibility = ["//visibility:public"],
3133
deps = [
32-
":proto_common.bzl",
34+
"//bazel/private:native_bzl",
3335
],
3436
)

bazel/common/proto_common.bzl

Lines changed: 348 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,350 @@
1-
"""proto_common"""
1+
# Protocol Buffers - Google's data interchange format
2+
# Copyright 2024 Google Inc. 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+
"""Definition of proto_common module, together with bazel providers for proto rules."""
29

3-
load("//bazel/private:native.bzl", "native_proto_common")
10+
load("@proto_bazel_features//:features.bzl", "bazel_features")
11+
load("//bazel/common:proto_lang_toolchain_info.bzl", "ProtoLangToolchainInfo")
12+
load("//bazel/private:toolchain_helpers.bzl", "toolchains")
413

5-
proto_common = native_proto_common
14+
def _import_virtual_proto_path(path):
15+
"""Imports all paths for virtual imports.
16+
17+
They're of the form:
18+
'bazel-out/k8-fastbuild/bin/external/foo/e/_virtual_imports/e' or
19+
'bazel-out/foo/k8-fastbuild/bin/e/_virtual_imports/e'"""
20+
if path.count("/") > 4:
21+
return "-I%s" % path
22+
return None
23+
24+
def _import_repo_proto_path(path):
25+
"""Imports all paths for generated files in external repositories.
26+
27+
They are of the form:
28+
'bazel-out/k8-fastbuild/bin/external/foo' or
29+
'bazel-out/foo/k8-fastbuild/bin'"""
30+
path_count = path.count("/")
31+
if path_count > 2 and path_count <= 4:
32+
return "-I%s" % path
33+
return None
34+
35+
def _import_main_output_proto_path(path):
36+
"""Imports all paths for generated files or source files in external repositories.
37+
38+
They're of the form:
39+
'bazel-out/k8-fastbuild/bin'
40+
'external/foo'
41+
'../foo'
42+
"""
43+
if path.count("/") <= 2 and path != ".":
44+
return "-I%s" % path
45+
return None
46+
47+
def _remove_repo(file):
48+
"""Removes `../repo/` prefix from path, e.g. `../repo/package/path -> package/path`"""
49+
short_path = file.short_path
50+
workspace_root = file.owner.workspace_root
51+
if workspace_root:
52+
if workspace_root.startswith("external/"):
53+
workspace_root = "../" + workspace_root.removeprefix("external/")
54+
return short_path.removeprefix(workspace_root + "/")
55+
return short_path
56+
57+
def _get_import_path(proto_file):
58+
"""Returns the import path of a .proto file
59+
60+
This is the path as used for the file that can be used in an `import` statement in another
61+
.proto file.
62+
63+
Args:
64+
proto_file: (File) The .proto file
65+
Returns:
66+
(str) import path
67+
"""
68+
repo_path = _remove_repo(proto_file)
69+
index = repo_path.find("_virtual_imports/")
70+
if index >= 0:
71+
index = repo_path.find("/", index + len("_virtual_imports/"))
72+
repo_path = repo_path[index + 1:]
73+
return repo_path
74+
75+
def _output_directory(proto_info, root):
76+
proto_source_root = proto_info.proto_source_root
77+
if proto_source_root.startswith(root.path):
78+
#TODO: remove this branch when bin_dir is removed from proto_source_root
79+
proto_source_root = proto_source_root.removeprefix(root.path).removeprefix("/")
80+
81+
if proto_source_root == "" or proto_source_root == ".":
82+
return root.path
83+
84+
return root.path + "/" + proto_source_root
85+
86+
def _check_collocated(label, proto_info, proto_lang_toolchain_info):
87+
"""Checks if lang_proto_library is collocated with proto_library.
88+
89+
Exceptions are allowed by an allowlist defined on `proto_lang_toolchain` and
90+
on an allowlist defined on `proto_library`'s `allow_exports` attribute.
91+
92+
If checks are not successful the function fails.
93+
94+
Args:
95+
label: (Label) The label of lang_proto_library
96+
proto_info: (ProtoInfo) The ProtoInfo from the proto_library dependency.
97+
proto_lang_toolchain_info: (ProtoLangToolchainInfo) The proto lang toolchain info.
98+
Obtained from a `proto_lang_toolchain` target.
99+
"""
100+
_PackageSpecificationInfo = bazel_features.globals.PackageSpecificationInfo
101+
if not _PackageSpecificationInfo:
102+
if proto_lang_toolchain_info.allowlist_different_package or getattr(proto_info, "allow_exports", None):
103+
fail("Allowlist checks not supported before Bazel 6.4.0")
104+
return
105+
106+
if (proto_info.direct_descriptor_set.owner.package != label.package and
107+
proto_lang_toolchain_info.allowlist_different_package):
108+
if not proto_lang_toolchain_info.allowlist_different_package[_PackageSpecificationInfo].contains(label):
109+
fail(("lang_proto_library '%s' may only be created in the same package " +
110+
"as proto_library '%s'") % (label, proto_info.direct_descriptor_set.owner))
111+
if (proto_info.direct_descriptor_set.owner.package != label.package and
112+
hasattr(proto_info, "allow_exports")):
113+
if not proto_info.allow_exports[_PackageSpecificationInfo].contains(label):
114+
fail(("lang_proto_library '%s' may only be created in the same package " +
115+
"as proto_library '%s'") % (label, proto_info.direct_descriptor_set.owner))
116+
117+
def _compile(
118+
actions,
119+
proto_info,
120+
proto_lang_toolchain_info,
121+
generated_files,
122+
plugin_output = None,
123+
additional_args = None,
124+
additional_tools = [],
125+
additional_inputs = depset(),
126+
resource_set = None,
127+
experimental_exec_group = None,
128+
experimental_progress_message = None,
129+
experimental_output_files = "legacy"):
130+
"""Creates proto compile action for compiling *.proto files to language specific sources.
131+
132+
Args:
133+
actions: (ActionFactory) Obtained by ctx.actions, used to register the actions.
134+
proto_info: (ProtoInfo) The ProtoInfo from proto_library to generate the sources for.
135+
proto_lang_toolchain_info: (ProtoLangToolchainInfo) The proto lang toolchain info.
136+
Obtained from a `proto_lang_toolchain` target or constructed ad-hoc..
137+
generated_files: (list[File]) The output files generated by the proto compiler.
138+
Callee needs to declare files using `ctx.actions.declare_file`.
139+
See also: `proto_common.declare_generated_files`.
140+
plugin_output: (File|str) Deprecated: Set `proto_lang_toolchain.output_files`
141+
and remove the parameter.
142+
For backwards compatibility, when the proto_lang_toolchain isn't updated
143+
the value is used.
144+
additional_args: (Args) Additional arguments to add to the action.
145+
Accepts a ctx.actions.args() object that is added at the beginning
146+
of the command line.
147+
additional_tools: (list[File]) Additional tools to add to the action.
148+
additional_inputs: (Depset[File]) Additional input files to add to the action.
149+
resource_set: (func) A callback function that is passed to the created action.
150+
See `ctx.actions.run`, `resource_set` parameter for full definition of
151+
the callback.
152+
experimental_exec_group: (str) Sets `exec_group` on proto compile action.
153+
Avoid using this parameter.
154+
experimental_progress_message: Overrides progress_message from the toolchain.
155+
Don't use this parameter. It's only intended for the transition.
156+
experimental_output_files: (str) Overwrites output_files from the toolchain.
157+
Don't use this parameter. It's only intended for the transition.
158+
"""
159+
if type(generated_files) != type([]):
160+
fail("generated_files is expected to be a list of Files")
161+
if not generated_files:
162+
return # nothing to do
163+
if experimental_output_files not in ["single", "multiple", "legacy"]:
164+
fail('experimental_output_files expected to be one of ["single", "multiple", "legacy"]')
165+
166+
args = actions.args()
167+
args.use_param_file(param_file_arg = "@%s")
168+
args.set_param_file_format("multiline")
169+
tools = list(additional_tools)
170+
171+
if experimental_output_files != "legacy":
172+
output_files = experimental_output_files
173+
else:
174+
output_files = getattr(proto_lang_toolchain_info, "output_files", "legacy")
175+
if output_files != "legacy":
176+
if proto_lang_toolchain_info.out_replacement_format_flag:
177+
if output_files == "single":
178+
if len(generated_files) > 1:
179+
fail("generated_files only expected a single file")
180+
plugin_output = generated_files[0]
181+
else:
182+
plugin_output = _output_directory(proto_info, generated_files[0].root)
183+
184+
if plugin_output:
185+
args.add(plugin_output, format = proto_lang_toolchain_info.out_replacement_format_flag)
186+
if proto_lang_toolchain_info.plugin:
187+
tools.append(proto_lang_toolchain_info.plugin)
188+
args.add(proto_lang_toolchain_info.plugin.executable, format = proto_lang_toolchain_info.plugin_format_flag)
189+
190+
# Protoc searches for .protos -I paths in order they are given and then
191+
# uses the path within the directory as the package.
192+
# This requires ordering the paths from most specific (longest) to least
193+
# specific ones, so that no path in the list is a prefix of any of the
194+
# following paths in the list.
195+
# For example: 'bazel-out/k8-fastbuild/bin/external/foo' needs to be listed
196+
# before 'bazel-out/k8-fastbuild/bin'. If not, protoc will discover file under
197+
# the shorter path and use 'external/foo/...' as its package path.
198+
args.add_all(proto_info.transitive_proto_path, map_each = _import_virtual_proto_path)
199+
args.add_all(proto_info.transitive_proto_path, map_each = _import_repo_proto_path)
200+
args.add_all(proto_info.transitive_proto_path, map_each = _import_main_output_proto_path)
201+
args.add("-I.") # Needs to come last
202+
203+
args.add_all(proto_lang_toolchain_info.protoc_opts)
204+
205+
args.add_all(proto_info.direct_sources)
206+
207+
if additional_args:
208+
additional_args.use_param_file(param_file_arg = "@%s")
209+
additional_args.set_param_file_format("multiline")
210+
211+
actions.run(
212+
mnemonic = proto_lang_toolchain_info.mnemonic,
213+
progress_message = experimental_progress_message if experimental_progress_message else proto_lang_toolchain_info.progress_message,
214+
executable = proto_lang_toolchain_info.proto_compiler,
215+
arguments = [additional_args, args] if additional_args else [args],
216+
inputs = depset(transitive = [proto_info.transitive_sources, additional_inputs]),
217+
outputs = generated_files,
218+
tools = tools,
219+
use_default_shell_env = True,
220+
resource_set = resource_set,
221+
exec_group = experimental_exec_group,
222+
toolchain = _toolchain_type(proto_lang_toolchain_info),
223+
)
224+
225+
_BAZEL_TOOLS_PREFIX = "external/bazel_tools/"
226+
227+
def _experimental_filter_sources(proto_info, proto_lang_toolchain_info):
228+
if not proto_info.direct_sources:
229+
return [], []
230+
231+
# Collect a set of provided protos
232+
provided_proto_sources = proto_lang_toolchain_info.provided_proto_sources
233+
provided_paths = {}
234+
for src in provided_proto_sources:
235+
path = src.path
236+
237+
# For listed protos bundled with the Bazel tools repository, their exec paths start
238+
# with external/bazel_tools/. This prefix needs to be removed first, because the protos in
239+
# user repositories will not have that prefix.
240+
if path.startswith(_BAZEL_TOOLS_PREFIX):
241+
provided_paths[path[len(_BAZEL_TOOLS_PREFIX):]] = None
242+
else:
243+
provided_paths[path] = None
244+
245+
# Filter proto files
246+
proto_files = proto_info._direct_proto_sources
247+
excluded = []
248+
included = []
249+
for proto_file in proto_files:
250+
if proto_file.path in provided_paths:
251+
excluded.append(proto_file)
252+
else:
253+
included.append(proto_file)
254+
return included, excluded
255+
256+
def _experimental_should_generate_code(
257+
proto_info,
258+
proto_lang_toolchain_info,
259+
rule_name,
260+
target_label):
261+
"""Checks if the code should be generated for the given proto_library.
262+
263+
The code shouldn't be generated only when the toolchain already provides it
264+
to the language through its runtime dependency.
265+
266+
It fails when the proto_library contains mixed proto files, that should and
267+
shouldn't generate code.
268+
269+
Args:
270+
proto_info: (ProtoInfo) The ProtoInfo from proto_library to check the generation for.
271+
proto_lang_toolchain_info: (ProtoLangToolchainInfo) The proto lang toolchain info.
272+
Obtained from a `proto_lang_toolchain` target or constructed ad-hoc.
273+
rule_name: (str) Name of the rule used in the failure message.
274+
target_label: (Label) The label of the target used in the failure message.
275+
276+
Returns:
277+
(bool) True when the code should be generated.
278+
"""
279+
included, excluded = _experimental_filter_sources(proto_info, proto_lang_toolchain_info)
280+
281+
if included and excluded:
282+
fail(("The 'srcs' attribute of '%s' contains protos for which '%s' " +
283+
"shouldn't generate code (%s), in addition to protos for which it should (%s).\n" +
284+
"Separate '%s' into 2 proto_library rules.") % (
285+
target_label,
286+
rule_name,
287+
", ".join([f.short_path for f in excluded]),
288+
", ".join([f.short_path for f in included]),
289+
target_label,
290+
))
291+
292+
return bool(included)
293+
294+
def _declare_generated_files(
295+
actions,
296+
proto_info,
297+
extension,
298+
name_mapper = None):
299+
"""Declares generated files with a specific extension.
300+
301+
Use this in lang_proto_library-es when protocol compiler generates files
302+
that correspond to .proto file names.
303+
304+
The function removes ".proto" extension with given one (e.g. ".pb.cc") and
305+
declares new output files.
306+
307+
Args:
308+
actions: (ActionFactory) Obtained by ctx.actions, used to declare the files.
309+
proto_info: (ProtoInfo) The ProtoInfo to declare the files for.
310+
extension: (str) The extension to use for generated files.
311+
name_mapper: (str->str) A function mapped over the base filename without
312+
the extension. Used it to replace characters in the name that
313+
cause problems in a specific programming language.
314+
315+
Returns:
316+
(list[File]) The list of declared files.
317+
"""
318+
proto_sources = proto_info.direct_sources
319+
outputs = []
320+
321+
for src in proto_sources:
322+
basename_no_ext = src.basename[:-(len(src.extension) + 1)]
323+
324+
if name_mapper:
325+
basename_no_ext = name_mapper(basename_no_ext)
326+
327+
# Note that two proto_library rules can have the same source file, so this is actually a
328+
# shared action. NB: This can probably result in action conflicts if the proto_library rules
329+
# are not the same.
330+
outputs.append(actions.declare_file(basename_no_ext + extension, sibling = src))
331+
332+
return outputs
333+
334+
def _toolchain_type(proto_lang_toolchain_info):
335+
if toolchains.INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION:
336+
return getattr(proto_lang_toolchain_info, "toolchain_type", None)
337+
else:
338+
return None
339+
340+
proto_common = struct(
341+
compile = _compile,
342+
declare_generated_files = _declare_generated_files,
343+
check_collocated = _check_collocated,
344+
experimental_should_generate_code = _experimental_should_generate_code,
345+
experimental_filter_sources = _experimental_filter_sources,
346+
get_import_path = _get_import_path,
347+
ProtoLangToolchainInfo = ProtoLangToolchainInfo,
348+
INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION = toolchains.INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION,
349+
INCOMPATIBLE_PASS_TOOLCHAIN_TYPE = True,
350+
)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""ProtoLangToolchainInfo"""
22

3-
load("//bazel/common:proto_common.bzl", "proto_common")
3+
load("//bazel/private:native.bzl", "native_proto_common")
44

5-
ProtoLangToolchainInfo = proto_common.ProtoLangToolchainInfo
5+
ProtoLangToolchainInfo = native_proto_common.ProtoLangToolchainInfo

0 commit comments

Comments
 (0)