• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://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,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""The implementation of the `py_proto_library` rule and its aspect."""
16
17load("@rules_proto//proto:defs.bzl", "ProtoInfo", "proto_common")
18load("//python:defs.bzl", "PyInfo")
19
20ProtoLangToolchainInfo = proto_common.ProtoLangToolchainInfo
21
22_PyProtoInfo = provider(
23    doc = "Encapsulates information needed by the Python proto rules.",
24    fields = {
25        "imports": """
26            (depset[str]) The field forwarding PyInfo.imports coming from
27            the proto language runtime dependency.""",
28        "runfiles_from_proto_deps": """
29            (depset[File]) Files from the transitive closure implicit proto
30            dependencies""",
31        "transitive_sources": """(depset[File]) The Python sources.""",
32    },
33)
34
35def _filter_provider(provider, *attrs):
36    return [dep[provider] for attr in attrs for dep in attr if provider in dep]
37
38def _py_proto_aspect_impl(target, ctx):
39    """Generates and compiles Python code for a proto_library.
40
41    The function runs protobuf compiler on the `proto_library` target generating
42    a .py file for each .proto file.
43
44    Args:
45      target: (Target) A target providing `ProtoInfo`. Usually this means a
46         `proto_library` target, but not always; you must expect to visit
47         non-`proto_library` targets, too.
48      ctx: (RuleContext) The rule context.
49
50    Returns:
51      ([_PyProtoInfo]) Providers collecting transitive information about
52      generated files.
53    """
54
55    _proto_library = ctx.rule.attr
56
57    # Check Proto file names
58    for proto in target[ProtoInfo].direct_sources:
59        if proto.is_source and "-" in proto.dirname:
60            fail("Cannot generate Python code for a .proto whose path contains '-' ({}).".format(
61                proto.path,
62            ))
63
64    proto_lang_toolchain_info = ctx.attr._aspect_proto_toolchain[ProtoLangToolchainInfo]
65    api_deps = [proto_lang_toolchain_info.runtime]
66
67    generated_sources = []
68    proto_info = target[ProtoInfo]
69    if proto_info.direct_sources:
70        # Generate py files
71        generated_sources = proto_common.declare_generated_files(
72            actions = ctx.actions,
73            proto_info = proto_info,
74            extension = "_pb2.py",
75            name_mapper = lambda name: name.replace("-", "_").replace(".", "/"),
76        )
77
78        # Handles multiple repository and virtual import cases
79        proto_root = proto_info.proto_source_root
80        if proto_root.startswith(ctx.bin_dir.path):
81            plugin_output = proto_root
82        else:
83            plugin_output = ctx.bin_dir.path + "/" + proto_root
84
85        if plugin_output == ".":
86            plugin_output = ctx.bin_dir.path
87
88        proto_common.compile(
89            actions = ctx.actions,
90            proto_info = proto_info,
91            proto_lang_toolchain_info = proto_lang_toolchain_info,
92            generated_files = generated_sources,
93            plugin_output = plugin_output,
94        )
95
96    # Generated sources == Python sources
97    python_sources = generated_sources
98
99    deps = _filter_provider(_PyProtoInfo, getattr(_proto_library, "deps", []))
100    runfiles_from_proto_deps = depset(
101        transitive = [dep[DefaultInfo].default_runfiles.files for dep in api_deps] +
102                     [dep.runfiles_from_proto_deps for dep in deps],
103    )
104    transitive_sources = depset(
105        direct = python_sources,
106        transitive = [dep.transitive_sources for dep in deps],
107    )
108
109    return [
110        _PyProtoInfo(
111            imports = depset(
112                transitive = [dep[PyInfo].imports for dep in api_deps],
113            ),
114            runfiles_from_proto_deps = runfiles_from_proto_deps,
115            transitive_sources = transitive_sources,
116        ),
117    ]
118
119_py_proto_aspect = aspect(
120    implementation = _py_proto_aspect_impl,
121    attrs = {
122        "_aspect_proto_toolchain": attr.label(
123            default = ":python_toolchain",
124        ),
125    },
126    attr_aspects = ["deps"],
127    required_providers = [ProtoInfo],
128    provides = [_PyProtoInfo],
129)
130
131def _py_proto_library_rule(ctx):
132    """Merges results of `py_proto_aspect` in `deps`.
133
134    Args:
135      ctx: (RuleContext) The rule context.
136    Returns:
137      ([PyInfo, DefaultInfo, OutputGroupInfo])
138    """
139    if not ctx.attr.deps:
140        fail("'deps' attribute mustn't be empty.")
141
142    pyproto_infos = _filter_provider(_PyProtoInfo, ctx.attr.deps)
143    default_outputs = depset(
144        transitive = [info.transitive_sources for info in pyproto_infos],
145    )
146
147    return [
148        DefaultInfo(
149            files = default_outputs,
150            default_runfiles = ctx.runfiles(transitive_files = depset(
151                transitive =
152                    [default_outputs] +
153                    [info.runfiles_from_proto_deps for info in pyproto_infos],
154            )),
155        ),
156        OutputGroupInfo(
157            default = depset(),
158        ),
159        PyInfo(
160            transitive_sources = default_outputs,
161            imports = depset(transitive = [info.imports for info in pyproto_infos]),
162            # Proto always produces 2- and 3- compatible source files
163            has_py2_only_sources = False,
164            has_py3_only_sources = False,
165        ),
166    ]
167
168py_proto_library = rule(
169    implementation = _py_proto_library_rule,
170    doc = """
171      Use `py_proto_library` to generate Python libraries from `.proto` files.
172
173      The convention is to name the `py_proto_library` rule `foo_py_pb2`,
174      when it is wrapping `proto_library` rule `foo_proto`.
175
176      `deps` must point to a `proto_library` rule.
177
178      Example:
179
180```starlark
181py_library(
182    name = "lib",
183    deps = [":foo_py_pb2"],
184)
185
186py_proto_library(
187    name = "foo_py_pb2",
188    deps = [":foo_proto"],
189)
190
191proto_library(
192    name = "foo_proto",
193    srcs = ["foo.proto"],
194)
195```""",
196    attrs = {
197        "deps": attr.label_list(
198            doc = """
199              The list of `proto_library` rules to generate Python libraries for.
200
201              Usually this is just the one target: the proto library of interest.
202              It can be any target providing `ProtoInfo`.""",
203            providers = [ProtoInfo],
204            aspects = [_py_proto_aspect],
205        ),
206    },
207    provides = [PyInfo],
208)
209