#!/usr/bin/env python3 """Build a universal (arm64 + x86_64) relocatable Python 3.12 runtime for the macOS .app bundles from python-build-standalone install_only_stripped tarballs (already extracted to ARM/python and X86/python). Usage: make_runtime.py ARM_DIR X86_DIR OUT_DIR (OUT gets MacOS/eddycoin-python + lib/...)""" import os, shutil, subprocess, sys ARM, X86, OUT = [os.path.abspath(p) for p in sys.argv[1:4]] LIPO = shutil.which("llvm-lipo") or shutil.which("llvm-lipo-19") or "/usr/lib/llvm-19/bin/llvm-lipo" MACHO = {b"\xcf\xfa\xed\xfe", b"\xca\xfe\xba\xbe", b"\xce\xfa\xed\xfe"} DROP_TOP = ["include", "share", "lib/pkgconfig", "lib/libpython3.12.dylib", "lib/itcl4.3.8", "lib/thread3.0.6"] DROP_STD = ["idlelib", "ensurepip", "lib2to3", "pydoc_data", "turtledemo", "turtle.py", "config-3.12-darwin", "site-packages", "tkinter/test", "test", "venv", "__phello__", # not used by the wallet/miner (keeps the .dmg under the 25 MiB Cloudflare Pages limit) "asyncio", "unittest", "doctest.py", "pydoc.py", "pdb.py", "xmlrpc", "wsgiref", "sqlite3", "curses", "dbm", "tomllib", "_pydecimal.py", "pickletools.py", "tabnanny.py", "lib-dynload/_dbm.cpython-312-darwin.so", "lib-dynload/_crypt.cpython-312-darwin.so"] KEEP_ENC = {"ascii", "cp1250", "cp1252", "iso8859-1", "iso8859-2", "iso8859-15", "macRoman", "macCentEuro", "symbol", "dingbats", "macDingbats"} def macho(p): with open(p, "rb") as f: return f.read(4) in MACHO def merge(a, x, o): if macho(a) and os.path.exists(x) and macho(x): subprocess.run([LIPO, "-create", a, x, "-output", o], check=True) shutil.copymode(a, o) else: shutil.copy2(a, o) if os.path.exists(OUT): shutil.rmtree(OUT) os.makedirs(os.path.join(OUT, "MacOS")) merge(os.path.join(ARM, "bin/python3.12"), os.path.join(X86, "bin/python3.12"), os.path.join(OUT, "MacOS/eddycoin-python")) for root, dirs, files in os.walk(os.path.join(ARM, "lib")): rel = os.path.relpath(root, ARM) if any(rel == d or rel.startswith(d + "/") for d in DROP_TOP) or \ any(rel == "lib/python3.12/" + d or rel.startswith("lib/python3.12/" + d + "/") for d in DROP_STD) or "__pycache__" in rel.split("/"): continue os.makedirs(os.path.join(OUT, rel), exist_ok=True) for f in files: r = os.path.join(rel, f) if r in DROP_TOP or (rel == "lib/python3.12" and f in DROP_STD) or f.endswith((".pyc", ".a", "AppInit.c")) or \ ("lib/python3.12/" + os.path.relpath(r, "lib/python3.12")) in ["lib/python3.12/" + d for d in DROP_STD] or \ (rel == "lib/tcl9.0/encoding" and f[:-4] not in KEEP_ENC): continue a = os.path.join(ARM, r) if os.path.islink(a): os.symlink(os.readlink(a), os.path.join(OUT, r)); continue merge(a, os.path.join(X86, r), os.path.join(OUT, r)) # report Mach-O files and their archs n = 0 for root, _, files in os.walk(OUT): for f in files: p = os.path.join(root, f) if not os.path.islink(p) and macho(p): archs = subprocess.run([LIPO, "-archs", p], capture_output=True, text=True).stdout.strip() assert archs.split() == ["x86_64", "arm64"] or set(archs.split()) == {"x86_64", "arm64"}, (p, archs) n += 1 print("runtime OK, universal Mach-O files:", n)