Some checks failed
Lint & Test / test (push) Failing after 30s
release.yml: add fallback for existing releases on tag re-push. installer.nsi: add .onInit file lock check, use LaunchApp function instead of RUN_PARAMETERS to fix NSIS quoting bug. build-dist.ps1: copy start-hidden.vbs to dist scripts/. start-hidden.vbs: embedded Python fallback for installed/dev envs. Update ci-cd.md with version detection, NSIS best practices, local build testing, Gitea vs GitHub differences, troubleshooting. Update frontend.md with full entity type checklist and common pitfalls.
250 lines
9.9 KiB
PowerShell
250 lines
9.9 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Build a portable Windows distribution of LedGrab.
|
|
|
|
.DESCRIPTION
|
|
Downloads embedded Python, installs all dependencies, copies app code,
|
|
builds the frontend bundle, and produces a self-contained ZIP.
|
|
|
|
.PARAMETER Version
|
|
Version string (e.g. "0.1.0" or "v0.1.0"). Auto-detected from git tag
|
|
or __init__.py if omitted.
|
|
|
|
.PARAMETER PythonVersion
|
|
Embedded Python version to download. Default: 3.11.9
|
|
|
|
.PARAMETER SkipFrontend
|
|
Skip npm ci + npm run build (use if frontend is already built).
|
|
|
|
.PARAMETER SkipPerf
|
|
Skip installing optional [perf] extras (dxcam, bettercam, windows-capture).
|
|
|
|
.EXAMPLE
|
|
.\build-dist.ps1
|
|
.\build-dist.ps1 -Version "0.2.0"
|
|
.\build-dist.ps1 -SkipFrontend -SkipPerf
|
|
#>
|
|
param(
|
|
[string]$Version = "",
|
|
[string]$PythonVersion = "3.11.9",
|
|
[switch]$SkipFrontend,
|
|
[switch]$SkipPerf
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$ProgressPreference = 'SilentlyContinue' # faster downloads
|
|
|
|
$ScriptRoot = $PSScriptRoot
|
|
$BuildDir = Join-Path $ScriptRoot "build"
|
|
$DistName = "LedGrab"
|
|
$DistDir = Join-Path $BuildDir $DistName
|
|
$ServerDir = Join-Path $ScriptRoot "server"
|
|
$PythonDir = Join-Path $DistDir "python"
|
|
$AppDir = Join-Path $DistDir "app"
|
|
|
|
# ── Version detection ──────────────────────────────────────────
|
|
|
|
if (-not $Version) {
|
|
# Try git tag
|
|
try {
|
|
$gitTag = git describe --tags --exact-match 2>$null
|
|
if ($gitTag) { $Version = $gitTag }
|
|
} catch {}
|
|
}
|
|
if (-not $Version) {
|
|
# Try env var (CI)
|
|
if ($env:GITEA_REF_NAME) { $Version = $env:GITEA_REF_NAME }
|
|
elseif ($env:GITHUB_REF_NAME) { $Version = $env:GITHUB_REF_NAME }
|
|
}
|
|
if (-not $Version) {
|
|
# Parse from __init__.py
|
|
$initFile = Join-Path $ServerDir "src\wled_controller\__init__.py"
|
|
$match = Select-String -Path $initFile -Pattern '__version__\s*=\s*"([^"]+)"'
|
|
if ($match) { $Version = $match.Matches[0].Groups[1].Value }
|
|
}
|
|
if (-not $Version) { $Version = "0.0.0" }
|
|
|
|
# Strip leading 'v' for filenames
|
|
$VersionClean = $Version -replace '^v', ''
|
|
$ZipName = "LedGrab-v${VersionClean}-win-x64.zip"
|
|
|
|
Write-Host "=== Building LedGrab v${VersionClean} ===" -ForegroundColor Cyan
|
|
Write-Host " Python: $PythonVersion"
|
|
Write-Host " Output: build\$ZipName"
|
|
Write-Host ""
|
|
|
|
# ── Clean ──────────────────────────────────────────────────────
|
|
|
|
if (Test-Path $DistDir) {
|
|
Write-Host "[1/8] Cleaning previous build..."
|
|
Remove-Item -Recurse -Force $DistDir
|
|
}
|
|
New-Item -ItemType Directory -Path $DistDir -Force | Out-Null
|
|
|
|
# ── Download embedded Python ───────────────────────────────────
|
|
|
|
$PythonZipUrl = "https://www.python.org/ftp/python/${PythonVersion}/python-${PythonVersion}-embed-amd64.zip"
|
|
$PythonZipPath = Join-Path $BuildDir "python-embed.zip"
|
|
|
|
Write-Host "[2/8] Downloading embedded Python ${PythonVersion}..."
|
|
if (-not (Test-Path $PythonZipPath)) {
|
|
Invoke-WebRequest -Uri $PythonZipUrl -OutFile $PythonZipPath
|
|
}
|
|
Write-Host " Extracting to python/..."
|
|
Expand-Archive -Path $PythonZipPath -DestinationPath $PythonDir -Force
|
|
|
|
# ── Patch ._pth to enable site-packages ────────────────────────
|
|
|
|
Write-Host "[3/8] Patching Python path configuration..."
|
|
$pthFile = Get-ChildItem -Path $PythonDir -Filter "python*._pth" | Select-Object -First 1
|
|
if (-not $pthFile) { throw "Could not find python*._pth in $PythonDir" }
|
|
|
|
$pthContent = Get-Content $pthFile.FullName -Raw
|
|
# Uncomment 'import site'
|
|
$pthContent = $pthContent -replace '#\s*import site', 'import site'
|
|
# Add Lib\site-packages if not present
|
|
if ($pthContent -notmatch 'Lib\\site-packages') {
|
|
$pthContent = $pthContent.TrimEnd() + "`nLib\site-packages`n"
|
|
}
|
|
# Embedded Python ._pth overrides PYTHONPATH, so add the app source path
|
|
# directly for wled_controller to be importable
|
|
if ($pthContent -notmatch '\.\.[/\\]app[/\\]src') {
|
|
$pthContent = $pthContent.TrimEnd() + "`n..\app\src`n"
|
|
}
|
|
Set-Content -Path $pthFile.FullName -Value $pthContent -NoNewline
|
|
Write-Host " Patched $($pthFile.Name)"
|
|
|
|
# ── Install pip ────────────────────────────────────────────────
|
|
|
|
Write-Host "[4/8] Installing pip..."
|
|
$GetPipPath = Join-Path $BuildDir "get-pip.py"
|
|
if (-not (Test-Path $GetPipPath)) {
|
|
Invoke-WebRequest -Uri "https://bootstrap.pypa.io/get-pip.py" -OutFile $GetPipPath
|
|
}
|
|
$python = Join-Path $PythonDir "python.exe"
|
|
$ErrorActionPreference = 'Continue'
|
|
& $python $GetPipPath --no-warn-script-location 2>&1 | Out-Null
|
|
$ErrorActionPreference = 'Stop'
|
|
if ($LASTEXITCODE -ne 0) { throw "Failed to install pip" }
|
|
|
|
# ── Install dependencies ──────────────────────────────────────
|
|
|
|
Write-Host "[5/8] Installing dependencies..."
|
|
$extras = "camera,notifications,tray"
|
|
if (-not $SkipPerf) { $extras += ",perf" }
|
|
|
|
# Install the project (pulls all deps via pyproject.toml), then remove
|
|
# the installed package itself — PYTHONPATH handles app code loading.
|
|
$ErrorActionPreference = 'Continue'
|
|
& $python -m pip install --no-warn-script-location "${ServerDir}[${extras}]" 2>&1 | ForEach-Object {
|
|
if ($_ -match 'ERROR|Failed') { Write-Host " $_" -ForegroundColor Red }
|
|
}
|
|
$ErrorActionPreference = 'Stop'
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Host " Some optional deps may have failed (continuing)..." -ForegroundColor Yellow
|
|
}
|
|
|
|
# Remove the installed wled_controller package to avoid duplication
|
|
$sitePackages = Join-Path $PythonDir "Lib\site-packages"
|
|
Get-ChildItem -Path $sitePackages -Filter "wled*" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
|
Get-ChildItem -Path $sitePackages -Filter "wled*.dist-info" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
# Clean up caches and test files to reduce size
|
|
Write-Host " Cleaning up caches..."
|
|
Get-ChildItem -Path $sitePackages -Recurse -Directory -Filter "__pycache__" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
|
Get-ChildItem -Path $sitePackages -Recurse -Directory -Filter "tests" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
|
Get-ChildItem -Path $sitePackages -Recurse -Directory -Filter "test" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
# ── Build frontend ─────────────────────────────────────────────
|
|
|
|
if (-not $SkipFrontend) {
|
|
Write-Host "[6/8] Building frontend bundle..."
|
|
Push-Location $ServerDir
|
|
try {
|
|
$ErrorActionPreference = 'Continue'
|
|
& npm ci --loglevel error 2>&1 | Out-Null
|
|
& npm run build 2>&1 | ForEach-Object {
|
|
$line = "$_"
|
|
if ($line -and $line -notmatch 'RemoteException') { Write-Host " $line" }
|
|
}
|
|
$ErrorActionPreference = 'Stop'
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
} else {
|
|
Write-Host "[6/8] Skipping frontend build (--SkipFrontend)"
|
|
}
|
|
|
|
# ── Copy application files ─────────────────────────────────────
|
|
|
|
Write-Host "[7/8] Copying application files..."
|
|
New-Item -ItemType Directory -Path $AppDir -Force | Out-Null
|
|
|
|
# Copy source code (includes static/dist bundle, templates, locales)
|
|
$srcDest = Join-Path $AppDir "src"
|
|
Copy-Item -Path (Join-Path $ServerDir "src") -Destination $srcDest -Recurse
|
|
|
|
# Copy config
|
|
$configDest = Join-Path $AppDir "config"
|
|
Copy-Item -Path (Join-Path $ServerDir "config") -Destination $configDest -Recurse
|
|
|
|
# Create empty data/ and logs/ directories
|
|
New-Item -ItemType Directory -Path (Join-Path $DistDir "data") -Force | Out-Null
|
|
New-Item -ItemType Directory -Path (Join-Path $DistDir "logs") -Force | Out-Null
|
|
|
|
# Clean up source maps and __pycache__ from app code
|
|
Get-ChildItem -Path $srcDest -Recurse -Filter "*.map" | Remove-Item -Force -ErrorAction SilentlyContinue
|
|
Get-ChildItem -Path $srcDest -Recurse -Directory -Filter "__pycache__" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
# ── Create launcher ────────────────────────────────────────────
|
|
|
|
Write-Host "[8/8] Creating launcher..."
|
|
|
|
$launcherContent = @'
|
|
@echo off
|
|
cd /d "%~dp0"
|
|
|
|
:: Set paths
|
|
set PYTHONPATH=%~dp0app\src
|
|
set WLED_CONFIG_PATH=%~dp0app\config\default_config.yaml
|
|
|
|
:: Create data directory if missing
|
|
if not exist "%~dp0data" mkdir "%~dp0data"
|
|
if not exist "%~dp0logs" mkdir "%~dp0logs"
|
|
|
|
:: Start the server (tray icon handles UI and exit)
|
|
"%~dp0python\pythonw.exe" -m wled_controller
|
|
'@
|
|
|
|
$launcherContent = $launcherContent -replace '%VERSION%', $VersionClean
|
|
$launcherPath = Join-Path $DistDir "LedGrab.bat"
|
|
Set-Content -Path $launcherPath -Value $launcherContent -Encoding ASCII
|
|
|
|
# Copy hidden launcher VBS
|
|
$scriptsDir = Join-Path $DistDir "scripts"
|
|
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
|
|
Copy-Item -Path (Join-Path $ServerDir "scripts\start-hidden.vbs") -Destination $scriptsDir
|
|
|
|
# ── Create ZIP ─────────────────────────────────────────────────
|
|
|
|
$ZipPath = Join-Path $BuildDir $ZipName
|
|
if (Test-Path $ZipPath) { Remove-Item -Force $ZipPath }
|
|
|
|
Write-Host ""
|
|
Write-Host "Creating $ZipName..." -ForegroundColor Cyan
|
|
|
|
# Use 7-Zip if available (faster, handles locked files), else fall back to Compress-Archive
|
|
$7z = Get-Command 7z -ErrorAction SilentlyContinue
|
|
if ($7z) {
|
|
& 7z a -tzip -mx=7 $ZipPath "$DistDir\*" | Select-Object -Last 3
|
|
} else {
|
|
Compress-Archive -Path "$DistDir\*" -DestinationPath $ZipPath -CompressionLevel Optimal
|
|
}
|
|
|
|
$zipSize = (Get-Item $ZipPath).Length / 1MB
|
|
Write-Host ""
|
|
Write-Host "=== Build complete ===" -ForegroundColor Green
|
|
Write-Host " Archive: $ZipPath"
|
|
Write-Host " Size: $([math]::Round($zipSize, 1)) MB"
|
|
Write-Host ""
|