<# Pacchetto Windows di TieMeasureFlow (TieMeasureFlow#76). Cosa fa: uv sync in un ambiente Python 3.13 separato (build\venv313), frontend (Tailwind + traduzioni), compila avvio.py con Nuitka in modalita' --standalone (cartella, NON onefile: l'onefile si decomprime in %TEMP% ad ogni avvio), aggiunge le migrazioni Alembic come soli .pyc, le DLL di WeasyPrint (piano B) e il commit del motore di visione, poi Inno Setup produce dist\TieMeasureFlow-setup-.exe. Sul PC del cliente non finisce nessun .py; i .pyc ci sono solo in src\backend\migrations (Alembic le carica dal disco). Come si usa (dalla radice del repo, con il submodule vendor/visionsuite inizializzato): powershell -ExecutionPolicy Bypass -File packaging\windows\build.ps1 [-Versione 3.0.0] -Versione accetta anche un tag (v3.0.0, v3.0.0-rc1): si usano i numeri iniziali. Senza -Versione si usa la variabile GITHUB_REF_NAME (tag nella CI) o 0.0.0. -SenzaSetup si ferma dopo Nuitka (cartella build\nuitka\avvio.dist). Dipende da: uv, node/npx (Tailwind 3.4.19 da npm: serve Internet SOLO a chi costruisce), Visual Studio con carico C++ (MSVC; Nuitka lo trova da solo), Inno Setup 6 (`winget install --id JRSoftware.InnoSetup -e`; ISCC.exe nel PATH, in Program Files o in %LOCALAPPDATA%\Programs). Nuitka e' nel gruppo uv "windows" (pyproject.toml). ⚠️ La prima compilazione e' lunga (decine di minuti: numpy/opencv/scipy/plotly); le successive riusano la cache di Nuitka (%LOCALAPPDATA%\Nuitka) e la cartella build\nuitka. ⚠️ I pacchetti di Core condividono il namespace visionsuite (extend_path): Nuitka non lo segue da solo, quindi i sottopacchetti si elencano uno per uno dagli __path__ installati. ⚠️ 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. #> param( [string]$Versione = $env:GITHUB_REF_NAME, [switch]$SenzaSetup ) $ErrorActionPreference = 'Stop' $radice = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path Set-Location $radice # Versione numerica a 4 parti per i metadati dell'exe (Nuitka la vuole cosi') if (-not $Versione) { $Versione = '0.0.0' } if ($Versione -notmatch '^v?(\d+)\.(\d+)\.(\d+)') { throw "Versione non valida: '$Versione' (atteso es. v3.0.0)" } $ver3 = "$($Matches[1]).$($Matches[2]).$($Matches[3])" $ver4 = "$ver3.0" Write-Host "== TieMeasureFlow $ver3 ==" # Controlli: se manca qualcosa il messaggio dice come installarlo if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { throw "uv non trovato. Installarlo: https://docs.astral.sh/uv/getting-started/installation/" } if (-not (Get-Command npx -ErrorAction SilentlyContinue)) { throw "node/npx non trovato (serve a compilare Tailwind). Installarlo da https://nodejs.org" } if (-not (Test-Path (Join-Path $radice 'vendor\visionsuite\packages\vs-license'))) { throw "Submodule vendor/visionsuite mancante o vecchio: git submodule update --init --recursive" } $iscc = (Get-Command ISCC.exe -ErrorAction SilentlyContinue).Source if (-not $iscc) { foreach ($c in @("${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", "$env:ProgramFiles\Inno Setup 6\ISCC.exe", "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe")) { if (Test-Path $c) { $iscc = $c; break } } } if (-not $iscc -and -not $SenzaSetup) { throw "Inno Setup 6 non trovato. Installarlo: winget install --id JRSoftware.InnoSetup -e" } # Ambiente separato da quello di sviluppo (3.11): extra server + client + vision-worker + gruppo windows $env:UV_PROJECT_ENVIRONMENT = Join-Path $radice 'build\venv313' uv sync --python 3.13 --frozen --extra server --extra client --extra vision-worker --group windows if ($LASTEXITCODE -ne 0) { throw "uv sync fallito" } $py = Join-Path $env:UV_PROJECT_ENVIRONMENT 'Scripts\python.exe' $vn = & $py -m nuitka --version # niente Select-Object -First: chiude la pipe e falsa l'exit code if ($LASTEXITCODE -ne 0) { throw "Nuitka non disponibile (serve anche un compilatore C: Visual Studio con 'Sviluppo di applicazioni desktop con C++')" } # Frontend: tailwind.css non e' nel repo (stessa versione esatta di Dockerfile.frontend) e traduzioni .mo $flask = Join-Path $radice 'src\frontend\flask_app' Push-Location $flask try { npx --yes tailwindcss@3.4.19 -i static/css/input.css -o static/css/tailwind.css --minify if ($LASTEXITCODE -ne 0) { throw "Tailwind fallito" } & (Join-Path $env:UV_PROJECT_ENVIRONMENT 'Scripts\pybabel.exe') compile -d translations if ($LASTEXITCODE -ne 0) { throw "pybabel compile fallito" } } finally { Pop-Location } # Sottopacchetti del namespace visionsuite (core, camera, task, tools, license, ...) $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-*: 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-..." # del LEGGIMI.md, con l'eventuale commento "# ..." in coda. I dati di src/backend/templates sono gia' sopra. $flagPdf = @() $leggimi = Join-Path $PSScriptRoot 'pdf\LEGGIMI.md' if (Test-Path $leggimi) { $flagPdf = @(Get-Content $leggimi | ForEach-Object { if ($_ -match '^\s*(--(?:include|nofollow|noinclude|enable)[^\s`#]*)\s*(?:#.*)?$' -and $Matches[1] -notlike '--include-data-dir=src/backend/templates*') { $Matches[1] } }) Write-Host "Flag Nuitka da pdf\LEGGIMI.md: $($flagPdf -join ' ')" } # 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 = @( '--standalone', '--assume-yes-for-downloads', '--msvc=latest', '--windows-console-mode=disable', # nessuna finestra nera: gli errori escono in MessageBox e nel log "--output-dir=$out", '--output-filename=TieMeasureFlow.exe', '--windows-icon-from-ico=packaging\windows\TieMeasureFlow.ico', '--company-name=Tielogic', '--product-name=TieMeasureFlow', "--file-version=$ver4", "--product-version=$ver4", '--file-description=TieMeasureFlow', '--copyright=Tielogic', # codice del prodotto: backend, worker di visione, runner (src e' un pacchetto, flask_app no) '--include-package=src.backend', '--include-package=src.vision', '--include-package=src.vision_worker', # le migrazioni NON si compilano: Alembic le carica dal disco, vanno come .pyc (sotto) '--nofollow-import-to=src.backend.migrations', '--nofollow-import-to=src.backend.tests', '--nofollow-import-to=src.vision.tests', '--nofollow-import-to=src.vision_worker.tests', # frontend Flask e servizio camera: moduli di primo livello trovati via PYTHONPATH '--include-module=app', '--include-module=config', '--include-package=blueprints', '--include-package=services', '--include-module=camera_service', '--nofollow-import-to=blueprints.tests', '--nofollow-import-to=tests', '--include-package=uvicorn', # loop e protocolli di uvicorn si caricano per nome '--include-package=waitress', '--include-package=alembic', '--include-module=aiosqlite', '--include-module=sqlalchemy.dialects.sqlite.aiosqlite', '--include-package=harvesters', # camere GenTL (GigE): il producer .cti e' del vendor '--include-package=genicam', '--include-package-data=genicam', '--include-package-data=babel', # locale-data di Babel (flask-babel) '--nofollow-import-to=numba', # vedi nota in testa: il ripiego in Python puro '--nofollow-import-to=llvmlite', # dati letti dal disco con __file__: setup page, report, Flask (templates/static/translations alla radice) '--include-data-dir=src\backend\templates=src\backend\templates', '--include-data-files=src\backend\migrations\alembic.ini=src\backend\migrations\alembic.ini', '--include-data-dir=src\frontend\flask_app\templates=templates', '--include-data-dir=src\frontend\flask_app\static=static', '--include-data-dir=src\frontend\flask_app\translations=translations', '--include-data-files=pyproject.toml=pyproject.toml' # config.py ne legge la versione per il piede di pagina ) + $includi + $flagPdf + @('packaging\windows\avvio.py') $t0 = Get-Date & $py -m nuitka @nuitka if ($LASTEXITCODE -ne 0) { throw "Nuitka fallito (codice $LASTEXITCODE)" } Write-Host ("Nuitka: {0:N0} minuti" -f ((Get-Date) - $t0).TotalMinutes) $sorgente = Join-Path $out 'avvio.dist' if (-not (Test-Path (Join-Path $sorgente 'TieMeasureFlow.exe'))) { throw "exe non trovato in $sorgente" } # Migrazioni Alembic: env.py e versions\ come soli .pyc (compileall -b: .pyc accanto al sorgente, che si cancella) $mig = Join-Path $sorgente 'src\backend\migrations' New-Item -ItemType Directory -Force (Join-Path $mig 'versions') | Out-Null Copy-Item 'src\backend\migrations\env.py' $mig -Force Copy-Item 'src\backend\migrations\versions\*.py' (Join-Path $mig 'versions') -Force & $py -m compileall -b -q $mig if ($LASTEXITCODE -ne 0) { throw "compileall delle migrazioni fallito" } Get-ChildItem $mig -Recurse -Filter *.py -File | Remove-Item -Force Get-ChildItem $mig -Recurse -Directory -Filter __pycache__ | Remove-Item -Recurse -Force # alembic.ini del pacchetto: sourceless, perche' anche la pagina di setup (command.stamp) apre # alembic.ini con un Config suo e non troverebbe env.py/versions (solo .pyc) $ini = Join-Path $mig 'alembic.ini' $testoIni = [System.IO.File]::ReadAllText($ini) if ($testoIni -notmatch '(?m)^script_location') { throw "alembic.ini senza script_location" } $testoIni = $testoIni -replace '(?m)^(script_location[^\r\n]*)', "`$1`nsourceless = true" [System.IO.File]::WriteAllText($ini, $testoIni, (New-Object System.Text.UTF8Encoding $false)) # Commit del motore di visione: avvio.py lo legge in VISION_ENGINE_VERSION $motore = (git -C vendor/visionsuite rev-parse HEAD).Trim() if ($LASTEXITCODE -ne 0 -or -not $motore) { throw "Commit di vendor/visionsuite non leggibile" } [System.IO.File]::WriteAllText((Join-Path $sorgente 'vision-engine-version.txt'), $motore, (New-Object System.Text.UTF8Encoding $false)) # DLL di WeasyPrint (piano PDF): lo script le scarica con sha256 pinnato $gtk = Join-Path $PSScriptRoot 'pdf\prendi-gtk.ps1' if (Test-Path $gtk) { powershell -NoProfile -ExecutionPolicy Bypass -File $gtk -Destinazione (Join-Path $sorgente 'gtk') if ($LASTEXITCODE -ne 0) { throw "prendi-gtk.ps1 fallito" } } else { Write-Warning "packaging\windows\pdf\prendi-gtk.ps1 assente: il pacchetto non avra' le DLL di WeasyPrint (niente report PDF)" } # Il pacchetto non deve contenere codice sorgente o bytecode, tranne i .pyc delle migrazioni $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' New-Item -ItemType Directory -Force $dist | Out-Null & $iscc "/DVersione=$ver3" "/DVersione4=$ver4" "/DOrigine=$sorgente" "/DUscita=$dist" (Join-Path $radice 'packaging\windows\TieMeasureFlow.iss') if ($LASTEXITCODE -ne 0) { throw "Inno Setup fallito" } $setup = Join-Path $dist "TieMeasureFlow-setup-$ver3.exe" Write-Host ("Pronto: {0} ({1:N1} MB)" -f $setup, ((Get-Item $setup).Length / 1MB))