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