aboutsummaryrefslogtreecommitdiff
path: root/pw_protobuf_compiler/pw_proto_library.bzl
blob: f9a30484654f3839337a407d01653405a131d644 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# 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(pwbug/621): Close these gaps and start using this implementation.

# Overview of implementation

(If you just want to use pw_proto_library, see its docstring; 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. The only difference
between the rules are captured in the PIGWEED_PLUGIN dictonary and the aspect
instantiations (_pw_proto_compiler_aspect, etc).

"""

load("//pw_build:pigweed.bzl", "pw_cc_library")
load("@rules_proto//proto:defs.bzl", "ProtoInfo")
load("//pw_protobuf_compiler:pw_nanopb_cc_library", "pw_nanopb_cc_library")

def pw_proto_library(name = "", deps = [], nanopb_options = None):
    """Generate Pigweed proto C++ code.

    This is the only public symbol in this file: everything else is
    implementation details.

    Args:
      name: The name of the target.
      deps: proto_library targets from which to generate Pigweed C++.
      nanopb_options: path to file containing nanopb options, if any
        (https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options).

    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.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.
    """

    # Use nanopb to generate the pb.h and pb.c files, and the target exposing
    # them.
    pw_nanopb_cc_library(name + ".nanopb", deps, options = nanopb_options)

    # Use Pigweed proto plugins to generate the remaining files and targets.
    for plugin_name, info in PIGWEED_PLUGIN.items():
        name_pb = name + "_pb." + plugin_name
        info["compiler"](
            name = name_pb,
            deps = deps,
        )

        # The rpc.pb.h header depends on the generated nanopb code.
        if info["include_nanopb_dep"]:
            lib_deps = info["deps"] + [":" + name + ".nanopb"]
        else:
            lib_deps = info["deps"]

        pw_cc_library(
            name = name + "." + plugin_name,
            hdrs = [name_pb],
            deps = lib_deps,
            linkstatic = 1,
        )

PwProtoInfo = provider(
    "Returned by PW proto compilation aspect",
    fields = {
        "genfiles": "generated C++ header files",
    },
)

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))

    args = ctx.actions.args()
    args.add("--plugin=protoc-gen-pwpb={}".format(ctx.executable._protoc_plugin.path))
    args.add("--pwpb_out={}".format(ctx.bin_dir.path))
    args.add_joined(
        "--descriptor_set_in",
        target[ProtoInfo].transitive_descriptor_sets,
        join_with = ctx.host_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(), 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 = "host",
            ),
            "_protoc_plugin": attr.label(
                default = Label(protoc_plugin),
                executable = True,
                cfg = "host",
            ),
        },
        implementation = _proto_compiler_aspect_impl,
    )

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(pwbug/621): 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_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],
        ),
    },
)

PIGWEED_PLUGIN = {
    "pwpb": {
        "compiler": _pw_proto_library,
        "deps": [
            "//pw_span",
            "//pw_protobuf:pw_protobuf",
        ],
        "include_nanopb_dep": False,
    },
    "raw_rpc": {
        "compiler": _pw_raw_rpc_proto_library,
        "deps": [
            "//pw_rpc",
            "//pw_rpc/raw:client_api",
            "//pw_rpc/raw:server_api",
        ],
        "include_nanopb_dep": False,
    },
    "nanopb_rpc": {
        "compiler": _pw_nanopb_rpc_proto_library,
        "deps": [
            "//pw_rpc",
            "//pw_rpc/nanopb:client_api",
            "//pw_rpc/nanopb:server_api",
        ],
        "include_nanopb_dep": True,
    },
}