8d4c4020f6
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
207 lines
12 KiB
PowerShell
207 lines
12 KiB
PowerShell
<#
|
|
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, 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 ';')
|
|
|
|
$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))
|