blob: d2c109398e15b0f1e57a2d35d49442dbe178ce13 [file] [edit]
#!/usr/bin/env python3
# Thin wrapper around swiftc that:
# - relays the command line through a response file on Windows
# - forwards the MSVC-style linker switches that CMake's own defaults inject
# - merges per-frontend depfiles into one depfile for the whole module
# - records when the module last compiled, for rebuild_trigger.py
# - optionally runs another wrapper in the compiler's place, so a tool that
# needs to spawn swiftc itself can be nested below this one
# - with --dry-run prints the command line it would run and exits without
# compiling, writing a depfile, or touching the stamp
#
# Flags that swiftc cannot accept are kept off the Swift command line by the
# CMake configuration, using $<COMPILE_LANGUAGE:Swift> / $<LINK_LANGUAGE:Swift>
# genexes and the LINKER: prefix (which CMake expands to -Xlinker for swiftc
# and -Wl, for clang). See WEBKIT_SCOPE_OPTIONS_TO_NON_SWIFT and
# webkit_target_compile_definitions in Source/cmake/WebKitMacros.cmake.
#
# Invoked with a --original-swift-compiler= flag mixed in that refers to the
# real compiler. CMake does not appear to call this script when set as a
# CMAKE_Swift_COMPILER_LAUNCHER.
import json
import os
import re
import shlex
import subprocess
import sys
import tempfile
from collections import namedtuple
from pathlib import Path
import depfile
def quote_response_file_token(arg):
if not re.search(r"\s", arg):
return arg
# Windows argv-unescaping rules (the platform this is used on):
# a backslash is literal unless it precedes a quote, and N backslashes
# before a quote collapse to N/2.
escaped = arg.replace("\\", "\\\\").replace('"', '\\"')
return '"' + escaped + '"'
# Where to write the merged depfile, which output to key it on, and which
# dependencies to drop. Populated from the --*ninja-depfile* flags that
# WebKitMacros.cmake passes; absent when the caller didn't ask for a depfile.
DepfileRequest = namedtuple("DepfileRequest", "path target excludes")
def _canonical(path):
"""A spelling-insensitive key for paths that name the same file."""
return os.path.normcase(os.path.normpath(path)).replace("\\", "/")
def excluded_paths(request):
"""Canonical keys of everything that must not appear in the depfile."""
excludes = {_canonical(exclude): exclude for exclude in request.excludes}
# A depfile may never list its own target or itself, whatever the caller
# asked to exclude.
excludes.setdefault(_canonical(request.target), request.target)
excludes.setdefault(_canonical(request.path), request.path)
return excludes
def dependency_files(output_file_map):
"""The depfiles swiftc wrote for this compile, per its output-file-map."""
try:
with open(output_file_map) as f:
entries = json.load(f)
except (OSError, ValueError):
return []
paths = [entry["dependencies"] for entry in entries.values() if "dependencies" in entry]
# The driver derives the depfile names of the emit-module job, which has the
# fullest view of the module's imports, and of a whole-module compile, which
# writes no per-source depfiles, instead of taking them from the
# output-file-map, so pick them up from the map's own directory.
paths += (str(path) for path in Path(output_file_map).parent.glob("*.d"))
return [path for path in paths if os.path.exists(path)]
def write_ninja_depfile(request, output_file_map):
sources = dependency_files(output_file_map) if output_file_map else []
if not sources:
sys.exit(
f"{Path(__file__).name}: error: no dependency files named by "
f"{output_file_map or '-output-file-map'}; "
"can't track Swift header dependencies"
)
excludes = excluded_paths(request)
# Every frontend job reports (nearly) the same ~16-25K module dependencies,
# so collect the distinct spellings first and only canonicalize those: the
# per-token normpath over ~1M tokens used to take >10s for WebKit.
raw = set()
for source in sources:
raw |= depfile.dependency_set(source)
# Sorted: swiftc reports a dependency once per frontend job that saw it, so
# discovery order follows job scheduling and would rewrite this file, and
# cost a build, for a dependency set that did not change.
# Drop dependencies that are not on disk. On Linux, swiftc overlays its
# libstdcxx.modulemap onto the libstdc++ include directory through a virtual
# file system and reports that virtual module.modulemap as a dependency.
# ninja considers a missing dependency always dirty, so it would recompile
# the module on every build.
deps = sorted(dep for dep in raw if _canonical(dep) not in excludes and os.path.exists(dep))
lines = [f"{depfile.escape(request.target)}:"]
lines += (f" {depfile.escape(dep)}" for dep in deps)
content = " \\\n".join(lines) + "\n"
try:
# Only rewrite the depfile when it changes, otherwise the rebuild
# trigger causes the module to rebuild forever.
if Path(request.path).read_text() == content:
return
except OSError:
pass
scratch = request.path + ".tmp"
Path(scratch).write_text(content)
os.replace(scratch, request.path)
def write_response_file(args):
fd, path = tempfile.mkstemp(prefix="swiftc-wrapper-", suffix=".rsp")
with os.fdopen(fd, "w") as f:
for arg in args:
f.write(quote_response_file_token(arg))
f.write("\n")
return path
def main(argv):
real_swiftc = "swiftc"
args = []
linking = any(arg in ("-emit-library", "-emit-executable") for arg in argv)
depfile_path = None
depfile_target = None
depfile_excludes = []
stamp_path = None
inner_wrapper = None
dry_run = False
for arg in argv:
if arg.startswith("--original-swift-compiler="):
real_swiftc = arg[len("--original-swift-compiler="):]
elif arg.startswith("--swift-wrapper="):
inner_wrapper = arg[len("--swift-wrapper="):]
elif arg.startswith("--emit-ninja-depfile="):
depfile_path = arg[len("--emit-ninja-depfile="):]
elif arg.startswith("--ninja-depfile-target="):
depfile_target = arg[len("--ninja-depfile-target="):]
elif arg.startswith("--ninja-depfile-exclude="):
depfile_excludes.append(arg[len("--ninja-depfile-exclude="):])
elif arg.startswith("--emit-compile-stamp="):
stamp_path = arg[len("--emit-compile-stamp="):]
elif os.name == "nt" and arg.startswith("/") and len(arg) > 1:
# Work around a bug in CMake: Its MSVC defaults
# (Platform/Windows-MSVC.cmake) put raw linker switches like
# /machine:x64 and /INCREMENTAL:NO into CMAKE_*_LINKER_FLAGS.
args.extend(["-Xlinker", arg])
elif arg == "--dry-run":
dry_run = True
else:
args.append(arg)
depfile_request = None
if depfile_path and depfile_target and not linking:
depfile_request = DepfileRequest(depfile_path, depfile_target, frozenset(depfile_excludes))
if inner_wrapper:
flat_command = [inner_wrapper, f"--original-swift-compiler={real_swiftc}"] + args
else:
flat_command = [real_swiftc] + args
if dry_run:
if os.name == "nt":
print(subprocess.list2cmdline(flat_command))
else:
print(shlex.join(flat_command))
return 0
if os.name == "nt" and "-explicit-module-build" not in args and not inner_wrapper:
# WebKit's swiftc invocations run tens of thousands of characters long
# (hundreds of -I flags); Windows' CreateProcess caps a command line at
# ~32767 characters. An inner wrapper is excluded because it would have
# to expand the response file itself.
response_file = write_response_file(args)
command = [real_swiftc, "@" + response_file]
else:
if os.name == "nt":
cmdline = subprocess.list2cmdline(flat_command)
if len(cmdline) >= 32767:
sys.stderr.write(
f"swiftc-wrapper: command line is {len(cmdline)} characters, "
"over the Windows 32767 limit, and a response file cannot be "
"used here\n")
return 1
command = flat_command
response_file = None
rc = None
try:
rc = subprocess.call(command)
return rc
finally:
if response_file:
os.remove(response_file)
if depfile_request and rc == 0:
write_ninja_depfile(depfile_request, args[args.index('-output-file-map') + 1])
if stamp_path and rc == 0 and not linking:
Path(stamp_path).touch()
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))