protobuf.bzl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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. """Utility functions for generating protobuf code."""
  15. load("@rules_proto//proto:defs.bzl", "ProtoInfo")
  16. _PROTO_EXTENSION = ".proto"
  17. _VIRTUAL_IMPORTS = "/_virtual_imports/"
  18. def well_known_proto_libs():
  19. return [
  20. "@com_google_protobuf//:any_proto",
  21. "@com_google_protobuf//:api_proto",
  22. "@com_google_protobuf//:compiler_plugin_proto",
  23. "@com_google_protobuf//:descriptor_proto",
  24. "@com_google_protobuf//:duration_proto",
  25. "@com_google_protobuf//:empty_proto",
  26. "@com_google_protobuf//:field_mask_proto",
  27. "@com_google_protobuf//:source_context_proto",
  28. "@com_google_protobuf//:struct_proto",
  29. "@com_google_protobuf//:timestamp_proto",
  30. "@com_google_protobuf//:type_proto",
  31. "@com_google_protobuf//:wrappers_proto",
  32. ]
  33. def is_well_known(label):
  34. return label in well_known_proto_libs()
  35. def get_proto_root(workspace_root):
  36. """Gets the root protobuf directory.
  37. Args:
  38. workspace_root: context.label.workspace_root
  39. Returns:
  40. The directory relative to which generated include paths should be.
  41. """
  42. if workspace_root:
  43. return "/{}".format(workspace_root)
  44. else:
  45. return ""
  46. def _strip_proto_extension(proto_filename):
  47. if not proto_filename.endswith(_PROTO_EXTENSION):
  48. fail('"{}" does not end with "{}"'.format(
  49. proto_filename,
  50. _PROTO_EXTENSION,
  51. ))
  52. return proto_filename[:-len(_PROTO_EXTENSION)]
  53. def proto_path_to_generated_filename(proto_path, fmt_str):
  54. """Calculates the name of a generated file for a protobuf path.
  55. For example, "examples/protos/helloworld.proto" might map to
  56. "helloworld.pb.h".
  57. Args:
  58. proto_path: The path to the .proto file.
  59. fmt_str: A format string used to calculate the generated filename. For
  60. example, "{}.pb.h" might be used to calculate a C++ header filename.
  61. Returns:
  62. The generated filename.
  63. """
  64. return fmt_str.format(_strip_proto_extension(proto_path))
  65. def get_include_directory(source_file):
  66. """Returns the include directory path for the source_file.
  67. All of the include statements within the given source_file are calculated
  68. relative to the directory returned by this method.
  69. The returned directory path can be used as the "--proto_path=" argument
  70. value.
  71. Args:
  72. source_file: A proto file.
  73. Returns:
  74. The include directory path for the source_file.
  75. """
  76. directory = source_file.path
  77. prefix_len = 0
  78. if is_in_virtual_imports(source_file):
  79. root, relative = source_file.path.split(_VIRTUAL_IMPORTS, 2)
  80. result = root + _VIRTUAL_IMPORTS + relative.split("/", 1)[0]
  81. return result
  82. if not source_file.is_source and directory.startswith(source_file.root.path):
  83. prefix_len = len(source_file.root.path) + 1
  84. if directory.startswith("external", prefix_len):
  85. external_separator = directory.find("/", prefix_len)
  86. repository_separator = directory.find("/", external_separator + 1)
  87. return directory[:repository_separator]
  88. else:
  89. return source_file.root.path if source_file.root.path else "."
  90. def get_plugin_args(
  91. plugin,
  92. flags,
  93. dir_out,
  94. generate_mocks,
  95. plugin_name = "PLUGIN"):
  96. """Returns arguments configuring protoc to use a plugin for a language.
  97. Args:
  98. plugin: An executable file to run as the protoc plugin.
  99. flags: The plugin flags to be passed to protoc.
  100. dir_out: The output directory for the plugin.
  101. generate_mocks: A bool indicating whether to generate mocks.
  102. plugin_name: A name of the plugin, it is required to be unique when there
  103. are more than one plugin used in a single protoc command.
  104. Returns:
  105. A list of protoc arguments configuring the plugin.
  106. """
  107. augmented_flags = list(flags)
  108. if generate_mocks:
  109. augmented_flags.append("generate_mock_code=true")
  110. augmented_dir_out = dir_out
  111. if augmented_flags:
  112. augmented_dir_out = ",".join(augmented_flags) + ":" + dir_out
  113. return [
  114. "--plugin=protoc-gen-{plugin_name}={plugin_path}".format(
  115. plugin_name = plugin_name,
  116. plugin_path = plugin.path,
  117. ),
  118. "--{plugin_name}_out={dir_out}".format(
  119. plugin_name = plugin_name,
  120. dir_out = augmented_dir_out,
  121. ),
  122. ]
  123. def _make_prefix(label):
  124. """Returns the directory prefix for a label.
  125. @repo//foo/bar:sub/dir/file.proto => 'external/repo/foo/bar/'
  126. //foo/bar:sub/dir/file.proto => 'foo/bar/'
  127. //:sub/dir/file.proto => ''
  128. That is, the prefix can be removed from a file's full path to
  129. obtain the file's relative location within the package's effective
  130. directory."""
  131. wsr = label.workspace_root
  132. pkg = label.package
  133. if not wsr and not pkg:
  134. return ""
  135. elif not wsr:
  136. return pkg + "/"
  137. elif not pkg:
  138. return wsr + "/"
  139. else:
  140. return wsr + "/" + pkg + "/"
  141. def get_staged_proto_file(label, context, source_file):
  142. """Copies a proto file to the appropriate location if necessary.
  143. Args:
  144. label: The label of the rule using the .proto file.
  145. context: The ctx object for the rule or aspect.
  146. source_file: The original .proto file.
  147. Returns:
  148. The original proto file OR a new file in the staged location.
  149. """
  150. if source_file.dirname == label.package or \
  151. is_in_virtual_imports(source_file):
  152. # Current target and source_file are in same package
  153. return source_file
  154. else:
  155. # Current target and source_file are in different packages (most
  156. # probably even in different repositories)
  157. prefix = _make_prefix(source_file.owner)
  158. copied_proto = context.actions.declare_file(source_file.path[len(prefix):])
  159. context.actions.run_shell(
  160. inputs = [source_file],
  161. outputs = [copied_proto],
  162. command = "cp {} {}".format(source_file.path, copied_proto.path),
  163. mnemonic = "CopySourceProto",
  164. )
  165. return copied_proto
  166. def protos_from_context(context):
  167. """Copies proto files to the appropriate location.
  168. Args:
  169. context: The ctx object for the rule.
  170. Returns:
  171. A list of the protos.
  172. """
  173. protos = []
  174. for src in context.attr.deps:
  175. for file in src[ProtoInfo].direct_sources:
  176. protos.append(get_staged_proto_file(context.label, context, file))
  177. return protos
  178. def includes_from_deps(deps):
  179. """Get includes from rule dependencies."""
  180. return [
  181. file
  182. for src in deps
  183. for file in src[ProtoInfo].transitive_imports.to_list()
  184. ]
  185. def get_proto_arguments(protos, genfiles_dir_path):
  186. """Get the protoc arguments specifying which protos to compile.
  187. Args:
  188. protos: The protob files to supply.
  189. genfiles_dir_path: The path to the genfiles directory.
  190. Returns:
  191. The arguments to supply to protoc.
  192. """
  193. arguments = []
  194. for proto in protos:
  195. strip_prefix_len = 0
  196. if is_in_virtual_imports(proto):
  197. incl_directory = get_include_directory(proto)
  198. if proto.path.startswith(incl_directory):
  199. strip_prefix_len = len(incl_directory) + 1
  200. elif proto.path.startswith(genfiles_dir_path):
  201. strip_prefix_len = len(genfiles_dir_path) + 1
  202. arguments.append(proto.path[strip_prefix_len:])
  203. return arguments
  204. def declare_out_files(protos, context, generated_file_format):
  205. """Declares and returns the files to be generated.
  206. Args:
  207. protos: A list of files. The protos to declare.
  208. context: The context object.
  209. generated_file_format: A format string. Will be passed to
  210. proto_path_to_generated_filename to generate the filename of each
  211. generated file.
  212. Returns:
  213. A list of file providers.
  214. """
  215. out_file_paths = []
  216. for proto in protos:
  217. if not is_in_virtual_imports(proto):
  218. prefix = _make_prefix(proto.owner)
  219. full_prefix = context.genfiles_dir.path + "/" + prefix
  220. if proto.path.startswith(full_prefix):
  221. out_file_paths.append(proto.path[len(full_prefix):])
  222. elif proto.path.startswith(prefix):
  223. out_file_paths.append(proto.path[len(prefix):])
  224. else:
  225. out_file_paths.append(proto.path[proto.path.index(_VIRTUAL_IMPORTS) + 1:])
  226. return [
  227. context.actions.declare_file(
  228. proto_path_to_generated_filename(
  229. out_file_path,
  230. generated_file_format,
  231. ),
  232. )
  233. for out_file_path in out_file_paths
  234. ]
  235. def get_out_dir(protos, context):
  236. """Returns the value to supply to the --<lang>_out= protoc flag.
  237. The result is based on the input source proto files and current context.
  238. Args:
  239. protos: A list of protos to be used as source files in protoc command
  240. context: A ctx object for the rule.
  241. Returns:
  242. The value of --<lang>_out= argument.
  243. """
  244. at_least_one_virtual = 0
  245. for proto in protos:
  246. if is_in_virtual_imports(proto):
  247. at_least_one_virtual = True
  248. elif at_least_one_virtual:
  249. fail("Proto sources must be either all virtual imports or all real")
  250. if at_least_one_virtual:
  251. out_dir = get_include_directory(protos[0])
  252. ws_root = protos[0].owner.workspace_root
  253. prefix = "/" + _make_prefix(protos[0].owner) + _VIRTUAL_IMPORTS[1:]
  254. return struct(
  255. path = out_dir,
  256. import_path = out_dir[out_dir.find(prefix) + 1:],
  257. )
  258. out_dir = context.genfiles_dir.path
  259. ws_root = context.label.workspace_root
  260. if ws_root:
  261. out_dir = out_dir + "/" + ws_root
  262. return struct(path = out_dir, import_path = None)
  263. def is_in_virtual_imports(source_file, virtual_folder = _VIRTUAL_IMPORTS):
  264. """Determines if source_file is virtual.
  265. A file is virtual if placed in the _virtual_imports subdirectory. The
  266. output of all proto_library targets which use import_prefix and/or
  267. strip_import_prefix arguments is placed under _virtual_imports directory.
  268. Args:
  269. source_file: A proto file.
  270. virtual_folder: The virtual folder name (is set to "_virtual_imports"
  271. by default)
  272. Returns:
  273. True if source_file is located under _virtual_imports, False otherwise.
  274. """
  275. return not source_file.is_source and virtual_folder in source_file.path