build_protos.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. #! /usr/bin/env python
  2. # Copyright 2018 Google
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Generates and massages protocol buffer outputs.
  16. """
  17. from __future__ import print_function
  18. import sys
  19. import argparse
  20. import os
  21. import os.path
  22. import re
  23. import subprocess
  24. CPP_GENERATOR = 'nanopb_cpp_generator.py'
  25. COPYRIGHT_NOTICE = '''
  26. /*
  27. * Copyright 2018 Google
  28. *
  29. * Licensed under the Apache License, Version 2.0 (the "License");
  30. * you may not use this file except in compliance with the License.
  31. * You may obtain a copy of the License at
  32. *
  33. * http://www.apache.org/licenses/LICENSE-2.0
  34. *
  35. * Unless required by applicable law or agreed to in writing, software
  36. * distributed under the License is distributed on an "AS IS" BASIS,
  37. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  38. * See the License for the specific language governing permissions and
  39. * limitations under the License.
  40. */
  41. '''.lstrip()
  42. def main():
  43. parser = argparse.ArgumentParser(
  44. description='Generates proto messages.')
  45. parser.add_argument(
  46. '--nanopb', action='store_true',
  47. help='Generates nanopb messages.')
  48. parser.add_argument(
  49. '--cpp', action='store_true',
  50. help='Generates C++ libprotobuf messages.')
  51. parser.add_argument(
  52. '--objc', action='store_true',
  53. help='Generates Objective-C messages.')
  54. parser.add_argument(
  55. '--protos_dir',
  56. help='Source directory containing .proto files.')
  57. parser.add_argument(
  58. '--output_dir', '-d',
  59. help='Directory to write files; subdirectories will be created.')
  60. parser.add_argument(
  61. '--protoc', default='protoc',
  62. help='Location of the protoc executable')
  63. parser.add_argument(
  64. '--pythonpath',
  65. help='Location of the protoc python library.')
  66. parser.add_argument(
  67. '--include', '-I', action='append', default=[],
  68. help='Adds INCLUDE to the proto path.')
  69. args = parser.parse_args()
  70. if args.nanopb is None and args.cpp is None and args.objc is None:
  71. parser.print_help()
  72. sys.exit(1)
  73. if args.protos_dir is None:
  74. root_dir = os.path.abspath(os.path.dirname(__file__))
  75. args.protos_dir = os.path.join(root_dir, 'protos')
  76. if args.output_dir is None:
  77. args.output_dir = os.getcwd()
  78. all_proto_files = collect_files(args.protos_dir, '.proto')
  79. if args.nanopb:
  80. NanopbGenerator(args, all_proto_files).run()
  81. proto_files = remove_well_known_protos(all_proto_files)
  82. if args.cpp:
  83. CppProtobufGenerator(args, proto_files).run()
  84. if args.objc:
  85. ObjcProtobufGenerator(args, proto_files).run()
  86. class NanopbGenerator(object):
  87. """Builds and runs the nanopb plugin to protoc."""
  88. def __init__(self, args, proto_files):
  89. self.args = args
  90. self.proto_files = proto_files
  91. def run(self):
  92. """Performs the action of the generator."""
  93. nanopb_out = os.path.join(self.args.output_dir, 'nanopb')
  94. mkdir(nanopb_out)
  95. self.__run_generator(nanopb_out)
  96. sources = collect_files(nanopb_out, '.nanopb.h', '.nanopb.cc')
  97. post_process_files(
  98. sources,
  99. add_copyright,
  100. nanopb_add_namespaces,
  101. nanopb_remove_extern_c,
  102. nanopb_rename_delete
  103. )
  104. def __run_generator(self, out_dir):
  105. """Invokes protoc using the nanopb plugin."""
  106. cmd = protoc_command(self.args)
  107. gen = os.path.join(os.path.dirname(__file__), CPP_GENERATOR)
  108. cmd.append('--plugin=protoc-gen-nanopb=%s' % gen)
  109. nanopb_flags = ' '.join([
  110. '--extension=.nanopb',
  111. '--source-extension=.cc',
  112. '--no-timestamp',
  113. ])
  114. cmd.append('--nanopb_out=%s:%s' % (nanopb_flags, out_dir))
  115. cmd.extend(self.proto_files)
  116. run_protoc(self.args, cmd)
  117. class ObjcProtobufGenerator(object):
  118. """Runs protoc for Objective-C."""
  119. def __init__(self, args, proto_files):
  120. self.args = args
  121. self.proto_files = proto_files
  122. def run(self):
  123. objc_out = os.path.join(self.args.output_dir, 'objc')
  124. mkdir(objc_out)
  125. self.__run_generator(objc_out)
  126. self.__stub_non_buildable_files(objc_out)
  127. sources = collect_files(objc_out, '.h', '.m')
  128. post_process_files(
  129. sources,
  130. add_copyright,
  131. strip_trailing_whitespace,
  132. objc_flatten_imports,
  133. objc_strip_extension_registry
  134. )
  135. def __run_generator(self, out_dir):
  136. """Invokes protoc using the objc plugin."""
  137. cmd = protoc_command(self.args)
  138. cmd.extend(['--objc_out=' + out_dir])
  139. cmd.extend(self.proto_files)
  140. run_protoc(self.args, cmd)
  141. def __stub_non_buildable_files(self, out_dir):
  142. """Stub out generated files that make no sense."""
  143. write_file(os.path.join(out_dir, 'google/api/Annotations.pbobjc.m'), [
  144. 'static int annotations_stub __attribute__((unused,used)) = 0;\n'
  145. ])
  146. write_file(os.path.join(out_dir, 'google/api/Annotations.pbobjc.h'), [
  147. '// Empty stub file\n'
  148. ])
  149. class CppProtobufGenerator(object):
  150. """Runs protoc for C++ libprotobuf (used in testing)."""
  151. def __init__(self, args, proto_files):
  152. self.args = args
  153. self.proto_files = proto_files
  154. def run(self):
  155. out_dir = os.path.join(self.args.output_dir, 'cpp')
  156. mkdir(out_dir)
  157. self.__run_generator(out_dir)
  158. sources = collect_files(out_dir, '.pb.h', '.pb.cc')
  159. # TODO(wilhuff): strip trailing whitespace?
  160. post_process_files(
  161. sources,
  162. add_copyright
  163. )
  164. def __run_generator(self, out_dir):
  165. """Invokes protoc using using the default C++ generator."""
  166. cmd = protoc_command(self.args)
  167. cmd.append('--cpp_out=' + out_dir)
  168. cmd.extend(self.proto_files)
  169. run_protoc(self.args, cmd)
  170. def protoc_command(args):
  171. """Composes the initial protoc command-line including its include path."""
  172. cmd = [args.protoc]
  173. if args.include is not None:
  174. cmd.extend(['-I%s' % path for path in args.include])
  175. return cmd
  176. def run_protoc(args, cmd):
  177. """Actually runs the given protoc command.
  178. Args:
  179. args: The command-line args (including pythonpath)
  180. cmd: The command to run expressed as a list of strings
  181. """
  182. kwargs = {}
  183. if args.pythonpath:
  184. env = os.environ.copy()
  185. old_path = env.get('PYTHONPATH')
  186. env['PYTHONPATH'] = args.pythonpath
  187. if old_path is not None:
  188. env['PYTHONPATH'] += os.pathsep + old_path
  189. kwargs['env'] = env
  190. subprocess.check_call(cmd, **kwargs)
  191. def remove_well_known_protos(filenames):
  192. """Remove "well-known" protos for objc and cpp.
  193. On those platforms we get these for free as a part of the protobuf runtime.
  194. We only need them for nanopb.
  195. Args:
  196. filenames: A list of filenames, each naming a .proto file.
  197. Returns:
  198. The filenames with members of google/protobuf removed.
  199. """
  200. return [f for f in filenames if 'protos/google/protobuf/' not in f]
  201. def post_process_files(filenames, *processors):
  202. for filename in filenames:
  203. lines = []
  204. with open(filename, 'r') as fd:
  205. lines = fd.readlines()
  206. for processor in processors:
  207. lines = processor(lines)
  208. write_file(filename, lines)
  209. def write_file(filename, lines):
  210. with open(filename, 'w') as fd:
  211. fd.write(''.join(lines))
  212. def add_copyright(lines):
  213. """Adds a copyright notice to the lines."""
  214. result = [COPYRIGHT_NOTICE, '\n']
  215. result.extend(lines)
  216. return result
  217. def nanopb_add_namespaces(lines):
  218. """Adds C++ namespaces to the lines.
  219. Args:
  220. lines: The lines to fix.
  221. Returns:
  222. The lines, fixed.
  223. """
  224. result = []
  225. for line in lines:
  226. if '@@protoc_insertion_point(includes)' in line:
  227. result.append('namespace firebase {\n')
  228. result.append('namespace firestore {\n')
  229. result.append('\n')
  230. if '@@protoc_insertion_point(eof)' in line:
  231. result.append('} // namespace firestore\n')
  232. result.append('} // namespace firebase\n')
  233. result.append('\n')
  234. result.append(line)
  235. return result
  236. def nanopb_remove_extern_c(lines):
  237. """Removes extern "C" directives from nanopb code.
  238. Args:
  239. lines: A nanobp-generated source file, split into lines.
  240. Returns:
  241. A list of strings, similar to the input but modified to remove extern "C".
  242. """
  243. result = []
  244. state = 'initial'
  245. for line in lines:
  246. if state == 'initial':
  247. if '#ifdef __cplusplus' in line:
  248. state = 'in-ifdef'
  249. continue
  250. result.append(line)
  251. elif state == 'in-ifdef':
  252. if '#endif' in line:
  253. state = 'initial'
  254. return result
  255. def nanopb_rename_delete(lines):
  256. """Renames a delete symbol to delete_.
  257. If a proto uses a field named 'delete', nanopb happily uses that in the
  258. message definition. Works fine for C; not so much for C++.
  259. Args:
  260. lines: The lines to fix.
  261. Returns:
  262. The lines, fixed.
  263. """
  264. delete_keyword = re.compile(r'\bdelete\b')
  265. return [delete_keyword.sub('delete_', line) for line in lines]
  266. def strip_trailing_whitespace(lines):
  267. """Removes trailing whitespace from the given lines."""
  268. return [line.rstrip() + '\n' for line in lines]
  269. def objc_flatten_imports(lines):
  270. """Flattens the import statements for compatibility with CocoaPods."""
  271. long_import = re.compile(r'#import ".*/')
  272. return [long_import.sub('#import "', line) for line in lines]
  273. def objc_strip_extension_registry(lines):
  274. """Removes extensionRegistry methods from the classes."""
  275. skip = False
  276. result = []
  277. for line in lines:
  278. if '+ (GPBExtensionRegistry*)extensionRegistry {' in line:
  279. skip = True
  280. if not skip:
  281. result.append(line)
  282. elif line == '}\n':
  283. skip = False
  284. return result
  285. def collect_files(root_dir, *extensions):
  286. """Finds files with the given extensions in the root_dir.
  287. Args:
  288. root_dir: The directory from which to start traversing.
  289. *extensions: Filename extensions (including the leading dot) to find.
  290. Returns:
  291. A list of filenames, all starting with root_dir, that have one of the given
  292. extensions.
  293. """
  294. result = []
  295. for root, _, files in os.walk(root_dir):
  296. for basename in files:
  297. for ext in extensions:
  298. if basename.endswith(ext):
  299. filename = os.path.join(root, basename)
  300. result.append(filename)
  301. return result
  302. def mkdir(dirname):
  303. if not os.path.isdir(dirname):
  304. os.makedirs(dirname)
  305. if __name__ == '__main__':
  306. main()