tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

midl.py (8439B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import functools
      6 import os
      7 import shutil
      8 import subprocess
      9 import sys
     10 
     11 import buildconfig
     12 
     13 
     14 def relativize(path, base=None):
     15    # For absolute path in Unix builds, we need relative paths because
     16    # Windows programs run via Wine don't like these Unix absolute paths
     17    # (they look like command line arguments).
     18    if path.startswith("/"):
     19        return os.path.relpath(path, base)
     20    # For Windows absolute paths, we can just use the unmodified path.
     21    # And if the path starts with '-', it's a command line argument.
     22    if os.path.isabs(path) or path.startswith("-"):
     23        return path
     24    # Remaining case is relative paths, which may be relative to a different
     25    # directory (os.getcwd()) than the needed `base`, so we "rebase" it.
     26    return os.path.relpath(path, base)
     27 
     28 
     29 @functools.cache
     30 def files_in(path):
     31    return {p.lower(): os.path.join(path, p) for p in os.listdir(path)}
     32 
     33 
     34 def search_path(paths, path):
     35    for p in paths:
     36        f = os.path.join(p, path)
     37        if os.path.isfile(f):
     38            return f
     39        # try an case-insensitive match
     40        maybe_match = files_in(p).get(path.lower())
     41        if maybe_match:
     42            return maybe_match
     43    raise RuntimeError(f"Cannot find {path}")
     44 
     45 
     46 # Filter-out -std= flag from the preprocessor command, as we're not preprocessing
     47 # C or C++, and the command would fail with the flag.
     48 def filter_preprocessor(cmd):
     49    for arg in cmd:
     50        if not arg.startswith(("-std=", "-std:")):
     51            yield arg
     52 
     53 
     54 # Preprocess all the direct and indirect inputs of midl, and put all the
     55 # preprocessed inputs in the given `base` directory. Returns a tuple containing
     56 # the path of the main preprocessed input, and the modified flags to use instead
     57 # of the flags given as argument.
     58 def preprocess(base, input, flags):
     59    import argparse
     60    import re
     61    from collections import deque
     62 
     63    IMPORT_RE = re.compile(r'import\s*"([^"]+)";')
     64 
     65    parser = argparse.ArgumentParser()
     66    parser.add_argument("-I", action="append")
     67    parser.add_argument("-D", action="append")
     68    parser.add_argument("-acf")
     69    args, remainder = parser.parse_known_args(flags)
     70    preprocessor = (
     71        list(filter_preprocessor(buildconfig.substs["CXXCPP"]))
     72        # Ideally we'd use the real midl version, but querying it adds a
     73        # significant overhead to configure. In practice, the version number
     74        # doesn't make a difference at the moment.
     75        + ["-D__midl=801"]
     76        + [f"-D{d}" for d in args.D or ()]
     77        + [f"-I{i}" for i in args.I or ()]
     78    )
     79    includes = ["."] + buildconfig.substs["INCLUDE"].split(";") + (args.I or [])
     80    seen = set()
     81    queue = deque([input])
     82    if args.acf:
     83        queue.append(args.acf)
     84    output = os.path.join(base, os.path.basename(input))
     85    while True:
     86        try:
     87            input = queue.popleft()
     88        except IndexError:
     89            break
     90        if os.path.basename(input) in seen:
     91            continue
     92        seen.add(os.path.basename(input))
     93        input = search_path(includes, input)
     94        # If there is a .acf file corresponding to the .idl we're processing,
     95        # we also want to preprocess that file because midl might look for it too.
     96        if input.lower().endswith(".idl"):
     97            try:
     98                acf = search_path(
     99                    [os.path.dirname(input)], os.path.basename(input)[:-4] + ".acf"
    100                )
    101                if acf:
    102                    queue.append(acf)
    103            except RuntimeError:
    104                pass
    105        command = preprocessor + [input]
    106        preprocessed = os.path.join(base, os.path.basename(input))
    107        subprocess.run(command, stdout=open(preprocessed, "wb"), check=True)
    108        # Read the resulting file, and search for imports, that we'll want to
    109        # preprocess as well.
    110        with open(preprocessed) as fh:
    111            for line in fh:
    112                if not line.startswith("import"):
    113                    continue
    114                m = IMPORT_RE.match(line)
    115                if not m:
    116                    continue
    117                imp = m.group(1)
    118                queue.append(imp)
    119    flags = []
    120    # Add -I<base> first in the flags, so that midl resolves imports to the
    121    # preprocessed files we created.
    122    for i in [base] + (args.I or []):
    123        flags.extend(["-I", i])
    124    # Add the preprocessed acf file if one was given on the command line.
    125    if args.acf:
    126        flags.extend(["-acf", os.path.join(base, os.path.basename(args.acf))])
    127    flags.extend(remainder)
    128    return output, flags
    129 
    130 
    131 def midl(out, input, *flags):
    132    out.avoid_writing_to_file()
    133    midl_flags = buildconfig.substs["MIDL_FLAGS"]
    134    base = os.path.dirname(out.name) or "."
    135    tmpdir = None
    136    try:
    137        # If the build system is asking to not use the preprocessor to midl,
    138        # we need to do the preprocessing ourselves.
    139        if "-no_cpp" in midl_flags:
    140            # Normally, we'd use tempfile.TemporaryDirectory, but in this specific
    141            # case, we actually want a deterministic directory name, because it's
    142            # recorded in the code midl generates.
    143            tmpdir = os.path.join(base, os.path.basename(input) + ".tmp")
    144            os.makedirs(tmpdir, exist_ok=True)
    145            try:
    146                input, flags = preprocess(tmpdir, input, flags)
    147            except subprocess.CalledProcessError as e:
    148                return e.returncode
    149        midl = buildconfig.substs["MIDL"]
    150        wine = buildconfig.substs.get("WINE")
    151        if midl.lower().endswith(".exe") and wine:
    152            command = [wine, midl]
    153        else:
    154            command = [midl]
    155        command.extend(midl_flags)
    156        command.extend([relativize(f, base) for f in flags])
    157        command.append("-Oicf")
    158        command.append(relativize(input, base))
    159        print("Executing:", " ".join(command))
    160        result = subprocess.run(command, check=False, cwd=base)
    161        return result.returncode
    162    finally:
    163        if tmpdir:
    164            shutil.rmtree(tmpdir)
    165 
    166 
    167 # midl outputs dlldata to a single dlldata.c file by default. This prevents running
    168 # midl in parallel in the same directory for idl files that would generate dlldata.c
    169 # because of race conditions updating the file. Instead, we ask midl to create
    170 # separate files, and we merge them manually.
    171 def merge_dlldata(out, *inputs):
    172    inputs = [open(i) for i in inputs]
    173    read_a_line = [True] * len(inputs)
    174    while True:
    175        lines = [
    176            f.readline() if read_a_line[n] else lines[n] for n, f in enumerate(inputs)
    177        ]
    178        unique_lines = set(lines)
    179        if len(unique_lines) == 1:
    180            # All the lines are identical
    181            if not lines[0]:
    182                break
    183            out.write(lines[0])
    184            read_a_line = [True] * len(inputs)
    185        elif (
    186            len(unique_lines) == 2
    187            and len([l for l in unique_lines if "#define" in l]) == 1
    188        ):
    189            # Most lines are identical. When they aren't, it's typically because some
    190            # files have an extra #define that others don't. When that happens, we
    191            # print out the #define, and get a new input line from the files that had
    192            # a #define on the next iteration. We expect that next line to match what
    193            # the other files had on this iteration.
    194            # Note: we explicitly don't support the case where there are different
    195            # defines across different files, except when there's a different one
    196            # for each file, in which case it's handled further below.
    197            a = unique_lines.pop()
    198            if "#define" in a:
    199                out.write(a)
    200            else:
    201                out.write(unique_lines.pop())
    202            read_a_line = ["#define" in l for l in lines]
    203        elif len(unique_lines) != len(lines):
    204            # If for some reason, we don't get lines that are entirely different
    205            # from each other, we have some unexpected input.
    206            print(
    207                f"Error while merging dlldata. Last lines read: {lines}",
    208                file=sys.stderr,
    209            )
    210            return 1
    211        else:
    212            for line in lines:
    213                out.write(line)
    214            read_a_line = [True] * len(inputs)
    215 
    216    return 0