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