Compiler projects using llvm
#!/usr/bin/env python3
r"""Emulates the bits of CMake's configure_file() function needed in LLVM.

The CMake build uses configure_file() for several things.  This emulates that
function for the GN build.  In the GN build, this runs at build time instead
of at generator time.

Takes a list of KEY=VALUE pairs (where VALUE can be empty).

The sequence `\` `n` in each VALUE is replaced by a newline character.

On each line, replaces '${KEY}' or '@KEY@' with VALUE.

Then, handles these special cases (note that FOO= sets the value of FOO to the
empty string, which is falsy, but FOO=0 sets it to '0' which is truthy):

1.) #cmakedefine01 FOO
    Checks if key FOO is set to a truthy value, and depending on that prints
    one of the following two lines:

        #define FOO 1
        #define FOO 0

2.) #cmakedefine FOO [...]
    Checks if key FOO is set to a truthy value, and depending on that prints
    one of the following two lines:

        #define FOO [...]
        /* #undef FOO */

Fails if any of the KEY=VALUE arguments aren't needed for processing the
input file, or if the input file references keys that weren't passed in.
"""

import argparse
import os
import re
import sys


def main():
    parser = argparse.ArgumentParser(
                 epilog=__doc__,
                 formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('input', help='input file')
    parser.add_argument('values', nargs='*', help='several KEY=VALUE pairs')
    parser.add_argument('-o', '--output', required=True,
                        help='output file')
    args = parser.parse_args()

    values = {}
    for value in args.values:
        key, val = value.split('=', 1)
        if key in values:
            print('duplicate key "%s" in args' % key, file=sys.stderr)
            return 1
        values[key] = val.replace('\\n', '\n')
    unused_values = set(values.keys())

    # Matches e.g. '${FOO}' or '@FOO@' and captures FOO in group 1 or 2.
    var_re = re.compile(r'\$\{([^}]*)\}|@([^@]*)@')

    with open(args.input) as f:
        in_lines = f.readlines()
    out_lines = []
    for in_line in in_lines:
        def repl(m):
            key = m.group(1) or m.group(2)
            unused_values.discard(key)
            return values[key]
        in_line = var_re.sub(repl, in_line)
        if in_line.startswith('#cmakedefine01 '):
            _, var = in_line.split()
            if values[var] == '0':
                print('error: "%s=0" used with #cmakedefine01 %s' % (var, var))
                print("       '0' evaluates as truthy with #cmakedefine01")
                print('       use "%s=" instead' % var)
                return 1
            in_line = '#define %s %d\n' % (var, 1 if values[var] else 0)
            unused_values.discard(var)
        elif in_line.startswith('#cmakedefine '):
            _, var = in_line.split(None, 1)
            try:
                var, val = var.split(None, 1)
                in_line = '#define %s %s' % (var, val)  # val ends in \n.
            except:
                var = var.rstrip()
                in_line = '#define %s\n' % var
            if not values[var]:
                in_line = '/* #undef %s */\n' % var
            unused_values.discard(var)
        out_lines.append(in_line)

    if unused_values:
        print('unused values args:', file=sys.stderr)
        print('    ' + '\n    '.join(unused_values), file=sys.stderr)
        return 1

    output = ''.join(out_lines)

    leftovers = var_re.findall(output)
    if leftovers:
        print(
            'unprocessed values:\n',
            '\n'.join([x[0] or x[1] for x in leftovers]),
            file=sys.stderr)
        return 1

    def read(filename):
        with open(args.output) as f:
            return f.read()

    if not os.path.exists(args.output) or read(args.output) != output:
        with open(args.output, 'w') as f:
            f.write(output)
        os.chmod(args.output, os.stat(args.input).st_mode & 0o777)


if __name__ == '__main__':
    sys.exit(main())