- build a lean onefile Launcher.exe from src/launcher.py (stdlib-only, no PySide6/Pillow) alongside the onedir app build - release dir is now the portable layout: Launcher.exe + app\ (CMBot.exe + config + version.txt) - emit two zips: CMBot-<ver>.zip (self-update payload = app\ contents, what manifest.url points to) and CMBot-<ver>-portable.zip (initial install) - launcher: harden logging setup for --windowed builds / read-only roots Actual PyInstaller build must run on Windows; script is syntax-verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
211 lines
7.3 KiB
PowerShell
211 lines
7.3 KiB
PowerShell
param(
|
|
[switch]$SkipBuild,
|
|
[string]$ManifestBaseUrl = "",
|
|
[string]$PublishDir = "",
|
|
[string]$ReleaseNotes = "",
|
|
[string]$MinSupported = "",
|
|
[switch]$Mandatory
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$RootDir = Split-Path -Parent $ScriptDir
|
|
$SrcDir = Join-Path $RootDir "src"
|
|
$VersionFile = Join-Path $SrcDir "version.py"
|
|
$DefaultConfigDir = Join-Path $RootDir "packaging\default_config"
|
|
$BuildDir = Join-Path $RootDir "build"
|
|
$DistDir = Join-Path $RootDir "dist"
|
|
$ReleaseRoot = Join-Path $RootDir "release"
|
|
$AppExeName = "CMBot"
|
|
|
|
function Read-VersionValue {
|
|
param(
|
|
[string]$Path,
|
|
[string]$Name
|
|
)
|
|
|
|
$pattern = "^\s*$Name\s*=\s*`"([^`"]+)`""
|
|
foreach ($line in Get-Content -LiteralPath $Path -Encoding UTF8) {
|
|
if ($line -match $pattern) {
|
|
return $Matches[1]
|
|
}
|
|
}
|
|
throw "Cannot find $Name in $Path"
|
|
}
|
|
|
|
function Assert-InProject {
|
|
param([string]$Path)
|
|
|
|
$resolvedRoot = (Resolve-Path -LiteralPath $RootDir).Path
|
|
if (Test-Path -LiteralPath $Path) {
|
|
$resolvedPath = (Resolve-Path -LiteralPath $Path).Path
|
|
if (-not $resolvedPath.StartsWith($resolvedRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Refusing to modify path outside project: $resolvedPath"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Join-Url {
|
|
param(
|
|
[string]$BaseUrl,
|
|
[string]$Name
|
|
)
|
|
|
|
if (-not $BaseUrl) {
|
|
return $Name
|
|
}
|
|
if (-not $BaseUrl.EndsWith("/")) {
|
|
$BaseUrl = "$BaseUrl/"
|
|
}
|
|
return (New-Object -TypeName System.Uri -ArgumentList ([System.Uri]$BaseUrl), $Name).AbsoluteUri
|
|
}
|
|
|
|
$LauncherName = "Launcher"
|
|
$AppName = Read-VersionValue -Path $VersionFile -Name "APP_NAME"
|
|
$AppVersion = Read-VersionValue -Path $VersionFile -Name "APP_VERSION"
|
|
$ReleaseName = "$AppExeName-$AppVersion"
|
|
$ReleaseDir = Join-Path $ReleaseRoot $ReleaseName # portable layout root
|
|
$AppDir = Join-Path $ReleaseDir "app" # becomes app\ on the client
|
|
$UpdateZip = Join-Path $ReleaseRoot "$ReleaseName.zip" # self-update payload (app\ contents)
|
|
$PortableZip = Join-Path $ReleaseRoot "$ReleaseName-portable.zip" # initial install
|
|
$ManifestPath = Join-Path $ReleaseRoot "manifest.json"
|
|
$AppPyiOut = Join-Path $DistDir $AppExeName # dist\CMBot (onedir)
|
|
$LauncherPyiOut = Join-Path $DistDir "$LauncherName.exe" # dist\Launcher.exe (onefile)
|
|
$AppExePath = Join-Path $AppDir "$AppExeName.exe"
|
|
$LauncherExePath = Join-Path $ReleaseDir "$LauncherName.exe"
|
|
|
|
Write-Host "Building $AppName $AppVersion"
|
|
|
|
$pythonVersionText = & python --version
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Python is not available."
|
|
}
|
|
if ($pythonVersionText -notmatch "Python 3\.7\.") {
|
|
throw "Python 3.7 is required, current version: $pythonVersionText"
|
|
}
|
|
|
|
& python -c "import PyInstaller"
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "PyInstaller is not installed. Run: pip install -r requirements.txt"
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $DefaultConfigDir)) {
|
|
throw "Default config directory not found: $DefaultConfigDir"
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $ReleaseRoot | Out-Null
|
|
|
|
foreach ($path in @($BuildDir, $AppPyiOut, $LauncherPyiOut, $ReleaseDir, $UpdateZip, $PortableZip, $ManifestPath)) {
|
|
Assert-InProject -Path $path
|
|
if (Test-Path -LiteralPath $path) {
|
|
Remove-Item -LiteralPath $path -Recurse -Force
|
|
}
|
|
}
|
|
|
|
if (-not $SkipBuild) {
|
|
# Main app (onedir, windowed GUI)
|
|
& python -m PyInstaller `
|
|
--noconfirm `
|
|
--clean `
|
|
--onedir `
|
|
--windowed `
|
|
--name $AppExeName `
|
|
--paths $SrcDir `
|
|
--add-data "$SrcDir\resources;resources" `
|
|
"$SrcDir\main.py"
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "PyInstaller (app) build failed."
|
|
}
|
|
|
|
# Update launcher (onefile; console shows download/swap progress).
|
|
# Lean: launcher only imports services/* (stdlib), no PySide6/Pillow.
|
|
& python -m PyInstaller `
|
|
--noconfirm `
|
|
--clean `
|
|
--onefile `
|
|
--console `
|
|
--name $LauncherName `
|
|
--paths $SrcDir `
|
|
"$SrcDir\launcher.py"
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "PyInstaller (launcher) build failed."
|
|
}
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $AppPyiOut)) {
|
|
throw "PyInstaller app output not found: $AppPyiOut"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $LauncherPyiOut)) {
|
|
throw "PyInstaller launcher output not found: $LauncherPyiOut"
|
|
}
|
|
|
|
# Assemble the portable layout: <ReleaseDir>\Launcher.exe + <ReleaseDir>\app\
|
|
New-Item -ItemType Directory -Force -Path $ReleaseDir | Out-Null
|
|
Copy-Item -LiteralPath $AppPyiOut -Destination $AppDir -Recurse
|
|
Copy-Item -LiteralPath $DefaultConfigDir -Destination (Join-Path $AppDir "config") -Recurse
|
|
Set-Content -LiteralPath (Join-Path $AppDir "version.txt") -Value $AppVersion -Encoding ascii -NoNewline
|
|
Copy-Item -LiteralPath $LauncherPyiOut -Destination $LauncherExePath -Force
|
|
|
|
$ReadmePath = Join-Path $ReleaseDir "README.txt"
|
|
$Readme = @(
|
|
"$AppName $AppVersion",
|
|
"",
|
|
"Start:",
|
|
"Double-click Launcher.exe.",
|
|
"",
|
|
"Notes:",
|
|
"- Extract this folder to any writable location (e.g. D:\CMBot or the Desktop).",
|
|
"- Do NOT put it under C:\Program Files or the C: drive root (not writable).",
|
|
"- Your settings, templates and exports live in %USERPROFILE%\.cmbot.",
|
|
"- The app itself is in app\; Launcher.exe checks for updates on start."
|
|
)
|
|
Set-Content -LiteralPath $ReadmePath -Value $Readme -Encoding UTF8
|
|
|
|
if (-not (Test-Path -LiteralPath $AppExePath)) {
|
|
throw "Executable not found: $AppExePath"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $LauncherExePath)) {
|
|
throw "Launcher not found: $LauncherExePath"
|
|
}
|
|
|
|
# Self-update payload: contents of app\ (CMBot.exe at the zip root). manifest.url
|
|
# points here; the launcher downloads, verifies and swaps it into app\.
|
|
Compress-Archive -Path (Join-Path $AppDir "*") -DestinationPath $UpdateZip -Force
|
|
# Initial install: the full portable layout (Launcher.exe + app\).
|
|
Compress-Archive -Path (Join-Path $ReleaseDir "*") -DestinationPath $PortableZip -Force
|
|
$ReleaseHash = (Get-FileHash -LiteralPath $UpdateZip -Algorithm SHA256).Hash
|
|
$ReleaseSize = (Get-Item -LiteralPath $UpdateZip).Length
|
|
if (-not $MinSupported) {
|
|
$MinSupported = $AppVersion
|
|
}
|
|
$Manifest = [ordered]@{
|
|
version = $AppVersion
|
|
url = Join-Url -BaseUrl $ManifestBaseUrl -Name (Split-Path -Leaf $UpdateZip)
|
|
sha256 = $ReleaseHash
|
|
size = $ReleaseSize
|
|
mandatory = [bool]$Mandatory
|
|
min_supported = $MinSupported
|
|
notes = $ReleaseNotes
|
|
}
|
|
# Write UTF-8 WITHOUT BOM: Set-Content -Encoding UTF8 emits a BOM that breaks
|
|
# json.loads in the in-app update check (services/update_service.py).
|
|
[System.IO.File]::WriteAllText(
|
|
$ManifestPath,
|
|
($Manifest | ConvertTo-Json),
|
|
(New-Object System.Text.UTF8Encoding $false)
|
|
)
|
|
|
|
if ($PublishDir) {
|
|
# Update server needs only the payload zip + manifest.
|
|
New-Item -ItemType Directory -Force -Path $PublishDir | Out-Null
|
|
Copy-Item -LiteralPath $UpdateZip -Destination $PublishDir -Force
|
|
Copy-Item -LiteralPath $ManifestPath -Destination $PublishDir -Force
|
|
Write-Host "Published update files to: $PublishDir"
|
|
}
|
|
|
|
Write-Host "Portable layout: $ReleaseDir (Launcher.exe + app\)"
|
|
Write-Host "Initial install zip: $PortableZip"
|
|
Write-Host "Update payload zip: $UpdateZip"
|
|
Write-Host "Manifest: $ManifestPath"
|