amalgamate.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  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. from __future__ import print_function
  27. import argparse
  28. from os import path
  29. import re
  30. import sys
  31. include_re = re.compile('^[ \t]*#include[ \t]+"(.*)"[ \t]*$')
  32. included = set()
  33. excluded = set()
  34. def find_header(name, abs_path, include_paths):
  35. samedir = path.join(path.dirname(abs_path), name)
  36. if path.exists(samedir):
  37. return samedir
  38. for include_path in include_paths:
  39. include_path = path.join(include_path, name)
  40. if path.exists(include_path):
  41. return include_path
  42. return None
  43. def expand_include(include_path, f, abs_path, source_out, header_out, include_paths, public_include_paths):
  44. if include_path in included:
  45. return False
  46. included.add(include_path)
  47. with open(include_path) as f:
  48. print('#line 1 "{}"'.format(include_path), file=source_out)
  49. process_file(f, include_path, source_out, header_out, include_paths, public_include_paths)
  50. return True
  51. def process_file(f, abs_path, source_out, header_out, include_paths, public_include_paths):
  52. for (line, text) in enumerate(f):
  53. m = include_re.match(text)
  54. if m:
  55. filename = m.groups()[0]
  56. # first check private headers
  57. include_path = find_header(filename, abs_path, include_paths)
  58. if include_path:
  59. if include_path in excluded:
  60. source_out.write(text)
  61. expanded = False
  62. else:
  63. expanded = expand_include(include_path, f, abs_path, source_out, header_out, include_paths, public_include_paths)
  64. else:
  65. # now try public headers
  66. include_path = find_header(filename, abs_path, public_include_paths)
  67. if include_path:
  68. # found public header
  69. expanded = False
  70. if include_path in excluded:
  71. source_out.write(text)
  72. else:
  73. expand_include(include_path, f, abs_path, header_out, None, public_include_paths, [])
  74. else:
  75. sys.exit("unable to find {}, included in {} on line {}".format(filename, abs_path, line))
  76. if expanded:
  77. print('#line {} "{}"'.format(line+1, abs_path), file=source_out)
  78. elif text != "#pragma once\n":
  79. source_out.write(text)
  80. def main():
  81. parser = argparse.ArgumentParser(description="Transform a unity build into an amalgamation")
  82. parser.add_argument("source", help="source file")
  83. parser.add_argument("-I", action="append", dest="include_paths", help="include paths for private headers")
  84. parser.add_argument("-i", action="append", dest="public_include_paths", help="include paths for public headers")
  85. parser.add_argument("-x", action="append", dest="excluded", help="excluded header files")
  86. parser.add_argument("-o", dest="source_out", help="output C++ file", required=True)
  87. parser.add_argument("-H", dest="header_out", help="output C++ header file", required=True)
  88. args = parser.parse_args()
  89. include_paths = list(map(path.abspath, args.include_paths or []))
  90. public_include_paths = list(map(path.abspath, args.public_include_paths or []))
  91. excluded.update(map(path.abspath, args.excluded or []))
  92. filename = args.source
  93. abs_path = path.abspath(filename)
  94. with open(filename) as f, open(args.source_out, 'w') as source_out, open(args.header_out, 'w') as header_out:
  95. print('#line 1 "{}"'.format(filename), file=source_out)
  96. print('#include "{}"'.format(header_out.name), file=source_out)
  97. process_file(f, abs_path, source_out, header_out, include_paths, public_include_paths)
  98. if __name__ == "__main__":
  99. main()