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