• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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# Overview of implementation
17
18(If you just want to use the macros, see their docstrings; this section is
19intended to orient future maintainers.)
20
21Proto code generation is carried out by the _pwpb_proto_library,
22_nanopb_proto_library, _pw_raw_rpc_proto_library and
23_pw_nanopb_rpc_proto_library rules using aspects
24(https://docs.bazel.build/versions/main/skylark/aspects.html).
25
26As an example, _pwpb_proto_library has a single proto_library as a dependency,
27but that proto_library may depend on other proto_library targets; as a result,
28the generated .pwpb.h file #include's .pwpb.h files generated from the
29dependency proto_libraries. The aspect propagates along the proto_library
30dependency graph, running the proto compiler on each proto_library in the
31original target's transitive dependencies, ensuring that we're not missing any
32.pwpb.h files at C++ compile time.
33
34Although we have a separate rule for each protocol compiler plugin
35(_pwpb_proto_library, _nanopb_proto_library, _pw_raw_rpc_proto_library,
36_pw_nanopb_rpc_proto_library), they actually share an implementation
37(_impl_pw_proto_library) and use similar aspects, all generated by
38_proto_compiler_aspect.
39"""
40
41load("@bazel_skylib//lib:paths.bzl", "paths")
42load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "use_cpp_toolchain")
43load(
44    "@pigweed//pw_build/bazel_internal:pigweed_internal.bzl",
45    _compile_cc = "compile_cc",
46)
47load("@rules_proto//proto:defs.bzl", "ProtoInfo")
48
49# For Copybara use only
50ADDITIONAL_PWPB_DEPS = []
51
52def pwpb_proto_library(name, deps, tags = None, visibility = None):
53    """A C++ proto library generated using pw_protobuf.
54
55    Attributes:
56      deps: proto_library targets for which to generate this library.
57    """
58    _pwpb_proto_library(
59        name = name,
60        protos = deps,
61        deps = [
62            Label("//pw_assert"),
63            Label("//pw_containers:vector"),
64            Label("//pw_preprocessor"),
65            Label("//pw_protobuf"),
66            Label("//pw_result"),
67            Label("//pw_span"),
68            Label("//pw_status"),
69            Label("//pw_string:string"),
70        ] + ADDITIONAL_PWPB_DEPS,
71        tags = tags,
72        visibility = visibility,
73    )
74
75def pwpb_rpc_proto_library(name, deps, pwpb_proto_library_deps, tags = None, visibility = None):
76    """A pwpb_rpc proto library target.
77
78    Attributes:
79      deps: proto_library targets for which to generate this library.
80      pwpb_proto_library_deps: A pwpb_proto_library generated
81        from the same proto_library. Required.
82    """
83    _pw_pwpb_rpc_proto_library(
84        name = name,
85        protos = deps,
86        deps = [
87            Label("//pw_protobuf"),
88            Label("//pw_rpc"),
89            Label("//pw_rpc/pwpb:client_api"),
90            Label("//pw_rpc/pwpb:server_api"),
91        ] + pwpb_proto_library_deps,
92        tags = tags,
93        visibility = visibility,
94    )
95
96def raw_rpc_proto_library(name, deps, tags = None, visibility = None):
97    """A raw C++ RPC proto library."""
98    _pw_raw_rpc_proto_library(
99        name = name,
100        protos = deps,
101        deps = [
102            Label("//pw_rpc"),
103            Label("//pw_rpc/raw:client_api"),
104            Label("//pw_rpc/raw:server_api"),
105        ],
106        tags = tags,
107        visibility = visibility,
108    )
109
110# TODO: b/234873954 - Enable unused variable check.
111# buildifier: disable=unused-variable
112def nanopb_proto_library(name, deps, tags = [], visibility = None, options = None):
113    """A C++ proto library generated using pw_protobuf.
114
115    Attributes:
116      deps: proto_library targets for which to generate this library.
117    """
118
119    # TODO(tpudlik): Find a way to get Nanopb to generate nested structs.
120    # Otherwise add the manual tag to the resulting library, preventing it
121    # from being built unless directly depended on.  e.g. The 'Pigweed'
122    # message in
123    # pw_protobuf/pw_protobuf_test_protos/full_test.proto will fail to
124    # compile as it has a self referring nested message. According to
125    # the docs
126    # https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
127    # and https://github.com/nanopb/nanopb/issues/433 it seems like it
128    # should be possible to configure nanopb to generate nested structs via
129    # flags in .options files.
130    #
131    # One issue is nanopb doesn't silently ignore unknown options in .options
132    # files so we can't share .options files with pwpb.
133    extra_tags = ["manual"]
134    _nanopb_proto_library(
135        name = name,
136        protos = deps,
137        deps = [
138            "@com_github_nanopb_nanopb//:nanopb",
139            Label("//pw_assert"),
140            Label("//pw_containers:vector"),
141            Label("//pw_preprocessor"),
142            Label("//pw_result"),
143            Label("//pw_span"),
144            Label("//pw_status"),
145            Label("//pw_string:string"),
146        ],
147        tags = tags + extra_tags,
148        visibility = visibility,
149    )
150
151def nanopb_rpc_proto_library(name, deps, nanopb_proto_library_deps, tags = [], visibility = None):
152    """A C++ RPC proto library using nanopb.
153
154    Attributes:
155      deps: proto_library targets for which to generate this library.
156      nanopb_proto_library_deps: A pw_nanopb_cc_library generated
157        from the same proto_library. Required.
158    """
159
160    # See comment in nanopb_proto_library.
161    extra_tags = ["manual"]
162    _pw_nanopb_rpc_proto_library(
163        name = name,
164        protos = deps,
165        # TODO: b/339280821 - This is required to avoid breaking internal
166        # Google builds but shouldn't matter for any external user. Remove this
167        # when possible.
168        features = ["-layering_check"],
169        deps = [
170            Label("//pw_rpc"),
171            Label("//pw_rpc/nanopb:client_api"),
172            Label("//pw_rpc/nanopb:server_api"),
173        ] + nanopb_proto_library_deps,
174        tags = tags + extra_tags,
175        visibility = visibility,
176    )
177
178def pw_proto_library(
179        name,
180        deps,
181        visibility = None,
182        tags = [],
183        nanopb_options = None,
184        enabled_targets = None):
185    """Generate Pigweed proto C++ code.
186
187    DEPRECATED. This macro is deprecated and will be removed in a future
188    Pigweed version. Please use the single-target macros above.
189
190    Args:
191      name: The name of the target.
192      deps: proto_library targets from which to generate Pigweed C++.
193      visibility: The visibility of the target. See
194         https://bazel.build/concepts/visibility.
195      tags: Tags for the target. See
196         https://bazel.build/reference/be/common-definitions#common-attributes.
197      nanopb_options: path to file containing nanopb options, if any
198        (https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options).
199      enabled_targets: Specifies which libraries should be generated. Libraries
200        will only be generated as needed, but unnecessary outputs may conflict
201        with other build rules and thus cause build failures. This filter allows
202        manual selection of which libraries should be supported by this build
203        target in order to prevent such conflicts. The argument, if provided,
204        should be a subset of ["pwpb", "nanopb", "raw_rpc", "nanopb_rpc"]. All
205        are enabled by default. Note that "nanopb_rpc" relies on "nanopb".
206
207    Example usage:
208
209      proto_library(
210        name = "benchmark_proto",
211        srcs = [
212          "benchmark.proto",
213        ],
214      )
215
216      pw_proto_library(
217        name = "benchmark_pw_proto",
218        deps = [":benchmark_proto"],
219      )
220
221      pw_cc_binary(
222        name = "proto_user",
223        srcs = ["proto_user.cc"],
224        deps = [":benchmark_pw_proto.pwpb"],
225      )
226
227    The pw_proto_library generates the following targets in this example:
228
229    "benchmark_pw_proto.pwpb": C++ library exposing the "benchmark.pwpb.h" header.
230    "benchmark_pw_proto.pwpb_rpc": C++ library exposing the
231        "benchmark.rpc.pwpb.h" header.
232    "benchmark_pw_proto.raw_rpc": C++ library exposing the "benchmark.raw_rpc.h"
233        header.
234    "benchmark_pw_proto.nanopb": C++ library exposing the "benchmark.pb.h"
235        header.
236    "benchmark_pw_proto.nanopb_rpc": C++ library exposing the
237        "benchmark.rpc.pb.h" header.
238    """
239
240    def is_plugin_enabled(plugin):
241        return (enabled_targets == None or plugin in enabled_targets)
242
243    if is_plugin_enabled("nanopb"):
244        # Use nanopb to generate the pb.h and pb.c files, and the target
245        # exposing them.
246        nanopb_proto_library(
247            name = name + ".nanopb",
248            deps = deps,
249            tags = tags,
250            visibility = visibility,
251            options = nanopb_options,
252        )
253
254    if is_plugin_enabled("pwpb"):
255        pwpb_proto_library(
256            name = name + ".pwpb",
257            deps = deps,
258            tags = tags,
259            visibility = visibility,
260        )
261
262    if is_plugin_enabled("pwpb_rpc"):
263        pwpb_rpc_proto_library(
264            name = name + ".pwpb_rpc",
265            deps = deps,
266            pwpb_proto_library_deps = [":" + name + ".pwpb"],
267            tags = tags,
268            visibility = visibility,
269        )
270
271    if is_plugin_enabled("raw_rpc"):
272        raw_rpc_proto_library(
273            name = name + ".raw_rpc",
274            deps = deps,
275            tags = tags,
276            visibility = visibility,
277        )
278
279    if is_plugin_enabled("nanopb_rpc"):
280        nanopb_rpc_proto_library(
281            name = name + ".nanopb_rpc",
282            deps = deps,
283            nanopb_proto_library_deps = [":" + name + ".nanopb"],
284            tags = tags,
285            visibility = visibility,
286        )
287
288PwProtoInfo = provider(
289    "Returned by PW proto compilation aspect",
290    fields = {
291        "hdrs": "generated C++ header files",
292        "includes": "include paths for generated C++ header files",
293        "srcs": "generated C++ src files",
294    },
295)
296
297PwProtoOptionsInfo = provider(
298    "Allows `pw_proto_filegroup` targets to pass along `.options` files " +
299    "without polluting the `DefaultInfo` provider, which means they can " +
300    "still be used in the `srcs` of `proto_library` targets.",
301    fields = {
302        "options_files": (".options file(s) associated with a proto_library " +
303                          "for Pigweed codegen."),
304    },
305)
306
307def _proto_compiler_aspect_impl(target, ctx):
308    # List the files we will generate for this proto_library target.
309    proto_info = target[ProtoInfo]
310
311    srcs = []
312    hdrs = []
313
314    # Setup the output root for the plugin to point to targets output
315    # directory. This allows us to declare the location of the files that protoc
316    # will output in a way that `ctx.actions.declare_file` will understand,
317    # since it works relative to the target.
318    out_path = ctx.bin_dir.path
319    if target.label.workspace_root:
320        out_path += "/" + target.label.workspace_root
321    if target.label.package:
322        out_path += "/" + target.label.package
323
324    # Add location of headers to cc include path.
325    # Depending on prefix rules, the include path can be directly from the
326    # output path, or underneath the package.
327    includes = [out_path]
328
329    for src in proto_info.direct_sources:
330        # Get the relative import path for this .proto file.
331        src_rel = paths.relativize(src.path, proto_info.proto_source_root)
332        proto_dir = paths.dirname(src_rel)
333
334        # Add location of headers to cc include path.
335        includes.append("{}/{}".format(out_path, src.owner.package))
336
337        for ext in ctx.attr._extensions:
338            # Declare all output files, in target package dir.
339            generated_filename = src.basename[:-len("proto")] + ext
340            if proto_dir:
341                out_file_name = "{}/{}".format(
342                    proto_dir,
343                    generated_filename,
344                )
345            else:
346                out_file_name = generated_filename
347
348            out_file = ctx.actions.declare_file(out_file_name)
349
350            if ext.endswith(".h"):
351                hdrs.append(out_file)
352            else:
353                srcs.append(out_file)
354
355    # List the `.options` files from any `pw_proto_filegroup` targets listed
356    # under this target's `srcs`.
357    options_files = [
358        options_file
359        for src in ctx.rule.attr.srcs
360        if PwProtoOptionsInfo in src
361        for options_file in src[PwProtoOptionsInfo].options_files.to_list()
362    ]
363
364    # Local repository options files.
365    options_file_include_paths = [paths.join(".", ctx.rule.attr.strip_import_prefix.lstrip("/"))]
366    for options_file in options_files:
367        # Handle .options files residing in external repositories.
368        if options_file.owner.workspace_root:
369            options_file_include_paths.append(
370                paths.join(
371                    options_file.owner.workspace_root,
372                    ctx.rule.attr.strip_import_prefix.lstrip("/"),
373                ),
374            )
375
376        # Handle generated .options files.
377        if options_file.root.path:
378            options_file_include_paths.append(
379                paths.join(
380                    options_file.root.path,
381                    ctx.rule.attr.strip_import_prefix.lstrip("/"),
382                ),
383            )
384
385    args = ctx.actions.args()
386    for path in proto_info.transitive_proto_path.to_list():
387        args.add("-I{}".format(path))
388
389    args.add("--plugin=protoc-gen-custom={}".format(ctx.executable._protoc_plugin.path))
390
391    # Convert include paths to a depset and back to deduplicate entries.
392    for options_file_include_path in depset(options_file_include_paths).to_list():
393        args.add("--custom_opt=-I{}".format(options_file_include_path))
394
395    for plugin_option in ctx.attr._plugin_options:
396        # if import_prefix is set, the .proto is placed under a virtual include path
397        # prefixed by `import_prefix`. That path is what is given to the proto
398        # plugin via plugin_pb2.CodeGeneratorRequest.proto_file.name, so the include
399        # paths we give to the plugin need to be able find the .options files based
400        # on the following logic in pw_protobuf/options.py:
401        #
402        #   options_file_name = include_path / proto_file_name.with_suffix(".options")
403        #
404        # This means that in order for the plugin to find the .options file, we need
405        # to let the plugin know the import prefix so it can modify the `proto_file_name`
406        # back to the original to be able to find the .options file.
407        if plugin_option == "--import-prefix={}":
408            if ctx.rule.attr.import_prefix:
409                plugin_option = plugin_option.format(ctx.rule.attr.import_prefix)
410            else:
411                continue
412        args.add("--custom_opt={}".format(plugin_option))
413
414    args.add("--custom_out={}".format(out_path))
415    args.add_all(proto_info.direct_sources)
416
417    all_tools = [
418        ctx.executable._protoc,
419        ctx.executable._python_runtime,
420        ctx.executable._protoc_plugin,
421    ]
422    run_path = [tool.dirname for tool in all_tools]
423
424    ctx.actions.run(
425        inputs = depset(
426            direct = proto_info.direct_sources +
427                     proto_info.transitive_sources.to_list() +
428                     options_files,
429            transitive = [proto_info.transitive_descriptor_sets],
430        ),
431        progress_message = "Generating %s C++ files for %s" % (ctx.attr._extensions, ctx.label.name),
432        tools = all_tools,
433        outputs = srcs + hdrs,
434        executable = ctx.executable._protoc,
435        arguments = [args],
436        env = {
437
438            # The nanopb protobuf plugin likes to compile some temporary protos
439            # next to source files. This forces them to be written to Bazel's
440            # genfiles directory.
441            "NANOPB_PB2_TEMP_DIR": str(ctx.genfiles_dir),
442            "PATH": ":".join(run_path),
443        },
444    )
445
446    transitive_srcs = srcs
447    transitive_hdrs = hdrs
448    transitive_includes = includes
449    for dep in ctx.rule.attr.deps:
450        transitive_srcs += dep[PwProtoInfo].srcs
451        transitive_hdrs += dep[PwProtoInfo].hdrs
452        transitive_includes += dep[PwProtoInfo].includes
453    return [PwProtoInfo(
454        srcs = transitive_srcs,
455        hdrs = transitive_hdrs,
456        includes = transitive_includes,
457    )]
458
459def _proto_compiler_aspect(extensions, protoc_plugin, plugin_options = []):
460    """Returns an aspect that runs the proto compiler.
461
462    The aspect propagates through the deps of proto_library targets, running
463    the proto compiler with the specified plugin for each of their source
464    files. The proto compiler is assumed to produce one output file per input
465    .proto file. That file is placed under bazel-bin at the same path as the
466    input file, but with the specified extension (i.e., with _extensions = [
467    .pwpb.h], the aspect converts pw_log/log.proto into
468    bazel-bin/pw_log/log.pwpb.h).
469
470    The aspect returns a provider exposing all the File objects generated from
471    the dependency graph.
472    """
473    return aspect(
474        attr_aspects = ["deps"],
475        attrs = {
476            "_extensions": attr.string_list(default = extensions),
477            "_plugin_options": attr.string_list(
478                default = plugin_options,
479            ),
480            "_protoc": attr.label(
481                default = Label("@com_google_protobuf//:protoc"),
482                executable = True,
483                cfg = "exec",
484            ),
485            "_protoc_plugin": attr.label(
486                default = Label(protoc_plugin),
487                executable = True,
488                cfg = "exec",
489            ),
490            "_python_runtime": attr.label(
491                default = Label("//:python3_interpreter"),
492                allow_single_file = True,
493                executable = True,
494                cfg = "exec",
495            ),
496        },
497        implementation = _proto_compiler_aspect_impl,
498        provides = [PwProtoInfo],
499    )
500
501def _impl_pw_proto_library(ctx):
502    """Implementation of the proto codegen rule.
503
504    The work of actually generating the code is done by the aspect, so here we
505    compile and return a CcInfo to link against.
506    """
507
508    # Note that we don't distinguish between the files generated from the
509    # target, and the files generated from its dependencies. We return all of
510    # them together, and in pw_proto_library expose all of them as hdrs.
511    # Pigweed's plugins happen to only generate .h files, so this works, but
512    # strictly speaking we should expose only the files generated from the
513    # target itself in hdrs, and place the headers generated from dependencies
514    # in srcs. We don't perform layering_check in Pigweed, so this is not a big
515    # deal.
516    #
517    # TODO: b/234873954 - Tidy this up.
518    all_srcs = []
519    all_hdrs = []
520    all_includes = []
521    for dep in ctx.attr.protos:
522        for f in dep[PwProtoInfo].hdrs:
523            all_hdrs.append(f)
524        for f in dep[PwProtoInfo].srcs:
525            all_srcs.append(f)
526        for i in dep[PwProtoInfo].includes:
527            all_includes.append(i)
528
529    return _compile_cc(
530        ctx,
531        all_srcs,
532        all_hdrs,
533        ctx.attr.deps,
534        all_includes,
535        defines = [],
536    )
537
538# Instantiate the aspects and rules for generating code using specific plugins.
539_pwpb_proto_compiler_aspect = _proto_compiler_aspect(
540    ["pwpb.h"],
541    "//pw_protobuf/py:plugin",
542    ["--no-legacy-namespace", "--import-prefix={}"],
543)
544
545_pwpb_proto_library = rule(
546    implementation = _impl_pw_proto_library,
547    attrs = {
548        "deps": attr.label_list(
549            providers = [CcInfo],
550        ),
551        "protos": attr.label_list(
552            providers = [ProtoInfo],
553            aspects = [_pwpb_proto_compiler_aspect],
554        ),
555    },
556    fragments = ["cpp"],
557    toolchains = use_cpp_toolchain(),
558)
559
560_nanopb_proto_compiler_aspect = _proto_compiler_aspect(
561    ["pb.h", "pb.c"],
562    "@com_github_nanopb_nanopb//:protoc-gen-nanopb",
563    ["--library-include-format=quote"],
564)
565
566_nanopb_proto_library = rule(
567    implementation = _impl_pw_proto_library,
568    attrs = {
569        "deps": attr.label_list(
570            providers = [CcInfo],
571        ),
572        "protos": attr.label_list(
573            providers = [ProtoInfo],
574            aspects = [_nanopb_proto_compiler_aspect],
575        ),
576    },
577    fragments = ["cpp"],
578    toolchains = use_cpp_toolchain(),
579)
580
581_pw_pwpb_rpc_proto_compiler_aspect = _proto_compiler_aspect(
582    ["rpc.pwpb.h"],
583    "//pw_rpc/py:plugin_pwpb",
584    ["--no-legacy-namespace"],
585)
586
587_pw_pwpb_rpc_proto_library = rule(
588    implementation = _impl_pw_proto_library,
589    attrs = {
590        "deps": attr.label_list(
591            providers = [CcInfo],
592        ),
593        "protos": attr.label_list(
594            providers = [ProtoInfo],
595            aspects = [_pw_pwpb_rpc_proto_compiler_aspect],
596        ),
597    },
598    fragments = ["cpp"],
599    toolchains = use_cpp_toolchain(),
600)
601
602_pw_raw_rpc_proto_compiler_aspect = _proto_compiler_aspect(
603    ["raw_rpc.pb.h"],
604    "//pw_rpc/py:plugin_raw",
605    ["--no-legacy-namespace"],
606)
607
608_pw_raw_rpc_proto_library = rule(
609    implementation = _impl_pw_proto_library,
610    attrs = {
611        "deps": attr.label_list(
612            providers = [CcInfo],
613        ),
614        "protos": attr.label_list(
615            providers = [ProtoInfo],
616            aspects = [_pw_raw_rpc_proto_compiler_aspect],
617        ),
618    },
619    fragments = ["cpp"],
620    toolchains = use_cpp_toolchain(),
621)
622
623_pw_nanopb_rpc_proto_compiler_aspect = _proto_compiler_aspect(
624    ["rpc.pb.h"],
625    "//pw_rpc/py:plugin_nanopb",
626    ["--no-legacy-namespace"],
627)
628
629_pw_nanopb_rpc_proto_library = rule(
630    implementation = _impl_pw_proto_library,
631    attrs = {
632        "deps": attr.label_list(
633            providers = [CcInfo],
634        ),
635        "protos": attr.label_list(
636            providers = [ProtoInfo],
637            aspects = [_pw_nanopb_rpc_proto_compiler_aspect],
638        ),
639    },
640    fragments = ["cpp"],
641    toolchains = use_cpp_toolchain(),
642)
643
644def _pw_proto_filegroup_impl(ctx):
645    source_files = list()
646    options_files = list()
647
648    for src in ctx.attr.srcs:
649        source_files += src.files.to_list()
650
651    for options_src in ctx.attr.options_files:
652        for file in options_src.files.to_list():
653            if file.extension == "options":
654                options_files.append(file)
655            else:
656                fail((
657                    "Files provided as `options_files` to a " +
658                    "`pw_proto_filegroup` must have the `.options` " +
659                    "extension; the file `{}` was provided."
660                ).format(file.basename))
661
662    return [
663        DefaultInfo(files = depset(source_files)),
664        PwProtoOptionsInfo(options_files = depset(options_files)),
665    ]
666
667pw_proto_filegroup = rule(
668    doc = (
669        "Acts like a `filegroup`, but with an additional `options_files` " +
670        "attribute that accepts a list of `.options` files. These `.options` " +
671        "files should typically correspond to `.proto` files provided under " +
672        "the `srcs` attribute." +
673        "\n\n" +
674        "A `pw_proto_filegroup` is intended to be passed into the `srcs` of " +
675        "a `proto_library` target as if it were a normal `filegroup` " +
676        "containing only `.proto` files. For the purposes of the " +
677        "`proto_library` itself, the `pw_proto_filegroup` does indeed act " +
678        "just like a normal `filegroup`; the `options_files` attribute is " +
679        "ignored. However, if that `proto_library` target is then passed " +
680        "(directly or transitively) into the `deps` of a `pw_proto_library` " +
681        "for code generation, the `pw_proto_library` target will have access " +
682        "to the provided `.options` files and will pass them to the code " +
683        "generator." +
684        "\n\n" +
685        "Note that, in order for a `pw_proto_filegroup` to be a valid `srcs` " +
686        "entry for a `proto_library`, it must meet the same conditions " +
687        "required of a standard `filegroup` in that context. Namely, its " +
688        "`srcs` must provide at least one `.proto` (or `.protodevel`) file. " +
689        "Put simply, a `pw_proto_filegroup` cannot be used as a vector for " +
690        "injecting solely `.options` files; it must contain at least one " +
691        "proto as well (generally one associated with an included `.options` " +
692        "file in the interest of clarity)." +
693        "\n\n" +
694        "Regarding the somewhat unusual usage, this feature's design was " +
695        "mostly preordained by the combination of Bazel's strict access " +
696        "controls, the restrictions imposed on inputs to the `proto_library` " +
697        "rule, and the need to support `.options` files from transitive " +
698        "dependencies."
699    ),
700    implementation = _pw_proto_filegroup_impl,
701    attrs = {
702        "options_files": attr.label_list(
703            allow_files = True,
704        ),
705        "srcs": attr.label_list(
706            allow_files = True,
707        ),
708    },
709    provides = [PwProtoOptionsInfo],
710)
711