Protocol Buffers - Google's data interchange format (grpc依赖)
https://developers.google.com/protocol-buffers/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
358 lines
15 KiB
358 lines
15 KiB
# Protocol Buffers - Google's data interchange format |
|
# Copyright 2024 Google Inc. All rights reserved. |
|
# |
|
# Use of this source code is governed by a BSD-style |
|
# license that can be found in the LICENSE file or at |
|
# https://developers.google.com/open-source/licenses/bsd |
|
# |
|
"""Definition of proto_common module, together with bazel providers for proto rules.""" |
|
|
|
load("@proto_bazel_features//:features.bzl", "bazel_features") |
|
load("//bazel/common:proto_lang_toolchain_info.bzl", "ProtoLangToolchainInfo") |
|
load("//bazel/private:toolchain_helpers.bzl", "toolchains") |
|
|
|
def _import_virtual_proto_path(path): |
|
"""Imports all paths for virtual imports. |
|
|
|
They're of the form: |
|
'bazel-out/k8-fastbuild/bin/external/foo/e/_virtual_imports/e' or |
|
'bazel-out/foo/k8-fastbuild/bin/e/_virtual_imports/e'""" |
|
if path.count("/") > 4: |
|
return "-I%s" % path |
|
return None |
|
|
|
def _import_repo_proto_path(path): |
|
"""Imports all paths for generated files in external repositories. |
|
|
|
They are of the form: |
|
'bazel-out/k8-fastbuild/bin/external/foo' or |
|
'bazel-out/foo/k8-fastbuild/bin'""" |
|
path_count = path.count("/") |
|
if path_count > 2 and path_count <= 4: |
|
return "-I%s" % path |
|
return None |
|
|
|
def _import_main_output_proto_path(path): |
|
"""Imports all paths for generated files or source files in external repositories. |
|
|
|
They're of the form: |
|
'bazel-out/k8-fastbuild/bin' |
|
'external/foo' |
|
'../foo' |
|
""" |
|
if path.count("/") <= 2 and path != ".": |
|
return "-I%s" % path |
|
return None |
|
|
|
def _remove_repo(file): |
|
"""Removes `../repo/` prefix from path, e.g. `../repo/package/path -> package/path`""" |
|
short_path = file.short_path |
|
workspace_root = file.owner.workspace_root |
|
if workspace_root: |
|
if workspace_root.startswith("external/"): |
|
workspace_root = "../" + workspace_root.removeprefix("external/") |
|
return short_path.removeprefix(workspace_root + "/") |
|
return short_path |
|
|
|
def _get_import_path(proto_file): |
|
"""Returns the import path of a .proto file |
|
|
|
This is the path as used for the file that can be used in an `import` statement in another |
|
.proto file. |
|
|
|
Args: |
|
proto_file: (File) The .proto file |
|
Returns: |
|
(str) import path |
|
""" |
|
repo_path = _remove_repo(proto_file) |
|
index = repo_path.find("_virtual_imports/") |
|
if index >= 0: |
|
index = repo_path.find("/", index + len("_virtual_imports/")) |
|
repo_path = repo_path[index + 1:] |
|
return repo_path |
|
|
|
def _output_directory(proto_info, root): |
|
proto_source_root = proto_info.proto_source_root |
|
if proto_source_root.startswith(root.path): |
|
#TODO: remove this branch when bin_dir is removed from proto_source_root |
|
proto_source_root = proto_source_root.removeprefix(root.path).removeprefix("/") |
|
|
|
if proto_source_root == "" or proto_source_root == ".": |
|
return root.path |
|
|
|
return root.path + "/" + proto_source_root |
|
|
|
def _check_collocated(label, proto_info, proto_lang_toolchain_info): |
|
"""Checks if lang_proto_library is collocated with proto_library. |
|
|
|
Exceptions are allowed by an allowlist defined on `proto_lang_toolchain` and |
|
on an allowlist defined on `proto_library`'s `allow_exports` attribute. |
|
|
|
If checks are not successful the function fails. |
|
|
|
Args: |
|
label: (Label) The label of lang_proto_library |
|
proto_info: (ProtoInfo) The ProtoInfo from the proto_library dependency. |
|
proto_lang_toolchain_info: (ProtoLangToolchainInfo) The proto lang toolchain info. |
|
Obtained from a `proto_lang_toolchain` target. |
|
""" |
|
_PackageSpecificationInfo = bazel_features.globals.PackageSpecificationInfo |
|
if not _PackageSpecificationInfo: |
|
if proto_lang_toolchain_info.allowlist_different_package or getattr(proto_info, "allow_exports", None): |
|
fail("Allowlist checks not supported before Bazel 6.4.0") |
|
return |
|
|
|
if (proto_info.direct_descriptor_set.owner.package != label.package and |
|
proto_lang_toolchain_info.allowlist_different_package): |
|
if not proto_lang_toolchain_info.allowlist_different_package[_PackageSpecificationInfo].contains(label): |
|
fail(("lang_proto_library '%s' may only be created in the same package " + |
|
"as proto_library '%s'") % (label, proto_info.direct_descriptor_set.owner)) |
|
if (proto_info.direct_descriptor_set.owner.package != label.package and |
|
hasattr(proto_info, "allow_exports")): |
|
if not proto_info.allow_exports[_PackageSpecificationInfo].contains(label): |
|
fail(("lang_proto_library '%s' may only be created in the same package " + |
|
"as proto_library '%s'") % (label, proto_info.direct_descriptor_set.owner)) |
|
|
|
def _compile( |
|
actions, |
|
proto_info, |
|
proto_lang_toolchain_info, |
|
generated_files, |
|
plugin_output = None, |
|
additional_args = None, |
|
additional_tools = [], |
|
additional_inputs = depset(), |
|
additional_proto_lang_toolchain_info = None, |
|
resource_set = None, |
|
experimental_exec_group = None, |
|
experimental_progress_message = None, |
|
experimental_output_files = "legacy"): |
|
"""Creates proto compile action for compiling *.proto files to language specific sources. |
|
|
|
Args: |
|
actions: (ActionFactory) Obtained by ctx.actions, used to register the actions. |
|
proto_info: (ProtoInfo) The ProtoInfo from proto_library to generate the sources for. |
|
proto_lang_toolchain_info: (ProtoLangToolchainInfo) The proto lang toolchain info. |
|
Obtained from a `proto_lang_toolchain` target or constructed ad-hoc.. |
|
generated_files: (list[File]) The output files generated by the proto compiler. |
|
Callee needs to declare files using `ctx.actions.declare_file`. |
|
See also: `proto_common.declare_generated_files`. |
|
plugin_output: (File|str) Deprecated: Set `proto_lang_toolchain.output_files` |
|
and remove the parameter. |
|
For backwards compatibility, when the proto_lang_toolchain isn't updated |
|
the value is used. |
|
additional_args: (Args) Additional arguments to add to the action. |
|
Accepts a ctx.actions.args() object that is added at the beginning |
|
of the command line. |
|
additional_tools: (list[File]) Additional tools to add to the action. |
|
additional_inputs: (Depset[File]) Additional input files to add to the action. |
|
additional_proto_lang_toolchain_info: (list[ProtoLangToolchainInfo]) Additional proto lang |
|
toolchain info for invoking multiple plugins in a single protoc invocation. For example, |
|
this is needed for plugin insertion points. |
|
resource_set: (func) A callback function that is passed to the created action. |
|
See `ctx.actions.run`, `resource_set` parameter for full definition of |
|
the callback. |
|
experimental_exec_group: (str) Sets `exec_group` on proto compile action. |
|
Avoid using this parameter. |
|
experimental_progress_message: Overrides progress_message from the toolchain. |
|
Don't use this parameter. It's only intended for the transition. |
|
experimental_output_files: (str) Overwrites output_files from the toolchain. |
|
Don't use this parameter. It's only intended for the transition. |
|
""" |
|
if type(generated_files) != type([]): |
|
fail("generated_files is expected to be a list of Files") |
|
if not generated_files: |
|
return # nothing to do |
|
if experimental_output_files not in ["single", "multiple", "legacy"]: |
|
fail('experimental_output_files expected to be one of ["single", "multiple", "legacy"]') |
|
|
|
args = actions.args() |
|
args.use_param_file(param_file_arg = "@%s") |
|
args.set_param_file_format("multiline") |
|
tools = list(additional_tools) |
|
|
|
if experimental_output_files != "legacy": |
|
output_files = experimental_output_files |
|
else: |
|
output_files = getattr(proto_lang_toolchain_info, "output_files", "legacy") |
|
if output_files != "legacy": |
|
if proto_lang_toolchain_info.out_replacement_format_flag: |
|
if output_files == "single": |
|
if len(generated_files) > 1: |
|
fail("generated_files only expected a single file") |
|
plugin_output = generated_files[0] |
|
else: |
|
plugin_output = _output_directory(proto_info, generated_files[0].root) |
|
|
|
if plugin_output: |
|
args.add(plugin_output, format = proto_lang_toolchain_info.out_replacement_format_flag) |
|
if proto_lang_toolchain_info.plugin: |
|
tools.append(proto_lang_toolchain_info.plugin) |
|
args.add(proto_lang_toolchain_info.plugin.executable, format = proto_lang_toolchain_info.plugin_format_flag) |
|
|
|
if additional_proto_lang_toolchain_info != None: |
|
tools.append(additional_proto_lang_toolchain_info.plugin) |
|
args.add(additional_proto_lang_toolchain_info.plugin.executable, format = additional_proto_lang_toolchain_info.plugin_format_flag) |
|
|
|
# Protoc searches for .protos -I paths in order they are given and then |
|
# uses the path within the directory as the package. |
|
# This requires ordering the paths from most specific (longest) to least |
|
# specific ones, so that no path in the list is a prefix of any of the |
|
# following paths in the list. |
|
# For example: 'bazel-out/k8-fastbuild/bin/external/foo' needs to be listed |
|
# before 'bazel-out/k8-fastbuild/bin'. If not, protoc will discover file under |
|
# the shorter path and use 'external/foo/...' as its package path. |
|
args.add_all(proto_info.transitive_proto_path, map_each = _import_virtual_proto_path) |
|
args.add_all(proto_info.transitive_proto_path, map_each = _import_repo_proto_path) |
|
args.add_all(proto_info.transitive_proto_path, map_each = _import_main_output_proto_path) |
|
args.add("-I.") # Needs to come last |
|
|
|
args.add_all(proto_lang_toolchain_info.protoc_opts) |
|
|
|
args.add_all(proto_info.direct_sources) |
|
|
|
if additional_args: |
|
additional_args.use_param_file(param_file_arg = "@%s") |
|
additional_args.set_param_file_format("multiline") |
|
|
|
actions.run( |
|
mnemonic = proto_lang_toolchain_info.mnemonic, |
|
progress_message = experimental_progress_message if experimental_progress_message else proto_lang_toolchain_info.progress_message, |
|
executable = proto_lang_toolchain_info.proto_compiler, |
|
arguments = [args, additional_args] if additional_args else [args], |
|
inputs = depset(transitive = [proto_info.transitive_sources, additional_inputs]), |
|
outputs = generated_files, |
|
tools = tools, |
|
use_default_shell_env = True, |
|
resource_set = resource_set, |
|
exec_group = experimental_exec_group, |
|
toolchain = _toolchain_type(proto_lang_toolchain_info), |
|
) |
|
|
|
_BAZEL_TOOLS_PREFIX = "external/bazel_tools/" |
|
|
|
def _experimental_filter_sources(proto_info, proto_lang_toolchain_info): |
|
if not proto_info.direct_sources: |
|
return [], [] |
|
|
|
# Collect a set of provided protos |
|
provided_proto_sources = proto_lang_toolchain_info.provided_proto_sources |
|
provided_paths = {} |
|
for src in provided_proto_sources: |
|
path = src.path |
|
|
|
# For listed protos bundled with the Bazel tools repository, their exec paths start |
|
# with external/bazel_tools/. This prefix needs to be removed first, because the protos in |
|
# user repositories will not have that prefix. |
|
if path.startswith(_BAZEL_TOOLS_PREFIX): |
|
provided_paths[path[len(_BAZEL_TOOLS_PREFIX):]] = None |
|
else: |
|
provided_paths[path] = None |
|
|
|
# Filter proto files |
|
proto_files = proto_info._direct_proto_sources |
|
excluded = [] |
|
included = [] |
|
for proto_file in proto_files: |
|
if proto_file.path in provided_paths: |
|
excluded.append(proto_file) |
|
else: |
|
included.append(proto_file) |
|
return included, excluded |
|
|
|
def _experimental_should_generate_code( |
|
proto_info, |
|
proto_lang_toolchain_info, |
|
rule_name, |
|
target_label): |
|
"""Checks if the code should be generated for the given proto_library. |
|
|
|
The code shouldn't be generated only when the toolchain already provides it |
|
to the language through its runtime dependency. |
|
|
|
It fails when the proto_library contains mixed proto files, that should and |
|
shouldn't generate code. |
|
|
|
Args: |
|
proto_info: (ProtoInfo) The ProtoInfo from proto_library to check the generation for. |
|
proto_lang_toolchain_info: (ProtoLangToolchainInfo) The proto lang toolchain info. |
|
Obtained from a `proto_lang_toolchain` target or constructed ad-hoc. |
|
rule_name: (str) Name of the rule used in the failure message. |
|
target_label: (Label) The label of the target used in the failure message. |
|
|
|
Returns: |
|
(bool) True when the code should be generated. |
|
""" |
|
included, excluded = _experimental_filter_sources(proto_info, proto_lang_toolchain_info) |
|
|
|
if included and excluded: |
|
fail(("The 'srcs' attribute of '%s' contains protos for which '%s' " + |
|
"shouldn't generate code (%s), in addition to protos for which it should (%s).\n" + |
|
"Separate '%s' into 2 proto_library rules.") % ( |
|
target_label, |
|
rule_name, |
|
", ".join([f.short_path for f in excluded]), |
|
", ".join([f.short_path for f in included]), |
|
target_label, |
|
)) |
|
|
|
return bool(included) |
|
|
|
def _declare_generated_files( |
|
actions, |
|
proto_info, |
|
extension, |
|
name_mapper = None): |
|
"""Declares generated files with a specific extension. |
|
|
|
Use this in lang_proto_library-es when protocol compiler generates files |
|
that correspond to .proto file names. |
|
|
|
The function removes ".proto" extension with given one (e.g. ".pb.cc") and |
|
declares new output files. |
|
|
|
Args: |
|
actions: (ActionFactory) Obtained by ctx.actions, used to declare the files. |
|
proto_info: (ProtoInfo) The ProtoInfo to declare the files for. |
|
extension: (str) The extension to use for generated files. |
|
name_mapper: (str->str) A function mapped over the base filename without |
|
the extension. Used it to replace characters in the name that |
|
cause problems in a specific programming language. |
|
|
|
Returns: |
|
(list[File]) The list of declared files. |
|
""" |
|
proto_sources = proto_info.direct_sources |
|
outputs = [] |
|
|
|
for src in proto_sources: |
|
basename_no_ext = src.basename[:-(len(src.extension) + 1)] |
|
|
|
if name_mapper: |
|
basename_no_ext = name_mapper(basename_no_ext) |
|
|
|
# Note that two proto_library rules can have the same source file, so this is actually a |
|
# shared action. NB: This can probably result in action conflicts if the proto_library rules |
|
# are not the same. |
|
outputs.append(actions.declare_file(basename_no_ext + extension, sibling = src)) |
|
|
|
return outputs |
|
|
|
def _toolchain_type(proto_lang_toolchain_info): |
|
if toolchains.INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION: |
|
return getattr(proto_lang_toolchain_info, "toolchain_type", None) |
|
else: |
|
return None |
|
|
|
proto_common = struct( |
|
compile = _compile, |
|
declare_generated_files = _declare_generated_files, |
|
check_collocated = _check_collocated, |
|
experimental_should_generate_code = _experimental_should_generate_code, |
|
experimental_filter_sources = _experimental_filter_sources, |
|
get_import_path = _get_import_path, |
|
ProtoLangToolchainInfo = ProtoLangToolchainInfo, |
|
INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION = toolchains.INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION, |
|
INCOMPATIBLE_PASS_TOOLCHAIN_TYPE = True, |
|
)
|
|
|