Make build_and_deploy.ps1 self-installing and fail loudly

pip install of pyinstaller/pystray/pillow was a separate manual step the
README only mentioned in prose, so the script silently died on missing
packages -- especially bad on Explorer double-click, where the window
closes before any error is visible. Consolidate all dependencies into
requirements.txt (also used by README's plain "pip install -r" flow), have
the script install it itself, wrap the whole build in try/catch with
exit-code checks after every native call, and pause on both success and
failure unless -NoPause is passed.

Also move the build output from %LOCALAPPDATA% into dist/ next to the
script, so it's easy to find and matches the already-gitignored dist/ entry.

(cherry picked from commit 13c3745479)
This commit is contained in:
Julian Appel 2026-08-14 23:15:25 +02:00 committed by cjjohn
parent 9e21360393
commit 02d6c0c5e9
3 changed files with 96 additions and 31 deletions

View file

@ -41,15 +41,18 @@ falls gewünscht.
## Voraussetzungen ## Voraussetzungen
- Python 3.11 oder neuer - Python 3.11 oder neuer
- Pakete: `pyserial` (Board-Kommunikation), `pystray` + `pillow` - Alle Pakete stehen in `requirements.txt` (`pyserial` fürs Board,
(Tray-Icon/Desktop-App), optional `mcp` (nur für den MCP-Server) `pystray` + `pillow` fürs Tray-Icon/Desktop-Fenster, `mcp` für den
MCP-Server, `pyinstaller` fürs `.exe`-Bauen):
```bash ```bash
pip install pyserial pystray pillow mcp pip install -r requirements.txt
``` ```
Für den Browser-Modus (`server.py`) reicht die Python-Standardbibliothek — Für den Browser-Modus (`server.py`) reicht die Python-Standardbibliothek —
keine zusätzlichen Pakete nötig. keine zusätzlichen Pakete nötig. `build_and_deploy.ps1` installiert
`requirements.txt` beim Bauen automatisch selbst — ein manuelles
`pip install` vorher ist dafür nicht nötig.
## Starten ## Starten
@ -69,8 +72,15 @@ Maschine/Python-Version unterschiedlich). Selbst bauen:
.\build_and_deploy.ps1 .\build_and_deploy.ps1
``` ```
Das Skript baut mit PyInstaller (`--onedir --windowed`, eigenes Icon) und Das Skript installiert/aktualisiert selbst alle nötigen Pakete aus
kopiert das Ergebnis nach `%LOCALAPPDATA%\VersaPadViewer\VersaPadViewer.exe`. `requirements.txt` (kein manuelles `pip install` vorher nötig), baut dann
mit PyInstaller (`--onedir --windowed`, eigenes Icon) und kopiert das
Ergebnis nach `dist\VersaPadViewer\VersaPadViewer.exe` im Projektordner.
Bricht ein Schritt ab (fehlendes Python, PyInstaller-Fehler, ...), zeigt das
Skript eine klare Fehlermeldung und wartet auf einen Tastendruck, statt sich
bei Doppelklick im Explorer kommentarlos zu schließen (`-NoPause`
unterdrückt das für automatisierte Aufrufe/CI).
**Wichtig:** Sowohl Bauen als auch Ausführen müssen auf einem lokalen **Wichtig:** Sowohl Bauen als auch Ausführen müssen auf einem lokalen
Laufwerk passieren — von einem Netzlaufwerk (SMB-Share) aus scheitert Laufwerk passieren — von einem Netzlaufwerk (SMB-Share) aus scheitert
PyInstaller beim Bauen (Pfadlängen-Problem mit Tcl/Tk-Zeitzonendaten) und PyInstaller beim Bauen (Pfadlängen-Problem mit Tcl/Tk-Zeitzonendaten) und
@ -79,8 +89,6 @@ das Nachladen der Bundle-DLLs von einem Netzwerkpfad, ohne jede
Fehlermeldung). `build_and_deploy.ps1` kopiert den Quellcode deshalb Fehlermeldung). `build_and_deploy.ps1` kopiert den Quellcode deshalb
automatisch zuerst nach `%TEMP%` und baut nur dort. automatisch zuerst nach `%TEMP%` und baut nur dort.
Voraussetzung: `pip install pyinstaller` zusätzlich zu den obigen Paketen.
## Aufbau ## Aufbau
| Datei | Zweck | | Datei | Zweck |

View file

@ -1,5 +1,5 @@
# Baut VersaPadViewer.exe (PyInstaller --onedir) und kopiert das Ergebnis # Baut VersaPadViewer.exe (PyInstaller --onedir) und kopiert das Ergebnis
# nach lokal (C:\Users\<user>\AppData\Local\VersaPadViewer). # nach $projectDir\dist\VersaPadViewer.
# #
# WICHTIG: Sowohl Bauen als auch Laufen muessen lokal passieren, NICHT auf # WICHTIG: Sowohl Bauen als auch Laufen muessen lokal passieren, NICHT auf
# dem Netzlaufwerk (Z:\Git\...): # dem Netzlaufwerk (Z:\Git\...):
@ -12,35 +12,87 @@
# SMB-Share, kombiniert mit dem eh schon langen Projektpfad. # SMB-Share, kombiniert mit dem eh schon langen Projektpfad.
# #
# Deshalb: Quellcode zuerst nach lokal (%TEMP%) kopieren, dort bauen, danach # Deshalb: Quellcode zuerst nach lokal (%TEMP%) kopieren, dort bauen, danach
# das fertige Bundle nach %LOCALAPPDATA% kopieren. Z: wird nur zum Lesen der # das fertige Bundle nach $projectDir\dist kopieren. Z: wird nur zum Lesen
# Quelldateien angefasst. # der Quelldateien angefasst.
#
# Fehlerverhalten: Bei Doppelklick im Explorer schliesst sich das Fenster
# sofort nach Skriptende -- ohne Pause waeren Fehlermeldungen unsichtbar
# ("stirbt ohne jede Fehlermeldung"). Deshalb: alles in try/catch, im
# Fehlerfall UND am Ende eine Pause, ausser bei -NoPause (fuer CI/Automation).
param(
[switch]$NoPause
)
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$projectDir = $PSScriptRoot $projectDir = $PSScriptRoot
$buildSrc = "$env:TEMP\versapad_build_src" $buildSrc = "$env:TEMP\versapad_build_src"
$localDir = "$env:LOCALAPPDATA\VersaPadViewer" $localDir = "$projectDir\dist\VersaPadViewer"
$requirementsFile = "$projectDir\requirements.txt"
if (Test-Path $buildSrc) { function Assert-LastExitCode([string]$step) {
Remove-Item $buildSrc -Recurse -Force if ($LASTEXITCODE -ne 0) {
throw "$step ist fehlgeschlagen (Exit-Code $LASTEXITCODE) -- Ausgabe oben pruefen."
}
}
function Wait-ForKeyIfInteractive {
if ($NoPause) { return }
try {
Read-Host "Taste druecken zum Schliessen"
} catch {
# z.B. nicht-interaktiver Aufruf (stdin nicht verfuegbar) -- einfach ignorieren
}
} }
New-Item -ItemType Directory -Path $buildSrc | Out-Null
Copy-Item "$projectDir\*.py" -Destination $buildSrc
Copy-Item "$projectDir\icon.ico" -Destination $buildSrc
Copy-Item "$projectDir\icon.png" -Destination $buildSrc
Push-Location $buildSrc
try { try {
py -m PyInstaller --windowed --onedir --name VersaPadViewer ` Write-Host "Pruefe Python-Installation..."
--icon icon.ico --add-data "icon.png;." --add-data "icon.ico;." ` py --version
desktop_viewer.py --noconfirm Assert-LastExitCode "'py --version' (ist Python installiert und im PATH?)"
} finally {
Pop-Location
}
if (Test-Path $localDir) { if (-not (Test-Path $requirementsFile)) {
Remove-Item $localDir -Recurse -Force throw "requirements.txt nicht gefunden unter $requirementsFile"
} }
Copy-Item "$buildSrc\dist\VersaPadViewer" -Destination $localDir -Recurse Write-Host "Installiere/aktualisiere benoetigte Pakete aus requirements.txt..."
Remove-Item $buildSrc -Recurse -Force py -m pip install --quiet --disable-pip-version-check -r $requirementsFile
Assert-LastExitCode "Paketinstallation (pip install -r requirements.txt)"
Write-Host "Fertig: $localDir\VersaPadViewer.exe" if (Test-Path $buildSrc) {
Remove-Item $buildSrc -Recurse -Force
}
New-Item -ItemType Directory -Path $buildSrc | Out-Null
Copy-Item "$projectDir\*.py" -Destination $buildSrc
Copy-Item "$projectDir\icon.ico" -Destination $buildSrc
Copy-Item "$projectDir\icon.png" -Destination $buildSrc
Push-Location $buildSrc
try {
Write-Host "Baue mit PyInstaller (--onedir --windowed)..."
py -m PyInstaller --windowed --onedir --name VersaPadViewer `
--icon icon.ico --add-data "icon.png;." --add-data "icon.ico;." `
desktop_viewer.py --noconfirm
Assert-LastExitCode "PyInstaller-Build"
} finally {
Pop-Location
}
if (-not (Test-Path "$buildSrc\dist\VersaPadViewer\VersaPadViewer.exe")) {
throw "PyInstaller hat keine VersaPadViewer.exe erzeugt, obwohl der Exit-Code 0 war -- Build-Ausgabe oben pruefen."
}
if (Test-Path $localDir) {
Remove-Item $localDir -Recurse -Force
}
Copy-Item "$buildSrc\dist\VersaPadViewer" -Destination $localDir -Recurse
Remove-Item $buildSrc -Recurse -Force
Write-Host ""
Write-Host "Fertig: $localDir\VersaPadViewer.exe" -ForegroundColor Green
Wait-ForKeyIfInteractive
}
catch {
Write-Host ""
Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red
Wait-ForKeyIfInteractive
exit 1
}

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
pyserial
pystray
pillow
mcp
pyinstaller