diff --git a/packaging/windows/TieMeasureFlow.ico b/packaging/windows/TieMeasureFlow.ico new file mode 100644 index 0000000..6079069 Binary files /dev/null and b/packaging/windows/TieMeasureFlow.ico differ diff --git a/packaging/windows/TieMeasureFlow.iss b/packaging/windows/TieMeasureFlow.iss new file mode 100644 index 0000000..9d35c11 --- /dev/null +++ b/packaging/windows/TieMeasureFlow.iss @@ -0,0 +1,81 @@ +; Installer di TieMeasureFlow (TieMeasureFlow#76). +; Cosa fa: copia la cartella standalone di Nuitka in Program Files\Tielogic\TieMeasureFlow, +; crea in ProgramData le cartelle scrivibili da tutti gli utenti (licenze, uploads, camera, log) +; e i collegamenti in menu Start e sul desktop. +; Come si usa: lo lancia build.ps1 con /DVersione /DVersione4 /DOrigine /DUscita; a mano: +; ISCC /DVersione=3.0.0 /DVersione4=3.0.0.0 /DOrigine=build\nuitka\avvio.dist /DUscita=dist TieMeasureFlow.iss +; Setup silenzioso: TieMeasureFlow-setup-X.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART +; (/CURRENTUSER per installare senza amministratore, in %LOCALAPPDATA%\Programs). +; ATTENZIONE: la licenza NON e' nel setup: va copiata in {commonappdata}\Tielogic\licenze\tiemeasureflow.json +; (la cartella e' scrivibile dagli utenti). La disinstallazione lascia licenze, database (tmf.db), +; uploads e config.env: sono dati del cliente. + +#ifndef Versione + #define Versione "0.0.0" +#endif +#ifndef Versione4 + #define Versione4 "0.0.0.0" +#endif +#ifndef Origine + #define Origine "..\..\build\nuitka\avvio.dist" +#endif +#ifndef Uscita + #define Uscita "..\..\dist" +#endif + +[Setup] +; AppId proprio di TieMeasureFlow (diverso da quello di TieVision Studio) +AppId={{B7D4E2A1-5C93-4F6E-8A0B-3D1F7E9C2A64} +AppName=TieMeasureFlow +AppVersion={#Versione} +AppVerName=TieMeasureFlow {#Versione} +AppPublisher=Tielogic +DefaultDirName={autopf}\Tielogic\TieMeasureFlow +DefaultGroupName=Tielogic\TieMeasureFlow +PrivilegesRequired=admin +; /CURRENTUSER solo da riga di comando: serve alle prove senza amministratore +PrivilegesRequiredOverridesAllowed=commandline +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +OutputDir={#Uscita} +OutputBaseFilename=TieMeasureFlow-setup-{#Versione} +SetupIconFile=TieMeasureFlow.ico +UninstallDisplayIcon={app}\TieMeasureFlow.exe +VersionInfoVersion={#Versione4} +VersionInfoCompany=Tielogic +VersionInfoProductName=TieMeasureFlow +Compression=lzma2/max +SolidCompression=yes +WizardStyle=modern +DisableProgramGroupPage=yes +; il programma in esecuzione tiene aperti i file: Inno chiede di chiuderlo (in silenzioso lo chiude) +CloseApplications=yes + +[Languages] +Name: "italiano"; MessagesFile: "compiler:Languages\Italian.isl" + +[Tasks] +Name: "desktopicon"; Description: "Crea un collegamento sul desktop"; GroupDescription: "Collegamenti:" + +[Dirs] +; scrivibili da tutti gli utenti: l'exe gira con l'utente che lo lancia, non con admin +Name: "{commonappdata}\Tielogic\licenze"; Permissions: users-modify +Name: "{commonappdata}\Tielogic\TieMeasureFlow"; Permissions: users-modify +Name: "{commonappdata}\Tielogic\TieMeasureFlow\uploads"; Permissions: users-modify +Name: "{commonappdata}\Tielogic\TieMeasureFlow\camera"; Permissions: users-modify +Name: "{commonappdata}\Tielogic\TieMeasureFlow\log"; Permissions: users-modify + +[Files] +Source: "{#Origine}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion + +[Icons] +Name: "{group}\TieMeasureFlow"; Filename: "{app}\TieMeasureFlow.exe" +Name: "{group}\Impronta di questo computer"; Filename: "{app}\TieMeasureFlow.exe"; Parameters: "--impronta" +; l'exe non ha finestra: per fermarlo si chiude il processo +Name: "{group}\Arresta TieMeasureFlow"; Filename: "{sys}\taskkill.exe"; Parameters: "/IM TieMeasureFlow.exe /F"; Flags: runminimized +Name: "{group}\Cartella dati e configurazione"; Filename: "{commonappdata}\Tielogic\TieMeasureFlow" +Name: "{group}\Disinstalla TieMeasureFlow"; Filename: "{uninstallexe}" +Name: "{autodesktop}\TieMeasureFlow"; Filename: "{app}\TieMeasureFlow.exe"; Tasks: desktopicon + +[Run] +Filename: "{app}\TieMeasureFlow.exe"; Description: "Avvia TieMeasureFlow"; Flags: nowait postinstall skipifsilent diff --git a/packaging/windows/build.ps1 b/packaging/windows/build.ps1 new file mode 100644 index 0000000..14b3382 --- /dev/null +++ b/packaging/windows/build.ps1 @@ -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-.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))