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