# 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(r
'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