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