69 lines
3.1 KiB
Python
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))
|