protoc_wrapper.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2012 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """
  6. A simple wrapper for protoc.
  7. Script for //third_party/protobuf/proto_library.gni .
  8. Features:
  9. - Inserts #include for extra header automatically.
  10. - Prevents bad proto names.
  11. - Works around protoc's bad descriptor file generation.
  12. Ninja expects the format:
  13. target: deps
  14. But protoc just outputs:
  15. deps
  16. This script adds the "target:" part.
  17. """
  18. from __future__ import print_function
  19. import argparse
  20. import os.path
  21. import subprocess
  22. import sys
  23. import tempfile
  24. PROTOC_INCLUDE_POINT = "// @@protoc_insertion_point(includes)"
  25. def FormatGeneratorOptions(options):
  26. if not options:
  27. return ""
  28. if options.endswith(":"):
  29. return options
  30. return options + ":"
  31. def VerifyProtoNames(protos):
  32. for filename in protos:
  33. if "-" in filename:
  34. raise RuntimeError("Proto file names must not contain hyphens "
  35. "(see http://crbug.com/386125 for more information).")
  36. def StripProtoExtension(filename):
  37. if not filename.endswith(".proto"):
  38. raise RuntimeError("Invalid proto filename extension: "
  39. "{0} .".format(filename))
  40. return filename.rsplit(".", 1)[0]
  41. def WriteIncludes(headers, include):
  42. for filename in headers:
  43. include_point_found = False
  44. contents = []
  45. with open(filename) as f:
  46. for line in f:
  47. stripped_line = line.strip()
  48. contents.append(stripped_line)
  49. if stripped_line == PROTOC_INCLUDE_POINT:
  50. if include_point_found:
  51. raise RuntimeError("Multiple include points found.")
  52. include_point_found = True
  53. extra_statement = "#include \"{0}\"".format(include)
  54. contents.append(extra_statement)
  55. if not include_point_found:
  56. raise RuntimeError("Include point not found in header: "
  57. "{0} .".format(filename))
  58. with open(filename, "w") as f:
  59. for line in contents:
  60. print(line, file=f)
  61. def main(argv):
  62. parser = argparse.ArgumentParser()
  63. parser.add_argument("--protoc", required=True,
  64. help="Relative path to compiler.")
  65. parser.add_argument("--proto-in-dir", required=True,
  66. help="Base directory with source protos.")
  67. parser.add_argument("--cc-out-dir",
  68. help="Output directory for standard C++ generator.")
  69. parser.add_argument("--py-out-dir",
  70. help="Output directory for standard Python generator.")
  71. parser.add_argument("--js-out-dir",
  72. help="Output directory for standard JS generator.")
  73. parser.add_argument("--plugin-out-dir",
  74. help="Output directory for custom generator plugin.")
  75. parser.add_argument('--enable-kythe-annotations', action='store_true',
  76. help='Enable generation of Kythe kzip, used for '
  77. 'codesearch.')
  78. parser.add_argument("--plugin",
  79. help="Relative path to custom generator plugin.")
  80. # TODO(crbug.com/1237958): Remove allow_optional when proto rolls to 3.15.
  81. parser.add_argument("--allow-optional",
  82. action='store_true',
  83. help="Enables experimental_allow_proto3_optional.")
  84. parser.add_argument("--plugin-options",
  85. help="Custom generator plugin options.")
  86. parser.add_argument("--cc-options",
  87. help="Standard C++ generator options.")
  88. parser.add_argument("--include",
  89. help="Name of include to insert into generated headers.")
  90. parser.add_argument("--import-dir", action="append", default=[],
  91. help="Extra import directory for protos, can be repeated."
  92. )
  93. parser.add_argument("--descriptor-set-out",
  94. help="Path to write a descriptor.")
  95. parser.add_argument(
  96. "--descriptor-set-dependency-file",
  97. help="Path to write the dependency file for descriptor set.")
  98. # The meaning of this flag is flipped compared to the corresponding protoc
  99. # flag due to this script previously passing --include_imports. Removing the
  100. # --include_imports is likely to have unintended consequences.
  101. parser.add_argument(
  102. "--exclude-imports",
  103. help="Do not include imported files into generated descriptor.",
  104. action="store_true",
  105. default=False)
  106. parser.add_argument("protos", nargs="+",
  107. help="Input protobuf definition file(s).")
  108. options = parser.parse_args(argv)
  109. proto_dir = os.path.relpath(options.proto_in_dir)
  110. protoc_cmd = [os.path.realpath(options.protoc)]
  111. protos = options.protos
  112. headers = []
  113. VerifyProtoNames(protos)
  114. if options.py_out_dir:
  115. protoc_cmd += ["--python_out", options.py_out_dir]
  116. if options.js_out_dir:
  117. protoc_cmd += [
  118. "--js_out",
  119. "one_output_file_per_input_file,binary:" + options.js_out_dir
  120. ]
  121. if options.cc_out_dir:
  122. cc_out_dir = options.cc_out_dir
  123. cc_options_list = []
  124. if options.enable_kythe_annotations:
  125. cc_options_list.extend([
  126. 'annotate_headers', 'annotation_pragma_name=kythe_metadata',
  127. 'annotation_guard_name=KYTHE_IS_RUNNING'
  128. ])
  129. # cc_options will likely have trailing colon so needs to be inserted at the
  130. # end.
  131. if options.cc_options:
  132. cc_options_list.append(options.cc_options)
  133. if options.allow_optional:
  134. protoc_cmd += ["--experimental_allow_proto3_optional"]
  135. cc_options = FormatGeneratorOptions(','.join(cc_options_list))
  136. protoc_cmd += ["--cpp_out", cc_options + cc_out_dir]
  137. for filename in protos:
  138. stripped_name = StripProtoExtension(filename)
  139. headers.append(os.path.join(cc_out_dir, stripped_name + ".pb.h"))
  140. if options.plugin_out_dir:
  141. plugin_options = FormatGeneratorOptions(options.plugin_options)
  142. protoc_cmd += [
  143. "--plugin", "protoc-gen-plugin=" + os.path.relpath(options.plugin),
  144. "--plugin_out", plugin_options + options.plugin_out_dir
  145. ]
  146. protoc_cmd += ["--proto_path", proto_dir]
  147. for path in options.import_dir:
  148. protoc_cmd += ["--proto_path", path]
  149. protoc_cmd += [os.path.join(proto_dir, name) for name in protos]
  150. if options.descriptor_set_out:
  151. protoc_cmd += ["--descriptor_set_out", options.descriptor_set_out]
  152. if not options.exclude_imports:
  153. protoc_cmd += ["--include_imports"]
  154. dependency_file_data = None
  155. if options.descriptor_set_out and options.descriptor_set_dependency_file:
  156. protoc_cmd += ['--dependency_out', options.descriptor_set_dependency_file]
  157. ret = subprocess.call(protoc_cmd)
  158. with open(options.descriptor_set_dependency_file, 'rb') as f:
  159. dependency_file_data = f.read().decode('utf-8')
  160. ret = subprocess.call(protoc_cmd)
  161. if ret != 0:
  162. if ret <= -100:
  163. # Windows error codes such as 0xC0000005 and 0xC0000409 are much easier to
  164. # recognize and differentiate in hex. In order to print them as unsigned
  165. # hex we need to add 4 Gig to them.
  166. error_number = "0x%08X" % (ret + (1 << 32))
  167. else:
  168. error_number = "%d" % ret
  169. raise RuntimeError("Protoc has returned non-zero status: "
  170. "{0}".format(error_number))
  171. if dependency_file_data:
  172. with open(options.descriptor_set_dependency_file, 'w') as f:
  173. f.write(options.descriptor_set_out + ":")
  174. f.write(dependency_file_data)
  175. if options.include:
  176. WriteIncludes(headers, options.include)
  177. if __name__ == "__main__":
  178. try:
  179. main(sys.argv[1:])
  180. except RuntimeError as e:
  181. print(e, file=sys.stderr)
  182. sys.exit(1)