2024-03-21 23:12:20 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
|
|
import re
|
|
|
|
import shutil
|
2025-01-02 00:04:22 -05:00
|
|
|
from collections.abc import Iterable
|
2024-03-21 23:12:20 +01:00
|
|
|
from pathlib import Path
|
2024-03-26 11:06:45 +01:00
|
|
|
from subprocess import check_output, run
|
2024-03-21 23:12:20 +01:00
|
|
|
|
|
|
|
IGNORE_LIB_PREFIXES = ("/usr/lib/", "/System/", "@executable_path")
|
2025-01-02 00:04:22 -05:00
|
|
|
RPATH_PATTERN = re.compile(r"@rpath/(.*)")
|
2024-03-21 23:12:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
|
|
parser = argparse.ArgumentParser(
|
2025-01-02 00:04:22 -05:00
|
|
|
description="Copies shared libraries into the macOS app bundle."
|
2024-03-21 23:12:20 +01:00
|
|
|
)
|
2025-01-02 00:04:22 -05:00
|
|
|
parser.add_argument("-a", "--app-name")
|
2024-03-21 23:12:20 +01:00
|
|
|
parser.add_argument(
|
|
|
|
"--copy-only",
|
|
|
|
action="store_true",
|
|
|
|
help="Only copy libraries, do not update links.",
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--links-only",
|
|
|
|
action="store_true",
|
|
|
|
help="Only update links, do not copy libraries.",
|
|
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
def should_ignore_lib(lib_path: str) -> bool:
|
|
|
|
return any(lib_path.startswith(prefix) for prefix in IGNORE_LIB_PREFIXES)
|
|
|
|
|
|
|
|
|
2025-01-02 00:04:22 -05:00
|
|
|
def gather_libs(
|
|
|
|
binary_path: Path, visited_paths: set[Path] | None = None
|
|
|
|
) -> Iterable[Path]:
|
|
|
|
if visited_paths is None:
|
|
|
|
visited_paths = set()
|
|
|
|
visited_paths.add(binary_path)
|
|
|
|
|
2024-03-21 23:12:20 +01:00
|
|
|
parent_path = binary_path.parent
|
2024-03-26 11:06:45 +01:00
|
|
|
output = check_output(["otool", "-L", str(binary_path)], text=True)
|
2024-03-21 23:12:20 +01:00
|
|
|
libs = [line.split()[0] for line in output.split("\n")[1:] if line]
|
|
|
|
|
|
|
|
for lib in libs:
|
2025-01-02 00:04:22 -05:00
|
|
|
match = RPATH_PATTERN.match(lib)
|
2024-03-21 23:12:20 +01:00
|
|
|
lib_path = parent_path / (match.group(1) if match else lib)
|
|
|
|
|
2025-01-02 00:04:22 -05:00
|
|
|
if should_ignore_lib(str(lib_path)) or lib_path == binary_path:
|
2024-03-21 23:12:20 +01:00
|
|
|
continue
|
|
|
|
|
2025-01-02 00:04:22 -05:00
|
|
|
yield lib_path
|
|
|
|
if lib_path not in visited_paths:
|
|
|
|
yield from gather_libs(lib_path, visited_paths=visited_paths)
|
2024-03-21 23:12:20 +01:00
|
|
|
|
|
|
|
|
2025-01-02 00:04:22 -05:00
|
|
|
def copy_libs(frameworks_path: Path, library_paths: set[Path]) -> None:
|
|
|
|
frameworks_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
for lib_path in library_paths:
|
|
|
|
target_path = frameworks_path / lib_path.name
|
2024-03-21 23:12:20 +01:00
|
|
|
if not target_path.exists():
|
|
|
|
print(f"Copying {lib_path} to {target_path}")
|
|
|
|
shutil.copy2(lib_path, target_path)
|
|
|
|
|
|
|
|
|
|
|
|
def update_links(binary_path: Path) -> None:
|
2024-03-26 11:06:45 +01:00
|
|
|
output = check_output(["otool", "-L", str(binary_path)], text=True)
|
2024-03-21 23:12:20 +01:00
|
|
|
libs = [line.split()[0] for line in output.split("\n")[1:] if line]
|
|
|
|
|
|
|
|
for lib in libs:
|
|
|
|
if should_ignore_lib(lib):
|
|
|
|
continue
|
|
|
|
|
|
|
|
lib_name = Path(lib).name
|
|
|
|
target = f"@executable_path/../Frameworks/{lib_name}"
|
|
|
|
print(f"Updating link for {lib_name} in {binary_path}")
|
2024-03-26 11:06:45 +01:00
|
|
|
run(["install_name_tool", "-change", lib, target, str(binary_path)])
|
2024-03-21 23:12:20 +01:00
|
|
|
|
|
|
|
if lib_name == binary_path.name:
|
|
|
|
print(f"Updating id for {lib_name} in {binary_path}")
|
2024-03-26 11:06:45 +01:00
|
|
|
run(["install_name_tool", "-id", target, str(binary_path)])
|
2024-03-21 23:12:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
args = parse_args()
|
|
|
|
|
2025-01-02 00:04:22 -05:00
|
|
|
app_bundle_path = Path(f"/tmp/{args.app_name}.app")
|
|
|
|
app_binary_path = app_bundle_path / f"Contents/MacOS/{args.app_name}"
|
|
|
|
frameworks_path = app_bundle_path / "Contents/Frameworks"
|
|
|
|
|
2024-03-21 23:12:20 +01:00
|
|
|
if args.copy_only or not args.links_only:
|
2025-01-02 00:04:22 -05:00
|
|
|
library_paths = set(gather_libs(app_binary_path))
|
|
|
|
copy_libs(frameworks_path, library_paths)
|
2024-03-21 23:12:20 +01:00
|
|
|
|
|
|
|
if args.links_only or not args.copy_only:
|
2025-01-02 00:04:22 -05:00
|
|
|
for lib_path in frameworks_path.glob("*"):
|
2024-03-21 23:12:20 +01:00
|
|
|
update_links(lib_path)
|
|
|
|
|
2025-01-02 00:04:22 -05:00
|
|
|
update_links(app_binary_path)
|
2024-03-21 23:12:20 +01:00
|
|
|
|
2025-01-02 00:04:22 -05:00
|
|
|
print(f"Libraries for {args.app_name} copied and updated.")
|
2024-03-21 23:12:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|