1# Protocol Buffers - Google's data interchange format 2# Copyright 2008 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""" 8Implementation of proto_library rule. 9""" 10 11load("@bazel_skylib//lib:paths.bzl", "paths") 12load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") 13load("@proto_bazel_features//:features.bzl", "bazel_features") 14load("//bazel/common:proto_common.bzl", "proto_common") 15load("//bazel/common:proto_info.bzl", "ProtoInfo") 16load("//bazel/private:toolchain_helpers.bzl", "toolchains") 17 18STRICT_DEPS_FLAG_TEMPLATE = ( 19 # 20 "--direct_dependencies_violation_msg=" + 21 "%%s is imported, but %s doesn't directly depend on a proto_library that 'srcs' it." 22) 23 24def _check_srcs_package(target_package, srcs): 25 """Check that .proto files in sources are from the same package. 26 27 This is done to avoid clashes with the generated sources.""" 28 29 #TODO: this does not work with filegroups that contain files that are not in the package 30 for src in srcs: 31 if target_package != src.label.package: 32 fail("Proto source with label '%s' must be in same package as consuming rule." % src.label) 33 34def _get_import_prefix(ctx): 35 """Gets and verifies import_prefix attribute if it is declared.""" 36 37 import_prefix = ctx.attr.import_prefix 38 39 if not paths.is_normalized(import_prefix): 40 fail("should be normalized (without uplevel references or '.' path segments)", attr = "import_prefix") 41 if paths.is_absolute(import_prefix): 42 fail("should be a relative path", attr = "import_prefix") 43 44 return import_prefix 45 46def _get_strip_import_prefix(ctx): 47 """Gets and verifies strip_import_prefix.""" 48 49 strip_import_prefix = ctx.attr.strip_import_prefix 50 51 if not paths.is_normalized(strip_import_prefix): 52 fail("should be normalized (without uplevel references or '.' path segments)", attr = "strip_import_prefix") 53 54 if paths.is_absolute(strip_import_prefix): 55 strip_import_prefix = strip_import_prefix[1:] 56 else: # Relative to current package 57 strip_import_prefix = _join(ctx.label.package, strip_import_prefix) 58 59 return strip_import_prefix.removesuffix("/") 60 61def _proto_library_impl(ctx): 62 # Verifies attributes. 63 _check_srcs_package(ctx.label.package, ctx.attr.srcs) 64 srcs = ctx.files.srcs 65 deps = [dep[ProtoInfo] for dep in ctx.attr.deps] 66 exports = [dep[ProtoInfo] for dep in ctx.attr.exports] 67 import_prefix = _get_import_prefix(ctx) 68 strip_import_prefix = _get_strip_import_prefix(ctx) 69 check_for_reexport = deps + exports if not srcs else exports 70 _PackageSpecificationInfo = bazel_features.globals.PackageSpecificationInfo 71 for proto in check_for_reexport: 72 if getattr(proto, "allow_exports", None): 73 if not _PackageSpecificationInfo: 74 fail("Allowlist checks not supported before Bazel 6.4.0") 75 if not proto.allow_exports[_PackageSpecificationInfo].contains(ctx.label): 76 fail("proto_library '%s' can't be reexported in package '//%s'" % (proto.direct_descriptor_set.owner, ctx.label.package)) 77 78 proto_path, virtual_srcs = _process_srcs(ctx, srcs, import_prefix, strip_import_prefix) 79 descriptor_set = ctx.actions.declare_file(ctx.label.name + "-descriptor-set.proto.bin") 80 proto_info = ProtoInfo( 81 srcs = virtual_srcs, 82 deps = deps, 83 descriptor_set = descriptor_set, 84 proto_path = proto_path, 85 workspace_root = ctx.label.workspace_root, 86 bin_dir = ctx.bin_dir.path, 87 allow_exports = ctx.attr.allow_exports, 88 ) 89 90 _write_descriptor_set(ctx, proto_info, deps, exports, descriptor_set) 91 92 # We assume that the proto sources will not have conflicting artifacts 93 # with the same root relative path 94 data_runfiles = ctx.runfiles( 95 files = [proto_info.direct_descriptor_set], 96 transitive_files = depset(transitive = [proto_info.transitive_sources]), 97 ) 98 return [ 99 proto_info, 100 DefaultInfo( 101 files = depset([proto_info.direct_descriptor_set]), 102 default_runfiles = ctx.runfiles(), # empty 103 data_runfiles = data_runfiles, 104 ), 105 ] 106 107def _process_srcs(ctx, srcs, import_prefix, strip_import_prefix): 108 """Returns proto_path and sources, optionally symlinking them to _virtual_imports. 109 110 Returns: 111 (str, [File]) A pair of proto_path and virtual_sources. 112 """ 113 if import_prefix != "" or strip_import_prefix != "": 114 # Use virtual source roots 115 return _symlink_to_virtual_imports(ctx, srcs, import_prefix, strip_import_prefix) 116 else: 117 # No virtual source roots 118 return "", srcs 119 120def _join(*path): 121 return "/".join([p for p in path if p != ""]) 122 123def _symlink_to_virtual_imports(ctx, srcs, import_prefix, strip_import_prefix): 124 """Symlinks srcs to _virtual_imports. 125 126 Returns: 127 A pair proto_path, directs_sources. 128 """ 129 virtual_imports = _join("_virtual_imports", ctx.label.name) 130 proto_path = _join(ctx.label.package, virtual_imports) 131 132 if ctx.label.workspace_name == "": 133 full_strip_import_prefix = strip_import_prefix 134 else: 135 full_strip_import_prefix = _join("..", ctx.label.workspace_name, strip_import_prefix) 136 if full_strip_import_prefix: 137 full_strip_import_prefix += "/" 138 139 virtual_srcs = [] 140 for src in srcs: 141 # Remove strip_import_prefix 142 if not src.short_path.startswith(full_strip_import_prefix): 143 fail(".proto file '%s' is not under the specified strip prefix '%s'" % 144 (src.short_path, full_strip_import_prefix)) 145 import_path = src.short_path[len(full_strip_import_prefix):] 146 147 # Add import_prefix 148 virtual_src = ctx.actions.declare_file(_join(virtual_imports, import_prefix, import_path)) 149 ctx.actions.symlink( 150 output = virtual_src, 151 target_file = src, 152 progress_message = "Symlinking virtual .proto sources for %{label}", 153 ) 154 virtual_srcs.append(virtual_src) 155 return proto_path, virtual_srcs 156 157def _write_descriptor_set(ctx, proto_info, deps, exports, descriptor_set): 158 """Writes descriptor set.""" 159 if proto_info.direct_sources == []: 160 ctx.actions.write(descriptor_set, "") 161 return 162 163 dependencies_descriptor_sets = depset(transitive = [dep.transitive_descriptor_sets for dep in deps]) 164 165 args = ctx.actions.args() 166 167 if ctx.attr._experimental_proto_descriptor_sets_include_source_info[BuildSettingInfo].value: 168 args.add("--include_source_info") 169 args.add("--retain_options") 170 171 strict_deps = ctx.attr._strict_proto_deps[BuildSettingInfo].value 172 if strict_deps: 173 if proto_info.direct_sources: 174 strict_importable_sources = depset( 175 direct = proto_info._direct_proto_sources, 176 transitive = [dep._exported_sources for dep in deps], 177 ) 178 else: 179 strict_importable_sources = None 180 if strict_importable_sources: 181 args.add_joined( 182 "--direct_dependencies", 183 strict_importable_sources, 184 map_each = proto_common.get_import_path, 185 join_with = ":", 186 ) 187 # Example: `--direct_dependencies a.proto:b.proto` 188 189 else: 190 # The proto compiler requires an empty list to turn on strict deps checking 191 args.add("--direct_dependencies=") 192 193 # Set `-direct_dependencies_violation_msg=` 194 args.add(ctx.label, format = STRICT_DEPS_FLAG_TEMPLATE) 195 196 strict_imports = ctx.attr._strict_public_imports[BuildSettingInfo].value 197 if strict_imports: 198 public_import_protos = depset(transitive = [export._exported_sources for export in exports]) 199 if not public_import_protos: 200 # This line is necessary to trigger the check. 201 args.add("--allowed_public_imports=") 202 else: 203 args.add_joined( 204 "--allowed_public_imports", 205 public_import_protos, 206 map_each = proto_common.get_import_path, 207 join_with = ":", 208 ) 209 if proto_common.INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION: 210 toolchain = ctx.toolchains[toolchains.PROTO_TOOLCHAIN] 211 if not toolchain: 212 fail("Protocol compiler toolchain could not be resolved.") 213 proto_lang_toolchain_info = toolchain.proto 214 else: 215 proto_lang_toolchain_info = proto_common.ProtoLangToolchainInfo( 216 out_replacement_format_flag = "--descriptor_set_out=%s", 217 output_files = "single", 218 mnemonic = "GenProtoDescriptorSet", 219 progress_message = "Generating Descriptor Set proto_library %{label}", 220 proto_compiler = ctx.executable._proto_compiler, 221 protoc_opts = ctx.fragments.proto.experimental_protoc_opts, 222 plugin = None, 223 ) 224 225 proto_common.compile( 226 ctx.actions, 227 proto_info, 228 proto_lang_toolchain_info, 229 generated_files = [descriptor_set], 230 additional_inputs = dependencies_descriptor_sets, 231 additional_args = args, 232 ) 233 234proto_library = rule( 235 _proto_library_impl, 236 # TODO: proto_common docs are missing 237 # TODO: ProtoInfo link doesn't work and docs are missing 238 doc = """ 239<p>If using Bazel, please load the rule from <a href="https://github.com/bazelbuild/rules_proto"> 240https://github.com/bazelbuild/rules_proto</a>. 241 242<p>Use <code>proto_library</code> to define libraries of protocol buffers which 243may be used from multiple languages. A <code>proto_library</code> may be listed 244in the <code>deps</code> clause of supported rules, such as 245<code>java_proto_library</code>. 246 247<p>When compiled on the command-line, a <code>proto_library</code> creates a file 248named <code>foo-descriptor-set.proto.bin</code>, which is the descriptor set for 249the messages the rule srcs. The file is a serialized 250<code>FileDescriptorSet</code>, which is described in 251<a href="https://developers.google.com/protocol-buffers/docs/techniques#self-description"> 252https://developers.google.com/protocol-buffers/docs/techniques#self-description</a>. 253 254<p>It only contains information about the <code>.proto</code> files directly 255mentioned by a <code>proto_library</code> rule; the collection of transitive 256descriptor sets is available through the 257<code>[ProtoInfo].transitive_descriptor_sets</code> Starlark provider. 258See documentation in <code>proto_info.bzl</code>. 259 260<p>Recommended code organization: 261<ul> 262<li>One <code>proto_library</code> rule per <code>.proto</code> file. 263<li>A file named <code>foo.proto</code> will be in a rule named <code>foo_proto</code>, 264 which is located in the same package. 265<li>A <code>[language]_proto_library</code> that wraps a <code>proto_library</code> 266 named <code>foo_proto</code> should be called <code>foo_[language]_proto</code>, 267 and be located in the same package. 268</ul>""", 269 attrs = { 270 "srcs": attr.label_list( 271 allow_files = [".proto", ".protodevel"], 272 flags = ["DIRECT_COMPILE_TIME_INPUT"], 273 # TODO: Should .protodevel be advertised or deprecated? 274 doc = """ 275The list of <code>.proto</code> and <code>.protodevel</code> files that are 276processed to create the target. This is usually a non empty list. One usecase 277where <code>srcs</code> can be empty is an <i>alias-library</i>. This is a 278proto_library rule having one or more other proto_library in <code>deps</code>. 279This pattern can be used to e.g. export a public api under a persistent name.""", 280 ), 281 "deps": attr.label_list( 282 providers = [ProtoInfo], 283 doc = """ 284The list of other <code>proto_library</code> rules that the target depends upon. 285A <code>proto_library</code> may only depend on other <code>proto_library</code> 286targets. It may not depend on language-specific libraries.""", 287 ), 288 "exports": attr.label_list( 289 providers = [ProtoInfo], 290 doc = """ 291List of proto_library targets that can be referenced via "import public" in the 292proto source. 293It's an error if you use "import public" but do not list the corresponding library 294in the exports attribute. 295Note that you have list the library both in deps and exports since not all 296lang_proto_library implementations have been changed yet.""", 297 ), 298 "strip_import_prefix": attr.string( 299 default = "/", 300 doc = """ 301The prefix to strip from the paths of the .proto files in this rule. 302 303<p>When set, .proto source files in the <code>srcs</code> attribute of this rule are 304accessible at their path with this prefix cut off. 305 306<p>If it's a relative path (not starting with a slash), it's taken as a package-relative 307one. If it's an absolute one, it's understood as a repository-relative path. 308 309<p>The prefix in the <code>import_prefix</code> attribute is added after this prefix is 310stripped.""", 311 ), 312 "import_prefix": attr.string( 313 doc = """ 314The prefix to add to the paths of the .proto files in this rule. 315 316<p>When set, the .proto source files in the <code>srcs</code> attribute of this rule are 317accessible at is the value of this attribute prepended to their repository-relative path. 318 319<p>The prefix in the <code>strip_import_prefix</code> attribute is removed before this 320prefix is added.""", 321 ), 322 "allow_exports": attr.label( 323 cfg = "exec", 324 providers = [bazel_features.globals.PackageSpecificationInfo] if bazel_features.globals.PackageSpecificationInfo else [], 325 doc = """ 326An optional allowlist that prevents proto library to be reexported or used in 327lang_proto_library that is not in one of the listed packages.""", 328 ), 329 "data": attr.label_list( 330 allow_files = True, 331 flags = ["SKIP_CONSTRAINTS_OVERRIDE"], 332 ), 333 # buildifier: disable=attr-license (calling attr.license()) 334 "licenses": attr.license() if hasattr(attr, "license") else attr.string_list(), 335 "_experimental_proto_descriptor_sets_include_source_info": attr.label( 336 default = "//bazel/private:experimental_proto_descriptor_sets_include_source_info", 337 ), 338 "_strict_proto_deps": attr.label( 339 default = 340 "//bazel/private:strict_proto_deps", 341 ), 342 "_strict_public_imports": attr.label( 343 default = "//bazel/private:strict_public_imports", 344 ), 345 } | toolchains.if_legacy_toolchain({ 346 "_proto_compiler": attr.label( 347 cfg = "exec", 348 executable = True, 349 allow_files = True, 350 default = configuration_field("proto", "proto_compiler"), 351 ), 352 }), # buildifier: disable=attr-licenses (attribute called licenses) 353 fragments = ["proto"], 354 provides = [ProtoInfo], 355 toolchains = toolchains.use_toolchain(toolchains.PROTO_TOOLCHAIN), 356) 357