build_protos.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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 contextlib
  21. import datetime
  22. import io
  23. import os
  24. import os.path
  25. import re
  26. import stat
  27. import subprocess
  28. import tempfile
  29. CPP_GENERATOR = 'nanopb_cpp_generator.py'
  30. COPYRIGHT_NOTICE = '''
  31. /*
  32. * Copyright {} Google LLC
  33. *
  34. * Licensed under the Apache License, Version 2.0 (the "License");
  35. * you may not use this file except in compliance with the License.
  36. * You may obtain a copy of the License at
  37. *
  38. * http://www.apache.org/licenses/LICENSE-2.0
  39. *
  40. * Unless required by applicable law or agreed to in writing, software
  41. * distributed under the License is distributed on an "AS IS" BASIS,
  42. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  43. * See the License for the specific language governing permissions and
  44. * limitations under the License.
  45. */
  46. '''.format(datetime.datetime.now().year).lstrip()
  47. def main():
  48. parser = argparse.ArgumentParser(
  49. description='Generates proto messages.')
  50. parser.add_argument(
  51. '--nanopb', action='store_true',
  52. help='Generates nanopb messages.')
  53. parser.add_argument(
  54. '--cpp', action='store_true',
  55. help='Generates C++ libprotobuf messages.')
  56. parser.add_argument(
  57. '--objc', action='store_true',
  58. help='Generates Objective-C messages.')
  59. parser.add_argument(
  60. '--protos_dir',
  61. help='Source directory containing .proto files.')
  62. parser.add_argument(
  63. '--output_dir', '-d',
  64. help='Directory to write files; subdirectories will be created.')
  65. parser.add_argument(
  66. '--protoc', default='protoc',
  67. help='Location of the protoc executable')
  68. parser.add_argument(
  69. '--pythonpath',
  70. help='Location of the protoc python library.')
  71. parser.add_argument(
  72. '--include', '-I', action='append', default=[],
  73. help='Adds INCLUDE to the proto path.')
  74. args = parser.parse_args()
  75. if args.nanopb is None and args.cpp is None and args.objc is None:
  76. parser.print_help()
  77. sys.exit(1)
  78. if args.protos_dir is None:
  79. root_dir = os.path.abspath(os.path.dirname(__file__))
  80. args.protos_dir = os.path.join(root_dir, 'protos')
  81. if args.output_dir is None:
  82. args.output_dir = os.getcwd()
  83. all_proto_files = collect_files(args.protos_dir, '.proto')
  84. if args.nanopb:
  85. NanopbGenerator(args, all_proto_files).run()
  86. proto_files = remove_well_known_protos(all_proto_files)
  87. if args.cpp:
  88. CppProtobufGenerator(args, proto_files).run()
  89. if args.objc:
  90. ObjcProtobufGenerator(args, proto_files).run()
  91. @contextlib.contextmanager
  92. def CppGeneratorScriptTweaked(path):
  93. """
  94. Set the shebang line of the CPP_GENERATOR script to use the same Python
  95. interpreter as this process.
  96. This is a workaround for the fact that `python` is hardcoded as the python
  97. interpreter, which does not always exist in the new world where Python2
  98. support has largely disappeared (e.g. macOS 12.3). Changing it to `python3`
  99. would possibly break some builds too.
  100. """
  101. # Read the script into memory.
  102. with io.open(path, 'rt', encoding='utf8') as f:
  103. lines = [line for line in f]
  104. # Verify that the read file looks like the right one.
  105. if lines[0] != u'#!/usr/bin/env python\n':
  106. raise RuntimeError('unexpected first line of ' + path + ': ' + lines[0])
  107. # Replace the shebang line with a custom one.
  108. lines[0] = u'#!' + sys.executable + u'\n'
  109. # Create a temporary file to which to write the tweaked script.
  110. (handle, temp_path) = tempfile.mkstemp('.py', dir=os.path.dirname(path))
  111. os.close(handle)
  112. try:
  113. # Write the lines of the tweaked script to the temporary file.
  114. with io.open(temp_path, 'wt', encoding='utf8') as f:
  115. f.writelines(lines)
  116. # Make sure that the temporary file is executable.
  117. st = os.stat(temp_path)
  118. os.chmod(temp_path, st.st_mode | stat.S_IEXEC)
  119. yield temp_path
  120. finally:
  121. os.unlink(temp_path)
  122. class NanopbGenerator(object):
  123. """Builds and runs the nanopb plugin to protoc."""
  124. def __init__(self, args, proto_files):
  125. self.args = args
  126. self.proto_files = proto_files
  127. def run(self):
  128. """Performs the action of the generator."""
  129. nanopb_out = os.path.join(self.args.output_dir, 'nanopb')
  130. mkdir(nanopb_out)
  131. self.__run_generator(nanopb_out)
  132. sources = collect_files(nanopb_out, '.nanopb.h', '.nanopb.cc')
  133. post_process_files(
  134. sources,
  135. add_copyright,
  136. nanopb_remove_extern_c
  137. )
  138. def __run_generator(self, out_dir):
  139. """Invokes protoc using the nanopb plugin."""
  140. cmd = protoc_command(self.args)
  141. nanopb_flags = ' '.join([
  142. '--extension=.nanopb',
  143. '--source-extension=.cc',
  144. '--no-timestamp',
  145. # Make sure Nanopb finds the `.options` files. See
  146. # https://jpa.kapsi.fi/nanopb/docs/reference.html#defining-the-options-in-a-options-file
  147. # "...if your .proto is in a subdirectory, nanopb may have trouble
  148. # finding the associated .options file. A workaround is to specify
  149. # include path separately to the nanopb plugin"
  150. '-I' + self.args.protos_dir,
  151. ])
  152. cmd.append('--nanopb_out=%s:%s' % (nanopb_flags, out_dir))
  153. gen = os.path.join(os.path.dirname(__file__), CPP_GENERATOR)
  154. with CppGeneratorScriptTweaked(gen) as gen_tweaked:
  155. cmd.append('--plugin=protoc-gen-nanopb=%s' % gen_tweaked)
  156. cmd.extend(self.proto_files)
  157. run_protoc(self.args, cmd)
  158. class ObjcProtobufGenerator(object):
  159. """Runs protoc for Objective-C."""
  160. def __init__(self, args, proto_files):
  161. self.args = args
  162. self.proto_files = proto_files
  163. def run(self):
  164. objc_out = os.path.join(self.args.output_dir, 'objc')
  165. mkdir(objc_out)
  166. self.__run_generator(objc_out)
  167. self.__stub_non_buildable_files(objc_out)
  168. sources = collect_files(objc_out, '.h', '.m')
  169. post_process_files(
  170. sources,
  171. add_copyright,
  172. strip_trailing_whitespace,
  173. objc_flatten_imports,
  174. objc_strip_extension_registry
  175. )
  176. def __run_generator(self, out_dir):
  177. """Invokes protoc using the objc plugin."""
  178. cmd = protoc_command(self.args)
  179. cmd.extend(['--objc_out=' + out_dir])
  180. cmd.extend(self.proto_files)
  181. run_protoc(self.args, cmd)
  182. def __stub_non_buildable_files(self, out_dir):
  183. """Stub out generated files that make no sense."""
  184. write_file(os.path.join(out_dir, 'google/api/Annotations.pbobjc.m'), [
  185. 'static int annotations_stub __attribute__((unused,used)) = 0;\n'
  186. ])
  187. write_file(os.path.join(out_dir, 'google/api/Annotations.pbobjc.h'), [
  188. '// Empty stub file\n'
  189. ])
  190. class CppProtobufGenerator(object):
  191. """Runs protoc for C++ libprotobuf (used in testing)."""
  192. def __init__(self, args, proto_files):
  193. self.args = args
  194. self.proto_files = proto_files
  195. def run(self):
  196. out_dir = os.path.join(self.args.output_dir, 'cpp')
  197. mkdir(out_dir)
  198. self.__run_generator(out_dir)
  199. sources = collect_files(out_dir, '.pb.h', '.pb.cc')
  200. # TODO(wilhuff): strip trailing whitespace?
  201. post_process_files(
  202. sources,
  203. add_copyright,
  204. cpp_rename_in,
  205. )
  206. def __run_generator(self, out_dir):
  207. """Invokes protoc using using the default C++ generator."""
  208. cmd = protoc_command(self.args)
  209. cmd.append('--cpp_out=' + out_dir)
  210. cmd.extend(self.proto_files)
  211. run_protoc(self.args, cmd)
  212. def protoc_command(args):
  213. """Composes the initial protoc command-line including its include path."""
  214. cmd = [args.protoc]
  215. if args.include is not None:
  216. cmd.extend(['-I%s' % path for path in args.include])
  217. return cmd
  218. def run_protoc(args, cmd):
  219. """Actually runs the given protoc command.
  220. Args:
  221. args: The command-line args (including pythonpath)
  222. cmd: The command to run expressed as a list of strings
  223. """
  224. kwargs = {}
  225. if args.pythonpath:
  226. env = os.environ.copy()
  227. old_path = env.get('PYTHONPATH')
  228. env['PYTHONPATH'] = args.pythonpath
  229. if old_path is not None:
  230. env['PYTHONPATH'] += os.pathsep + old_path
  231. kwargs['env'] = env
  232. subprocess.check_call(cmd, **kwargs)
  233. def remove_well_known_protos(filenames):
  234. """Remove "well-known" protos for objc and cpp.
  235. On those platforms we get these for free as a part of the protobuf runtime.
  236. We only need them for nanopb.
  237. Args:
  238. filenames: A list of filenames, each naming a .proto file.
  239. Returns:
  240. The filenames with members of google/protobuf removed.
  241. """
  242. return [f for f in filenames if 'protos/google/protobuf/' not in f]
  243. def post_process_files(filenames, *processors):
  244. for filename in filenames:
  245. lines = []
  246. with open(filename, 'r') as fd:
  247. lines = fd.readlines()
  248. for processor in processors:
  249. lines = processor(lines)
  250. write_file(filename, lines)
  251. def write_file(filename, lines):
  252. with open(filename, 'w') as fd:
  253. fd.write(''.join(lines))
  254. def add_copyright(lines):
  255. """Adds a copyright notice to the lines."""
  256. result = [COPYRIGHT_NOTICE, '\n']
  257. result.extend(lines)
  258. return result
  259. # TODO(varconst|wilhuff): move this to `nanopb_cpp_generator.py`.
  260. def nanopb_remove_extern_c(lines):
  261. """Removes extern "C" directives from nanopb code.
  262. Args:
  263. lines: A nanobp-generated source file, split into lines.
  264. Returns:
  265. A list of strings, similar to the input but modified to remove extern "C".
  266. """
  267. result = []
  268. state = 'initial'
  269. for line in lines:
  270. if state == 'initial':
  271. if '#ifdef __cplusplus' in line:
  272. state = 'in-ifdef'
  273. continue
  274. result.append(line)
  275. elif state == 'in-ifdef':
  276. if '#endif' in line:
  277. state = 'initial'
  278. return result
  279. def cpp_rename_in(lines):
  280. """Renames an IN symbol to IN_.
  281. If a proto uses a enum member named 'IN', protobuf happily uses that in the
  282. message definition. This conflicts with the IN parameter annotation macro in
  283. windows.h.
  284. Args:
  285. lines: The lines to fix.
  286. Returns:
  287. The lines, fixed.
  288. """
  289. in_macro = re.compile(r'\bIN\b')
  290. return [in_macro.sub('IN_', line) for line in lines]
  291. def strip_trailing_whitespace(lines):
  292. """Removes trailing whitespace from the given lines."""
  293. return [line.rstrip() + '\n' for line in lines]
  294. def objc_flatten_imports(lines):
  295. """Flattens the import statements for compatibility with CocoaPods."""
  296. long_import = re.compile(r'#import ".*/')
  297. return [long_import.sub('#import "', line) for line in lines]
  298. def objc_strip_extension_registry(lines):
  299. """Removes extensionRegistry methods from the classes."""
  300. skip = False
  301. result = []
  302. for line in lines:
  303. if '+ (GPBExtensionRegistry*)extensionRegistry {' in line:
  304. skip = True
  305. if not skip:
  306. result.append(line)
  307. elif line == '}\n':
  308. skip = False
  309. return result
  310. def collect_files(root_dir, *extensions):
  311. """Finds files with the given extensions in the root_dir.
  312. Args:
  313. root_dir: The directory from which to start traversing.
  314. *extensions: Filename extensions (including the leading dot) to find.
  315. Returns:
  316. A list of filenames, all starting with root_dir, that have one of the given
  317. extensions.
  318. """
  319. result = []
  320. for root, _, files in os.walk(root_dir):
  321. for basename in files:
  322. for ext in extensions:
  323. if basename.endswith(ext):
  324. filename = os.path.join(root, basename)
  325. result.append(filename)
  326. return result
  327. def mkdir(dirname):
  328. if not os.path.isdir(dirname):
  329. os.makedirs(dirname)
  330. if __name__ == '__main__':
  331. main()