xref: /llvm-project/llvm/utils/gn/build/write_cmake_config.py (revision b71edfaa4ec3c998aadb35255ce2f60bba2940b0)
1#!/usr/bin/env python3
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 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
35import argparse
36import os
37import re
38import sys
39
40
41def main():
42    parser = argparse.ArgumentParser(
43        epilog=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
44    )
45    parser.add_argument("input", help="input file")
46    parser.add_argument("values", nargs="*", help="several KEY=VALUE pairs")
47    parser.add_argument("-o", "--output", required=True, help="output file")
48    args = parser.parse_args()
49
50    values = {}
51    for value in args.values:
52        key, val = value.split("=", 1)
53        if key in values:
54            print('duplicate key "%s" in args' % key, file=sys.stderr)
55            return 1
56        values[key] = val.replace("\\n", "\n")
57    unused_values = set(values.keys())
58
59    # Matches e.g. '${FOO}' or '@FOO@' and captures FOO in group 1 or 2.
60    var_re = re.compile(r"\$\{([^}]*)\}|@([^@]*)@")
61
62    with open(args.input) as f:
63        in_lines = f.readlines()
64    out_lines = []
65    for in_line in in_lines:
66
67        def repl(m):
68            key = m.group(1) or m.group(2)
69            unused_values.discard(key)
70            return values[key]
71
72        in_line = var_re.sub(repl, in_line)
73        if in_line.startswith("#cmakedefine01 "):
74            _, var = in_line.split()
75            if values[var] == "0":
76                print('error: "%s=0" used with #cmakedefine01 %s' % (var, var))
77                print("       '0' evaluates as truthy with #cmakedefine01")
78                print('       use "%s=" instead' % var)
79                return 1
80            in_line = "#define %s %d\n" % (var, 1 if values[var] else 0)
81            unused_values.discard(var)
82        elif in_line.startswith("#cmakedefine "):
83            _, var = in_line.split(None, 1)
84            try:
85                var, val = var.split(None, 1)
86                in_line = "#define %s %s" % (var, val)  # val ends in \n.
87            except:
88                var = var.rstrip()
89                in_line = "#define %s\n" % var
90            if not values[var]:
91                in_line = "/* #undef %s */\n" % var
92            unused_values.discard(var)
93        out_lines.append(in_line)
94
95    if unused_values:
96        print("unused values args:", file=sys.stderr)
97        print("    " + "\n    ".join(unused_values), file=sys.stderr)
98        return 1
99
100    output = "".join(out_lines)
101
102    leftovers = var_re.findall(output)
103    if leftovers:
104        print(
105            "unprocessed values:\n",
106            "\n".join([x[0] or x[1] for x in leftovers]),
107            file=sys.stderr,
108        )
109        return 1
110
111    def read(filename):
112        with open(args.output) as f:
113            return f.read()
114
115    if not os.path.exists(args.output) or read(args.output) != output:
116        with open(args.output, "w") as f:
117            f.write(output)
118        os.chmod(args.output, os.stat(args.input).st_mode & 0o777)
119
120
121if __name__ == "__main__":
122    sys.exit(main())
123