wechat_msg_crawler/npm/scripts/build.py
canghe f51e89ce12 Add npm distribution support with PyInstaller binary
- Add npm package structure (@canghe_ai/wechat-cli) with platform-specific optionalDependencies
- Add JS wrapper (bin/wechat-cli.js) and postinstall script
- Add PyInstaller entry point and build script
- Update scanner_macos.py for PyInstaller compatibility (sys._MEIPASS)
- Update README with npm install instructions (macOS arm64)
- Fix repo URLs to freestylefly/wechat-cli
2026-04-04 12:36:50 +08:00

140 lines
4.0 KiB
Python

#!/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()