| # Copyright 2022 The Pigweed Authors |
| # |
| # 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 |
| # |
| # https://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. |
| """WORK IN PROGRESS! |
| |
| This is intended to be a replacement for the proto codegen in proto.bzl, which |
| relies on the transitive proto compilation support removed from newer versions |
| of rules_proto_grpc. However, the version checked in here does not yet support, |
| |
| 1. Proto libraries with dependencies in external repositories. |
| 2. Proto libraries with strip_import_prefix or import_prefix attributes. |
| |
| In addition, nanopb proto files are not yet generated. |
| |
| TODO(b/234873954): Close these gaps and start using this implementation. |
| |
| # Overview of implementation |
| |
| (If you just want to use the macros, see their docstrings; this section is |
| intended to orient future maintainers.) |
| |
| Proto code generation is carried out by the _pw_proto_library, |
| _pw_raw_rpc_proto_library and _pw_nanopb_rpc_proto_library rules using aspects |
| (https://docs.bazel.build/versions/main/skylark/aspects.html). A |
| _pw_proto_library has a single proto_library as a dependency, but that |
| proto_library may depend on other proto_library targets; as a result, the |
| generated .pwpb.h file #include's .pwpb.h files generated from the dependency |
| proto_libraries. The aspect propagates along the proto_library dependency |
| graph, running the proto compiler on each proto_library in the original |
| target's transitive dependencies, ensuring that we're not missing any .pwpb.h |
| files at C++ compile time. |
| |
| Although we have a separate rule for each protocol compiler plugin |
| (_pw_proto_library, _pw_raw_rpc_proto_library, _pw_nanopb_rpc_proto_library), |
| they actually share an implementation (_pw _impl_pw_proto_library) and use |
| similar aspects, all generated by _proto_compiler_aspect. |
| """ |
| |
| load("//pw_build:pigweed.bzl", "pw_cc_library") |
| load("@rules_proto//proto:defs.bzl", "ProtoInfo") |
| load("//pw_protobuf_compiler:pw_nanopb_cc_library.bzl", "pw_nanopb_cc_library") |
| |
| def pwpb_proto_library(name, deps, tags = None, visibility = None): |
| """A C++ proto library generated using pw_protobuf. |
| |
| Attributes: |
| deps: proto_library targets for which to generate this library. |
| """ |
| name_pb = name + ".pb" |
| |
| _pw_proto_library( |
| name = name_pb, |
| deps = deps, |
| ) |
| |
| pw_cc_library( |
| name = name, |
| hdrs = [":" + name_pb], |
| deps = [ |
| "//pw_assert:facade", |
| "//pw_containers:vector", |
| "//pw_preprocessor", |
| "//pw_protobuf", |
| "//pw_result", |
| "//pw_span", |
| "//pw_status", |
| "//pw_string:string", |
| ], |
| linkstatic = 1, |
| tags = tags, |
| visibility = visibility, |
| ) |
| |
| def pwpb_rpc_proto_library(name, deps, pwpb_proto_library_deps, tags = None, visibility = None): |
| """A pwpb_rpc proto library target. |
| |
| Attributes: |
| deps: proto_library targets for which to generate this library. |
| pwpb_proto_library_deps: A pwpb_proto_library generated |
| from the same proto_library. Required. |
| """ |
| name_pb = name + ".pb" |
| |
| _pw_pwpb_rpc_proto_library( |
| name = name_pb, |
| deps = deps, |
| ) |
| |
| pw_cc_library( |
| name = name, |
| hdrs = [":" + name_pb], |
| deps = [ |
| "//pw_protobuf", |
| "//pw_rpc", |
| "//pw_rpc/pwpb:client_api", |
| "//pw_rpc/pwpb:server_api", |
| ] + pwpb_proto_library_deps, |
| linkstatic = 1, |
| tags = tags, |
| visibility = visibility, |
| ) |
| |
| def raw_rpc_proto_library(name, deps, tags = None, visibility = None): |
| """A raw C++ RPC proto library.""" |
| name_pb = name + ".pb" |
| |
| _pw_raw_rpc_proto_library( |
| name = name_pb, |
| deps = deps, |
| ) |
| |
| pw_cc_library( |
| name = name, |
| hdrs = [":" + name_pb], |
| deps = [ |
| "//pw_rpc", |
| "//pw_rpc/raw:client_api", |
| "//pw_rpc/raw:server_api", |
| ], |
| linkstatic = 1, |
| tags = tags, |
| visibility = visibility, |
| ) |
| |
| def nanopb_rpc_proto_library(name, deps, nanopb_proto_library_deps, tags = None, visibility = None): |
| """A C++ RPC proto library using nanopb. |
| |
| Attributes: |
| deps: proto_library targets for which to generate this library. |
| nanopb_proto_library_deps: A pw_nanopb_cc_library generated |
| from the same proto_library. Required. |
| """ |
| name_pb = name + ".pb" |
| |
| _pw_nanopb_rpc_proto_library( |
| name = name_pb, |
| deps = deps, |
| ) |
| |
| pw_cc_library( |
| name = name, |
| hdrs = [":" + name_pb], |
| deps = [ |
| "//pw_rpc", |
| "//pw_rpc/nanopb:client_api", |
| "//pw_rpc/nanopb:server_api", |
| ] + nanopb_proto_library_deps, |
| linkstatic = 1, |
| tags = tags, |
| visibility = visibility, |
| ) |
| |
| def pw_proto_library( |
| name, |
| deps, |
| visibility = None, |
| tags = None, |
| nanopb_options = None, |
| enabled_targets = None): |
| """Generate Pigweed proto C++ code. |
| |
| DEPRECATED. This macro is deprecated and will be removed in a future |
| Pigweed version. Please use the single-target macros above. |
| |
| Args: |
| name: The name of the target. |
| deps: proto_library targets from which to generate Pigweed C++. |
| visibility: The visibility of the target. See |
| https://bazel.build/concepts/visibility. |
| tags: Tags for the target. See |
| https://bazel.build/reference/be/common-definitions#common-attributes. |
| nanopb_options: path to file containing nanopb options, if any |
| (https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options). |
| enabled_targets: Specifies which libraries should be generated. Libraries |
| will only be generated as needed, but unnecessary outputs may conflict |
| with other build rules and thus cause build failures. This filter allows |
| manual selection of which libraries should be supported by this build |
| target in order to prevent such conflicts. The argument, if provided, |
| should be a subset of ["pwpb", "nanopb", "raw_rpc", "nanopb_rpc"]. All |
| are enabled by default. Note that "nanopb_rpc" relies on "nanopb". |
| |
| Example usage: |
| |
| proto_library( |
| name = "benchmark_proto", |
| srcs = [ |
| "benchmark.proto", |
| ], |
| ) |
| |
| pw_proto_library( |
| name = "benchmark_pw_proto", |
| deps = [":benchmark_proto"], |
| ) |
| |
| pw_cc_binary( |
| name = "proto_user", |
| srcs = ["proto_user.cc"], |
| deps = [":benchmark_pw_proto.pwpb"], |
| ) |
| |
| The pw_proto_library generates the following targets in this example: |
| |
| "benchmark_pw_proto.pwpb": C++ library exposing the "benchmark.pwpb.h" header. |
| "benchmark_pw_proto.pwpb_rpc": C++ library exposing the |
| "benchmark.rpc.pwpb.h" header. |
| "benchmark_pw_proto.raw_rpc": C++ library exposing the "benchmark.raw_rpc.h" |
| header. |
| "benchmark_pw_proto.nanopb": C++ library exposing the "benchmark.pb.h" |
| header. |
| "benchmark_pw_proto.nanopb_rpc": C++ library exposing the |
| "benchmark.rpc.pb.h" header. |
| """ |
| |
| def is_plugin_enabled(plugin): |
| return (enabled_targets == None or plugin in enabled_targets) |
| |
| if is_plugin_enabled("nanopb"): |
| # Use nanopb to generate the pb.h and pb.c files, and the target |
| # exposing them. |
| pw_nanopb_cc_library( |
| name = name + ".nanopb", |
| deps = deps, |
| visibility = visibility, |
| tags = tags, |
| options = nanopb_options, |
| ) |
| |
| if is_plugin_enabled("pwpb"): |
| pwpb_proto_library( |
| name = name + ".pwpb", |
| deps = deps, |
| tags = tags, |
| visibility = visibility, |
| ) |
| |
| if is_plugin_enabled("pwpb_rpc"): |
| pwpb_rpc_proto_library( |
| name = name + ".pwpb_rpc", |
| deps = deps, |
| pwpb_proto_library_deps = [":" + name + ".pwpb"], |
| tags = tags, |
| visibility = visibility, |
| ) |
| |
| if is_plugin_enabled("raw_rpc"): |
| raw_rpc_proto_library( |
| name = name + ".raw_rpc", |
| deps = deps, |
| tags = tags, |
| visibility = visibility, |
| ) |
| |
| if is_plugin_enabled("nanopb_rpc"): |
| nanopb_rpc_proto_library( |
| name = name + ".nanopb_rpc", |
| deps = deps, |
| nanopb_proto_library_deps = [":" + name + ".nanopb"], |
| tags = tags, |
| visibility = visibility, |
| ) |
| |
| PwProtoInfo = provider( |
| "Returned by PW proto compilation aspect", |
| fields = { |
| "genfiles": "generated C++ header files", |
| }, |
| ) |
| |
| PwProtoOptionsInfo = provider( |
| "Allows `pw_proto_filegroup` targets to pass along `.options` files " + |
| "without polluting the `DefaultInfo` provider, which means they can " + |
| "still be used in the `srcs` of `proto_library` targets.", |
| fields = { |
| "options_files": (".options file(s) associated with a proto_library " + |
| "for Pigweed codegen."), |
| }, |
| ) |
| |
| def _get_short_path(source): |
| return source.short_path |
| |
| def _get_path(file): |
| return file.path |
| |
| def _proto_compiler_aspect_impl(target, ctx): |
| # List the files we will generate for this proto_library target. |
| genfiles = [] |
| |
| for src in target[ProtoInfo].direct_sources: |
| path = src.basename[:-len("proto")] + ctx.attr._extension |
| genfiles.append(ctx.actions.declare_file(path, sibling = src)) |
| |
| # List the `.options` files from any `pw_proto_filegroup` targets listed |
| # under this target's `srcs`. |
| options_files = [ |
| options_file |
| for src in ctx.rule.attr.srcs |
| if PwProtoOptionsInfo in src |
| for options_file in src[PwProtoOptionsInfo].options_files.to_list() |
| ] |
| |
| # Convert include paths to a depset and back to deduplicate entries. |
| # Note that this will probably evaluate to either [] or ["."] in most cases. |
| options_file_include_paths = depset([ |
| "." if options_file.root.path == "" else options_file.root.path |
| for options_file in options_files |
| ]).to_list() |
| |
| args = ctx.actions.args() |
| args.add("--plugin=protoc-gen-pwpb={}".format(ctx.executable._protoc_plugin.path)) |
| for options_file_include_path in options_file_include_paths: |
| args.add("--pwpb_opt=-I{}".format(options_file_include_path)) |
| args.add("--pwpb_opt=--no-legacy-namespace") |
| args.add("--pwpb_out={}".format(ctx.bin_dir.path)) |
| args.add_joined( |
| "--descriptor_set_in", |
| target[ProtoInfo].transitive_descriptor_sets, |
| join_with = ctx.configuration.host_path_separator, |
| map_each = _get_path, |
| ) |
| |
| args.add_all(target[ProtoInfo].direct_sources, map_each = _get_short_path) |
| |
| ctx.actions.run( |
| inputs = depset( |
| target[ProtoInfo].transitive_sources.to_list() + options_files, |
| transitive = [target[ProtoInfo].transitive_descriptor_sets], |
| ), |
| progress_message = "Generating %s C++ files for %s" % (ctx.attr._extension, ctx.label.name), |
| tools = [ctx.executable._protoc_plugin], |
| outputs = genfiles, |
| executable = ctx.executable._protoc, |
| arguments = [args], |
| ) |
| |
| transitive_genfiles = genfiles |
| for dep in ctx.rule.attr.deps: |
| transitive_genfiles += dep[PwProtoInfo].genfiles |
| return [PwProtoInfo(genfiles = transitive_genfiles)] |
| |
| def _proto_compiler_aspect(extension, protoc_plugin): |
| """Returns an aspect that runs the proto compiler. |
| |
| The aspect propagates through the deps of proto_library targets, running |
| the proto compiler with the specified plugin for each of their source |
| files. The proto compiler is assumed to produce one output file per input |
| .proto file. That file is placed under bazel-bin at the same path as the |
| input file, but with the specified extension (i.e., with _extension = |
| .pwpb.h, the aspect converts pw_log/log.proto into |
| bazel-bin/pw_log/log.pwpb.h). |
| |
| The aspect returns a provider exposing all the File objects generated from |
| the dependency graph. |
| """ |
| return aspect( |
| attr_aspects = ["deps"], |
| attrs = { |
| "_extension": attr.string(default = extension), |
| "_protoc": attr.label( |
| default = Label("@com_google_protobuf//:protoc"), |
| executable = True, |
| cfg = "exec", |
| ), |
| "_protoc_plugin": attr.label( |
| default = Label(protoc_plugin), |
| executable = True, |
| cfg = "exec", |
| ), |
| }, |
| implementation = _proto_compiler_aspect_impl, |
| provides = [PwProtoInfo], |
| ) |
| |
| def _impl_pw_proto_library(ctx): |
| """Implementation of the proto codegen rule. |
| |
| The work of actually generating the code is done by the aspect, so here we |
| just gather up all the generated files and return them. |
| """ |
| |
| # Note that we don't distinguish between the files generated from the |
| # target, and the files generated from its dependencies. We return all of |
| # them together, and in pw_proto_library expose all of them as hdrs. |
| # Pigweed's plugins happen to only generate .h files, so this works, but |
| # strictly speaking we should expose only the files generated from the |
| # target itself in hdrs, and place the headers generated from dependencies |
| # in srcs. We don't perform layering_check in Pigweed, so this is not a big |
| # deal. |
| # |
| # TODO(b/234873954): Tidy this up. |
| all_genfiles = [] |
| for dep in ctx.attr.deps: |
| for f in dep[PwProtoInfo].genfiles: |
| all_genfiles.append(f) |
| |
| return [DefaultInfo(files = depset(all_genfiles))] |
| |
| # Instantiate the aspects and rules for generating code using specific plugins. |
| _pw_proto_compiler_aspect = _proto_compiler_aspect("pwpb.h", "//pw_protobuf/py:plugin") |
| |
| _pw_proto_library = rule( |
| implementation = _impl_pw_proto_library, |
| attrs = { |
| "deps": attr.label_list( |
| providers = [ProtoInfo], |
| aspects = [_pw_proto_compiler_aspect], |
| ), |
| }, |
| ) |
| |
| _pw_pwpb_rpc_proto_compiler_aspect = _proto_compiler_aspect("rpc.pwpb.h", "//pw_rpc/py:plugin_pwpb") |
| |
| _pw_pwpb_rpc_proto_library = rule( |
| implementation = _impl_pw_proto_library, |
| attrs = { |
| "deps": attr.label_list( |
| providers = [ProtoInfo], |
| aspects = [_pw_pwpb_rpc_proto_compiler_aspect], |
| ), |
| }, |
| ) |
| |
| _pw_raw_rpc_proto_compiler_aspect = _proto_compiler_aspect("raw_rpc.pb.h", "//pw_rpc/py:plugin_raw") |
| |
| _pw_raw_rpc_proto_library = rule( |
| implementation = _impl_pw_proto_library, |
| attrs = { |
| "deps": attr.label_list( |
| providers = [ProtoInfo], |
| aspects = [_pw_raw_rpc_proto_compiler_aspect], |
| ), |
| }, |
| ) |
| |
| _pw_nanopb_rpc_proto_compiler_aspect = _proto_compiler_aspect("rpc.pb.h", "//pw_rpc/py:plugin_nanopb") |
| |
| _pw_nanopb_rpc_proto_library = rule( |
| implementation = _impl_pw_proto_library, |
| attrs = { |
| "deps": attr.label_list( |
| providers = [ProtoInfo], |
| aspects = [_pw_nanopb_rpc_proto_compiler_aspect], |
| ), |
| }, |
| ) |
| |
| def _pw_proto_filegroup_impl(ctx): |
| source_files = list() |
| options_files = list() |
| |
| for src in ctx.attr.srcs: |
| source_files += src.files.to_list() |
| |
| for options_src in ctx.attr.options_files: |
| for file in options_src.files.to_list(): |
| if file.extension == "options": |
| options_files.append(file) |
| else: |
| fail(( |
| "Files provided as `options_files` to a " + |
| "`pw_proto_filegroup` must have the `.options` " + |
| "extension; the file `{}` was provided." |
| ).format(file.basename)) |
| |
| return [ |
| DefaultInfo(files = depset(source_files)), |
| PwProtoOptionsInfo(options_files = depset(options_files)), |
| ] |
| |
| pw_proto_filegroup = rule( |
| doc = ( |
| "Acts like a `filegroup`, but with an additional `options_files` " + |
| "attribute that accepts a list of `.options` files. These `.options` " + |
| "files should typically correspond to `.proto` files provided under " + |
| "the `srcs` attribute." + |
| "\n\n" + |
| "A `pw_proto_filegroup` is intended to be passed into the `srcs` of " + |
| "a `proto_library` target as if it were a normal `filegroup` " + |
| "containing only `.proto` files. For the purposes of the " + |
| "`proto_library` itself, the `pw_proto_filegroup` does indeed act " + |
| "just like a normal `filegroup`; the `options_files` attribute is " + |
| "ignored. However, if that `proto_library` target is then passed " + |
| "(directly or transitively) into the `deps` of a `pw_proto_library` " + |
| "for code generation, the `pw_proto_library` target will have access " + |
| "to the provided `.options` files and will pass them to the code " + |
| "generator." + |
| "\n\n" + |
| "Note that, in order for a `pw_proto_filegroup` to be a valid `srcs` " + |
| "entry for a `proto_library`, it must meet the same conditions " + |
| "required of a standard `filegroup` in that context. Namely, its " + |
| "`srcs` must provide at least one `.proto` (or `.protodevel`) file. " + |
| "Put simply, a `pw_proto_filegroup` cannot be used as a vector for " + |
| "injecting solely `.options` files; it must contain at least one " + |
| "proto as well (generally one associated with an included `.options` " + |
| "file in the interest of clarity)." + |
| "\n\n" + |
| "Regarding the somewhat unusual usage, this feature's design was " + |
| "mostly preordained by the combination of Bazel's strict access " + |
| "controls, the restrictions imposed on inputs to the `proto_library` " + |
| "rule, and the need to support `.options` files from transitive " + |
| "dependencies." |
| ), |
| implementation = _pw_proto_filegroup_impl, |
| attrs = { |
| "srcs": attr.label_list( |
| allow_files = True, |
| ), |
| "options_files": attr.label_list( |
| allow_files = True, |
| ), |
| }, |
| provides = [PwProtoOptionsInfo], |
| ) |