acdream/tools/publish-bin.ps1
Erik f260260caf fix(launcher-feed): strip debug symbols from distribution payloads (103 MB -> 77 MB)
A stock publish shipped native debug symbols to players: libSkiaSharp.pdb
(80 MB) and libHarfBuzzSharp.pdb (20 MB) from Avalonia's rendering packages
were 100 MB of a 278 MB launcher payload. MSBuild's DebugType switches only
govern our own managed symbols, not native .pdb files arriving as package
runtime assets, so the payload build drops every .pdb before zipping.

launcher-win-x64.zip 103.4 -> 77.4 MB, client 44.5 -> 43.6 MB. The launcher
payload now also fits under GitHub's 100 MB per-file limit, though the feed
stays on the Gitea-only dist branch to keep main's history clean.

Also fixes a StrictMode crash in the lock-file warning: an empty git status
result is null, not an empty array, so .Count threw at the end of a
successful publish.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 19:56:20 +02:00

246 lines
9.8 KiB
PowerShell

<#
.SYNOPSIS
Publishes the alpha distribution feed into the repo's /bin folder.
.DESCRIPTION
Builds self-contained client and launcher payloads, zips them, and writes
bin/manifest.json pointing at this repository's PUBLIC Gitea raw URLs.
Commit and push /bin afterwards and the launcher's "Check for updates"
finds the new build — no release server, no tokens, no CDN.
Payload contents (what the launcher expects to find at each zip root):
client-<rid>.zip AcDream.App[.exe] + acdream-headless[.exe]
launcher-<rid>.zip acdream-launcher[.exe] + acdream-bake[.exe]
(the launcher csproj co-deploys the bake CLI)
The launcher's own published version is stamped to the manifest version so
a freshly published launcher does NOT report itself as out of date.
.PARAMETER Version
SemVer 2.0 release version. Defaults to a monotonic UTC build stamp,
e.g. 0.1.0-build.202608181552. Must sort ABOVE the previous published
version or the launcher will not offer it as an update.
.PARAMETER IncludeLinux
Also publish linux-x64 payloads. Off by default: Linux graphical is parked
at Slice L1, and each RID roughly doubles build time and feed size.
.EXAMPLE
pwsh -NoProfile -File tools/publish-bin.ps1
git add bin
git commit -m "release: alpha build"
git push origin main
#>
[CmdletBinding()]
param(
[string]$Version,
[switch]$IncludeLinux,
[string]$MinimumLauncherVersion = '0.0.1'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ($PSVersionTable.PSVersion.Major -lt 7) {
throw 'publish-bin requires PowerShell 7 or newer.'
}
$RepoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if (-not (Test-Path (Join-Path $RepoRoot 'AcDream.slnx'))) {
throw "Could not locate AcDream.slnx above '$PSScriptRoot'."
}
if ([string]::IsNullOrWhiteSpace($Version)) {
$Version = '0.1.0-build.{0}' -f ([DateTime]::UtcNow.ToString('yyyyMMddHHmm'))
}
$semver = '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$'
foreach ($candidate in @($Version, $MinimumLauncherVersion)) {
if ($candidate -notmatch $semver) {
throw "Version '$candidate' is not SemVer 2.0 (build metadata '+' is not allowed here)."
}
}
# Raw-file base for the PUBLIC Gitea repo's dist branch. Must agree with
# ReleaseManifestClient.ProductionManifestUri.
$RawBase = 'https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin'
$BinRoot = Join-Path $RepoRoot 'bin'
$Staging = Join-Path $BinRoot 'payload'
$Rids = @('win-x64')
if ($IncludeLinux) { $Rids += 'linux-x64' }
Write-Host "acdream alpha feed" -ForegroundColor Cyan
Write-Host " version : $Version"
Write-Host " rids : $($Rids -join ', ')"
Write-Host " output : $BinRoot"
Write-Host ''
if (Test-Path $Staging) { Remove-Item -LiteralPath $Staging -Recurse -Force }
$null = New-Item -ItemType Directory -Path $Staging -Force
function Invoke-Publish {
param(
[Parameter(Mandatory)][string]$Project,
[Parameter(Mandatory)][string]$Rid,
[Parameter(Mandatory)][string]$OutputDirectory,
[switch]$SingleFile
)
$arguments = @(
'publish', (Join-Path $RepoRoot $Project),
'-c', 'Release',
'-r', $Rid,
'--self-contained', 'true',
# InformationalVersion ONLY — never -p:Version. The launcher reads
# AssemblyInformationalVersion (App.GetLauncherVersion), while Version
# also rewrites project-reference versions inside the committed
# packages.<rid>.lock.json files, so stamping it churned every lock
# file with a throwaway build stamp (observed 2026-08-18).
"-p:InformationalVersion=$Version",
# No SourceLink '+<sha>' suffix: LauncherVersion parses this as SemVer.
'-p:IncludeSourceRevisionInInformationalVersion=false',
'-o', $OutputDirectory,
'--nologo'
)
if ($SingleFile) { $arguments += '-p:PublishSingleFile=true' }
& dotnet @arguments | Out-Null
if ($LASTEXITCODE) { throw "publish failed: $Project ($Rid)" }
}
function Remove-DebugSymbols {
param([Parameter(Mandatory)][string]$Directory)
# Players never load these, and the native ones are enormous: a stock
# Avalonia publish ships libSkiaSharp.pdb (80 MB) and
# libHarfBuzzSharp.pdb (20 MB), which alone were 100 MB of a 278 MB
# launcher payload (measured 2026-08-18). MSBuild's DebugType switches
# only govern OUR managed symbols, not the native .pdb files that arrive
# as package runtime assets, so drop them from the payload directly.
$symbols = @(Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter *.pdb)
if ($symbols.Count -eq 0) { return }
$freed = ($symbols | Measure-Object -Property Length -Sum).Sum
$symbols | Remove-Item -Force
' stripped {0} debug symbol file(s), {1:N1} MB' -f $symbols.Count, ($freed / 1MB) |
Write-Host -ForegroundColor DarkGray
}
function New-PayloadZip {
param(
[Parameter(Mandatory)][string]$SourceDirectory,
[Parameter(Mandatory)][string]$ZipPath,
[Parameter(Mandatory)][string[]]$RequiredFiles
)
Remove-DebugSymbols $SourceDirectory
foreach ($required in $RequiredFiles) {
if (-not (Test-Path -LiteralPath (Join-Path $SourceDirectory $required))) {
throw "Payload '$SourceDirectory' is missing required file '$required'."
}
}
if (Test-Path -LiteralPath $ZipPath) { Remove-Item -LiteralPath $ZipPath -Force }
# includeBaseDirectory:$false -> entries sit at the zip ROOT, which is where
# LauncherExecutableSet resolves the hosts after extraction.
[IO.Compression.ZipFile]::CreateFromDirectory(
$SourceDirectory,
$ZipPath,
[IO.Compression.CompressionLevel]::Optimal,
$false)
}
function Get-Artifact {
param(
[Parameter(Mandatory)][string]$ZipPath,
[Parameter(Mandatory)][string]$Url
)
$item = Get-Item -LiteralPath $ZipPath
return [ordered]@{
url = $Url
sha256 = (Get-FileHash -LiteralPath $ZipPath -Algorithm SHA256).Hash.ToLowerInvariant()
size = $item.Length
}
}
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
$clients = [ordered]@{}
$launchers = [ordered]@{}
foreach ($rid in $Rids) {
$suffix = if ($rid -like 'win-*') { '.exe' } else { '' }
$clientDirectory = Join-Path $Staging "client-$rid"
$launcherDirectory = Join-Path $Staging "launcher-$rid"
Write-Host "[$rid] publishing client (App + Headless)..." -ForegroundColor Yellow
Invoke-Publish 'src/AcDream.App/AcDream.App.csproj' $rid $clientDirectory
Invoke-Publish 'src/AcDream.Headless/AcDream.Headless.csproj' $rid $clientDirectory
# PublishSingleFile/SelfContained come from the launcher csproj itself, and
# its PublishCoDeployedBakeTool target adds acdream-bake as a SECOND
# self-contained single file in the same directory. Two independent .NET
# runtimes is deliberate (see that csproj's comments: a framework-dependent
# bake would scatter AcDream.Content assemblies into the launcher's output),
# so this payload is ~103 MB and CANNOT be pushed to GitHub, whose hard
# per-file limit is 100 MB. The alpha feed is Gitea-only by design.
Write-Host "[$rid] publishing launcher (+ co-deployed bake)..." -ForegroundColor Yellow
Invoke-Publish 'src/AcDream.Launcher/AcDream.Launcher.csproj' $rid $launcherDirectory
$clientZip = Join-Path $BinRoot "client-$rid.zip"
$launcherZip = Join-Path $BinRoot "launcher-$rid.zip"
Write-Host "[$rid] packing..." -ForegroundColor Yellow
New-PayloadZip $clientDirectory $clientZip @("AcDream.App$suffix", "acdream-headless$suffix")
New-PayloadZip $launcherDirectory $launcherZip @("acdream-launcher$suffix", "acdream-bake$suffix")
$clients[$rid] = Get-Artifact $clientZip "$RawBase/client-$rid.zip"
$launchers[$rid] = Get-Artifact $launcherZip "$RawBase/launcher-$rid.zip"
}
$manifest = [ordered]@{
schemaVersion = 1
version = $Version
minimumLauncherVersion = $MinimumLauncherVersion
clients = $clients
launchers = $launchers
}
$manifestPath = Join-Path $BinRoot 'manifest.json'
$json = $manifest | ConvertTo-Json -Depth 6
[IO.File]::WriteAllText($manifestPath, $json + "`n", [Text.UTF8Encoding]::new($false))
Remove-Item -LiteralPath $Staging -Recurse -Force
Write-Host ''
Write-Host 'Feed written:' -ForegroundColor Green
$total = 0L
foreach ($file in (Get-ChildItem -LiteralPath $BinRoot -File | Sort-Object Name)) {
$total += $file.Length
' {0,-26} {1,10:N1} MB' -f $file.Name, ($file.Length / 1MB) | Write-Host
}
' {0,-26} {1,10:N1} MB' -f 'TOTAL', ($total / 1MB) | Write-Host
# A RID-specific restore rewrites packages.<rid>.lock.json for the projects it
# touches, so a feed build can leave the working tree dirty even though nothing
# about the source changed. Report it rather than silently reverting: the files
# are the developer's, and a real dependency change must not be swallowed here.
$dirtyLocks = @(
@(& git -C $RepoRoot status --porcelain -- '*packages.*.lock.json' 2>$null) |
Where-Object { $_ }
)
if ($dirtyLocks.Count -gt 0) {
Write-Host ''
Write-Host 'Note: the RID restore modified these lock files:' -ForegroundColor Yellow
$dirtyLocks | ForEach-Object { " $($_.Trim())" | Write-Host }
Write-Host ' If you did not change dependencies, discard them:' -ForegroundColor DarkGray
Write-Host " git checkout -- '*packages.*.lock.json'" -ForegroundColor DarkGray
}
Write-Host ''
Write-Host 'Next — publish the feed to Gitea:' -ForegroundColor Cyan
Write-Host " pwsh -NoProfile -File tools/publish-dist.ps1"
Write-Host ''
Write-Host ' (bin/ is gitignored on purpose: publish-dist puts it on the' -ForegroundColor DarkGray
Write-Host ' Gitea-only dist branch, never on main / GitHub.)' -ForegroundColor DarkGray