Pacchetto Windows: registra gli strumenti senza entry point e controlla il catalogo nel build (#82)
rilascio-windows / setup-windows (push) Successful in 32m30s
rilascio-windows / setup-windows (push) Successful in 32m30s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMYrdz6uBbuwbaQ9MqDmMc
This commit is contained in:
@@ -17,6 +17,11 @@ Come si usa: e' l'exe compilato con Nuitka (build.ps1); da sorgente, dalla radic
|
||||
con l'ambiente 3.13 del pacchetto: `python packaging/windows/avvio.py [--impronta] [--host H]`.
|
||||
Opzioni di servizio: `--senza-finestre` (niente MessageBox, solo stderr: serve alle prove
|
||||
automatiche; NON salta il controllo di licenza) e `--senza-browser`.
|
||||
`--controlla-strumenti` (TieMeasureFlow#82): NON richiede la licenza, non avvia server, migrazioni
|
||||
ne' database; registra gli strumenti, scrive su stdout e in
|
||||
%PROGRAMDATA%\\Tielogic\\TieMeasureFlow\\log\\controllo-strumenti.json un JSON
|
||||
{strumenti, kind, falliti, unavailable} ed esce con 0 (strumenti > 0 e nessun fallito) o 3.
|
||||
La usa build.ps1 prima di Inno Setup.
|
||||
|
||||
Dipende da: visionsuite.license (vs-license, contratto licenza v1), uvicorn, waitress, alembic.
|
||||
|
||||
@@ -25,11 +30,15 @@ Dipende da: visionsuite.license (vs-license, contratto licenza v1), uvicorn, wai
|
||||
⚠️ Le variabili d'ambiente vanno impostate PRIMA di importare backend e frontend (leggono
|
||||
la configurazione all'import): per questo gli import pesanti stanno dentro le funzioni.
|
||||
⚠️ La cartella ProgramData la rende scrivibile agli utenti l'installer (.iss).
|
||||
⚠️ Nel binario Nuitka mancano i metadati di vs-tools, quindi `load_tools()` (che cerca gli
|
||||
strumenti come entry point) non ne trova nessuno: `build.ps1` genera `_strumenti.py`
|
||||
(MODULI, dagli entry point del venv) e `registra_strumenti()` li importa per nome (#82).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -264,6 +273,50 @@ def _prepara_log(dati: Path | None = None) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- strumenti di visione
|
||||
|
||||
def registra_strumenti() -> list[str]:
|
||||
"""Registra gli strumenti di Core; torna i moduli che non si sono importati (#82).
|
||||
|
||||
Nel pacchetto Nuitka gli entry point non esistono: `load_tools()` non trova nulla e
|
||||
l'elenco dei moduli arriva da `_strumenti.py`, generato da build.ps1. Da sorgente
|
||||
`_strumenti` non c'e' e bastano gli entry point. Il worker di visione gira in un thread
|
||||
di questo processo: registrare una volta sola qui vale per tutti."""
|
||||
from visionsuite.task.registry import load_tools
|
||||
load_tools()
|
||||
try:
|
||||
from _strumenti import MODULI # generato da build.ps1, accanto ad avvio.py
|
||||
except ImportError:
|
||||
MODULI = () # da sorgente: gli entry point di load_tools() bastano
|
||||
falliti: list[str] = []
|
||||
for modulo in MODULI:
|
||||
try:
|
||||
importlib.import_module(modulo) # eseguire il modulo registra i suoi @tool
|
||||
except Exception: # un modulo rotto non ferma l'app: si logga e si raccoglie
|
||||
log.error("strumenti %s non importati", modulo, exc_info=True)
|
||||
falliti.append(modulo)
|
||||
return falliti
|
||||
|
||||
|
||||
def controlla_strumenti() -> int:
|
||||
"""Senza licenza, server ne' DB: scrive il JSON del catalogo strumenti. Exit 0 se completo, 3 altrimenti."""
|
||||
from visionsuite.task.graph import registry
|
||||
from visionsuite.task.registry import unavailable
|
||||
falliti = registra_strumenti()
|
||||
kind = sorted(registry())
|
||||
esito = {"strumenti": len(kind), "kind": kind, "falliti": falliti, "unavailable": unavailable()}
|
||||
riga = json.dumps(esito, ensure_ascii=False)
|
||||
if sys.stdout is not None:
|
||||
print(riga)
|
||||
try:
|
||||
f = cartella_dati() / "log" / "controllo-strumenti.json"
|
||||
f.parent.mkdir(parents=True, exist_ok=True)
|
||||
f.write_text(riga + "\n", encoding="utf-8") # l'exe e' senza console: il file e' l'unica uscita sicura
|
||||
except OSError as e:
|
||||
log.warning("controllo-strumenti.json non scritto: %s", e)
|
||||
return 0 if kind and not falliti else 3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- rete
|
||||
|
||||
def _porta_aperta(porta: int) -> bool:
|
||||
@@ -402,6 +455,8 @@ def main(argv=None) -> int:
|
||||
p.add_argument("--host", default="127.0.0.1", help="indirizzo di ascolto dell'interfaccia web (default 127.0.0.1)")
|
||||
p.add_argument("--senza-finestre", action="store_true", help="niente MessageBox (prove automatiche)")
|
||||
p.add_argument("--senza-browser", action="store_true", help="non aprire il browser (prove automatiche)")
|
||||
p.add_argument("--controlla-strumenti", action="store_true",
|
||||
help="registra gli strumenti, scrive il JSON ed esce (senza licenza ne' server; usata da build.ps1)")
|
||||
args = p.parse_args(argv)
|
||||
_prepara_log()
|
||||
|
||||
@@ -415,6 +470,9 @@ def main(argv=None) -> int:
|
||||
messaggio(f"Impronta di questo computer:\n{impronta}", errore=False, senza_finestre=args.senza_finestre)
|
||||
return 0
|
||||
|
||||
if args.controlla_strumenti:
|
||||
return controlla_strumenti() # prima della licenza: la build non ha una licenza
|
||||
|
||||
if not controlla_licenza(args.senza_finestre):
|
||||
return 1
|
||||
|
||||
@@ -449,6 +507,7 @@ def main(argv=None) -> int:
|
||||
_prepara_log(dati) # alembic ha riconfigurato il logging
|
||||
log.info("migrazioni applicate (primo avvio: %s)", primo_avvio)
|
||||
|
||||
registra_strumenti() # prima del server: il worker di visione legge registry() gia' popolato (#82)
|
||||
thread = avvia_server(args.host, dati)
|
||||
if not args.senza_browser:
|
||||
# database senza utenti: si apre la pagina di setup del backend (SETUP_PASSWORD in config.env)
|
||||
|
||||
@@ -26,6 +26,10 @@ segue da solo, quindi i sottopacchetti si elencano uno per uno dagli __path__ in
|
||||
⚠️ numba e' escluso di proposito: le funzioni compilate da Nuitka non hanno bytecode e numba
|
||||
non puo' farne il JIT. Core e pm2d hanno gia' il ripiego in Python puro (HAS_NUMBA=False):
|
||||
funziona ma e' piu' lento nel matching.
|
||||
⚠️ Gli strumenti di vs-tools si registrano solo come entry point (visionsuite.task.tools) e i
|
||||
metadati di vs-tools non stanno nel binario (TieMeasureFlow#82): il build genera
|
||||
packaging\windows\_strumenti.py (MODULI, gitignorato) che avvio.py importa, e prima di
|
||||
Inno Setup esegue l'exe con --controlla-strumenti: se il catalogo e' vuoto il build fallisce.
|
||||
⚠️ Il frontend Flask non e' un pacchetto (`from config import Config`, `blueprints`, `services`):
|
||||
si compila mettendo flask_app e scripts su PYTHONPATH, come fa avvio.py in sviluppo.
|
||||
#>
|
||||
@@ -85,8 +89,9 @@ try {
|
||||
$pacchetti = & $py -c "import visionsuite, pkgutil; print('\n'.join('visionsuite.' + m.name for m in pkgutil.iter_modules(visionsuite.__path__) if m.ispkg))"
|
||||
if ($LASTEXITCODE -ne 0 -or -not $pacchetti) { throw "Elenco dei pacchetti visionsuite non riuscito" }
|
||||
$includi = @($pacchetti -split "\r?\n" | Where-Object { $_ } | ForEach-Object { "--include-package=$_" })
|
||||
# Nessun --include-distribution-metadata per vs-*: nulla li legge e Nuitka rifiuta i metadati di una
|
||||
# distribuzione il cui pacchetto (visionsuite.camera, namespace) non e' di primo livello.
|
||||
# Nessun --include-distribution-metadata per vs-*: Nuitka rifiuta i metadati di una distribuzione il cui
|
||||
# pacchetto (visionsuite.camera, namespace) non e' di primo livello. Gli entry point di vs-tools, che
|
||||
# load_tools() cercherebbe nei metadati, si rimpiazzano con _strumenti.py (sotto, TieMeasureFlow#82).
|
||||
Write-Host "Pacchetti visionsuite: $($includi -join ' ')"
|
||||
|
||||
# Flag Nuitka del piano PDF (WeasyPrint, kaleido, pyphen, plotly, tzdata...): le righe "--include-..."
|
||||
@@ -103,6 +108,25 @@ if (Test-Path $leggimi) {
|
||||
# Nuitka cerca i moduli non-pacchetto (config, services, blueprints, app, camera_service) su PYTHONPATH
|
||||
$env:PYTHONPATH = (@($radice, $flask, (Join-Path $radice 'scripts')) -join ';')
|
||||
|
||||
# TieMeasureFlow#82: i moduli-strumento dagli entry point del venv del build (nel binario non ci sono i
|
||||
# metadati di vs-tools, quindi load_tools() non li troverebbe). Si usa $py, non `uv run`.
|
||||
$strumenti = Join-Path $radice 'packaging\windows\_strumenti.py'
|
||||
$genera = @'
|
||||
import sys
|
||||
from importlib.metadata import entry_points
|
||||
moduli = sorted({ep.value for ep in entry_points(group="visionsuite.task.tools")})
|
||||
if not moduli:
|
||||
sys.exit(1)
|
||||
righe = ["# Generato da build.ps1 (TieMeasureFlow#82): non modificare, non committare.", "MODULI = ("]
|
||||
righe += [f" {m!r}," for m in moduli] + [")"]
|
||||
with open(sys.argv[1], "w", encoding="utf-8", newline="") as f:
|
||||
print(*righe, sep=chr(10), file=f)
|
||||
print(len(moduli))
|
||||
'@
|
||||
$nModuli = $genera | & $py - $strumenti
|
||||
if ($LASTEXITCODE -ne 0) { throw "Entry point visionsuite.task.tools non trovati nel venv: nessuno strumento da includere" }
|
||||
Write-Host "Moduli-strumento ($nModuli) in $strumenti"
|
||||
|
||||
$out = Join-Path $radice 'build\nuitka'
|
||||
New-Item -ItemType Directory -Force $out | Out-Null
|
||||
$nuitka = @(
|
||||
@@ -201,6 +225,17 @@ $sporchi = Get-ChildItem $sorgente -Recurse -Include *.py, *.pyc -File |
|
||||
Where-Object { $_.FullName -notlike "$mig\*" -or $_.Extension -eq '.py' }
|
||||
if ($sporchi) { throw "Il pacchetto contiene sorgenti/bytecode:`n$($sporchi.FullName -join "`n")" }
|
||||
|
||||
# TieMeasureFlow#82: l'exe deve vedere gli strumenti (--controlla-strumenti non vuole licenza ne' apre server/DB)
|
||||
$exe = Join-Path $sorgente 'TieMeasureFlow.exe'
|
||||
$jsonControllo = Join-Path $env:PROGRAMDATA 'Tielogic\TieMeasureFlow\log\controllo-strumenti.json'
|
||||
Remove-Item $jsonControllo -ErrorAction SilentlyContinue # un JSON vecchio non deve far passare il controllo
|
||||
# l'exe e' GUI-subsystem: con & non si aspetta, serve Start-Process -Wait
|
||||
$proc = Start-Process -FilePath $exe -ArgumentList '--controlla-strumenti' -Wait -PassThru
|
||||
$esito = if (Test-Path $jsonControllo) { Get-Content $jsonControllo -Raw } else { '(controllo-strumenti.json non scritto)' }
|
||||
if ($proc.ExitCode -ne 0) { throw "Controllo strumenti fallito (codice $($proc.ExitCode)): $esito" }
|
||||
$nStrumenti = ($esito | ConvertFrom-Json).strumenti
|
||||
Write-Host "Controllo strumenti: $nStrumenti tipi registrati nell'exe"
|
||||
|
||||
if ($SenzaSetup) { Write-Host "Fermo qui (-SenzaSetup): $sorgente"; return }
|
||||
|
||||
$dist = Join-Path $radice 'dist'
|
||||
|
||||
@@ -13,6 +13,8 @@ import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import types
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
@@ -195,3 +197,77 @@ def test_chiavi_extra_di_config_env_passano_ma_non_sovrascrivono_il_contratto(av
|
||||
avvio.main(["--senza-finestre", "--senza-browser"])
|
||||
assert os.environ["STATION_CODE"] == "ST-FIERA" and os.environ["STATION_SWITCH_ENABLED"] == "1"
|
||||
assert os.environ["APP_ENV"] == "production" # le variabili fisse vincono
|
||||
|
||||
|
||||
# --- TieMeasureFlow#82: strumenti nel pacchetto (nel binario non ci sono gli entry point) ---
|
||||
|
||||
def _modulo_strumenti(monkeypatch, moduli):
|
||||
# finto _strumenti.py generato da build.ps1
|
||||
finto = types.ModuleType("_strumenti")
|
||||
finto.MODULI = tuple(moduli)
|
||||
monkeypatch.setitem(sys.modules, "_strumenti", finto)
|
||||
|
||||
|
||||
def test_registra_strumenti_da_sorgente_senza_modulo_generato(avvio, monkeypatch):
|
||||
# _strumenti.py esiste solo dopo build.ps1: se manca (sorgente) bastano gli entry point
|
||||
monkeypatch.setitem(sys.modules, "_strumenti", None) # None in sys.modules = ImportError all'import
|
||||
from visionsuite.task.graph import registry
|
||||
assert avvio.registra_strumenti() == []
|
||||
assert len(registry()) > 0
|
||||
|
||||
|
||||
def test_registra_strumenti_modulo_inesistente_non_solleva(avvio, monkeypatch):
|
||||
_modulo_strumenti(monkeypatch, ("visionsuite.tools.image", "visionsuite.tools.non_esiste"))
|
||||
assert avvio.registra_strumenti() == ["visionsuite.tools.non_esiste"] # raccolto, non sollevato
|
||||
|
||||
|
||||
def test_registra_strumenti_importa_i_moduli_elencati(avvio, monkeypatch):
|
||||
# senza entry point (come nell'exe) i moduli di MODULI sono l'unica via: li si importa per nome
|
||||
_modulo_strumenti(monkeypatch, ("visionsuite.tools.image",))
|
||||
visti = []
|
||||
monkeypatch.setattr(avvio.importlib, "import_module", lambda nome: visti.append(nome))
|
||||
assert avvio.registra_strumenti() == [] and visti == ["visionsuite.tools.image"]
|
||||
|
||||
|
||||
def test_controlla_strumenti_senza_licenza_scrive_json(avvio, monkeypatch, capsys, tmp_path):
|
||||
monkeypatch.setitem(sys.modules, "_strumenti", None)
|
||||
|
||||
# la licenza non deve essere nemmeno chiesta, e server e migrazioni non devono partire
|
||||
def mai(*a, **k):
|
||||
raise AssertionError("licenza, server o migrazioni chiamati da --controlla-strumenti")
|
||||
|
||||
monkeypatch.setattr(avvio.lic, "verifica", mai)
|
||||
monkeypatch.setattr(avvio.lic, "cerca_licenza", mai)
|
||||
monkeypatch.setattr(avvio, "avvia_server", mai)
|
||||
monkeypatch.setattr(avvio, "esegui_migrazioni", mai)
|
||||
assert avvio.main(["--controlla-strumenti", "--senza-finestre"]) == 0
|
||||
dati = json.loads(capsys.readouterr().out)
|
||||
assert dati["strumenti"] > 0 and dati["strumenti"] == len(dati["kind"])
|
||||
assert dati["falliti"] == []
|
||||
scritto = tmp_path / "Tielogic" / "TieMeasureFlow" / "log" / "controllo-strumenti.json"
|
||||
assert json.loads(scritto.read_text(encoding="utf-8")) == dati
|
||||
assert avvio.aperti == [] and not (tmp_path / "Tielogic" / "TieMeasureFlow" / "tmf.db").exists() # nessun DB
|
||||
|
||||
|
||||
def test_controlla_strumenti_exit_3_se_un_modulo_fallisce(avvio, monkeypatch, capsys):
|
||||
_modulo_strumenti(monkeypatch, ("visionsuite.tools.non_esiste",))
|
||||
assert avvio.main(["--controlla-strumenti"]) == 3
|
||||
assert json.loads(capsys.readouterr().out)["falliti"] == ["visionsuite.tools.non_esiste"]
|
||||
|
||||
|
||||
def test_controlla_strumenti_exit_3_se_il_catalogo_e_vuoto(avvio, monkeypatch, capsys):
|
||||
# niente strumenti registrati (es. entry point assenti e _strumenti vuoto): il controllo deve fallire
|
||||
import visionsuite.task.graph as graph
|
||||
monkeypatch.setattr(avvio, "registra_strumenti", lambda: [])
|
||||
monkeypatch.setattr(graph, "registry", lambda: {})
|
||||
assert avvio.main(["--controlla-strumenti"]) == 3
|
||||
assert json.loads(capsys.readouterr().out)["strumenti"] == 0
|
||||
|
||||
|
||||
def test_avvio_normale_registra_gli_strumenti_prima_del_server(avvio, tmp_path, monkeypatch):
|
||||
# main() chiama registra_strumenti() dopo licenza e migrazioni e prima di avvia_server
|
||||
_scrivi_licenza(tmp_path)
|
||||
monkeypatch.setattr(avvio, "registra_strumenti", lambda: avvio.chiamate.append("strumenti") or [])
|
||||
assert avvio.main(["--senza-finestre", "--senza-browser"]) == 0
|
||||
dati = tmp_path / "Tielogic" / "TieMeasureFlow"
|
||||
assert avvio.chiamate == ["migrazioni", "strumenti", ("127.0.0.1", dati)]
|
||||
|
||||
Reference in New Issue
Block a user