summaryrefslogtreecommitdiffstats
path: root/build/unix/rewrite_sanitizer_dylib.py
diff options
context:
space:
mode:
Diffstat (limited to 'build/unix/rewrite_sanitizer_dylib.py')
-rw-r--r--build/unix/rewrite_sanitizer_dylib.py124
1 files changed, 124 insertions, 0 deletions
diff --git a/build/unix/rewrite_sanitizer_dylib.py b/build/unix/rewrite_sanitizer_dylib.py
new file mode 100644
index 0000000000..2fa8a92b27
--- /dev/null
+++ b/build/unix/rewrite_sanitizer_dylib.py
@@ -0,0 +1,124 @@
+# 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)