#!/usr/bin/env python3 """Build wechat-cli standalone binaries with PyInstaller.""" import os import shutil import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent.parent NPM_DIR = ROOT / "npm" PLATFORMS_DIR = NPM_DIR / "platforms" PLATFORM_MAP = { "darwin-arm64": {"target": "macos"}, "darwin-x64": {"target": "macos"}, "linux-x64": {"target": "linux"}, "linux-arm64": {"target": "linux"}, "win32-x64": {"target": "win"}, } def ensure_pyinstaller(): try: import PyInstaller # noqa: F401 return except ImportError: pass print("[+] Installing PyInstaller...") subprocess.check_call([sys.executable, "-m", "pip", "install", "pyinstaller"]) def build_platform(platform: str): info = PLATFORM_MAP[platform] os_name, arch = platform.split("-") ext = ".exe" if os_name == "win32" else "" binary_name = f"wechat-cli{ext}" output_dir = PLATFORMS_DIR / platform / "bin" output_dir.mkdir(parents=True, exist_ok=True) print(f"\n{'='*60}") print(f"Building for {platform}...") print(f"{'='*60}") cmd = [ sys.executable, "-m", "PyInstaller", "--onefile", "--name", "wechat-cli", "--distpath", str(output_dir), "--workpath", str(ROOT / "build" / f"wechat-cli_{platform}"), "--specpath", str(ROOT / "build"), "--noconfirm", "--clean", ] # Bundle C binaries for key extraction bin_dir = ROOT / "wechat_cli" / "bin" if bin_dir.exists(): for f in bin_dir.iterdir(): if not f.name.startswith(".") and f.is_file(): cmd.extend(["--add-binary", f"{f}:wechat_cli/bin"]) # Hidden imports hidden = ["pysqlcipher3", "sqlcipher3", "Cryptodome", "zstandard"] for h in hidden: cmd.extend(["--hidden-import", h]) cmd.append(str(ROOT / "entry.py")) print(f"[+] Running: {' '.join(cmd)}") try: subprocess.check_call(cmd, cwd=str(ROOT)) except subprocess.CalledProcessError as e: print(f"[-] Build failed for {platform}: {e}") return False binary_path = output_dir / binary_name if not binary_path.exists(): print(f"[-] Binary not found: {binary_path}") return False print(f"[+] Built: {binary_path}") print(f" Size: {binary_path.stat().st_size / 1024 / 1024:.1f} MB") return True def main(): if len(sys.argv) > 1: platforms = sys.argv[1:] else: # Default: build for current platform only import platform as _pf current = f"{_pf.system().lower()}-{_pf.machine()}" # Normalize if current == "darwin-arm64": platforms = ["darwin-arm64"] elif current == "darwin-x86_64" or current == "darwin-amd64": platforms = ["darwin-x64"] else: # Try to match platforms = [] for p in PLATFORM_MAP: os_name, arch = p.split("-") if os_name in current and (arch in current or (arch == "x64" and ("x86_64" in current or "amd64" in current))): platforms = [p] break if not platforms: print(f"Cannot determine platform from '{current}'") print(f"Usage: {sys.argv[0]} [platform...]") print(f" Platforms: {', '.join(PLATFORM_MAP.keys())}") sys.exit(1) print(f"[+] Building for: {', '.join(platforms)}") ensure_pyinstaller() results = {} for p in platforms: if p not in PLATFORM_MAP: print(f"[-] Unknown platform: {p}") results[p] = False continue results[p] = build_platform(p) print(f"\n{'='*60}") print("Build Summary:") for p, ok in results.items(): status = "OK" if ok else "FAILED" print(f" {p}: {status}") print(f"{'='*60}") if not all(results.values()): sys.exit(1) if __name__ == "__main__": main()