nanopb_build_protos.py 8.7 KB

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