🛠️🐜 Antkeeper superbuild with dependencies included https://antkeeper.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
11 KiB

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. # amalgamate.py - Amalgamate C source and header files.
  4. # Copyright (c) 2012, Erik Edlund <erik.edlund@32767.se>
  5. #
  6. # Redistribution and use in source and binary forms, with or without modification,
  7. # are permitted provided that the following conditions are met:
  8. #
  9. # * Redistributions of source code must retain the above copyright notice,
  10. # this list of conditions and the following disclaimer.
  11. #
  12. # * Redistributions in binary form must reproduce the above copyright notice,
  13. # this list of conditions and the following disclaimer in the documentation
  14. # and/or other materials provided with the distribution.
  15. #
  16. # * Neither the name of Erik Edlund, nor the names of its contributors may
  17. # be used to endorse or promote products derived from this software without
  18. # specific prior written permission.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  21. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  22. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  23. # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
  24. # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  25. # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  26. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  27. # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  28. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  29. # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30. from __future__ import division
  31. from __future__ import print_function
  32. from __future__ import unicode_literals
  33. import argparse
  34. import datetime
  35. import json
  36. import os
  37. import re
  38. class Amalgamation(object):
  39. # Prepends self.source_path to file_path if needed.
  40. def actual_path(self, file_path):
  41. if not os.path.isabs(file_path):
  42. file_path = os.path.join(self.source_path, file_path)
  43. return file_path
  44. # Search included file_path in self.include_paths and
  45. # in source_dir if specified.
  46. def find_included_file(self, file_path, source_dir):
  47. search_dirs = self.include_paths[:]
  48. if source_dir:
  49. search_dirs.insert(0, source_dir)
  50. for search_dir in search_dirs:
  51. search_path = os.path.join(search_dir, file_path)
  52. if os.path.isfile(self.actual_path(search_path)):
  53. return search_path
  54. return None
  55. def __init__(self, args):
  56. with open(args.config, 'r') as f:
  57. config = json.loads(f.read())
  58. for key in config:
  59. setattr(self, key, config[key])
  60. self.verbose = args.verbose == "yes"
  61. self.prologue = args.prologue
  62. self.source_path = args.source_path
  63. self.included_files = []
  64. # Generate the amalgamation and write it to the target file.
  65. def generate(self):
  66. amalgamation = ""
  67. if self.prologue:
  68. with open(self.prologue, 'r') as f:
  69. amalgamation += datetime.datetime.now().strftime(f.read())
  70. if self.verbose:
  71. print("Config:")
  72. print(" target = {0}".format(self.target))
  73. print(" working_dir = {0}".format(os.getcwd()))
  74. print(" include_paths = {0}".format(self.include_paths))
  75. print("Creating amalgamation:")
  76. for file_path in self.sources:
  77. # Do not check the include paths while processing the source
  78. # list, all given source paths must be correct.
  79. # actual_path = self.actual_path(file_path)
  80. print(" - processing \"{0}\"".format(file_path))
  81. t = TranslationUnit(file_path, self, True)
  82. amalgamation += t.content
  83. with open(self.target, 'w') as f:
  84. f.write(amalgamation)
  85. print("...done!\n")
  86. if self.verbose:
  87. print("Files processed: {0}".format(self.sources))
  88. print("Files included: {0}".format(self.included_files))
  89. print("")
  90. def _is_within(match, matches):
  91. for m in matches:
  92. if match.start() > m.start() and \
  93. match.end() < m.end():
  94. return True
  95. return False
  96. class TranslationUnit(object):
  97. # // C++ comment.
  98. cpp_comment_pattern = re.compile(r"//.*?\n")
  99. # /* C comment. */
  100. c_comment_pattern = re.compile(r"/\*.*?\*/", re.S)
  101. # "complex \"stri\\\ng\" value".
  102. string_pattern = re.compile("[^']" r'".*?(?<=[^\\])"', re.S)
  103. # Handle simple include directives. Support for advanced
  104. # directives where macros and defines needs to expanded is
  105. # not a concern right now.
  106. include_pattern = re.compile(
  107. r'#\s*include\s+(<|")(?P<path>.*?)("|>)', re.S)
  108. # #pragma once
  109. pragma_once_pattern = re.compile(r'#\s*pragma\s+once', re.S)
  110. # Search for pattern in self.content, add the match to
  111. # contexts if found and update the index accordingly.
  112. def _search_content(self, index, pattern, contexts):
  113. match = pattern.search(self.content, index)
  114. if match:
  115. contexts.append(match)
  116. return match.end()
  117. return index + 2
  118. # Return all the skippable contexts, i.e., comments and strings
  119. def _find_skippable_contexts(self):
  120. # Find contexts in the content in which a found include
  121. # directive should not be processed.
  122. skippable_contexts = []
  123. # Walk through the content char by char, and try to grab
  124. # skippable contexts using regular expressions when found.
  125. i = 1
  126. content_len = len(self.content)
  127. while i < content_len:
  128. j = i - 1
  129. current = self.content[i]
  130. previous = self.content[j]
  131. if current == '"':
  132. # String value.
  133. i = self._search_content(j, self.string_pattern,
  134. skippable_contexts)
  135. elif current == '*' and previous == '/':
  136. # C style comment.
  137. i = self._search_content(j, self.c_comment_pattern,
  138. skippable_contexts)
  139. elif current == '/' and previous == '/':
  140. # C++ style comment.
  141. i = self._search_content(j, self.cpp_comment_pattern,
  142. skippable_contexts)
  143. else:
  144. # Skip to the next char.
  145. i += 1
  146. return skippable_contexts
  147. # Returns True if the match is within list of other matches
  148. # Removes pragma once from content
  149. def _process_pragma_once(self):
  150. content_len = len(self.content)
  151. if content_len < len("#include <x>"):
  152. return 0
  153. # Find contexts in the content in which a found include
  154. # directive should not be processed.
  155. skippable_contexts = self._find_skippable_contexts()
  156. pragmas = []
  157. pragma_once_match = self.pragma_once_pattern.search(self.content)
  158. while pragma_once_match:
  159. if not _is_within(pragma_once_match, skippable_contexts):
  160. pragmas.append(pragma_once_match)
  161. pragma_once_match = self.pragma_once_pattern.search(self.content,
  162. pragma_once_match.end())
  163. # Handle all collected pragma once directives.
  164. prev_end = 0
  165. tmp_content = ''
  166. for pragma_match in pragmas:
  167. tmp_content += self.content[prev_end:pragma_match.start()]
  168. prev_end = pragma_match.end()
  169. tmp_content += self.content[prev_end:]
  170. self.content = tmp_content
  171. # Include all trivial #include directives into self.content.
  172. def _process_includes(self):
  173. content_len = len(self.content)
  174. if content_len < len("#include <x>"):
  175. return 0
  176. # Find contexts in the content in which a found include
  177. # directive should not be processed.
  178. skippable_contexts = self._find_skippable_contexts()
  179. # Search for include directives in the content, collect those
  180. # which should be included into the content.
  181. includes = []
  182. include_match = self.include_pattern.search(self.content)
  183. while include_match:
  184. if not _is_within(include_match, skippable_contexts):
  185. include_path = include_match.group("path")
  186. search_same_dir = include_match.group(1) == '"'
  187. found_included_path = self.amalgamation.find_included_file(
  188. include_path, self.file_dir if search_same_dir else None)
  189. if found_included_path:
  190. includes.append((include_match, found_included_path))
  191. include_match = self.include_pattern.search(self.content,
  192. include_match.end())
  193. # Handle all collected include directives.
  194. prev_end = 0
  195. tmp_content = ''
  196. for include in includes:
  197. include_match, found_included_path = include
  198. tmp_content += self.content[prev_end:include_match.start()]
  199. tmp_content += "// {0}\n".format(include_match.group(0))
  200. if found_included_path not in self.amalgamation.included_files:
  201. t = TranslationUnit(found_included_path, self.amalgamation, False)
  202. tmp_content += t.content
  203. prev_end = include_match.end()
  204. tmp_content += self.content[prev_end:]
  205. self.content = tmp_content
  206. return len(includes)
  207. # Make all content processing
  208. def _process(self):
  209. if not self.is_root:
  210. self._process_pragma_once()
  211. self._process_includes()
  212. def __init__(self, file_path, amalgamation, is_root):
  213. self.file_path = file_path
  214. self.file_dir = os.path.dirname(file_path)
  215. self.amalgamation = amalgamation
  216. self.is_root = is_root
  217. self.amalgamation.included_files.append(self.file_path)
  218. actual_path = self.amalgamation.actual_path(file_path)
  219. if not os.path.isfile(actual_path):
  220. raise IOError("File not found: \"{0}\"".format(file_path))
  221. with open(actual_path, 'r') as f:
  222. self.content = f.read()
  223. self._process()
  224. def main():
  225. description = "Amalgamate C source and header files."
  226. usage = " ".join([
  227. "amalgamate.py",
  228. "[-v]",
  229. "-c path/to/config.json",
  230. "-s path/to/source/dir",
  231. "[-p path/to/prologue.(c|h)]"
  232. ])
  233. argsparser = argparse.ArgumentParser(
  234. description=description, usage=usage)
  235. argsparser.add_argument("-v", "--verbose", dest="verbose",
  236. choices=["yes", "no"], metavar="", help="be verbose")
  237. argsparser.add_argument("-c", "--config", dest="config",
  238. required=True, metavar="", help="path to a JSON config file")
  239. argsparser.add_argument("-s", "--source", dest="source_path",
  240. required=True, metavar="", help="source code path")
  241. argsparser.add_argument("-p", "--prologue", dest="prologue",
  242. required=False, metavar="", help="path to a C prologue file")
  243. amalgamation = Amalgamation(argsparser.parse_args())
  244. amalgamation.generate()
  245. if __name__ == "__main__":
  246. main()