1# Copyright 2021 The gRPC Authors 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"""Generates and compiles Python gRPC stubs from proto_library rules.""" 15 16load("@rules_proto//proto:defs.bzl", "ProtoInfo") 17load("@rules_python//python:py_info.bzl", "PyInfo") 18load( 19 "//bazel:protobuf.bzl", 20 "declare_out_files", 21 "get_include_directory", 22 "get_out_dir", 23 "get_plugin_args", 24 "get_proto_arguments", 25 "get_staged_proto_file", 26 "includes_from_deps", 27 "is_well_known", 28 "protos_from_context", 29) 30 31_GENERATED_PROTO_FORMAT = "{}_pb2.py" 32_GENERATED_PROTO_STUB_FORMAT = "{}_pb2.pyi" 33_GENERATED_GRPC_PROTO_FORMAT = "{}_pb2_grpc.py" 34 35PyProtoInfo = provider( 36 "The Python outputs from the Protobuf compiler.", 37 fields = { 38 "py_info": "A PyInfo provider for the generated code.", 39 "generated_py_srcs": "The direct (not transitive) generated Python source files.", 40 }, 41) 42 43def _merge_pyinfos(pyinfos): 44 return PyInfo( 45 transitive_sources = depset(transitive = [p.transitive_sources for p in pyinfos]), 46 imports = depset(transitive = [p.imports for p in pyinfos]), 47 ) 48 49def _gen_py_aspect_impl(target, context): 50 # Early return for well-known protos. 51 if is_well_known(str(context.label)): 52 return [ 53 PyProtoInfo( 54 py_info = context.attr._protobuf_library[PyInfo], 55 generated_py_srcs = [], 56 ), 57 ] 58 59 protos = [] 60 for p in target[ProtoInfo].direct_sources: 61 protos.append(get_staged_proto_file(target.label, context, p)) 62 63 includes = depset(direct = protos, transitive = [target[ProtoInfo].transitive_imports]) 64 out_files = (declare_out_files(protos, context, _GENERATED_PROTO_FORMAT) + 65 declare_out_files(protos, context, _GENERATED_PROTO_STUB_FORMAT)) 66 generated_py_srcs = out_files 67 68 tools = [context.executable._protoc] 69 70 out_dir = get_out_dir(protos, context) 71 72 arguments = ([ 73 "--python_out={}".format(out_dir.path), 74 "--pyi_out={}".format(out_dir.path), 75 ] + [ 76 "--proto_path={}".format(get_include_directory(i)) 77 for i in includes.to_list() 78 ] + [ 79 "--proto_path={}".format(context.genfiles_dir.path), 80 ]) 81 82 arguments += get_proto_arguments(protos, context.genfiles_dir.path) 83 84 context.actions.run( 85 inputs = protos + includes.to_list(), 86 tools = tools, 87 outputs = out_files, 88 executable = context.executable._protoc, 89 arguments = arguments, 90 mnemonic = "ProtocInvocation", 91 ) 92 93 imports = [] 94 if out_dir.import_path: 95 imports.append("{}/{}".format(context.workspace_name, out_dir.import_path)) 96 97 py_info = PyInfo(transitive_sources = depset(direct = out_files), imports = depset(direct = imports)) 98 return PyProtoInfo( 99 py_info = _merge_pyinfos( 100 [ 101 py_info, 102 context.attr._protobuf_library[PyInfo], 103 ] + [dep[PyProtoInfo].py_info for dep in context.rule.attr.deps], 104 ), 105 generated_py_srcs = generated_py_srcs, 106 ) 107 108_gen_py_aspect = aspect( 109 implementation = _gen_py_aspect_impl, 110 attr_aspects = ["deps"], 111 fragments = ["py"], 112 attrs = { 113 "_protoc": attr.label( 114 default = Label("@com_google_protobuf//:protoc"), 115 providers = ["files_to_run"], 116 executable = True, 117 cfg = "exec", 118 ), 119 "_protobuf_library": attr.label( 120 default = Label("@com_google_protobuf//:protobuf_python"), 121 providers = [PyInfo], 122 ), 123 }, 124) 125 126def _generate_py_impl(context): 127 if (len(context.attr.deps) != 1): 128 fail("Can only compile a single proto at a time.") 129 130 py_sources = [] 131 132 # If the proto_library this rule *directly* depends on is in another 133 # package, then we generate .py files to import them in this package. This 134 # behavior is needed to allow rearranging of import paths to make Bazel 135 # outputs align with native python workflows. 136 # 137 # Note that this approach is vulnerable to protoc defining __all__ or other 138 # symbols with __ prefixes that need to be directly imported. Since these 139 # names are likely to be reserved for private APIs, the risk is minimal. 140 if context.label.package != context.attr.deps[0].label.package: 141 for py_src in context.attr.deps[0][PyProtoInfo].generated_py_srcs: 142 reimport_py_file = context.actions.declare_file(py_src.basename) 143 py_sources.append(reimport_py_file) 144 import_line = "from %s import *" % py_src.short_path.replace("..", "external").replace("/", ".")[:-len(".py")] 145 context.actions.write(reimport_py_file, import_line) 146 147 # Collect output PyInfo provider. 148 imports = [context.label.package + "/" + i for i in context.attr.imports] 149 py_info = PyInfo(transitive_sources = depset(direct = py_sources), imports = depset(direct = imports)) 150 out_pyinfo = _merge_pyinfos([py_info, context.attr.deps[0][PyProtoInfo].py_info]) 151 152 runfiles = context.runfiles(files = out_pyinfo.transitive_sources.to_list()).merge(context.attr._protobuf_library[DefaultInfo].data_runfiles) 153 return [ 154 DefaultInfo( 155 files = out_pyinfo.transitive_sources, 156 runfiles = runfiles, 157 ), 158 out_pyinfo, 159 ] 160 161py_proto_library = rule( 162 attrs = { 163 "deps": attr.label_list( 164 mandatory = True, 165 allow_empty = False, 166 providers = [ProtoInfo], 167 aspects = [_gen_py_aspect], 168 ), 169 "_protoc": attr.label( 170 default = Label("@com_google_protobuf//:protoc"), 171 providers = ["files_to_run"], 172 executable = True, 173 cfg = "exec", 174 ), 175 "_protobuf_library": attr.label( 176 default = Label("@com_google_protobuf//:protobuf_python"), 177 providers = [PyInfo], 178 ), 179 "imports": attr.string_list(), 180 }, 181 implementation = _generate_py_impl, 182) 183 184def _generate_pb2_grpc_src_impl(context): 185 protos = protos_from_context(context) 186 includes = includes_from_deps(context.attr.deps) 187 out_files = declare_out_files(protos, context, _GENERATED_GRPC_PROTO_FORMAT) 188 189 plugin_flags = ["grpc_2_0"] + context.attr.strip_prefixes 190 191 arguments = [] 192 tools = [context.executable._protoc, context.executable._grpc_plugin] 193 out_dir = get_out_dir(protos, context) 194 if out_dir.import_path: 195 # is virtual imports 196 out_path = out_dir.path 197 else: 198 out_path = context.genfiles_dir.path 199 arguments += get_plugin_args( 200 context.executable._grpc_plugin, 201 plugin_flags, 202 out_path, 203 False, 204 ) 205 206 arguments += [ 207 "--proto_path={}".format(get_include_directory(i)) 208 for i in includes 209 ] 210 arguments.append("--proto_path={}".format(context.genfiles_dir.path)) 211 arguments += get_proto_arguments(protos, context.genfiles_dir.path) 212 213 context.actions.run( 214 inputs = protos + includes, 215 tools = tools, 216 outputs = out_files, 217 executable = context.executable._protoc, 218 arguments = arguments, 219 mnemonic = "ProtocInvocation", 220 ) 221 222 p = PyInfo(transitive_sources = depset(direct = out_files)) 223 py_info = _merge_pyinfos( 224 [ 225 p, 226 context.attr.grpc_library[PyInfo], 227 ] + [dep[PyInfo] for dep in context.attr.py_deps], 228 ) 229 230 runfiles = context.runfiles(files = out_files, transitive_files = py_info.transitive_sources).merge(context.attr.grpc_library[DefaultInfo].data_runfiles) 231 232 return [ 233 DefaultInfo( 234 files = depset(direct = out_files), 235 runfiles = runfiles, 236 ), 237 py_info, 238 ] 239 240_generate_pb2_grpc_src = rule( 241 attrs = { 242 "deps": attr.label_list( 243 mandatory = True, 244 allow_empty = False, 245 providers = [ProtoInfo], 246 ), 247 "py_deps": attr.label_list( 248 mandatory = True, 249 allow_empty = False, 250 providers = [PyInfo], 251 ), 252 "strip_prefixes": attr.string_list(), 253 "_grpc_plugin": attr.label( 254 executable = True, 255 providers = ["files_to_run"], 256 cfg = "exec", 257 default = Label("//src/compiler:grpc_python_plugin"), 258 ), 259 "_protoc": attr.label( 260 executable = True, 261 providers = ["files_to_run"], 262 cfg = "exec", 263 default = Label("@com_google_protobuf//:protoc"), 264 ), 265 "grpc_library": attr.label( 266 default = Label("//src/python/grpcio/grpc:grpcio"), 267 providers = [PyInfo], 268 ), 269 }, 270 implementation = _generate_pb2_grpc_src_impl, 271) 272 273def py_grpc_library( 274 name, 275 srcs, 276 deps, 277 strip_prefixes = [], 278 grpc_library = Label("//src/python/grpcio/grpc:grpcio"), 279 **kwargs): 280 """Generate python code for gRPC services defined in a protobuf. 281 282 Args: 283 name: The name of the target. 284 srcs: (List of `labels`) a single proto_library target containing the 285 schema of the service. 286 deps: (List of `labels`) a single py_proto_library target for the 287 proto_library in `srcs`. 288 strip_prefixes: (List of `strings`) If provided, this prefix will be 289 stripped from the beginning of foo_pb2 modules imported by the 290 generated stubs. This is useful in combination with the `imports` 291 attribute of the `py_library` rule. 292 grpc_library: (`label`) a single `py_library` target representing the 293 python gRPC library target to be depended upon. This can be used to 294 generate code that depends on `grpcio` from the Python Package Index. 295 **kwargs: Additional arguments to be supplied to the invocation of 296 py_library. 297 """ 298 if len(srcs) != 1: 299 fail("Can only compile a single proto at a time.") 300 301 if len(deps) != 1: 302 fail("Deps must have length 1.") 303 304 _generate_pb2_grpc_src( 305 name = name, 306 deps = srcs, 307 py_deps = deps, 308 strip_prefixes = strip_prefixes, 309 grpc_library = grpc_library, 310 **kwargs 311 ) 312