• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Protocol Buffers - Google's data interchange format
2# Copyright 2008 Google Inc.  All rights reserved.
3#
4# Use of this source code is governed by a BSD-style
5# license that can be found in the LICENSE file or at
6# https://developers.google.com/open-source/licenses/bsd
7"""
8Implementation of proto_library rule.
9"""
10
11load("@bazel_skylib//lib:paths.bzl", "paths")
12load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
13load("@proto_bazel_features//:features.bzl", "bazel_features")
14load("//bazel/common:proto_common.bzl", "proto_common")
15load("//bazel/common:proto_info.bzl", "ProtoInfo")
16load("//bazel/private:toolchain_helpers.bzl", "toolchains")
17
18STRICT_DEPS_FLAG_TEMPLATE = (
19    #
20    "--direct_dependencies_violation_msg=" +
21    "%%s is imported, but %s doesn't directly depend on a proto_library that 'srcs' it."
22)
23
24def _check_srcs_package(target_package, srcs):
25    """Check that .proto files in sources are from the same package.
26
27    This is done to avoid clashes with the generated sources."""
28
29    #TODO: this does not work with filegroups that contain files that are not in the package
30    for src in srcs:
31        if target_package != src.label.package:
32            fail("Proto source with label '%s' must be in same package as consuming rule." % src.label)
33
34def _get_import_prefix(ctx):
35    """Gets and verifies import_prefix attribute if it is declared."""
36
37    import_prefix = ctx.attr.import_prefix
38
39    if not paths.is_normalized(import_prefix):
40        fail("should be normalized (without uplevel references or '.' path segments)", attr = "import_prefix")
41    if paths.is_absolute(import_prefix):
42        fail("should be a relative path", attr = "import_prefix")
43
44    return import_prefix
45
46def _get_strip_import_prefix(ctx):
47    """Gets and verifies strip_import_prefix."""
48
49    strip_import_prefix = ctx.attr.strip_import_prefix
50
51    if not paths.is_normalized(strip_import_prefix):
52        fail("should be normalized (without uplevel references or '.' path segments)", attr = "strip_import_prefix")
53
54    if paths.is_absolute(strip_import_prefix):
55        strip_import_prefix = strip_import_prefix[1:]
56    else:  # Relative to current package
57        strip_import_prefix = _join(ctx.label.package, strip_import_prefix)
58
59    return strip_import_prefix.removesuffix("/")
60
61def _proto_library_impl(ctx):
62    # Verifies attributes.
63    _check_srcs_package(ctx.label.package, ctx.attr.srcs)
64    srcs = ctx.files.srcs
65    deps = [dep[ProtoInfo] for dep in ctx.attr.deps]
66    exports = [dep[ProtoInfo] for dep in ctx.attr.exports]
67    import_prefix = _get_import_prefix(ctx)
68    strip_import_prefix = _get_strip_import_prefix(ctx)
69    check_for_reexport = deps + exports if not srcs else exports
70    _PackageSpecificationInfo = bazel_features.globals.PackageSpecificationInfo
71    for proto in check_for_reexport:
72        if getattr(proto, "allow_exports", None):
73            if not _PackageSpecificationInfo:
74                fail("Allowlist checks not supported before Bazel 6.4.0")
75            if not proto.allow_exports[_PackageSpecificationInfo].contains(ctx.label):
76                fail("proto_library '%s' can't be reexported in package '//%s'" % (proto.direct_descriptor_set.owner, ctx.label.package))
77
78    proto_path, virtual_srcs = _process_srcs(ctx, srcs, import_prefix, strip_import_prefix)
79    descriptor_set = ctx.actions.declare_file(ctx.label.name + "-descriptor-set.proto.bin")
80    proto_info = ProtoInfo(
81        srcs = virtual_srcs,
82        deps = deps,
83        descriptor_set = descriptor_set,
84        proto_path = proto_path,
85        workspace_root = ctx.label.workspace_root,
86        bin_dir = ctx.bin_dir.path,
87        allow_exports = ctx.attr.allow_exports,
88    )
89
90    _write_descriptor_set(ctx, proto_info, deps, exports, descriptor_set)
91
92    # We assume that the proto sources will not have conflicting artifacts
93    # with the same root relative path
94    data_runfiles = ctx.runfiles(
95        files = [proto_info.direct_descriptor_set],
96        transitive_files = depset(transitive = [proto_info.transitive_sources]),
97    )
98    return [
99        proto_info,
100        DefaultInfo(
101            files = depset([proto_info.direct_descriptor_set]),
102            default_runfiles = ctx.runfiles(),  # empty
103            data_runfiles = data_runfiles,
104        ),
105    ]
106
107def _process_srcs(ctx, srcs, import_prefix, strip_import_prefix):
108    """Returns proto_path and sources, optionally symlinking them to _virtual_imports.
109
110    Returns:
111      (str, [File]) A pair of proto_path and virtual_sources.
112    """
113    if import_prefix != "" or strip_import_prefix != "":
114        # Use virtual source roots
115        return _symlink_to_virtual_imports(ctx, srcs, import_prefix, strip_import_prefix)
116    else:
117        # No virtual source roots
118        return "", srcs
119
120def _join(*path):
121    return "/".join([p for p in path if p != ""])
122
123def _symlink_to_virtual_imports(ctx, srcs, import_prefix, strip_import_prefix):
124    """Symlinks srcs to _virtual_imports.
125
126    Returns:
127          A pair proto_path, directs_sources.
128    """
129    virtual_imports = _join("_virtual_imports", ctx.label.name)
130    proto_path = _join(ctx.label.package, virtual_imports)
131
132    if ctx.label.workspace_name == "":
133        full_strip_import_prefix = strip_import_prefix
134    else:
135        full_strip_import_prefix = _join("..", ctx.label.workspace_name, strip_import_prefix)
136    if full_strip_import_prefix:
137        full_strip_import_prefix += "/"
138
139    virtual_srcs = []
140    for src in srcs:
141        # Remove strip_import_prefix
142        if not src.short_path.startswith(full_strip_import_prefix):
143            fail(".proto file '%s' is not under the specified strip prefix '%s'" %
144                 (src.short_path, full_strip_import_prefix))
145        import_path = src.short_path[len(full_strip_import_prefix):]
146
147        # Add import_prefix
148        virtual_src = ctx.actions.declare_file(_join(virtual_imports, import_prefix, import_path))
149        ctx.actions.symlink(
150            output = virtual_src,
151            target_file = src,
152            progress_message = "Symlinking virtual .proto sources for %{label}",
153        )
154        virtual_srcs.append(virtual_src)
155    return proto_path, virtual_srcs
156
157def _write_descriptor_set(ctx, proto_info, deps, exports, descriptor_set):
158    """Writes descriptor set."""
159    if proto_info.direct_sources == []:
160        ctx.actions.write(descriptor_set, "")
161        return
162
163    dependencies_descriptor_sets = depset(transitive = [dep.transitive_descriptor_sets for dep in deps])
164
165    args = ctx.actions.args()
166
167    if ctx.attr._experimental_proto_descriptor_sets_include_source_info[BuildSettingInfo].value:
168        args.add("--include_source_info")
169    args.add("--retain_options")
170
171    strict_deps = ctx.attr._strict_proto_deps[BuildSettingInfo].value
172    if strict_deps:
173        if proto_info.direct_sources:
174            strict_importable_sources = depset(
175                direct = proto_info._direct_proto_sources,
176                transitive = [dep._exported_sources for dep in deps],
177            )
178        else:
179            strict_importable_sources = None
180        if strict_importable_sources:
181            args.add_joined(
182                "--direct_dependencies",
183                strict_importable_sources,
184                map_each = proto_common.get_import_path,
185                join_with = ":",
186            )
187            # Example: `--direct_dependencies a.proto:b.proto`
188
189        else:
190            # The proto compiler requires an empty list to turn on strict deps checking
191            args.add("--direct_dependencies=")
192
193        # Set `-direct_dependencies_violation_msg=`
194        args.add(ctx.label, format = STRICT_DEPS_FLAG_TEMPLATE)
195
196    strict_imports = ctx.attr._strict_public_imports[BuildSettingInfo].value
197    if strict_imports:
198        public_import_protos = depset(transitive = [export._exported_sources for export in exports])
199        if not public_import_protos:
200            # This line is necessary to trigger the check.
201            args.add("--allowed_public_imports=")
202        else:
203            args.add_joined(
204                "--allowed_public_imports",
205                public_import_protos,
206                map_each = proto_common.get_import_path,
207                join_with = ":",
208            )
209    if proto_common.INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION:
210        toolchain = ctx.toolchains[toolchains.PROTO_TOOLCHAIN]
211        if not toolchain:
212            fail("Protocol compiler toolchain could not be resolved.")
213        proto_lang_toolchain_info = toolchain.proto
214    else:
215        proto_lang_toolchain_info = proto_common.ProtoLangToolchainInfo(
216            out_replacement_format_flag = "--descriptor_set_out=%s",
217            output_files = "single",
218            mnemonic = "GenProtoDescriptorSet",
219            progress_message = "Generating Descriptor Set proto_library %{label}",
220            proto_compiler = ctx.executable._proto_compiler,
221            protoc_opts = ctx.fragments.proto.experimental_protoc_opts,
222            plugin = None,
223        )
224
225    proto_common.compile(
226        ctx.actions,
227        proto_info,
228        proto_lang_toolchain_info,
229        generated_files = [descriptor_set],
230        additional_inputs = dependencies_descriptor_sets,
231        additional_args = args,
232    )
233
234proto_library = rule(
235    _proto_library_impl,
236    # TODO: proto_common docs are missing
237    # TODO: ProtoInfo link doesn't work and docs are missing
238    doc = """
239<p>If using Bazel, please load the rule from <a href="https://github.com/bazelbuild/rules_proto">
240https://github.com/bazelbuild/rules_proto</a>.
241
242<p>Use <code>proto_library</code> to define libraries of protocol buffers which
243may be used from multiple languages. A <code>proto_library</code> may be listed
244in the <code>deps</code> clause of supported rules, such as
245<code>java_proto_library</code>.
246
247<p>When compiled on the command-line, a <code>proto_library</code> creates a file
248named <code>foo-descriptor-set.proto.bin</code>, which is the descriptor set for
249the messages the rule srcs. The file is a serialized
250<code>FileDescriptorSet</code>, which is described in
251<a href="https://developers.google.com/protocol-buffers/docs/techniques#self-description">
252https://developers.google.com/protocol-buffers/docs/techniques#self-description</a>.
253
254<p>It only contains information about the <code>.proto</code> files directly
255mentioned by a <code>proto_library</code> rule; the collection of transitive
256descriptor sets is available through the
257<code>[ProtoInfo].transitive_descriptor_sets</code> Starlark provider.
258See documentation in <code>proto_info.bzl</code>.
259
260<p>Recommended code organization:
261<ul>
262<li>One <code>proto_library</code> rule per <code>.proto</code> file.
263<li>A file named <code>foo.proto</code> will be in a rule named <code>foo_proto</code>,
264  which is located in the same package.
265<li>A <code>[language]_proto_library</code> that wraps a <code>proto_library</code>
266  named <code>foo_proto</code> should be called <code>foo_[language]_proto</code>,
267  and be located in the same package.
268</ul>""",
269    attrs = {
270        "srcs": attr.label_list(
271            allow_files = [".proto", ".protodevel"],
272            flags = ["DIRECT_COMPILE_TIME_INPUT"],
273            # TODO: Should .protodevel be advertised or deprecated?
274            doc = """
275The list of <code>.proto</code> and <code>.protodevel</code> files that are
276processed to create the target. This is usually a non empty list. One usecase
277where <code>srcs</code> can be empty is an <i>alias-library</i>. This is a
278proto_library rule having one or more other proto_library in <code>deps</code>.
279This pattern can be used to e.g. export a public api under a persistent name.""",
280        ),
281        "deps": attr.label_list(
282            providers = [ProtoInfo],
283            doc = """
284The list of other <code>proto_library</code> rules that the target depends upon.
285A <code>proto_library</code> may only depend on other <code>proto_library</code>
286targets. It may not depend on language-specific libraries.""",
287        ),
288        "exports": attr.label_list(
289            providers = [ProtoInfo],
290            doc = """
291List of proto_library targets that can be referenced via "import public" in the
292proto source.
293It's an error if you use "import public" but do not list the corresponding library
294in the exports attribute.
295Note that you have list the library both in deps and exports since not all
296lang_proto_library implementations have been changed yet.""",
297        ),
298        "strip_import_prefix": attr.string(
299            default = "/",
300            doc = """
301The prefix to strip from the paths of the .proto files in this rule.
302
303<p>When set, .proto source files in the <code>srcs</code> attribute of this rule are
304accessible at their path with this prefix cut off.
305
306<p>If it's a relative path (not starting with a slash), it's taken as a package-relative
307one. If it's an absolute one, it's understood as a repository-relative path.
308
309<p>The prefix in the <code>import_prefix</code> attribute is added after this prefix is
310stripped.""",
311        ),
312        "import_prefix": attr.string(
313            doc = """
314The prefix to add to the paths of the .proto files in this rule.
315
316<p>When set, the .proto source files in the <code>srcs</code> attribute of this rule are
317accessible at is the value of this attribute prepended to their repository-relative path.
318
319<p>The prefix in the <code>strip_import_prefix</code> attribute is removed before this
320prefix is added.""",
321        ),
322        "allow_exports": attr.label(
323            cfg = "exec",
324            providers = [bazel_features.globals.PackageSpecificationInfo] if bazel_features.globals.PackageSpecificationInfo else [],
325            doc = """
326An optional allowlist that prevents proto library to be reexported or used in
327lang_proto_library that is not in one of the listed packages.""",
328        ),
329        "data": attr.label_list(
330            allow_files = True,
331            flags = ["SKIP_CONSTRAINTS_OVERRIDE"],
332        ),
333        # buildifier: disable=attr-license (calling attr.license())
334        "licenses": attr.license() if hasattr(attr, "license") else attr.string_list(),
335        "_experimental_proto_descriptor_sets_include_source_info": attr.label(
336            default = "//bazel/private:experimental_proto_descriptor_sets_include_source_info",
337        ),
338        "_strict_proto_deps": attr.label(
339            default =
340                "//bazel/private:strict_proto_deps",
341        ),
342        "_strict_public_imports": attr.label(
343            default = "//bazel/private:strict_public_imports",
344        ),
345    } | toolchains.if_legacy_toolchain({
346        "_proto_compiler": attr.label(
347            cfg = "exec",
348            executable = True,
349            allow_files = True,
350            default = configuration_field("proto", "proto_compiler"),
351        ),
352    }),  # buildifier: disable=attr-licenses (attribute called licenses)
353    fragments = ["proto"],
354    provides = [ProtoInfo],
355    toolchains = toolchains.use_toolchain(toolchains.PROTO_TOOLCHAIN),
356)
357