Ted Pudlik | df015c6 | 2022-02-02 22:32:26 +0000 | [diff] [blame] | 1 | # Copyright 2022 The Pigweed Authors |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| 4 | # use this file except in compliance with the License. You may obtain a copy of |
| 5 | # the License at |
| 6 | # |
| 7 | # https://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, WITHOUT |
| 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 12 | # License for the specific language governing permissions and limitations under |
| 13 | # the License. |
| 14 | """WORK IN PROGRESS! |
| 15 | |
| 16 | This is intended to be a replacement for the proto codegen in proto.bzl, which |
| 17 | relies on the transitive proto compilation support removed from newer versions |
| 18 | of rules_proto_grpc. However, the version checked in here does not yet support, |
| 19 | |
| 20 | 1. Proto libraries with dependencies in external repositories. |
| 21 | 2. Proto libraries with strip_import_prefix or import_prefix attributes. |
| 22 | |
| 23 | In addition, nanopb proto files are not yet generated. |
| 24 | |
| 25 | TODO(pwbug/621): Close these gaps and start using this implementation. |
| 26 | |
| 27 | # Overview of implementation |
| 28 | |
| 29 | (If you just want to use pw_proto_library, see its docstring; this section is |
| 30 | intended to orient future maintainers.) |
| 31 | |
| 32 | Proto code generation is carried out by the _pw_proto_library, |
| 33 | _pw_raw_rpc_proto_library and _pw_nanopb_rpc_proto_library rules using aspects |
| 34 | (https://docs.bazel.build/versions/main/skylark/aspects.html). A |
| 35 | _pw_proto_library has a single proto_library as a dependency, but that |
| 36 | proto_library may depend on other proto_library targets; as a result, the |
| 37 | generated .pwpb.h file #include's .pwpb.h files generated from the dependency |
| 38 | proto_libraries. The aspect propagates along the proto_library dependency |
| 39 | graph, running the proto compiler on each proto_library in the original |
| 40 | target's transitive dependencies, ensuring that we're not missing any .pwpb.h |
| 41 | files at C++ compile time. |
| 42 | |
| 43 | Although we have a separate rule for each protocol compiler plugin |
| 44 | (_pw_proto_library, _pw_raw_rpc_proto_library, _pw_nanopb_rpc_proto_library), |
| 45 | they actually share an implementation (_pw _impl_pw_proto_library) and use |
| 46 | similar aspects, all generated by _proto_compiler_aspect. The only difference |
| 47 | between the rules are captured in the PIGWEED_PLUGIN dictonary and the aspect |
| 48 | instantiations (_pw_proto_compiler_aspect, etc). |
| 49 | |
| 50 | """ |
| 51 | |
| 52 | load("//pw_build:pigweed.bzl", "pw_cc_library") |
| 53 | load("@rules_proto//proto:defs.bzl", "ProtoInfo") |
| 54 | load("//pw_protobuf_compiler:pw_nanopb_cc_library", "pw_nanopb_cc_library") |
| 55 | |
| 56 | def pw_proto_library(name = "", deps = [], nanopb_options = None): |
| 57 | """Generate Pigweed proto C++ code. |
| 58 | |
| 59 | This is the only public symbol in this file: everything else is |
| 60 | implementation details. |
| 61 | |
| 62 | Args: |
| 63 | name: The name of the target. |
| 64 | deps: proto_library targets from which to generate Pigweed C++. |
| 65 | nanopb_options: path to file containing nanopb options, if any |
| 66 | (https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options). |
| 67 | |
| 68 | Example usage: |
| 69 | |
| 70 | proto_library( |
| 71 | name = "benchmark_proto", |
| 72 | srcs = [ |
| 73 | "benchmark.proto", |
| 74 | ], |
| 75 | ) |
| 76 | |
| 77 | pw_proto_library( |
| 78 | name = "benchmark_pw_proto", |
| 79 | deps = [":benchmark_proto"], |
| 80 | ) |
| 81 | |
| 82 | pw_cc_binary( |
| 83 | name = "proto_user", |
| 84 | srcs = ["proto_user.cc"], |
| 85 | deps = [":benchmark_pw_proto.pwpb"], |
| 86 | ) |
| 87 | |
| 88 | The pw_proto_library generates the following targets in this example: |
| 89 | |
| 90 | "benchmark_pw_proto.pwpb": C++ library exposing the "benchmark.pwpb.h" header. |
| 91 | "benchmark_pw_proto.raw_rpc": C++ library exposing the "benchmark.raw_rpc.h" |
| 92 | header. |
| 93 | "benchmark_pw_proto.nanopb": C++ library exposing the "benchmark.pb.h" |
| 94 | header. |
| 95 | "benchmark_pw_proto.nanopb_rpc": C++ library exposing the |
| 96 | "benchmark.rpc.pb.h" header. |
| 97 | """ |
| 98 | |
| 99 | # Use nanopb to generate the pb.h and pb.c files, and the target exposing |
| 100 | # them. |
| 101 | pw_nanopb_cc_library(name + ".nanopb", deps, options = nanopb_options) |
| 102 | |
| 103 | # Use Pigweed proto plugins to generate the remaining files and targets. |
| 104 | for plugin_name, info in PIGWEED_PLUGIN.items(): |
| 105 | name_pb = name + "_pb." + plugin_name |
| 106 | info["compiler"]( |
| 107 | name = name_pb, |
| 108 | deps = deps, |
| 109 | ) |
| 110 | |
| 111 | # The rpc.pb.h header depends on the generated nanopb code. |
| 112 | if info["include_nanopb_dep"]: |
| 113 | lib_deps = info["deps"] + [":" + name + ".nanopb"] |
| 114 | else: |
| 115 | lib_deps = info["deps"] |
| 116 | |
| 117 | pw_cc_library( |
| 118 | name = name + "." + plugin_name, |
| 119 | hdrs = [name_pb], |
| 120 | deps = lib_deps, |
| 121 | linkstatic = 1, |
| 122 | ) |
| 123 | |
| 124 | PwProtoInfo = provider( |
| 125 | "Returned by PW proto compilation aspect", |
| 126 | fields = { |
| 127 | "genfiles": "generated C++ header files", |
| 128 | }, |
| 129 | ) |
| 130 | |
| 131 | def _get_short_path(source): |
| 132 | return source.short_path |
| 133 | |
| 134 | def _get_path(file): |
| 135 | return file.path |
| 136 | |
| 137 | def _proto_compiler_aspect_impl(target, ctx): |
| 138 | # List the files we will generate for this proto_library target. |
| 139 | genfiles = [] |
| 140 | for src in target[ProtoInfo].direct_sources: |
| 141 | path = src.basename[:-len("proto")] + ctx.attr._extension |
| 142 | genfiles.append(ctx.actions.declare_file(path, sibling = src)) |
| 143 | |
| 144 | args = ctx.actions.args() |
| 145 | args.add("--plugin=protoc-gen-pwpb={}".format(ctx.executable._protoc_plugin.path)) |
| 146 | args.add("--pwpb_out={}".format(ctx.bin_dir.path)) |
| 147 | args.add_joined( |
| 148 | "--descriptor_set_in", |
| 149 | target[ProtoInfo].transitive_descriptor_sets, |
| 150 | join_with = ctx.host_configuration.host_path_separator, |
| 151 | map_each = _get_path, |
| 152 | ) |
| 153 | args.add_all(target[ProtoInfo].direct_sources, map_each = _get_short_path) |
| 154 | |
| 155 | ctx.actions.run( |
| 156 | inputs = depset(target[ProtoInfo].transitive_sources.to_list(), transitive = [target[ProtoInfo].transitive_descriptor_sets]), |
| 157 | progress_message = "Generating %s C++ files for %s" % (ctx.attr._extension, ctx.label.name), |
| 158 | tools = [ctx.executable._protoc_plugin], |
| 159 | outputs = genfiles, |
| 160 | executable = ctx.executable._protoc, |
| 161 | arguments = [args], |
| 162 | ) |
| 163 | |
| 164 | transitive_genfiles = genfiles |
| 165 | for dep in ctx.rule.attr.deps: |
| 166 | transitive_genfiles += dep[PwProtoInfo].genfiles |
| 167 | return [PwProtoInfo(genfiles = transitive_genfiles)] |
| 168 | |
| 169 | def _proto_compiler_aspect(extension, protoc_plugin): |
| 170 | """Returns an aspect that runs the proto compiler. |
| 171 | |
| 172 | The aspect propagates through the deps of proto_library targets, running |
| 173 | the proto compiler with the specified plugin for each of their source |
| 174 | files. The proto compiler is assumed to produce one output file per input |
| 175 | .proto file. That file is placed under bazel-bin at the same path as the |
| 176 | input file, but with the specified extension (i.e., with _extension = |
| 177 | .pwpb.h, the aspect converts pw_log/log.proto into |
| 178 | bazel-bin/pw_log/log.pwpb.h). |
| 179 | |
| 180 | The aspect returns a provider exposing all the File objects generated from |
| 181 | the dependency graph. |
| 182 | """ |
| 183 | return aspect( |
| 184 | attr_aspects = ["deps"], |
| 185 | attrs = { |
| 186 | "_extension": attr.string(default = extension), |
| 187 | "_protoc": attr.label( |
| 188 | default = Label("@com_google_protobuf//:protoc"), |
| 189 | executable = True, |
| 190 | cfg = "host", |
| 191 | ), |
| 192 | "_protoc_plugin": attr.label( |
| 193 | default = Label(protoc_plugin), |
| 194 | executable = True, |
| 195 | cfg = "host", |
| 196 | ), |
| 197 | }, |
| 198 | implementation = _proto_compiler_aspect_impl, |
| 199 | ) |
| 200 | |
| 201 | def _impl_pw_proto_library(ctx): |
| 202 | """Implementation of the proto codegen rule. |
| 203 | |
| 204 | The work of actually generating the code is done by the aspect, so here we |
| 205 | just gather up all the generated files and return them. |
| 206 | """ |
| 207 | |
| 208 | # Note that we don't distinguish between the files generated from the |
| 209 | # target, and the files generated from its dependencies. We return all of |
| 210 | # them together, and in pw_proto_library expose all of them as hdrs. |
| 211 | # Pigweed's plugins happen to only generate .h files, so this works, but |
| 212 | # strictly speaking we should expose only the files generated from the |
| 213 | # target itself in hdrs, and place the headers generated from dependencies |
| 214 | # in srcs. We don't perform layering_check in Pigweed, so this is not a big |
| 215 | # deal. |
| 216 | # |
| 217 | # TODO(pwbug/621): Tidy this up. |
| 218 | all_genfiles = [] |
| 219 | for dep in ctx.attr.deps: |
| 220 | for f in dep[PwProtoInfo].genfiles: |
| 221 | all_genfiles.append(f) |
| 222 | |
| 223 | return [DefaultInfo(files = depset(all_genfiles))] |
| 224 | |
| 225 | # Instantiate the aspects and rules for generating code using specific plugins. |
| 226 | _pw_proto_compiler_aspect = _proto_compiler_aspect("pwpb.h", "//pw_protobuf/py:plugin") |
| 227 | |
| 228 | _pw_proto_library = rule( |
| 229 | implementation = _impl_pw_proto_library, |
| 230 | attrs = { |
| 231 | "deps": attr.label_list( |
| 232 | providers = [ProtoInfo], |
| 233 | aspects = [_pw_proto_compiler_aspect], |
| 234 | ), |
| 235 | }, |
| 236 | ) |
| 237 | |
Ted Pudlik | c0dc8e0 | 2022-02-08 23:10:32 +0000 | [diff] [blame] | 238 | _pw_raw_rpc_proto_compiler_aspect = _proto_compiler_aspect("raw_rpc.pb.h", "//pw_rpc/py:plugin_raw") |
Ted Pudlik | df015c6 | 2022-02-02 22:32:26 +0000 | [diff] [blame] | 239 | |
| 240 | _pw_raw_rpc_proto_library = rule( |
| 241 | implementation = _impl_pw_proto_library, |
| 242 | attrs = { |
| 243 | "deps": attr.label_list( |
| 244 | providers = [ProtoInfo], |
| 245 | aspects = [_pw_raw_rpc_proto_compiler_aspect], |
| 246 | ), |
| 247 | }, |
| 248 | ) |
| 249 | |
Ted Pudlik | c0dc8e0 | 2022-02-08 23:10:32 +0000 | [diff] [blame] | 250 | _pw_nanopb_rpc_proto_compiler_aspect = _proto_compiler_aspect("rpc.pb.h", "//pw_rpc/py:plugin_nanopb") |
Ted Pudlik | df015c6 | 2022-02-02 22:32:26 +0000 | [diff] [blame] | 251 | |
| 252 | _pw_nanopb_rpc_proto_library = rule( |
| 253 | implementation = _impl_pw_proto_library, |
| 254 | attrs = { |
| 255 | "deps": attr.label_list( |
| 256 | providers = [ProtoInfo], |
| 257 | aspects = [_pw_nanopb_rpc_proto_compiler_aspect], |
| 258 | ), |
| 259 | }, |
| 260 | ) |
| 261 | |
| 262 | PIGWEED_PLUGIN = { |
| 263 | "pwpb": { |
| 264 | "compiler": _pw_proto_library, |
| 265 | "deps": [ |
| 266 | "//pw_span", |
| 267 | "//pw_protobuf:pw_protobuf", |
| 268 | ], |
| 269 | "include_nanopb_dep": False, |
| 270 | }, |
| 271 | "raw_rpc": { |
| 272 | "compiler": _pw_raw_rpc_proto_library, |
| 273 | "deps": [ |
| 274 | "//pw_rpc", |
| 275 | "//pw_rpc/raw:client_api", |
| 276 | "//pw_rpc/raw:server_api", |
| 277 | ], |
| 278 | "include_nanopb_dep": False, |
| 279 | }, |
| 280 | "nanopb_rpc": { |
| 281 | "compiler": _pw_nanopb_rpc_proto_library, |
| 282 | "deps": [ |
| 283 | "//pw_rpc", |
Ted Pudlik | c0dc8e0 | 2022-02-08 23:10:32 +0000 | [diff] [blame] | 284 | "//pw_rpc/nanopb:client_api", |
| 285 | "//pw_rpc/nanopb:server_api", |
Ted Pudlik | df015c6 | 2022-02-02 22:32:26 +0000 | [diff] [blame] | 286 | ], |
| 287 | "include_nanopb_dep": True, |
| 288 | }, |
| 289 | } |