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