pkgs/rewrite-nix-paths-macho/rewrite-nix-paths-macho.py (71 lines of code) (raw):

#!/usr/bin/env python3 import argparse import os import re import subprocess import sys from pathlib import Path from typing import Dict, List _SYSTEM_LIBS: Dict[str, str] = { "libc++.1.dylib": "/usr/lib/libc++.1.dylib", "libc++.1.0.dylib": "/usr/lib/libc++.1.dylib", } def run_command(command: List[str], check: bool = True) -> str: """Executes a shell command and returns its output.""" try: result = subprocess.run( command, check=check, capture_output=True, text=True, encoding="utf-8" ) return result.stdout.strip() except FileNotFoundError: print( f"Error: Command '{command[0]}' not found. Is it in your PATH?", file=sys.stderr, ) sys.exit(1) except subprocess.CalledProcessError as e: print(f"Error executing command: {' '.join(command)}", file=sys.stderr) print(f"Output:\n{e.stderr}", file=sys.stderr) sys.exit(1) def get_dependencies(file_path: Path) -> List[str]: """ Uses `otool -l` to parse the LC_LOAD_DYLIB commands and find the raw, un-resolved dependency paths stored in the binary. """ output = run_command(["otool", "-l", str(file_path)]) lines = output.splitlines() dependencies = [] name_regex = re.compile(r"^\s+name\s+(\S+)\s+\(offset \d+\)") for i, line in enumerate(lines): if "cmd LC_LOAD_DYLIB" in line: # The 'name' field, which contains the path, is consistently # located two lines after the 'cmd' line in the otool output. if i + 2 < len(lines): name_line = lines[i + 2] match = name_regex.match(name_line) if match: dependencies.append(match.group(1)) return dependencies def rewrite_nix_paths(file_path: Path): """ Finds and rewrites Nix store paths in a binary's dependencies to be @rpath-relative. """ if not file_path.exists(): raise FileNotFoundError(f"Error: File not found at {file_path}") dependencies = get_dependencies(file_path) for old_path in dependencies: if old_path.startswith("/nix/store/"): lib_name = os.path.basename(old_path) # Hmpf, since the Big Sur dynamic linker cache, we cannot # simply test for the existence of system libraries. So # use a table instead. if lib_name in _SYSTEM_LIBS: new_path = _SYSTEM_LIBS[lib_name] else: new_path = f"@rpath/{lib_name}" print(f"{old_path} -> {new_path}") command = [ "install_name_tool", "-change", old_path, new_path, str(file_path), ] run_command(command) def main(): parser = argparse.ArgumentParser( description="Rewrite Nix store paths in a macOS dynamic library (.so or .dylib) to be @rpath-relative.", ) parser.add_argument( "file", type=Path, help="Path to the .so or .dylib file to modify." ) args = parser.parse_args() rewrite_nix_paths(args.file) if __name__ == "__main__": main()