#!/usr/bin/env python3 """ MeManga DEV build — single-file executable. python build.py Output: ./MeManga-Dev.exe (Windows) ./MeManga-Dev (macOS / Linux) What this does: 1. Installs requirements + PyInstaller into the current env. 2. Verifies critical imports. 3. Runs PyInstaller against packaging/memanga-dev.spec (one-file mode, console=True so tracebacks surface). 4. Moves the final executable from dist/ to the repo root. 5. Cleans up the PyInstaller scratch dirs (build/, dist/) so no folder of loose files lingers — just the one .exe. If you want a release build (no console, named MeManga.exe, ready for the GitHub release page) run `python build_app.py` instead. """ from __future__ import annotations import os import platform import shutil import subprocess import sys from pathlib import Path # Force UTF-8 on stdout/stderr so the unicode glyphs we print (✓ ✗ —) # don't crash on Windows CI runners, whose default stdout encoding is # cp1252 and explodes with UnicodeEncodeError on those characters. # Safe to call on any platform — reconfigure exists since Python 3.7 # and a no-op when the stream is already UTF-8 (macOS, Linux). try: sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") except (AttributeError, ValueError): pass ROOT = Path(__file__).resolve().parent PACKAGING = ROOT / "packaging" SPEC = PACKAGING / "memanga-dev.spec" BUILD_TMP = ROOT / "build" DIST_TMP = ROOT / "dist" # ───────────────────────────────────────────────────────────────────── # Steps # ───────────────────────────────────────────────────────────────────── def install_dependencies() -> bool: """Install runtime deps + PyInstaller into the current env, at the versions pinned in requirements/build.txt (the release build's).""" print("=== Installing dependencies ===") name = "requirements/build.txt" r = subprocess.run( [sys.executable, "-m", "pip", "install", "-r", str(ROOT / name), "-q"], ) if r.returncode != 0: print(f" ! pip install -r {name} failed") return False print(f" ✓ {name}") return True def verify_imports() -> bool: """Verify critical modules import — surface missing deps before a 20-minute PyInstaller run.""" print("\n=== Verifying imports ===") modules = [ ("img2pdf", "img2pdf"), ("PIL", "Pillow"), ("bs4", "beautifulsoup4"), ("cloudscraper", "cloudscraper"), ("pikepdf", "pikepdf"), ("yaml", "pyyaml"), ("PySide6", "PySide6"), ("certifi", "certifi"), ("requests", "requests"), ("rich", "rich"), ("playwright", "playwright"), ("playwright_stealth", "playwright-stealth"), ] missing = [] for mod, pip_name in modules: try: __import__(mod) print(f" ✓ {mod}") except ImportError: print(f" ✗ {mod} (pip install {pip_name})") missing.append(pip_name) if missing: print(f"\nMissing: {' '.join(missing)}") return False return True def run_pyinstaller() -> bool: """Run PyInstaller against the dev spec.""" print("\n=== Building (PyInstaller, one-file) ===") if not SPEC.exists(): print(f" ! spec missing: {SPEC}") return False cmd = [ sys.executable, "-m", "PyInstaller", str(SPEC), "--noconfirm", "--clean", # Keep PyInstaller's temp output in the standard dist/ + build/ # dirs — we sweep them at the end of this script. "--distpath", str(DIST_TMP), "--workpath", str(BUILD_TMP), ] r = subprocess.run(cmd) return r.returncode == 0 def _exe_name() -> str: """The PyInstaller-emitted basename for our dev build.""" return "MeManga-Dev.exe" if platform.system() == "Windows" else "MeManga-Dev" def collect_artifact() -> Path | None: """Move the final exe from dist/ to the repo root and clean up.""" src = DIST_TMP / _exe_name() if not src.exists(): print(f"\n! Expected output not found: {src}") return None dest = ROOT / _exe_name() if dest.exists(): dest.unlink() shutil.move(str(src), str(dest)) # On macOS PyInstaller chmods correctly; ensure +x just in case. try: dest.chmod(dest.stat().st_mode | 0o755) except Exception: pass # Sweep PyInstaller scratch dirs so the only build artefact left # behind is the single executable at the repo root. for d in (BUILD_TMP, DIST_TMP): if d.exists(): shutil.rmtree(d, ignore_errors=True) return dest # ───────────────────────────────────────────────────────────────────── def main() -> int: os.chdir(ROOT) if not install_dependencies(): return 1 if not verify_imports(): return 1 if not run_pyinstaller(): print("\n! PyInstaller failed — leaving build/ + dist/ for inspection") return 1 artifact = collect_artifact() if not artifact: return 1 size_mb = artifact.stat().st_size / (1024 * 1024) print(f"\n=== Done ===") print(f"Output: {artifact} ({size_mb:.1f} MB)") if platform.system() == "Windows": print("Run by double-clicking MeManga-Dev.exe") else: print(f"Run with: ./{artifact.name}") return 0 if __name__ == "__main__": sys.exit(main())