fix(macos): replace dylibbundler with custom Python script for dependency bundling

This commit is contained in:
2026-06-03 20:34:56 +08:00
parent 3205a27985
commit 830f5f76af
+87 -7
View File
@@ -121,17 +121,97 @@ jobs:
run: | run: |
export PATH="$(brew --prefix qt@6)/bin:$PATH" export PATH="$(brew --prefix qt@6)/bin:$PATH"
macdeployqt build/app/Olive.app macdeployqt build/app/Olive.app
# Re-sign after macdeployqt (it breaks signatures when modifying libs)
codesign --force --deep --sign - build/app/Olive.app
- name: Bundle non-Qt libraries - name: Bundle non-Qt libraries
run: | run: |
mkdir -p build/app/Olive.app/Contents/Frameworks mkdir -p build/app/Olive.app/Contents/Frameworks
dylibbundler -od -b \ python3 << 'PYEOF'
-x build/app/Olive.app/Contents/MacOS/Olive \ import os, shutil, subprocess
-d build/app/Olive.app/Contents/Frameworks/ \
-p @executable_path/../Frameworks \ APP = "build/app/Olive.app"
-s /opt/homebrew/lib \ BINARY = f"{APP}/Contents/MacOS/Olive"
-s /usr/local/lib \ DEST = f"{APP}/Contents/Frameworks"
-s "${PWD}/otio-install/lib" os.makedirs(DEST, exist_ok=True)
def get_rpaths(binary):
out = subprocess.run(["otool", "-l", binary], capture_output=True, text=True).stdout
rpaths = []
for i, line in enumerate(out.splitlines()):
if "cmd LC_RPATH" in line and i + 2 < len(out.splitlines()):
parts = out.splitlines()[i + 2].strip().split()
if len(parts) >= 2:
rpaths.append(parts[1])
return rpaths
def get_deps(binary):
out = subprocess.run(["otool", "-L", binary], capture_output=True, text=True).stdout
deps = []
for line in out.splitlines()[1:]:
line = line.strip()
if not line:
continue
dep = line.split()[0]
if dep.startswith("/usr/lib/") or dep.startswith("/System/"):
continue
deps.append(dep)
return deps
def resolve(ref, rpaths, loader_dir=""):
if ref.startswith("/"):
return ref if os.path.isfile(ref) else None
if ref.startswith("@rpath/"):
for rp in rpaths:
p = os.path.join(rp, ref[7:])
if os.path.isfile(p):
return p
if ref.startswith("@loader_path/") and loader_dir:
p = os.path.join(loader_dir, ref[13:])
if os.path.isfile(p):
return p
if ref.startswith("@executable_path/"):
p = os.path.join(APP, "Contents/MacOS", ref[17:])
if os.path.isfile(p):
return p
return None
EXEC_RPATHS = get_rpaths(BINARY)
PROCESSED = set()
def process(target):
tdir = os.path.dirname(target)
print(f"Processing: {target}")
for dep in get_deps(target):
resolved = resolve(dep, EXEC_RPATHS if target == BINARY else [], tdir)
if not resolved or not os.path.isfile(resolved):
continue
resolved = os.path.realpath(resolved)
if not (resolved.startswith("/opt/homebrew/") or resolved.startswith("/usr/local/") or resolved.startswith(os.path.realpath(os.getcwd()))):
continue
base = os.path.basename(resolved)
if base in PROCESSED:
subprocess.run(["install_name_tool", "-change", dep, f"@rpath/{base}", target], capture_output=True)
continue
PROCESSED.add(base)
dst = os.path.join(DEST, base)
print(f" Copying: {resolved} -> {dst}")
shutil.copy2(resolved, dst)
subprocess.run(["install_name_tool", "-id", f"@rpath/{base}", dst], capture_output=True)
subprocess.run(["install_name_tool", "-change", dep, f"@rpath/{base}", target], capture_output=True)
process(dst)
process(BINARY)
for rp in EXEC_RPATHS:
subprocess.run(["install_name_tool", "-delete_rpath", rp, BINARY], capture_output=True)
subprocess.run(["install_name_tool", "-add_rpath", "@executable_path/../Frameworks", BINARY], capture_output=True)
subprocess.run(["codesign", "--force", "--deep", "--sign", "-", APP], check=True)
print("Done. Frameworks:")
for f in sorted(os.listdir(DEST)):
print(f" {f}")
PYEOF
- name: Debug bundle contents - name: Debug bundle contents
run: | run: |