ci: add CCMA release builds

This commit is contained in:
Marcel Peterkau
2026-06-23 20:19:53 +02:00
parent 302170230a
commit 0e3087a780
16 changed files with 842 additions and 24 deletions
+16
View File
@@ -0,0 +1,16 @@
@echo off
setlocal
set SCRIPT_DIR=%~dp0
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build.ps1"
if errorlevel 1 (
echo.
echo Build failed.
pause
exit /b 1
)
echo.
echo Build finished.
pause
+98
View File
@@ -0,0 +1,98 @@
[CmdletBinding()]
param(
[switch]$NoClean
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Resolve-Path (Join-Path $scriptDir '..')
Set-Location $repoRoot
function Get-VersionString {
$versionFile = Join-Path $repoRoot 'VERSION'
if (!(Test-Path $versionFile)) { return 'unknown' }
return (Get-Content $versionFile -Raw).Trim()
}
function Sanitize-FilePart([string]$text) {
if ([string]::IsNullOrWhiteSpace($text)) { return 'unknown' }
$s = $text.Trim()
foreach ($ch in @('\','/','?',':','*','"','<','>','|')) {
$s = $s.Replace($ch, '_')
}
$s = $s -replace '\s+', '_'
return $s
}
function Get-PlatformToken {
return "windows"
}
function Get-ArchToken {
switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()) {
"x64" { return "amd64" }
"arm64" { return "arm64" }
default { return ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()) }
}
}
$python = Join-Path $repoRoot '.venv\Scripts\python.exe'
if (!(Test-Path $python)) {
Write-Host "No .venv found. Creating venv..." -ForegroundColor Yellow
python -m venv .venv
if (!(Test-Path $python)) {
throw "Failed to create venv; expected at $python"
}
}
Write-Host "Using Python: $python" -ForegroundColor Cyan
& $python -m pip install -r requirements.txt
& $python -m pip install pyinstaller pillow
$pngIcon = Join-Path $repoRoot 'src\ccma\assets\icons\ccma.png'
$icoOut = Join-Path $repoRoot 'build\app.ico'
if (Test-Path $pngIcon) {
Write-Host "Generating ICO: $icoOut" -ForegroundColor Cyan
& $python -c @"
from pathlib import Path
from PIL import Image
png = Path(r'$pngIcon')
ico = Path(r'$icoOut')
ico.parent.mkdir(parents=True, exist_ok=True)
img = Image.open(png).convert('RGBA')
img.save(ico, format='ICO', sizes=[(16,16),(24,24),(32,32),(48,48),(64,64),(128,128),(256,256)])
print(str(ico))
"@
} else {
Write-Host "PNG icon not found at: $pngIcon (building without icon)" -ForegroundColor Yellow
}
if (-not $NoClean) {
if (Test-Path (Join-Path $repoRoot 'dist')) { Remove-Item -Recurse -Force (Join-Path $repoRoot 'dist') }
if (Test-Path (Join-Path $repoRoot 'build\pyinstaller')) { Remove-Item -Recurse -Force (Join-Path $repoRoot 'build\pyinstaller') }
}
Write-Host "Building with PyInstaller spec..." -ForegroundColor Cyan
& $python -m PyInstaller --noconfirm --clean --distpath dist --workpath build\pyinstaller build\ccma.spec
$exe = Join-Path $repoRoot 'dist\ccma.exe'
if (!(Test-Path $exe)) {
throw "Build finished but exe not found at: $exe"
}
$version = Get-VersionString
$versionSafe = Sanitize-FilePart $version
$platformToken = Get-PlatformToken
$archToken = Get-ArchToken
$versionedExe = Join-Path $repoRoot ("dist\ccma-$versionSafe-$platformToken-$archToken.exe")
Copy-Item -Force $exe $versionedExe
Write-Host ""
Write-Host "Build OK" -ForegroundColor Green
Write-Host "Base EXE: $exe" -ForegroundColor Green
Write-Host "Versioned EXE: $versionedExe" -ForegroundColor Green
Write-Host "Version: $version" -ForegroundColor Green
+100
View File
@@ -0,0 +1,100 @@
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec for CCMA.
Packaged output:
- Windows: dist/ccma.exe
- Linux: dist/ccma
Icon:
- On Windows the spec auto-generates build/app.ico from the PNG icon if it does
not already exist, so direct PyInstaller and scripted builds behave the same.
"""
import sys
from pathlib import Path
from PyInstaller.utils.hooks import collect_data_files
block_cipher = None
spec_dir = Path(globals().get("SPECPATH", Path.cwd())).resolve()
project_root = spec_dir.parent
entry_script = project_root / "main.py"
package_src = project_root / "src" / "ccma"
assets_src = package_src / "assets"
app_name_slug = "ccma"
win_icon_path = project_root / "build" / "app.ico"
png_icon_path = assets_src / "icons" / "ccma.png"
if sys.platform == "win32" and not win_icon_path.exists() and png_icon_path.exists():
try:
from PIL import Image
img = Image.open(png_icon_path).convert("RGBA")
win_icon_path.parent.mkdir(parents=True, exist_ok=True)
img.save(
win_icon_path,
format="ICO",
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
)
print(f"[spec] Generated ICO: {win_icon_path}")
except Exception as exc:
print(f"[spec] WARNING: ICO generation failed: {exc}")
win_icon = str(win_icon_path) if win_icon_path.exists() else None
datas: list[tuple[str, str]] = []
if assets_src.exists():
datas.append((str(assets_src), "ccma/assets"))
version_file = project_root / "VERSION"
if version_file.exists():
datas.append((str(version_file), "."))
datas.append((str(version_file), "ccma"))
for optional_pkg in ("ttkbootstrap_icons", "ttkbootstrap_icons_mat"):
try:
datas += collect_data_files(optional_pkg)
except Exception:
pass
hiddenimports = ["ttkbootstrap_icons", "ttkbootstrap_icons_mat"]
a = Analysis(
[str(entry_script)],
pathex=[str(project_root), str(project_root / "src")],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name=app_name_slug,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=win_icon,
)