Ctrl K
ComunidadGuías…

build.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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())