Ctrl K
ComunidadGuías…

build_server.py

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""
build_server.py — builds HAM-RADIO-CTRL.exe (the server) via PyInstaller.

RUN (on Windows, in the server code directory):
    py build_server.py

Steps:
  1. Check for Python 3.10+
  2. Install dependencies (pyinstaller + server libraries)
  3. Check for ham_audio.exe + opus DLLs (warn if missing)
  4. Run PyInstaller against hamctrl.spec
  5. Report the result: dist/HAM-RADIO-CTRL.exe

Output: dist/HAM-RADIO-CTRL.exe (approx. 150-250 MB - aiortc+scipy are large)
"""
import subprocess
import sys
from pathlib import Path

BASE = Path(__file__).parent
DIST = BASE / "dist"

# Required dependencies for the server to run
REQUIRED = ["pyinstaller", "aiohttp", "pyserial", "numpy", "scipy"]
# Optional - audio/WebRTC (the server works without them, just no audio streaming)
OPTIONAL = ["aiortc", "pyaudio", "opuslib", "cryptography"]


def check_python():
    v = sys.version_info
    if v < (3, 10):
        print(f"[X] Python 3.10+ required, you have {v.major}.{v.minor}")
        return False
    print(f"[OK] Python {v.major}.{v.minor}.{v.micro}")
    return True


def install_deps():
    print("\n[*] Installing required dependencies...")
    try:
        subprocess.check_call([sys.executable, "-m", "pip", "install",
                               "--upgrade", *REQUIRED])
        print("[OK] Required dependencies installed")
    except subprocess.CalledProcessError as e:
        print(f"[X] pip failed (required): {e}")
        return False

    print("\n[*] Installing optional dependencies (audio/WebRTC)...")
    print("    (if one fails, the server still works without that feature)")
    for pkg in OPTIONAL:
        try:
            subprocess.check_call([sys.executable, "-m", "pip", "install",
                                   "--upgrade", pkg])
            print(f"[OK] {pkg}")
        except subprocess.CalledProcessError:
            print(f"[!] {pkg} - failed (skipping, the EXE will build without it)")
    return True


def check_binaries():
    print("\n[*] Checking external binaries...")
    ham = BASE / "ham_audio.exe"
    if ham.exists():
        mb = ham.stat().st_size / 1024 / 1024
        print(f"[OK] ham_audio.exe ({mb:.1f} MB) - will be bundled")
    else:
        print("[!] ham_audio.exe MISSING - the EXE will build, but audio won't work")
        print("    Copy ham_audio.exe into this directory before building.")

    opus_found = []
    for name in ("libopus.dll", "opus.dll", "opuslib.dll"):
        if (BASE / name).exists():
            print(f"[OK] {name} - bundled")
            opus_found.append(name)
    if not opus_found:
        print("[!] Opus DLLs MISSING - Opus audio may not work")
        print("    Copy libopus.dll, opus.dll, opuslib.dll into this directory.")
    else:
        print(f"[OK] opus: {len(opus_found)}/3 DLLs found")

    return True  # don't block - the build can proceed without audio


def patch_scipy():
    """
    Fix the scipy.stats._distn_infrastructure 'del obj' NameError that
    crashes the app under PyInstaller + Python 3.12.

    The file ends with:
        for obj in [...]:
            exec('del ' + obj)
        del obj          # <- this blows up
    We replace 'del obj' with a safe version in the NAMESPACE (try/except).
    This patches the installed scipy package (not our project code).
    """
    print("\n[*] Checking/patching the scipy 'del obj' bug (Py3.12 + PyInstaller)...")
    try:
        import scipy.stats._distn_infrastructure as _m
        fpath = Path(_m.__file__)
    except Exception as e:
        print(f"[!] Could not find scipy to patch: {e}")
        return True  # don't block

    try:
        src = fpath.read_text(encoding="utf-8")
    except Exception as e:
        print(f"[!] Could not read {fpath}: {e}")
        return True

    # Look for a lone 'del obj' (after the docstring-cleanup loop)
    if "\ndel obj\n" in src and "try:\n    del obj\nexcept NameError" not in src:
        patched = src.replace(
            "\ndel obj\n",
            "\ntry:\n    del obj\nexcept NameError:\n    pass\n",
            1,
        )
        try:
            fpath.write_text(patched, encoding="utf-8")
            print(f"[OK] Patched {fpath.name} (del obj -> try/except)")
        except Exception as e:
            print(f"[!] Could not write the patch (maybe missing permissions): {e}")
            print("    Run as administrator or patch it manually.")
    else:
        print("[OK] scipy already patched or a different version - skipping")
    return True


def build():
    spec = BASE / "hamctrl.spec"
    if not spec.exists():
        print(f"[X] {spec} missing")
        return False

    # Clean the previous build
    import shutil
    for d in ("build", "dist", "__pycache__"):
        p = BASE / d
        if p.exists():
            print(f"[*] Cleaning {d}/")
            shutil.rmtree(p, ignore_errors=True)

    print("\n[*] PyInstaller build (this will take a few minutes)...")
    try:
        subprocess.check_call([sys.executable, "-m", "PyInstaller",
                               "--clean", "--noconfirm", str(spec)])
    except subprocess.CalledProcessError as e:
        print(f"[X] PyInstaller failed: {e}")
        return False

    exe = DIST / "HAM-RADIO-CTRL.exe"
    if not exe.exists():
        print(f"[X] {exe} missing after the build")
        return False
    mb = exe.stat().st_size / 1024 / 1024
    print(f"\n[OK] {exe} ({mb:.1f} MB)")
    return True


def main():
    print("=" * 56)
    print("  HAM RADIO CTRL - server build (EXE)")
    print("=" * 56)

    if not check_python():
        return 1
    if not install_deps():
        return 1
    check_binaries()
    patch_scipy()
    if not build():
        return 1

    print("\n" + "=" * 56)
    print("  BUILD SUCCESSFUL!")
    print("=" * 56)
    print(f"Output: {DIST / 'HAM-RADIO-CTRL.exe'}")
    print("\nTest:")
    print("  1. Run the EXE on a machine WITHOUT Python")
    print("  2. It should open a browser at https://localhost:8001")
    print("  3. Log in as admin / Admin1234! -> the password-change wizard")
    print("\nUser data (config.json, users.json, .env) is created NEXT TO the exe.")
    return 0


if __name__ == "__main__":
    sys.exit(main())