1#!/usr/bin/env python
2r"""Emulates the bits of CMake's configure_file() function needed in LLVM.
3
4The CMake build uses configure_file() for several things.  This emulates that
5function for the GN build.  In the GN build, this runs at build time instead
6of at generator time.
7
8Takes a list of KEY=VALUE pairs (where VALUE can be empty).
9
10The sequence `\` `n` in each VALUE is replaced by a newline character.
11
12On each line, replaces '${KEY}' or '@KEY@' with VALUE.
13
14Then, handles these special cases (note that FOO= sets the value of FOO to the
15empty string, which is falsy, but FOO=0 sets it to '0' which is truthy):
16
171.) #cmakedefine01 FOO
18    Checks if key FOO is set to a truthy value, and depending on that prints
19    one of the following two lines:
20
21        #define FOO 1
22        #define FOO 0
23
242.) #cmakedefine FOO [...]
25    Checks if key FOO is set to a truthy in value, and depending on that prints
26    one of the following two lines:
27
28        #define FOO [...]
29        /* #undef FOO */
30
31Fails if any of the KEY=VALUE arguments aren't needed for processing the
32input file, or if the input file references keys that weren't passed in.
33"""
34
35from __future__ import print_function
36
37import argparse
38import os
39import re
40import sys
41
42
43def main():
44    parser = argparse.ArgumentParser(
45                 epilog=__doc__,
46                 formatter_class=argparse.RawDescriptionHelpFormatter)
47    parser.add_argument('input', help='input file')
48    parser.add_argument('values', nargs='*', help='several KEY=VALUE pairs')
49    parser.add_argument('-o', '--output', required=True,
50                        help='output file')
51    args = parser.parse_args()
52
53    values = {}
54    for value in args.values:
55        key, val = value.split('=', 1)
56        if key in values:
57            print('duplicate key "%s" in args' % key, file=sys.stderr)
58            return 1
59        values[key] = val.replace('\\n', '\n')
60    unused_values = set(values.keys())
61
62    # Matches e.g. '${FOO}' or '@FOO@' and captures FOO in group 1 or 2.
63    var_re = re.compile(r'\$\{([^}]*)\}|@([^@]*)@')
64
65    with open(args.input) as f:
66        in_lines = f.readlines()
67    out_lines = []
68    for in_line in in_lines:
69        def repl(m):
70            key = m.group(1) or m.group(2)
71            unused_values.discard(key)
72            return values[key]
73        in_line = var_re.sub(repl, in_line)
74        if in_line.startswith('#cmakedefine01 '):
75            _, var = in_line.split()
76            in_line = '#define %s %d\n' % (var, 1 if values[var] else 0)
77            unused_values.discard(var)
78        elif in_line.startswith('#cmakedefine '):
79            _, var = in_line.split(None, 1)
80            try:
81                var, val = var.split(None, 1)
82                in_line = '#define %s %s' % (var, val)  # val ends in \n.
83            except:
84                var = var.rstrip()
85                in_line = '#define %s\n' % var
86            if not values[var]:
87                in_line = '/* #undef %s */\n' % var
88            unused_values.discard(var)
89        out_lines.append(in_line)
90
91    if unused_values:
92        print('unused values args:', file=sys.stderr)
93        print('    ' + '\n    '.join(unused_values), file=sys.stderr)
94        return 1
95
96    output = ''.join(out_lines)
97
98    leftovers = var_re.findall(output)
99    if leftovers:
100        print(
101            'unprocessed values:\n',
102            '\n'.join([x[0] or x[1] for x in leftovers]),
103            file=sys.stderr)
104        return 1
105
106    def read(filename):
107        with open(args.output) as f:
108            return f.read()
109
110    if not os.path.exists(args.output) or read(args.output) != output:
111        with open(args.output, 'w') as f:
112            f.write(output)
113        os.chmod(args.output, os.stat(args.input).st_mode & 0o777)
114
115
116if __name__ == '__main__':
117    sys.exit(main())
118