# 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 os
import re
import shutil
import subprocess
import sys
from argparse
import ArgumentParser
from pathlib
import Path
from buildconfig
import substs
"""
Scans the given directories
for binaries referencing the AddressSanitizer
runtime library, copies it to the main directory.
"""
# This is the dylib name pattern
DYLIB_NAME_PATTERN = re.compile(r
"libclang_rt\.(a|ub)san_osx_dynamic\.dylib")
def resolve_rpath(filename):
otoolOut = subprocess.check_output([substs[
"OTOOL"],
"-l", filename], text=
True)
currentCmd =
None
# The lines we need to find look like this:
# ...
# Load command 22
# cmd LC_RPATH
# cmdsize 80
# path /home/build/src/clang/bin/../lib/clang/3.8.0/lib/darwin (offset 12)
# Load command 23
# ...
# Other load command types have a varying number of fields.
for line
in otoolOut.splitlines():
cmdMatch = re.match(r
"^\s+cmd ([A-Z_]+)", line)
if cmdMatch
is not None:
currentCmd = cmdMatch.group(1)
continue
if currentCmd ==
"LC_RPATH":
pathMatch = re.match(r
"^\s+path (.*) \(offset \d+\)", line)
if pathMatch
is not None:
path = pathMatch.group(1)
if Path(path).is_dir():
return path
print(f
"@rpath could not be resolved from {filename}", file=sys.stderr)
sys.exit(1)
def scan_directory(path):
dylibsCopied = set()
dylibsRequired = set()
if not path.is_dir():
print(f
"Input path {path} is not a folder", file=sys.stderr)
sys.exit(1)
for file
in path.rglob(
"*"):
if not file.is_file():
continue
# Skip all files that aren't either dylibs or executable
if not (file.suffix ==
".dylib" or os.access(str(file), os.X_OK)):
continue
try:
otoolOut = subprocess.check_output(
[substs[
"OTOOL"],
"-L", str(file)], text=
True
)
except Exception:
# Errors are expected on non-mach executables, ignore them and continue
continue
for line
in otoolOut.splitlines():
match = DYLIB_NAME_PATTERN.search(line)
if match
is not None:
dylibName = match.group(0)
absDylibPath = line.split()[0]
dylibsRequired.add(dylibName)
if dylibName
not in dylibsCopied:
if absDylibPath.startswith(
"@rpath/"):
rpath = resolve_rpath(str(file))
copyDylibPath = absDylibPath.replace(
"@rpath", rpath)
else:
copyDylibPath = absDylibPath
if Path(copyDylibPath).is_file():
# Copy the runtime once to the main directory, which is passed
# as the argument to this function.
shutil.copy(copyDylibPath, str(path))
dylibsCopied.add(dylibName)
else:
print(
f
"dylib path in {file} was not found at: {copyDylibPath}",
file=sys.stderr,
)
break
dylibsMissing = dylibsRequired - dylibsCopied
if dylibsMissing:
for dylibName
in dylibsMissing:
print(f
"{dylibName} could not be found", file=sys.stderr)
sys.exit(1)
def parse_args(argv=
None):
parser = ArgumentParser()
parser.add_argument(
"paths", metavar=
"path", type=Path, nargs=
"+")
return parser.parse_args(argv)
if __name__ ==
"__main__":
args = parse_args()
for d
in args.paths:
scan_directory(d)