proto_generator.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. #! /usr/bin/env python
  2. # Copyright 2022 Google LLC
  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. Example usage:
  17. python Crashlytics/ProtoSupport/build_protos.py \
  18. --nanopb \
  19. --protos_dir=Crashlytics/Classes/Protos/ \
  20. --pythonpath=~/Downloads/nanopb-0.3.9.2-macosx-x86/generator/ \
  21. --output_dir=Crashlytics/Protogen/
  22. """
  23. from __future__ import print_function
  24. from inspect import signature
  25. import sys
  26. import argparse
  27. import os
  28. import os.path
  29. import re
  30. import subprocess
  31. OBJC_GENERATOR = 'nanopb_objc_generator.py'
  32. COPYRIGHT_NOTICE = '''
  33. /*
  34. * Copyright 2022 Google LLC
  35. *
  36. * Licensed under the Apache License, Version 2.0 (the "License");
  37. * you may not use this file except in compliance with the License.
  38. * You may obtain a copy of the License at
  39. *
  40. * http://www.apache.org/licenses/LICENSE-2.0
  41. *
  42. * Unless required by applicable law or agreed to in writing, software
  43. * distributed under the License is distributed on an "AS IS" BASIS,
  44. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  45. * See the License for the specific language governing permissions and
  46. * limitations under the License.
  47. */
  48. '''.lstrip()
  49. def main():
  50. parser = argparse.ArgumentParser(
  51. description='Generates proto messages.')
  52. parser.add_argument(
  53. '--nanopb', action='store_true',
  54. help='Generates nanopb messages.')
  55. parser.add_argument(
  56. '--objc', action='store_true',
  57. help='Generates Objective-C messages.')
  58. parser.add_argument(
  59. '--protos_dir',
  60. help='Source directory containing .proto files.')
  61. parser.add_argument(
  62. '--output_dir', '-d',
  63. help='Directory to write files; subdirectories will be created.')
  64. parser.add_argument(
  65. '--protoc', default='protoc',
  66. help='Location of the protoc executable')
  67. parser.add_argument(
  68. '--pythonpath',
  69. help='Location of the protoc python library.')
  70. parser.add_argument(
  71. '--include', '-I', action='append', default=[],
  72. help='Adds INCLUDE to the proto path.')
  73. parser.add_argument(
  74. '--include_prefix', '-p', action='append', default=[],
  75. help='Adds include_prefix to the <product>.nanopb.h include in'
  76. ' .nanopb.c')
  77. args = parser.parse_args()
  78. if args.nanopb is None and args.objc is None:
  79. parser.print_help()
  80. sys.exit(1)
  81. if args.protos_dir is None:
  82. root_dir = os.path.abspath(os.path.dirname(__file__))
  83. args.protos_dir = os.path.join(root_dir, 'protos')
  84. if args.output_dir is None:
  85. root_dir = os.path.abspath(os.path.dirname(__file__))
  86. args.output_dir = os.path.join(
  87. root_dir, 'protogen-please-supply-an-outputdir')
  88. all_proto_files = collect_files(args.protos_dir, '.proto')
  89. if args.nanopb:
  90. NanopbGenerator(args, all_proto_files).run()
  91. if args.objc:
  92. print('Generating objc code is unsupported because it depends on the'
  93. 'main protobuf Podspec that adds a lot of size to SDKs.')
  94. class NanopbGenerator(object):
  95. """Builds and runs the nanopb plugin to protoc."""
  96. def __init__(self, args, proto_files):
  97. self.args = args
  98. self.proto_files = proto_files
  99. def run(self):
  100. """Performs the action of the generator."""
  101. nanopb_out = os.path.join(self.args.output_dir, 'nanopb')
  102. mkdir(nanopb_out)
  103. self.__run_generator(nanopb_out)
  104. sources = collect_files(nanopb_out, '.nanopb.h', '.nanopb.c')
  105. post_process_files(
  106. sources,
  107. add_copyright,
  108. nanopb_remove_extern_c,
  109. nanopb_rename_delete,
  110. nanopb_use_module_import,
  111. make_use_absolute_import(nanopb_out, self.args)
  112. )
  113. def __run_generator(self, out_dir):
  114. """Invokes protoc using the nanopb plugin."""
  115. cmd = protoc_command(self.args)
  116. gen = os.path.join(os.path.dirname(__file__), OBJC_GENERATOR)
  117. cmd.append('--plugin=protoc-gen-nanopb=%s' % gen)
  118. nanopb_flags = [
  119. '--extension=.nanopb',
  120. '--source-extension=.c',
  121. '--no-timestamp'
  122. ]
  123. nanopb_flags.extend(['-I%s' % path for path in self.args.include])
  124. cmd.append('--nanopb_out=%s:%s' % (' '.join(nanopb_flags), out_dir))
  125. cmd.extend(self.proto_files)
  126. run_protoc(self.args, cmd)
  127. def protoc_command(args):
  128. """Composes the initial protoc command-line including its include path."""
  129. cmd = [args.protoc]
  130. if args.include is not None:
  131. cmd.extend(['-I=%s' % path for path in args.include])
  132. return cmd
  133. def run_protoc(args, cmd):
  134. """Actually runs the given protoc command.
  135. Args:
  136. args: The command-line args (including pythonpath)
  137. cmd: The command to run expressed as a list of strings
  138. """
  139. kwargs = {}
  140. if args.pythonpath:
  141. env = os.environ.copy()
  142. old_path = env.get('PYTHONPATH')
  143. env['PYTHONPATH'] = os.path.expanduser(args.pythonpath)
  144. if old_path is not None:
  145. env['PYTHONPATH'] += os.pathsep + old_path
  146. kwargs['env'] = env
  147. try:
  148. outputString = subprocess.check_output(
  149. cmd, stderr=subprocess.STDOUT, **kwargs)
  150. print(outputString.decode("utf-8"))
  151. except subprocess.CalledProcessError as error:
  152. print('command failed: ', ' '.join(cmd), '\nerror: ', error.output)
  153. def post_process_files(filenames, *processors):
  154. for filename in filenames:
  155. lines = []
  156. with open(filename, 'r') as fd:
  157. lines = fd.readlines()
  158. for processor in processors:
  159. sig = signature(processor)
  160. if len(sig.parameters) == 1:
  161. lines = processor(lines)
  162. else:
  163. lines = processor(lines, filename)
  164. write_file(filename, lines)
  165. def write_file(filename, lines):
  166. mkdir(os.path.dirname(filename))
  167. with open(filename, 'w') as fd:
  168. fd.write(''.join(lines))
  169. def add_copyright(lines):
  170. """Adds a copyright notice to the lines."""
  171. if COPYRIGHT_NOTICE in lines:
  172. return lines
  173. result = [COPYRIGHT_NOTICE, '\n']
  174. result.extend(lines)
  175. return result
  176. def nanopb_remove_extern_c(lines):
  177. """Removes extern "C" directives from nanopb code.
  178. Args:
  179. lines: A nanobp-generated source file, split into lines.
  180. Returns:
  181. A list of strings, similar to the input but modified to remove extern "C".
  182. """
  183. result = []
  184. state = 'initial'
  185. for line in lines:
  186. if state == 'initial':
  187. if '#ifdef __cplusplus' in line:
  188. state = 'in-ifdef'
  189. continue
  190. result.append(line)
  191. elif state == 'in-ifdef':
  192. if '#endif' in line:
  193. state = 'initial'
  194. return result
  195. def nanopb_rename_delete(lines):
  196. """Renames a delete symbol to delete_.
  197. If a proto uses a field named 'delete', nanopb happily uses that in the
  198. message definition. Works fine for C; not so much for C++.
  199. Args:
  200. lines: The lines to fix.
  201. Returns:
  202. The lines, fixed.
  203. """
  204. delete_keyword = re.compile(r'\bdelete\b')
  205. return [delete_keyword.sub('delete_', line) for line in lines]
  206. # Don't let Copybara alter these lines.
  207. def nanopb_use_module_import(lines):
  208. """Changes #include <pb.h> to include <nanopb/pb.h>"""
  209. return [line.replace('#include <pb.h>',
  210. '{}include <nanopb/pb.h>'.format("#"))
  211. for line in lines]
  212. def make_use_absolute_import(nanopb_out, args):
  213. import_file = collect_files(nanopb_out, '.nanopb.h')[0]
  214. def nanopb_use_absolute_import(lines, filename):
  215. """Makes repo-relative imports
  216. #include "crashlytics.nanopb.h" =>
  217. #include "Crashlytics/Protogen/nanopb/crashlytics.nanopb.h"
  218. This only applies to .nanopb.c files because it causes errors if
  219. .nanopb.h files import other .nanopb.h files with full relative
  220. paths.
  221. """
  222. if ".h" in filename:
  223. return lines
  224. include_prefix = args.include_prefix[0]
  225. header = os.path.basename(import_file)
  226. return [line.replace('#include "{0}"'.format(header),
  227. '#include "{0}{1}"'.format(include_prefix, header))
  228. for line in lines]
  229. return nanopb_use_absolute_import
  230. def strip_trailing_whitespace(lines):
  231. """Removes trailing whitespace from the given lines."""
  232. return [line.rstrip() + '\n' for line in lines]
  233. def objc_flatten_imports(lines):
  234. """Flattens the import statements for compatibility with CocoaPods."""
  235. long_import = re.compile(r'#import ".*/')
  236. return [long_import.sub('#import "', line) for line in lines]
  237. def objc_strip_extension_registry(lines):
  238. """Removes extensionRegistry methods from the classes."""
  239. skip = False
  240. result = []
  241. for line in lines:
  242. if '+ (GPBExtensionRegistry*)extensionRegistry {' in line:
  243. skip = True
  244. if not skip:
  245. result.append(line)
  246. elif line == '}\n':
  247. skip = False
  248. return result
  249. def collect_files(root_dir, *extensions):
  250. """Finds files with the given extensions in the root_dir.
  251. Args:
  252. root_dir: The directory from which to start traversing.
  253. *extensions: Filename extensions (including the leading dot) to find.
  254. Returns:
  255. A list of filenames, all starting with root_dir, that have one of the
  256. given extensions.
  257. """
  258. result = []
  259. for root, _, files in os.walk(root_dir):
  260. for basename in files:
  261. for ext in extensions:
  262. if basename.endswith(ext):
  263. filename = os.path.join(root, basename)
  264. result.append(filename)
  265. return result
  266. def mkdir(dirname):
  267. if not os.path.isdir(dirname):
  268. os.makedirs(dirname)
  269. if __name__ == '__main__':
  270. main()