ituoke/scripts/bundle-macos-libs.py

69 lines
3.1 KiB
Python

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