# 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)