build_server.py
Rama main · réplica de lectura del repositorio original.
#!/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())