amalgamate.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. #!/usr/bin/python
  2. # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
  3. # amalgamate.py creates an amalgamation from a unity build.
  4. # It can be run with either Python 2 or 3.
  5. # An amalgamation consists of a header that includes the contents of all public
  6. # headers and a source file that includes the contents of all source files and
  7. # private headers.
  8. #
  9. # This script works by starting with the unity build file and recursively expanding
  10. # #include directives. If the #include is found in a public include directory,
  11. # that header is expanded into the amalgamation header.
  12. #
  13. # A particular header is only expanded once, so this script will
  14. # break if there are multiple inclusions of the same header that are expected to
  15. # expand differently. Similarly, this type of code causes issues:
  16. #
  17. # #ifdef FOO
  18. # #include "bar.h"
  19. # // code here
  20. # #else
  21. # #include "bar.h" // oops, doesn't get expanded
  22. # // different code here
  23. # #endif
  24. #
  25. # The solution is to move the include out of the #ifdef.
  26. import argparse
  27. import re
  28. import sys
  29. from os import path
  30. include_re = re.compile('^[ \t]*#include[ \t]+"(.*)"[ \t]*$')
  31. included = set()
  32. excluded = set()
  33. def find_header(name, abs_path, include_paths):
  34. samedir = path.join(path.dirname(abs_path), name)
  35. if path.exists(samedir):
  36. return samedir
  37. for include_path in include_paths:
  38. include_path = path.join(include_path, name)
  39. if path.exists(include_path):
  40. return include_path
  41. return None
  42. def expand_include(
  43. include_path,
  44. f,
  45. abs_path,
  46. source_out,
  47. header_out,
  48. include_paths,
  49. public_include_paths,
  50. ):
  51. if include_path in included:
  52. return False
  53. included.add(include_path)
  54. with open(include_path) as f:
  55. print(f'#line 1 "{include_path}"', file=source_out)
  56. process_file(
  57. f, include_path, source_out, header_out, include_paths, public_include_paths
  58. )
  59. return True
  60. def process_file(
  61. f, abs_path, source_out, header_out, include_paths, public_include_paths
  62. ):
  63. for (line, text) in enumerate(f):
  64. m = include_re.match(text)
  65. if m:
  66. filename = m.groups()[0]
  67. # first check private headers
  68. include_path = find_header(filename, abs_path, include_paths)
  69. if include_path:
  70. if include_path in excluded:
  71. source_out.write(text)
  72. expanded = False
  73. else:
  74. expanded = expand_include(
  75. include_path,
  76. f,
  77. abs_path,
  78. source_out,
  79. header_out,
  80. include_paths,
  81. public_include_paths,
  82. )
  83. else:
  84. # now try public headers
  85. include_path = find_header(filename, abs_path, public_include_paths)
  86. if include_path:
  87. # found public header
  88. expanded = False
  89. if include_path in excluded:
  90. source_out.write(text)
  91. else:
  92. expand_include(
  93. include_path,
  94. f,
  95. abs_path,
  96. header_out,
  97. None,
  98. public_include_paths,
  99. [],
  100. )
  101. else:
  102. sys.exit(
  103. "unable to find {}, included in {} on line {}".format(
  104. filename, abs_path, line
  105. )
  106. )
  107. if expanded:
  108. print(f'#line {line + 1} "{abs_path}"', file=source_out)
  109. elif text != "#pragma once\n":
  110. source_out.write(text)
  111. def main():
  112. parser = argparse.ArgumentParser(
  113. description="Transform a unity build into an amalgamation"
  114. )
  115. parser.add_argument("source", help="source file")
  116. parser.add_argument(
  117. "-I",
  118. action="append",
  119. dest="include_paths",
  120. help="include paths for private headers",
  121. )
  122. parser.add_argument(
  123. "-i",
  124. action="append",
  125. dest="public_include_paths",
  126. help="include paths for public headers",
  127. )
  128. parser.add_argument(
  129. "-x", action="append", dest="excluded", help="excluded header files"
  130. )
  131. parser.add_argument("-o", dest="source_out", help="output C++ file", required=True)
  132. parser.add_argument(
  133. "-H", dest="header_out", help="output C++ header file", required=True
  134. )
  135. args = parser.parse_args()
  136. include_paths = list(map(path.abspath, args.include_paths or []))
  137. public_include_paths = list(map(path.abspath, args.public_include_paths or []))
  138. excluded.update(map(path.abspath, args.excluded or []))
  139. filename = args.source
  140. abs_path = path.abspath(filename)
  141. with open(filename) as f, open(args.source_out, "w") as source_out, open(
  142. args.header_out, "w"
  143. ) as header_out:
  144. print(f'#line 1 "{filename}"', file=source_out)
  145. print(f'#include "{header_out.name}"', file=source_out)
  146. process_file(
  147. f, abs_path, source_out, header_out, include_paths, public_include_paths
  148. )
  149. if __name__ == "__main__":
  150. main()