Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame^] | 1 | # Copyright 2018 The Bazel Authors. All rights reserved. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | """Rust Protobuf Rules |
| 16 | |
| 17 | These build rules are used for building [protobufs][protobuf]/[gRPC][grpc] in [Rust][rust] with Bazel. |
| 18 | |
| 19 | [rust]: http://www.rust-lang.org/ |
| 20 | [protobuf]: https://developers.google.com/protocol-buffers/ |
| 21 | [grpc]: https://grpc.io |
| 22 | |
| 23 | ### Setup |
| 24 | |
| 25 | To use the Rust proto rules, add the following to your `WORKSPACE` file to add the |
| 26 | external repositories for the Rust proto toolchain (in addition to the [rust rules setup](..)): |
| 27 | |
| 28 | ```python |
| 29 | load("//proto:repositories.bzl", "rust_proto_repositories") |
| 30 | |
| 31 | rust_proto_repositories() |
| 32 | ``` |
| 33 | """ |
| 34 | |
| 35 | load("@rules_proto//proto:defs.bzl", "ProtoInfo") |
| 36 | load( |
| 37 | "//proto:toolchain.bzl", |
| 38 | _generate_proto = "rust_generate_proto", |
| 39 | _generated_file_stem = "generated_file_stem", |
| 40 | ) |
| 41 | load("//rust:defs.bzl", "rust_common") |
| 42 | |
| 43 | # buildifier: disable=bzl-visibility |
| 44 | load("//rust/private:rustc.bzl", "rustc_compile_action") |
| 45 | |
| 46 | # buildifier: disable=bzl-visibility |
| 47 | load("//rust/private:utils.bzl", "compute_crate_name", "determine_output_hash", "find_toolchain", "transform_deps") |
| 48 | |
| 49 | RustProtoInfo = provider( |
| 50 | doc = "Rust protobuf provider info", |
| 51 | fields = { |
| 52 | "proto_sources": "List[string]: list of source paths of protos", |
| 53 | "transitive_proto_sources": "depset[string]", |
| 54 | }, |
| 55 | ) |
| 56 | |
| 57 | def _compute_proto_source_path(file, source_root_attr): |
| 58 | """Take the short path of file and make it suitable for protoc. |
| 59 | |
| 60 | Args: |
| 61 | file (File): The target source file. |
| 62 | source_root_attr (str): The directory relative to which the `.proto` \ |
| 63 | files defined in the proto_library are defined. |
| 64 | |
| 65 | Returns: |
| 66 | str: The protoc suitible path of `file` |
| 67 | """ |
| 68 | |
| 69 | # Bazel creates symlinks to the .proto files under a directory called |
| 70 | # "_virtual_imports/<rule name>" if we do any sort of munging of import |
| 71 | # paths (e.g. using strip_import_prefix / import_prefix attributes) |
| 72 | virtual_imports = "/_virtual_imports/" |
| 73 | if virtual_imports in file.path: |
| 74 | return file.path.split(virtual_imports)[1].split("/", 1)[1] |
| 75 | |
| 76 | # For proto, they need to be requested with their absolute name to be |
| 77 | # compatible with the descriptor_set passed by proto_library. |
| 78 | # I.e. if you compile a protobuf at @repo1//package:file.proto, the proto |
| 79 | # compiler would generate a file descriptor with the path |
| 80 | # `package/file.proto`. Since we compile from the proto descriptor, we need |
| 81 | # to pass the list of descriptors and the list of path to compile. |
| 82 | # For the precedent example, the file (noted `f`) would have |
| 83 | # `f.short_path` returns `external/repo1/package/file.proto`. |
| 84 | # In addition, proto_library can provide a proto_source_path to change the base |
| 85 | # path, which should a be a prefix. |
| 86 | path = file.short_path |
| 87 | |
| 88 | # Strip external prefix. |
| 89 | path = path.split("/", 2)[2] if path.startswith("../") else path |
| 90 | |
| 91 | # Strip source_root. |
| 92 | if path.startswith(source_root_attr): |
| 93 | return path[len(source_root_attr):] |
| 94 | else: |
| 95 | return path |
| 96 | |
| 97 | def _rust_proto_aspect_impl(target, ctx): |
| 98 | """The implementation of the `rust_proto_aspect` aspect |
| 99 | |
| 100 | Args: |
| 101 | target (Target): The target to which the aspect is applied |
| 102 | ctx (ctx): The rule context which the targetis created from |
| 103 | |
| 104 | Returns: |
| 105 | list: A list containg a `RustProtoInfo` provider |
| 106 | """ |
| 107 | if ProtoInfo not in target: |
| 108 | return None |
| 109 | |
| 110 | if hasattr(ctx.rule.attr, "proto_source_root"): |
| 111 | source_root = ctx.rule.attr.proto_source_root |
| 112 | else: |
| 113 | source_root = "" |
| 114 | |
| 115 | if source_root and source_root[-1] != "/": |
| 116 | source_root += "/" |
| 117 | |
| 118 | sources = [ |
| 119 | _compute_proto_source_path(f, source_root) |
| 120 | for f in target[ProtoInfo].direct_sources |
| 121 | ] |
| 122 | transitive_sources = [ |
| 123 | f[RustProtoInfo].transitive_proto_sources |
| 124 | for f in ctx.rule.attr.deps |
| 125 | if RustProtoInfo in f |
| 126 | ] |
| 127 | return [RustProtoInfo( |
| 128 | proto_sources = sources, |
| 129 | transitive_proto_sources = depset(transitive = transitive_sources, direct = sources), |
| 130 | )] |
| 131 | |
| 132 | _rust_proto_aspect = aspect( |
| 133 | doc = "An aspect that gathers rust proto direct and transitive sources", |
| 134 | implementation = _rust_proto_aspect_impl, |
| 135 | attr_aspects = ["deps"], |
| 136 | ) |
| 137 | |
| 138 | def _gen_lib(ctx, grpc, srcs, lib): |
| 139 | """Generate a lib.rs file for the crates. |
| 140 | |
| 141 | Args: |
| 142 | ctx (ctx): The current rule's context object |
| 143 | grpc (bool): True if the current rule is a `gRPC` rule. |
| 144 | srcs (list): A list of protoc suitible file paths (str). |
| 145 | lib (File): The File object where the rust source file should be written |
| 146 | """ |
| 147 | content = ["extern crate protobuf;"] |
| 148 | if grpc: |
| 149 | content.append("extern crate grpc;") |
| 150 | content.append("extern crate tls_api;") |
| 151 | for f in srcs.to_list(): |
| 152 | content.append("pub mod %s;" % _generated_file_stem(f)) |
| 153 | content.append("pub use %s::*;" % _generated_file_stem(f)) |
| 154 | if grpc: |
| 155 | content.append("pub mod %s_grpc;" % _generated_file_stem(f)) |
| 156 | content.append("pub use %s_grpc::*;" % _generated_file_stem(f)) |
| 157 | ctx.actions.write(lib, "\n".join(content)) |
| 158 | |
| 159 | def _expand_provider(lst, provider): |
| 160 | """Gathers a list of a specific provider from a list of targets. |
| 161 | |
| 162 | Args: |
| 163 | lst (list): A list of Targets |
| 164 | provider (Provider): The target provider type to extract `lst` |
| 165 | |
| 166 | Returns: |
| 167 | list: A list of providers of the type from `provider`. |
| 168 | """ |
| 169 | return [el[provider] for el in lst if provider in el] |
| 170 | |
| 171 | def _rust_proto_compile(protos, descriptor_sets, imports, crate_name, ctx, is_grpc, compile_deps, toolchain): |
| 172 | """Create and run a rustc compile action based on the current rule's attributes |
| 173 | |
| 174 | Args: |
| 175 | protos (depset): Paths of protos to compile. |
| 176 | descriptor_sets (depset): A set of transitive protobuf `FileDescriptorSet`s |
| 177 | imports (depset): A set of transitive protobuf Imports. |
| 178 | crate_name (str): The name of the Crate for the current target |
| 179 | ctx (ctx): The current rule's context object |
| 180 | is_grpc (bool): True if the current rule is a `gRPC` rule. |
| 181 | compile_deps (list): A list of Rust dependencies (`List[Target]`) |
| 182 | toolchain (rust_toolchain): the current `rust_toolchain`. |
| 183 | |
| 184 | Returns: |
| 185 | list: A list of providers, see `rustc_compile_action` |
| 186 | """ |
| 187 | |
| 188 | # Create all the source in a specific folder |
| 189 | proto_toolchain = ctx.toolchains[Label("//proto:toolchain")] |
| 190 | output_dir = "%s.%s.rust" % (crate_name, "grpc" if is_grpc else "proto") |
| 191 | |
| 192 | # Generate the proto stubs |
| 193 | srcs = _generate_proto( |
| 194 | ctx, |
| 195 | descriptor_sets, |
| 196 | protos = protos, |
| 197 | imports = imports, |
| 198 | output_dir = output_dir, |
| 199 | proto_toolchain = proto_toolchain, |
| 200 | is_grpc = is_grpc, |
| 201 | ) |
| 202 | |
| 203 | # and lib.rs |
| 204 | lib_rs = ctx.actions.declare_file("%s/lib.rs" % output_dir) |
| 205 | _gen_lib(ctx, is_grpc, protos, lib_rs) |
| 206 | srcs.append(lib_rs) |
| 207 | |
| 208 | # And simulate rust_library behavior |
| 209 | output_hash = determine_output_hash(lib_rs, ctx.label) |
| 210 | rust_lib = ctx.actions.declare_file("%s/lib%s-%s.rlib" % ( |
| 211 | output_dir, |
| 212 | crate_name, |
| 213 | output_hash, |
| 214 | )) |
| 215 | |
| 216 | # Gather all dependencies for compilation |
| 217 | compile_action_deps = depset( |
| 218 | transform_deps( |
| 219 | compile_deps + |
| 220 | proto_toolchain.grpc_compile_deps if is_grpc else proto_toolchain.proto_compile_deps, |
| 221 | ), |
| 222 | ) |
| 223 | |
| 224 | return rustc_compile_action( |
| 225 | ctx = ctx, |
| 226 | attr = ctx.attr, |
| 227 | toolchain = toolchain, |
| 228 | crate_info = rust_common.create_crate_info( |
| 229 | name = crate_name, |
| 230 | type = "rlib", |
| 231 | root = lib_rs, |
| 232 | srcs = depset(srcs), |
| 233 | deps = compile_action_deps, |
| 234 | proc_macro_deps = depset([]), |
| 235 | aliases = {}, |
| 236 | output = rust_lib, |
| 237 | edition = proto_toolchain.edition, |
| 238 | rustc_env = {}, |
| 239 | is_test = False, |
| 240 | compile_data = depset([target.files for target in getattr(ctx.attr, "compile_data", [])]), |
| 241 | wrapped_crate_type = None, |
| 242 | owner = ctx.label, |
| 243 | ), |
| 244 | output_hash = output_hash, |
| 245 | ) |
| 246 | |
| 247 | def _rust_protogrpc_library_impl(ctx, is_grpc): |
| 248 | """Implementation of the rust_(proto|grpc)_library. |
| 249 | |
| 250 | Args: |
| 251 | ctx (ctx): The current rule's context object |
| 252 | is_grpc (bool): True if the current rule is a `gRPC` rule. |
| 253 | |
| 254 | Returns: |
| 255 | list: A list of providers, see `_rust_proto_compile` |
| 256 | """ |
| 257 | proto = _expand_provider(ctx.attr.deps, ProtoInfo) |
| 258 | transitive_sources = [ |
| 259 | f[RustProtoInfo].transitive_proto_sources |
| 260 | for f in ctx.attr.deps |
| 261 | if RustProtoInfo in f |
| 262 | ] |
| 263 | |
| 264 | toolchain = find_toolchain(ctx) |
| 265 | crate_name = compute_crate_name(ctx.workspace_name, ctx.label, toolchain) |
| 266 | |
| 267 | return _rust_proto_compile( |
| 268 | protos = depset(transitive = transitive_sources), |
| 269 | descriptor_sets = depset(transitive = [p.transitive_descriptor_sets for p in proto]), |
| 270 | imports = depset(transitive = [p.transitive_imports for p in proto]), |
| 271 | crate_name = crate_name, |
| 272 | ctx = ctx, |
| 273 | is_grpc = is_grpc, |
| 274 | compile_deps = ctx.attr.rust_deps, |
| 275 | toolchain = toolchain, |
| 276 | ) |
| 277 | |
| 278 | def _rust_proto_library_impl(ctx): |
| 279 | """The implementation of the `rust_proto_library` rule |
| 280 | |
| 281 | Args: |
| 282 | ctx (ctx): The rule's context object. |
| 283 | |
| 284 | Returns: |
| 285 | list: A list of providers, see `_rust_protogrpc_library_impl` |
| 286 | """ |
| 287 | return _rust_protogrpc_library_impl(ctx, False) |
| 288 | |
| 289 | rust_proto_library = rule( |
| 290 | implementation = _rust_proto_library_impl, |
| 291 | attrs = { |
| 292 | "deps": attr.label_list( |
| 293 | doc = ( |
| 294 | "List of proto_library dependencies that will be built. " + |
| 295 | "One crate for each proto_library will be created with the corresponding stubs." |
| 296 | ), |
| 297 | mandatory = True, |
| 298 | providers = [ProtoInfo], |
| 299 | aspects = [_rust_proto_aspect], |
| 300 | ), |
| 301 | "rust_deps": attr.label_list( |
| 302 | doc = "The crates the generated library depends on.", |
| 303 | ), |
| 304 | "_cc_toolchain": attr.label( |
| 305 | default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), |
| 306 | ), |
| 307 | "_optional_output_wrapper": attr.label( |
| 308 | executable = True, |
| 309 | cfg = "exec", |
| 310 | default = Label("//proto:optional_output_wrapper"), |
| 311 | ), |
| 312 | "_process_wrapper": attr.label( |
| 313 | default = Label("//util/process_wrapper"), |
| 314 | executable = True, |
| 315 | allow_single_file = True, |
| 316 | cfg = "exec", |
| 317 | ), |
| 318 | }, |
| 319 | fragments = ["cpp"], |
| 320 | host_fragments = ["cpp"], |
| 321 | toolchains = [ |
| 322 | str(Label("//proto:toolchain")), |
| 323 | str(Label("//rust:toolchain")), |
| 324 | "@bazel_tools//tools/cpp:toolchain_type", |
| 325 | ], |
| 326 | # TODO: Remove once (bazelbuild/bazel#11584) is closed and the rules use |
| 327 | # the version of Bazel that issue was closed on as the min supported version |
| 328 | incompatible_use_toolchain_transition = True, |
| 329 | doc = """\ |
| 330 | Builds a Rust library crate from a set of `proto_library`s. |
| 331 | |
| 332 | Example: |
| 333 | |
| 334 | ```python |
| 335 | load("@rules_rust//proto:proto.bzl", "rust_proto_library") |
| 336 | |
| 337 | proto_library( |
| 338 | name = "my_proto", |
| 339 | srcs = ["my.proto"] |
| 340 | ) |
| 341 | |
| 342 | rust_proto_library( |
| 343 | name = "rust", |
| 344 | deps = [":my_proto"], |
| 345 | ) |
| 346 | |
| 347 | rust_binary( |
| 348 | name = "my_proto_binary", |
| 349 | srcs = ["my_proto_binary.rs"], |
| 350 | deps = [":rust"], |
| 351 | ) |
| 352 | ``` |
| 353 | """, |
| 354 | ) |
| 355 | |
| 356 | def _rust_grpc_library_impl(ctx): |
| 357 | """The implementation of the `rust_grpc_library` rule |
| 358 | |
| 359 | Args: |
| 360 | ctx (ctx): The rule's context object |
| 361 | |
| 362 | Returns: |
| 363 | list: A list of providers. See `_rust_protogrpc_library_impl` |
| 364 | """ |
| 365 | return _rust_protogrpc_library_impl(ctx, True) |
| 366 | |
| 367 | rust_grpc_library = rule( |
| 368 | implementation = _rust_grpc_library_impl, |
| 369 | attrs = { |
| 370 | "deps": attr.label_list( |
| 371 | doc = ( |
| 372 | "List of proto_library dependencies that will be built. " + |
| 373 | "One crate for each proto_library will be created with the corresponding gRPC stubs." |
| 374 | ), |
| 375 | mandatory = True, |
| 376 | providers = [ProtoInfo], |
| 377 | aspects = [_rust_proto_aspect], |
| 378 | ), |
| 379 | "rust_deps": attr.label_list( |
| 380 | doc = "The crates the generated library depends on.", |
| 381 | ), |
| 382 | "_cc_toolchain": attr.label( |
| 383 | default = "@bazel_tools//tools/cpp:current_cc_toolchain", |
| 384 | ), |
| 385 | "_optional_output_wrapper": attr.label( |
| 386 | executable = True, |
| 387 | cfg = "exec", |
| 388 | default = Label("//proto:optional_output_wrapper"), |
| 389 | ), |
| 390 | "_process_wrapper": attr.label( |
| 391 | default = Label("//util/process_wrapper"), |
| 392 | executable = True, |
| 393 | allow_single_file = True, |
| 394 | cfg = "exec", |
| 395 | ), |
| 396 | }, |
| 397 | fragments = ["cpp"], |
| 398 | host_fragments = ["cpp"], |
| 399 | toolchains = [ |
| 400 | str(Label("//proto:toolchain")), |
| 401 | str(Label("//rust:toolchain")), |
| 402 | "@bazel_tools//tools/cpp:toolchain_type", |
| 403 | ], |
| 404 | # TODO: Remove once (bazelbuild/bazel#11584) is closed and the rules use |
| 405 | # the version of Bazel that issue was closed on as the min supported version |
| 406 | incompatible_use_toolchain_transition = True, |
| 407 | doc = """\ |
| 408 | Builds a Rust library crate from a set of `proto_library`s suitable for gRPC. |
| 409 | |
| 410 | Example: |
| 411 | |
| 412 | ```python |
| 413 | load("//proto:proto.bzl", "rust_grpc_library") |
| 414 | |
| 415 | proto_library( |
| 416 | name = "my_proto", |
| 417 | srcs = ["my.proto"] |
| 418 | ) |
| 419 | |
| 420 | rust_grpc_library( |
| 421 | name = "rust", |
| 422 | deps = [":my_proto"], |
| 423 | ) |
| 424 | |
| 425 | rust_binary( |
| 426 | name = "my_service", |
| 427 | srcs = ["my_service.rs"], |
| 428 | deps = [":rust"], |
| 429 | ) |
| 430 | ``` |
| 431 | """, |
| 432 | ) |