Ctrl K
ComunidadGuías…

hamctrl.spec

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

Archivo original ↗Preguntar a KiBot
# -*- mode: python ; coding: utf-8 -*-
"""
hamctrl.spec — PyInstaller spec dla HAM RADIO CTRL (serwer jako EXE).

Buduje jeden plik: dist/HAM-RADIO-CTRL.exe

Uruchomienie (na Windows, w tym katalogu):
    py -m pip install pyinstaller
    py -m PyInstaller --clean --noconfirm hamctrl.spec

WYMAGANE PLIKI OBOK SPEC (dolozyc przed buildem):
    ham_audio.exe        — Rust audio server (binarny)
    libopus.dll, opus.dll, opuslib.dll   — biblioteki Opus (wszystkie trzy)

Zaleznosci Python do zainstalowania przed buildem:
    py -m pip install aiohttp aiortc numpy scipy pyserial pyaudio opuslib cryptography
    (winloop i orjson opcjonalne - przyspieszaja, ale nie wymagane)
"""
import os
from pathlib import Path
from PyInstaller.utils.hooks import collect_all, collect_submodules

BASE = Path(os.getcwd())

# ── scipy: tylko to, co faktycznie uzywane ────────────────────────────────────
# FIX (2026-10-05, zgloszenie: "soft ma 400mb, trzeba zejsc z wagi"): cala
# baza kodu uzywa WYLACZNIE scipy.special.erf (ft8_encoder.py/ft4_encoder.py,
# ksztaltowanie impulsu GFSK) i scipy.signal.resample_poly (tamze, resampling
# PCM 12k->48k) - zweryfikowane grepem po calym repo. collect_all("scipy")
# pakowal CALY pakiet (115 MB na dysku: stats, sparse, spatial, optimize,
# integrate, ndimage, cluster...), z czego ponizsze dwa submoduly to ulamek.
# Zawezone do dwoch faktycznie importowanych poddrzew - ten sam mechanizm
# (zbiera dynamiczne importy/pliki danych ktorych PyInstaller sam nie zlapie,
# stad historyczny powod uzycia collect_all zamiast zwyklego importu), tylko
# scopowany. Jesli po tej zmianie build/test pokaze "NameError"/"DLL load
# failed" z innego zakatka scipy - NIE poszerzac z powrotem na caly pakiet
# od razu, dopisac dokladnie ten brakujacy submodul (np. "scipy.fft", jesli
# resample_poly lazy-importuje je wewnetrznie).
_scipy_datas, _scipy_binaries, _scipy_hidden = collect_all("scipy.signal")
_scipy_sp_datas, _scipy_sp_binaries, _scipy_sp_hidden = collect_all("scipy.special")
_scipy_datas    += _scipy_sp_datas
_scipy_binaries += _scipy_sp_binaries
_scipy_hidden   += _scipy_sp_hidden
_numpy_datas, _numpy_binaries, _numpy_hidden = collect_all("numpy")

# onnxruntime (DeepCW) ma natywne DLL-e — bez collect_all PyInstaller ich nie
# zabierze i przy starcie dostaniemy "DLL load failed". Opcjonalne: gdy pakiet
# nie jest zainstalowany, build i tak przejdzie (DeepCW bedzie niedostepny).
try:
    _ort_datas, _ort_binaries, _ort_hidden = collect_all("onnxruntime")
except Exception:
    _ort_datas, _ort_binaries, _ort_hidden = [], [], []

# ── Pliki danych (read-only, ida do paczki) ───────────────────────────────────
datas = [
    ("public", "public"),   # cały frontend (html/css/js)
    ("rigs",   "rigs"),     # profile radia (jesli PyInstaller nie zlapie jako modul)
]
# Most COM (HAM-RADIO-CTRL.exe dla CW Skimmer/Logger32) - do pobrania przez
# operatorow przez /download/com-bridge. Dokladany jesli lezy obok spec.
_com_bridge = BASE / "HAM-RADIO-CTRL-bridge.exe"
if _com_bridge.exists():
    datas.append((str(_com_bridge), "bridge"))
    print(f"[spec] dolaczam most COM: {_com_bridge.name}")
else:
    # Fallback: oryginalna nazwa TYLKO z bridge_client/dist (tam jest most,
    # NIE serwer). NIE szukamy w BASE bo tam lezy serwer o tej samej nazwie!
    _cand = BASE / "bridge_client" / "dist" / "HAM-RADIO-CTRL.exe"
    if _cand.exists():
        datas.append((str(_cand), "bridge"))
        print(f"[spec] dolaczam most COM z bridge_client/dist: {_cand}")
    else:
        print("[spec] UWAGA: brak mostu COM (HAM-RADIO-CTRL-bridge.exe) - "
              "/download/com-bridge nie zadziala. Skopiuj most pod nazwa "
              "HAM-RADIO-CTRL-bridge.exe obok spec.")
datas += _scipy_datas + _numpy_datas + _ort_datas

# ── Binaria zewnetrzne (ham_audio.exe + opus DLL) ─────────────────────────────
# Dokladane tylko jesli istnieja obok spec - build nie pada gdy ich brak
# (mozna zbudowac wersje bez audio do testow).
binaries = []
for _bin in ("ham_audio.exe", "libopus.dll", "opus.dll", "opuslib.dll"):
    _p = BASE / _bin
    if _p.exists():
        binaries.append((str(_p), "."))
        print(f"[spec] dolaczam binarke: {_bin}")
    else:
        print(f"[spec] pomijam (brak): {_bin}")
binaries += _scipy_binaries + _numpy_binaries + _ort_binaries

# ── Hidden imports ────────────────────────────────────────────────────────────
# PyInstaller nie zawsze wykrywa dynamiczne importy. Te paczki maja duzo
# podmodulow ladowanych dynamicznie - trzeba je wskazac jawnie.
hiddenimports = [
    # aiohttp + async
    "aiohttp", "aiohttp.web", "multidict", "yarl", "async_timeout",
    "attr", "frozenlist", "aiosignal",
    # aiortc (WebRTC - TX audio) - CIEZKIE, duzo zaleznosci
    "aiortc", "aiortc.contrib.media", "av", "pylibsrtp",
    "cryptography", "cryptography.hazmat.bindings._openssl",
    "google.protobuf", "pyee",
    # numpy / scipy (DSP - FT8/FT4 decode)
    "numpy", "scipy", "scipy.signal", "scipy.special",
    "scipy.fft", "scipy.fftpack", "scipy._lib.messagestream",
    # audio
    "pyaudio", "opuslib", "opuslib.api",
    # serial
    "serial", "serial.tools", "serial.tools.list_ports",
    # przyspieszacze (opcjonalne - jesli zainstalowane)
    # "winloop", "orjson",  # odkomentuj jesli uzywasz
    # moduly projektu (zeby na pewno weszly)
    "config", "webapp", "server", "civ", "data", "auth",
    "audio", "audio_stream", "audio_rust_bridge", "webrtc_audio", "upnp_ports", "ft8_all_log",
    "qso_db", "qso_engine", "clock_check", "dxcluster", "rotator", "relay_controller",
    "tunnel_manager", "wsjtx_udp", "wsjtx_local", "com_bridge_ws",
    "rigcat", "hamlib_server", "hamlib_caps",
    "ft8_encoder", "ft8_rx_decoder", "ft8_rust_receiver",
    "ft4_encoder", "ft4_rx_decoder",
    # DeepCW (dekoder CW przez siec neuronowa - ONNX)
    "deepcw_engine", "deepcw_model", "deepcw_lang",
    "onnxruntime", "onnxruntime.capi", "onnxruntime.capi._pybind_state",
    "demod", "demod_ft4", "sync", "sync_ft4", "ldpc_decode",
    "params", "params_ft4", "unpack", "waterfall",
    "rigs", "rigs.civ_profiles", "rigs.features",
]
hiddenimports += _scipy_hidden + _numpy_hidden + _ort_hidden

# ── Wykluczenia (odchudzenie EXE) ─────────────────────────────────────────────
# Rzeczy ktorych na pewno nie uzywamy - zmniejsza rozmiar.
excludes = [
    "tkinter", "matplotlib", "PyQt5", "PyQt6", "PySide2", "PySide6",
    "pytest", "IPython", "notebook", "jupyter", "pandas",
    "PIL",  # jesli nie uzywasz Pillow po stronie serwera
    # FIX (2026-10-05, zgloszenie: "soft ma 400mb"): ten srodowisko Pythona
    # ma zainstalowany caly, niezwiazany z HAMCTRL stos ML/NLP (torch samo
    # w sobie 528 MB na dysku) - prawdopodobnie do innego projektu na tym
    # samym komputerze. onnxruntime (uzywany tu tylko do prostej inferencji
    # DeepCW) ma WEWNATRZ SIEBIE opcjonalne/delayed sciezki importu do
    # narzedzi quantization/profiling, ktore z kolei odwoluja sie do torch/
    # transformers/itd. - PyInstaller, nie wiedzac ze te sciezki nigdy nie
    # wykonaja sie w runtime HAMCTRL, zbieral je "na wszelki wypadek".
    # Zweryfikowane grepem po calym repo - zero realnych importow ponizszych
    # pakietow w kodzie HAMCTRL. Razem to byl prawdopodobnie najwiekszy
    # pojedynczy skladnik 400 MB, wiekszy niz samo scipy.
    "torch", "transformers", "sklearn", "scikit-learn", "nltk", "sympy",
    "huggingface_hub", "networkx", "einops", "accelerate", "hf_xet",
]

block_cipher = None

a = Analysis(
    ["launcher.py"],
    pathex=[str(BASE)],
    binaries=binaries,
    datas=datas,
    hiddenimports=hiddenimports,
    hookspath=[],
    hooksconfig={},
    runtime_hooks=["rthook_scipy.py"],
    excludes=excludes,
    # optimize=0: zachowaj docstringi (scipy.stats generuje z nich kod).
    optimize=0,
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name="HAM-RADIO-CTRL",
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=False,            # UPX moze psuc niektore DLL - wylaczone dla stabilnosci
    upx_exclude=[],
    # FIX (2026-09-21, live report: "co ~7 dni caly uklad sie rozjezdza,
    # strona robi sie biala" na serwerze pracujacym 24/7). Domyslnie
    # (runtime_tmpdir=None) PyInstaller onefile rozpakowuje public/ (css,
    # js, index.html...) do losowego folderu pod %TEMP%\_MEIxxxxxx przy
    # KAZDYM starcie i zostawia go tam bez modyfikacji przez caly czas
    # dzialania serwera - dokladnie profil plikow, ktore Windows Storage
    # Sense (domyslne czyszczenie %TEMP%, opcja "co tydzien") albo inny
    # czyszczacz dysku kasuje jako "nieuzywane". config.py juz od dawna
    # swiadomie trzyma DANE UZYTKOWNIKA (config.json/users.json/qso.db)
    # poza %TEMP% z dokladnie tego samego powodu ("User data ... MUST be
    # persistent and writable - next to the EXE, not in PyInstaller's temp
    # folder") - ten sam rozumowanie nigdy nie zostalo zastosowane do
    # WLASNEGO folderu rozpakowania onefile, co jest luka ktora ten fix
    # zamyka. %APPDATA%\HAMCTRL juz istnieje i jest sprawdzone (tam zyje
    # m.in. qso.db z 30k+ QSO) - _runtime obok tego jest tak samo trwale,
    # zawsze zapisywalne bez podnoszenia uprawnien (w przeciwienstwie do
    # katalogu obok EXE w Program Files) i POZA zasiegiem sprzatania
    # %TEMP%. Windows bootloader PyInstallera rozwija %APPDATA% w tym
    # polu (w przeciwienstwie do POSIX, patrz PyInstaller --runtime-tmpdir
    # docs) - zweryfikowane dzialajacym buildem.
    runtime_tmpdir=r"%APPDATA%\HAMCTRL\_runtime",
    console=True,         # KONSOLA WIDOCZNA - user widzi logi/bledy serwera
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon="icon.ico" if (BASE / "icon.ico").exists() else None,
)