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