#!/usr/bin/python # amalgamate.py creates an amalgamation from a unity build. # It can be run with either Python 2 or 3. # An amalgamation consists of a header that includes the contents of all public # headers and a source file that includes the contents of all source files and # private headers. # # This script works by starting with the unity build file and recursively expanding # #include directives. If the #include is found in a public include directory, # that header is expanded into the amalgamation header. # # A particular header is only expanded once, so this script will # break if there are multiple inclusions of the same header that are expected to # expand differently. Similarly, this type of code causes issues: # # #ifdef FOO # #include "bar.h" # // code here # #else # #include "bar.h" // oops, doesn't get expanded # // different code here # #endif # # The solution is to move the include out of the #ifdef. from __future__ import print_function import argparse from os import path import re import sys include_re = re.compile('^[ \t]*#include[ \t]+"(.*)"[ \t]*$') included = set() excluded = set() def find_header(name, abs_path, include_paths): samedir = path.join(path.dirname(abs_path), name) if path.exists(samedir): return samedir for include_path in include_paths: include_path = path.join(include_path, name) if path.exists(include_path): return include_path return None def expand_include(include_path, f, abs_path, source_out, header_out, include_paths, public_include_paths): if include_path in included: return False included.add(include_path) with open(include_path) as f: print('#line 1 "{}"'.format(include_path), file=source_out) process_file(f, include_path, source_out, header_out, include_paths, public_include_paths) return True def process_file(f, abs_path, source_out, header_out, include_paths, public_include_paths): for (line, text) in enumerate(f): m = include_re.match(text) if m: filename = m.groups()[0] # first check private headers include_path = find_header(filename, abs_path, include_paths) if include_path: if include_path in excluded: source_out.write(text) expanded = False else: expanded = expand_include(include_path, f, abs_path, source_out, header_out, include_paths, public_include_paths) else: # now try public headers include_path = find_header(filename, abs_path, public_include_paths) if include_path: # found public header expanded = False if include_path in excluded: source_out.write(text) else: expand_include(include_path, f, abs_path, header_out, None, public_include_paths, []) else: sys.exit("unable to find {}, included in {} on line {}".format(filename, abs_path, line)) if expanded: print('#line {} "{}"'.format(line+1, abs_path), file=source_out) elif text != "#pragma once\n": source_out.write(text) def main(): parser = argparse.ArgumentParser(description="Transform a unity build into an amalgamation") parser.add_argument("source", help="source file") parser.add_argument("-I", action="append", dest="include_paths", help="include paths for private headers") parser.add_argument("-i", action="append", dest="public_include_paths", help="include paths for public headers") parser.add_argument("-x", action="append", dest="excluded", help="excluded header files") parser.add_argument("-o", dest="source_out", help="output C++ file", required=True) parser.add_argument("-H", dest="header_out", help="output C++ header file", required=True) args = parser.parse_args() include_paths = list(map(path.abspath, args.include_paths or [])) public_include_paths = list(map(path.abspath, args.public_include_paths or [])) excluded.update(map(path.abspath, args.excluded or [])) filename = args.source abs_path = path.abspath(filename) with open(filename) as f, open(args.source_out, 'w') as source_out, open(args.header_out, 'w') as header_out: print('#line 1 "{}"'.format(filename), file=source_out) print('#include "{}"'.format(header_out.name), file=source_out) process_file(f, abs_path, source_out, header_out, include_paths, public_include_paths) if __name__ == "__main__": main()