#!/usr/bin/env python3 """Assemble the two macOS app bundles (Eddycoin Wallet.app, Eddycoin Miner.app). Usage: build_mac_apps.py RUNTIME_DIR WHEELS_ARM WHEELS_X86 ART_DIR PROGRAMS_DIR OUT_DIR RUNTIME_DIR = output of make_runtime.py (MacOS/eddycoin-python + lib/). For the Linux self-test pass a Linux runtime in the same layout and the Linux wheels twice.""" import glob, os, shutil, subprocess, sys, zipfile RT, WA, WX, ART, PROG, OUT = [os.path.abspath(p) for p in sys.argv[1:7]] HERE = os.path.dirname(os.path.abspath(__file__)) LIPO = shutil.which("llvm-lipo") or shutil.which("llvm-lipo-19") or "/usr/lib/llvm-19/bin/llvm-lipo" VERSION = "1.01" APPS = [ # mainnet build: miner only (no GPU miner on macOS - Apple deprecated OpenCL) dict(name="Eddycoin Mainnet Miner", kind="miner", bid="io.eddycoin.miner.mainnet", icon="miner.icns", log="eddycoin-mainnet-miner-mac.log", scripts=["eddycoin_echain_cpu_miner.py", "eddycoin_miner_gui.py", "eddycoin_chain.py", "eddycoin_old_keys.py"], wheels=["certifi"], seed=["eddycoin_chain.json", "eddycoin_miner_defaults.json"]), ] def macho(p): with open(p, "rb") as f: return f.read(4) in (b"\xcf\xfa\xed\xfe", b"\xca\xfe\xba\xbe") def wheel(d, name): w = glob.glob(os.path.join(d, name + "-*.whl")); assert len(w) == 1, (d, name, w); return w[0] def install_wheels(names, sp): os.makedirs(sp, exist_ok=True) for n in names: wa, wx = wheel(WA, n), wheel(WX, n) with zipfile.ZipFile(wa) as z: z.extractall(sp) if wa != wx and os.path.basename(wa) != os.path.basename(wx): # arch-specific wheel: merge binaries with lipo with zipfile.ZipFile(wx) as z: for i in z.infolist(): if i.filename.endswith(".so"): tx = os.path.join(OUT, "_x86tmp.so"); open(tx, "wb").write(z.read(i)) ta = os.path.join(sp, i.filename) if macho(ta) and macho(tx): subprocess.run([LIPO, "-create", ta, tx, "-output", ta + ".fat"], check=True); os.replace(ta + ".fat", ta) os.remove(tx) for root, _, files in os.walk(sp): for f in files: p = os.path.join(root, f) if f.endswith(".pyi"): os.remove(p); continue os.chmod(p, 0o755 if f.endswith(".so") else 0o644) os.chmod(root, 0o755) os.makedirs(OUT, exist_ok=True) for a in APPS: app = os.path.join(OUT, a["name"] + ".app"); C = os.path.join(app, "Contents") if os.path.exists(app): shutil.rmtree(app) shutil.copytree(RT, C, symlinks=True) install_wheels(a["wheels"], os.path.join(C, "lib/python3.12/site-packages")) os.makedirs(os.path.join(C, "Resources/app")) for s in a["scripts"]: shutil.copy2(os.path.join(PROG, s), os.path.join(C, "Resources/app", s)); os.chmod(os.path.join(C, "Resources/app", s), 0o644) shutil.copy2(os.path.join(HERE, "eddycoin_mac_start.py"), os.path.join(C, "Resources/app/eddycoin_mac_start.py")) os.makedirs(os.path.join(C, "Resources/app/seed")) for s in a.get("seed", []): shutil.copy2(os.path.join(PROG, s), os.path.join(C, "Resources/app/seed", s)); os.chmod(os.path.join(C, "Resources/app/seed", s), 0o644) shutil.copy2(os.path.join(ART, a["icon"]), os.path.join(C, "Resources/AppIcon.icns")) rep = {"@NAME@": a["name"], "@KIND@": a["kind"], "@LOG@": a["log"], "@BUNDLEID@": a["bid"], "@VERSION@": VERSION} for src, dst, mode in (("launcher.sh.in", "MacOS/" + a["name"], 0o755), ("Info.plist.in", "Info.plist", 0o644)): t = open(os.path.join(HERE, src)).read() for k, v in rep.items(): t = t.replace(k, v) assert "@" not in t.replace("apple.com", ""), src open(os.path.join(C, dst), "w", newline="\n").write(t); os.chmod(os.path.join(C, dst), mode) open(os.path.join(C, "PkgInfo"), "w").write("APPL????") os.chmod(os.path.join(C, "MacOS/eddycoin-python"), 0o755) for root, dirs, files in os.walk(app): for d in dirs: os.chmod(os.path.join(root, d), 0o755) print("built", app)