diff options
Diffstat (limited to 'build/midl.py')
-rw-r--r-- | build/midl.py | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/build/midl.py b/build/midl.py new file mode 100644 index 0000000000..bec0fe0f60 --- /dev/null +++ b/build/midl.py @@ -0,0 +1,223 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import functools +import os +import shutil +import subprocess +import sys + +import buildconfig + + +def relativize(path, base=None): + # For absolute path in Unix builds, we need relative paths because + # Windows programs run via Wine don't like these Unix absolute paths + # (they look like command line arguments). + if path.startswith("/"): + return os.path.relpath(path, base) + # For Windows absolute paths, we can just use the unmodified path. + # And if the path starts with '-', it's a command line argument. + if os.path.isabs(path) or path.startswith("-"): + return path + # Remaining case is relative paths, which may be relative to a different + # directory (os.getcwd()) than the needed `base`, so we "rebase" it. + return os.path.relpath(path, base) + + +@functools.lru_cache(maxsize=None) +def files_in(path): + return {p.lower(): os.path.join(path, p) for p in os.listdir(path)} + + +def search_path(paths, path): + for p in paths: + f = os.path.join(p, path) + if os.path.isfile(f): + return f + # try an case-insensitive match + maybe_match = files_in(p).get(path.lower()) + if maybe_match: + return maybe_match + raise RuntimeError(f"Cannot find {path}") + + +# Filter-out -std= flag from the preprocessor command, as we're not preprocessing +# C or C++, and the command would fail with the flag. +def filter_preprocessor(cmd): + prev = None + for arg in cmd: + if arg == "-Xclang": + prev = arg + continue + if not arg.startswith("-std="): + if prev: + yield prev + yield arg + prev = None + + +# Preprocess all the direct and indirect inputs of midl, and put all the +# preprocessed inputs in the given `base` directory. Returns a tuple containing +# the path of the main preprocessed input, and the modified flags to use instead +# of the flags given as argument. +def preprocess(base, input, flags): + import argparse + import re + from collections import deque + + IMPORT_RE = re.compile('import\s*"([^"]+)";') + + parser = argparse.ArgumentParser() + parser.add_argument("-I", action="append") + parser.add_argument("-D", action="append") + parser.add_argument("-acf") + args, remainder = parser.parse_known_args(flags) + preprocessor = ( + list(filter_preprocessor(buildconfig.substs["CXXCPP"])) + # Ideally we'd use the real midl version, but querying it adds a + # significant overhead to configure. In practice, the version number + # doesn't make a difference at the moment. + + ["-D__midl=801"] + + [f"-D{d}" for d in args.D or ()] + + [f"-I{i}" for i in args.I or ()] + ) + includes = ["."] + buildconfig.substs["INCLUDE"].split(";") + (args.I or []) + seen = set() + queue = deque([input]) + if args.acf: + queue.append(args.acf) + output = os.path.join(base, os.path.basename(input)) + while True: + try: + input = queue.popleft() + except IndexError: + break + if os.path.basename(input) in seen: + continue + seen.add(os.path.basename(input)) + input = search_path(includes, input) + # If there is a .acf file corresponding to the .idl we're processing, + # we also want to preprocess that file because midl might look for it too. + if input.lower().endswith(".idl"): + try: + acf = search_path( + [os.path.dirname(input)], os.path.basename(input)[:-4] + ".acf" + ) + if acf: + queue.append(acf) + except RuntimeError: + pass + command = preprocessor + [input] + preprocessed = os.path.join(base, os.path.basename(input)) + subprocess.run(command, stdout=open(preprocessed, "wb"), check=True) + # Read the resulting file, and search for imports, that we'll want to + # preprocess as well. + with open(preprocessed, "r") as fh: + for line in fh: + if not line.startswith("import"): + continue + m = IMPORT_RE.match(line) + if not m: + continue + imp = m.group(1) + queue.append(imp) + flags = [] + # Add -I<base> first in the flags, so that midl resolves imports to the + # preprocessed files we created. + for i in [base] + (args.I or []): + flags.extend(["-I", i]) + # Add the preprocessed acf file if one was given on the command line. + if args.acf: + flags.extend(["-acf", os.path.join(base, os.path.basename(args.acf))]) + flags.extend(remainder) + return output, flags + + +def midl(out, input, *flags): + out.avoid_writing_to_file() + midl_flags = buildconfig.substs["MIDL_FLAGS"] + base = os.path.dirname(out.name) or "." + tmpdir = None + try: + # If the build system is asking to not use the preprocessor to midl, + # we need to do the preprocessing ourselves. + if "-no_cpp" in midl_flags: + # Normally, we'd use tempfile.TemporaryDirectory, but in this specific + # case, we actually want a deterministic directory name, because it's + # recorded in the code midl generates. + tmpdir = os.path.join(base, os.path.basename(input) + ".tmp") + os.makedirs(tmpdir, exist_ok=True) + try: + input, flags = preprocess(tmpdir, input, flags) + except subprocess.CalledProcessError as e: + return e.returncode + midl = buildconfig.substs["MIDL"] + wine = buildconfig.substs.get("WINE") + if midl.lower().endswith(".exe") and wine: + command = [wine, midl] + else: + command = [midl] + command.extend(midl_flags) + command.extend([relativize(f, base) for f in flags]) + command.append("-Oicf") + command.append(relativize(input, base)) + print("Executing:", " ".join(command)) + result = subprocess.run(command, cwd=base) + return result.returncode + finally: + if tmpdir: + shutil.rmtree(tmpdir) + + +# midl outputs dlldata to a single dlldata.c file by default. This prevents running +# midl in parallel in the same directory for idl files that would generate dlldata.c +# because of race conditions updating the file. Instead, we ask midl to create +# separate files, and we merge them manually. +def merge_dlldata(out, *inputs): + inputs = [open(i) for i in inputs] + read_a_line = [True] * len(inputs) + while True: + lines = [ + f.readline() if read_a_line[n] else lines[n] for n, f in enumerate(inputs) + ] + unique_lines = set(lines) + if len(unique_lines) == 1: + # All the lines are identical + if not lines[0]: + break + out.write(lines[0]) + read_a_line = [True] * len(inputs) + elif ( + len(unique_lines) == 2 + and len([l for l in unique_lines if "#define" in l]) == 1 + ): + # Most lines are identical. When they aren't, it's typically because some + # files have an extra #define that others don't. When that happens, we + # print out the #define, and get a new input line from the files that had + # a #define on the next iteration. We expect that next line to match what + # the other files had on this iteration. + # Note: we explicitly don't support the case where there are different + # defines across different files, except when there's a different one + # for each file, in which case it's handled further below. + a = unique_lines.pop() + if "#define" in a: + out.write(a) + else: + out.write(unique_lines.pop()) + read_a_line = ["#define" in l for l in lines] + elif len(unique_lines) != len(lines): + # If for some reason, we don't get lines that are entirely different + # from each other, we have some unexpected input. + print( + "Error while merging dlldata. Last lines read: {}".format(lines), + file=sys.stderr, + ) + return 1 + else: + for line in lines: + out.write(line) + read_a_line = [True] * len(inputs) + + return 0 |