<# .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-.zip AcDream.App[.exe] + acdream-headless[.exe] launcher-.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, [string]$BaseUrl, [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)." } } # Where the manifest says the payloads live. The CI pipeline passes the Gitea # RELEASE asset base for the tag it is publishing, so payloads live outside git # entirely; the default keeps the older dist-branch layout working for a manual # local publish. The manifest itself always stays at the stable dist raw URL # that ReleaseManifestClient.ProductionManifestUri points at. $RawBase = if ([string]::IsNullOrWhiteSpace($BaseUrl)) { 'https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin' } else { $BaseUrl.TrimEnd('/') } $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..lock.json files, so stamping it churned every lock # file with a throwaway build stamp (observed 2026-08-18). "-p:InformationalVersion=$Version", # No SourceLink '+' 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..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