python_rules.bzl 9.6 KB

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