blob: 062dad34e3d3512ca0dc8c880d7af836f886ce12 [file] [log] [blame]
# Copyright 2021 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Definition of proto_common module, together with bazel providers for proto rules."""
ProtoLangToolchainInfo = provider(
doc = """Specifies how to generate language-specific code from .proto files.
Used by LANG_proto_library rules.""",
fields = dict(
out_replacement_format_flag = """(str) Format string used when passing output to the plugin
used by proto compiler.""",
output_files = """("single","multiple","legacy") Format out_replacement_format_flag with
a path to single file or a directory in case of multiple files.""",
plugin_format_flag = "(str) Format string used when passing plugin to proto compiler.",
plugin = "(FilesToRunProvider) Proto compiler plugin.",
runtime = "(Target) Runtime.",
provided_proto_sources = "(list[ProtoSource]) Proto sources provided by the toolchain.",
proto_compiler = "(FilesToRunProvider) Proto compiler.",
protoc_opts = "(list[str]) Options to pass to proto compiler.",
progress_message = "(str) Progress message to set on the proto compiler action.",
mnemonic = "(str) Mnemonic to set on the proto compiler action.",
toolchain_type = """(Label) Toolchain type that was used to obtain this info""",
),
)
def _proto_path_flag(path):
if path == ".":
return None
return "--proto_path=%s" % path
def _Iimport_path_equals_fullpath(proto_source):
return "-I%s=%s" % (proto_source.import_path(), proto_source.source_file().path)
def _output_directory(proto_info, root):
proto_source_root = proto_info.proto_source_root
if proto_source_root.startswith(root.path):
#TODO(b/281812523): 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 _compile(
actions,
proto_info,
proto_lang_toolchain_info,
generated_files,
plugin_output = None,
additional_args = None,
additional_tools = [],
additional_inputs = depset(),
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 an 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.
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)
args.add_all(proto_info.transitive_proto_path, map_each = _proto_path_flag)
# Example: `--proto_path=--proto_path=bazel-bin/target/third_party/pkg/_virtual_imports/subpkg`
args.add_all(proto_lang_toolchain_info.protoc_opts)
# Include maps
# For each import, include both the import as well as the import relativized against its
# protoSourceRoot. This ensures that protos can reference either the full path or the short
# path when including other protos.
args.add_all(proto_info.transitive_proto_sources(), map_each = _Iimport_path_equals_fullpath)
# Example: `-Ia.proto=bazel-bin/target/third_party/pkg/_virtual_imports/subpkg/a.proto`
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 = [additional_args, 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 = getattr(proto_lang_toolchain_info, "toolchain_type", None),
)
_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.original_source_file().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 = [src.original_source_file() for src in 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 _find_toolchain(ctx, legacy_attr, toolchain_type):
if _builtins.toplevel.proto_common.incompatible_enable_proto_toolchain_resolution():
toolchain = ctx.toolchains[toolchain_type]
if not toolchain:
fail("No toolchains registered for '%s'." % toolchain_type)
return toolchain.proto
else:
return getattr(ctx.attr, legacy_attr)[ProtoLangToolchainInfo]
def _use_toolchain(toolchain_type):
if _builtins.toplevel.proto_common.incompatible_enable_proto_toolchain_resolution():
return [_builtins.toplevel.config_common.toolchain_type(toolchain_type, mandatory = False)]
else:
return []
def _if_legacy_toolchain(legacy_attr_dict):
if _builtins.toplevel.proto_common.incompatible_enable_proto_toolchain_resolution():
return {}
else:
return legacy_attr_dict
toolchains = struct(
use_toolchain = _use_toolchain,
find_toolchain = _find_toolchain,
if_legacy_toolchain = _if_legacy_toolchain,
)
proto_common_do_not_use = struct(
compile = _compile,
declare_generated_files = _declare_generated_files,
experimental_should_generate_code = _experimental_should_generate_code,
experimental_filter_sources = _experimental_filter_sources,
ProtoLangToolchainInfo = ProtoLangToolchainInfo,
INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION = _builtins.toplevel.proto_common.incompatible_enable_proto_toolchain_resolution(),
INCOMPATIBLE_PASS_TOOLCHAIN_TYPE = True,
)