Pacchetto Windows: build.ps1 (Nuitka standalone + migrazioni .pyc + Inno Setup), installer .iss, icona (TieMeasureFlow#76)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 09:14:01 +02:00
parent d89a2e6c8d
commit 69cbf53060
3 changed files with 286 additions and 0 deletions
+205
View File
@@ -0,0 +1,205 @@
<#
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-<versione>.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.
⚠️ 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=$_" })
# Metadati delle distribuzioni di Core (importlib.metadata): visionsuite, vs-*
$distribuzioni = & $py -c "import importlib.metadata as m; print('\n'.join(sorted({d.metadata['Name'] for d in m.distributions() if d.metadata['Name'].lower() in ('visionsuite',) or d.metadata['Name'].lower().startswith('vs-')})))"
if ($LASTEXITCODE -ne 0) { throw "Elenco delle distribuzioni di Core non riuscito" }
$includi += @($distribuzioni -split "\r?\n" | Where-Object { $_ } | ForEach-Object { "--include-distribution-metadata=$_" })
Write-Host "Pacchetti visionsuite: $($includi -join ' ')"
# Flag Nuitka del piano PDF (WeasyPrint, kaleido, pyphen...): righe "--include-..." di pdf\LEGGIMI.md
$flagPdf = @()
$leggimi = Join-Path $PSScriptRoot 'pdf\LEGGIMI.md'
if (Test-Path $leggimi) {
$flagPdf = @(Get-Content $leggimi | ForEach-Object {
if ($_ -match '^\s*(?:[-*]\s*)?`?(--(?:include|nofollow|noinclude|enable|plugin)[^\s`]*)`?\s*$') { $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 ';')
$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
# 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")" }
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))