TRX/tools/shared/mac/bundle_dylibs

107 lines
3.3 KiB
Text
Raw Permalink Normal View History

#!/usr/bin/env python3
import argparse
import re
import shutil
2025-01-02 00:04:22 -05:00
from collections.abc import Iterable
from pathlib import Path
2024-03-26 11:06:45 +01:00
from subprocess import check_output, run
IGNORE_LIB_PREFIXES = ("/usr/lib/", "/System/", "@executable_path")
2025-01-02 00:04:22 -05:00
RPATH_PATTERN = re.compile(r"@rpath/(.*)")
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."
)
2025-01-02 00:04:22 -05:00
parser.add_argument("-a", "--app-name")
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)
parent_path = binary_path.parent
2024-03-26 11:06:45 +01:00
output = check_output(["otool", "-L", str(binary_path)], text=True)
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)
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:
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)
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
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)
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)])
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)])
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"
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)
if args.links_only or not args.copy_only:
2025-01-02 00:04:22 -05:00
for lib_path in frameworks_path.glob("*"):
update_links(lib_path)
2025-01-02 00:04:22 -05:00
update_links(app_binary_path)
2025-01-02 00:04:22 -05:00
print(f"Libraries for {args.app_name} copied and updated.")
if __name__ == "__main__":
main()