"""Copy a Mach-O executable and rewrite its non-system dylib dependency closure.""" import pathlib import shutil import subprocess import sys source = pathlib.Path(sys.argv[1]).resolve() destination = pathlib.Path(sys.argv[2]).resolve() (destination / 'lib').mkdir(parents=True, exist_ok=True) seen = {} names = {} def output(*args): return subprocess.check_output(args, text=True) def bundle(original, target): original = original.resolve() if original in seen: return if target.name in names and names[target.name] != original: raise RuntimeError(f'Dylib basename collision: {target.name}') names[target.name] = original seen[original] = target shutil.copy2(original, target) target.chmod(0o755) subprocess.run(['codesign', '--remove-signature', str(target)], capture_output=True) lines = output('otool', '-L', str(original)).splitlines()[1:] # Resolve @rpath against the original image, not the destination directory. loads = output('otool', '-l', str(original)).splitlines() rpaths = [loads[i + 2].strip().split(' (offset')[0].removeprefix('path ') for i, line in enumerate(loads) if line.strip() == 'cmd LC_RPATH'] for line in lines: dependency = line.strip().split(' (compatibility')[0] if dependency.startswith(('/usr/lib/', '/System/Library/')): continue candidates = [dependency] if dependency.startswith('@rpath/'): candidates = [str(pathlib.Path(r) / dependency[len('@rpath/'):]) for r in rpaths] resolved = None for candidate in candidates: candidate = candidate.replace('@loader_path', str(original.parent)).replace('@executable_path', str(source.parent)) if pathlib.Path(candidate).is_file(): resolved = pathlib.Path(candidate).resolve() break if resolved == original: # dylib's own install name continue if resolved is None: raise RuntimeError(f'Cannot resolve {dependency} required by {original}') copied = destination / 'lib' / resolved.name bundle(resolved, copied) relative = f'@loader_path/{"lib/" if target.parent == destination else ""}{copied.name}' subprocess.run(['install_name_tool', '-change', dependency, relative, str(target)], check=True) if target.parent.name == 'lib': subprocess.run(['install_name_tool', '-id', '@loader_path/' + target.name, str(target)], check=True) subprocess.run(['codesign', '--force', '--sign', '-', str(target)], check=True) bundle(source, destination / 'ffmpeg') # Preserve Homebrew's corresponding source and license metadata on the builder. formula = source.parent.parent / '.brew' / 'ffmpeg.rb' licenses = destination / 'licenses' licenses.mkdir(exist_ok=True) if formula.exists(): shutil.copy2(formula, licenses / formula.name) for original in seen: prefix = original.parent.parent for name in ('LICENSE', 'COPYING', 'COPYING.GPLv2', 'COPYING.GPLv3', 'COPYING.LGPLv2.1', 'COPYING.LGPLv3'): if (prefix / name).is_file(): shutil.copy2(prefix / name, licenses / (original.name + '-' + name))