|
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.""" |
2 | 9 |
|
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") |
4 | 13 |
|
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 | +) |
0 commit comments