rewrite_sanitizer_dylib.py (3934B)
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 os 6 import re 7 import shutil 8 import subprocess 9 import sys 10 from argparse import ArgumentParser 11 from pathlib import Path 12 13 from buildconfig import substs 14 15 """ 16 Scans the given directories for binaries referencing the AddressSanitizer 17 runtime library, copies it to the main directory. 18 """ 19 20 # This is the dylib name pattern 21 DYLIB_NAME_PATTERN = re.compile(r"libclang_rt\.(a|ub)san_osx_dynamic\.dylib") 22 23 24 def resolve_rpath(filename): 25 otoolOut = subprocess.check_output([substs["OTOOL"], "-l", filename], text=True) 26 currentCmd = None 27 28 # The lines we need to find look like this: 29 # ... 30 # Load command 22 31 # cmd LC_RPATH 32 # cmdsize 80 33 # path /home/build/src/clang/bin/../lib/clang/3.8.0/lib/darwin (offset 12) 34 # Load command 23 35 # ... 36 # Other load command types have a varying number of fields. 37 for line in otoolOut.splitlines(): 38 cmdMatch = re.match(r"^\s+cmd ([A-Z_]+)", line) 39 if cmdMatch is not None: 40 currentCmd = cmdMatch.group(1) 41 continue 42 43 if currentCmd == "LC_RPATH": 44 pathMatch = re.match(r"^\s+path (.*) \(offset \d+\)", line) 45 if pathMatch is not None: 46 path = pathMatch.group(1) 47 if Path(path).is_dir(): 48 return path 49 50 print(f"@rpath could not be resolved from {filename}", file=sys.stderr) 51 sys.exit(1) 52 53 54 def scan_directory(path): 55 dylibsCopied = set() 56 dylibsRequired = set() 57 58 if not path.is_dir(): 59 print(f"Input path {path} is not a folder", file=sys.stderr) 60 sys.exit(1) 61 62 for file in path.rglob("*"): 63 if not file.is_file(): 64 continue 65 66 # Skip all files that aren't either dylibs or executable 67 if not (file.suffix == ".dylib" or os.access(str(file), os.X_OK)): 68 continue 69 70 try: 71 otoolOut = subprocess.check_output( 72 [substs["OTOOL"], "-L", str(file)], text=True 73 ) 74 except Exception: 75 # Errors are expected on non-mach executables, ignore them and continue 76 continue 77 78 for line in otoolOut.splitlines(): 79 match = DYLIB_NAME_PATTERN.search(line) 80 81 if match is not None: 82 dylibName = match.group(0) 83 absDylibPath = line.split()[0] 84 85 dylibsRequired.add(dylibName) 86 87 if dylibName not in dylibsCopied: 88 if absDylibPath.startswith("@rpath/"): 89 rpath = resolve_rpath(str(file)) 90 copyDylibPath = absDylibPath.replace("@rpath", rpath) 91 else: 92 copyDylibPath = absDylibPath 93 94 if Path(copyDylibPath).is_file(): 95 # Copy the runtime once to the main directory, which is passed 96 # as the argument to this function. 97 shutil.copy(copyDylibPath, str(path)) 98 dylibsCopied.add(dylibName) 99 else: 100 print( 101 f"dylib path in {file} was not found at: {copyDylibPath}", 102 file=sys.stderr, 103 ) 104 105 break 106 107 dylibsMissing = dylibsRequired - dylibsCopied 108 if dylibsMissing: 109 for dylibName in dylibsMissing: 110 print(f"{dylibName} could not be found", file=sys.stderr) 111 sys.exit(1) 112 113 114 def parse_args(argv=None): 115 parser = ArgumentParser() 116 parser.add_argument("paths", metavar="path", type=Path, nargs="+") 117 return parser.parse_args(argv) 118 119 120 if __name__ == "__main__": 121 args = parse_args() 122 for d in args.paths: 123 scan_directory(d)