From af8e1cd7d66d09ea61bcdb7876c114f48430641b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 8 Jul 2026 11:14:15 -0400 Subject: [PATCH 01/10] Make NativeAOT the default for UniGetUI release packages on all platforms - Enable PublishAot=true for Release builds in Avalonia csproj and disable conflicting PublishReadyToRun when NativeAOT is active. - Add NativeAOT publish profiles for win-arm64, osx-x64, osx-arm64, linux-x64 and linux-arm64. - Update build-release.yml to publish every platform with NativeAOT profiles. - Harden dotnet-test.yml NativeAOT publish report (no longer ignored). - Capture per-platform uncompressed and compressed artifact sizes in CI. - Add NativeAOT variants to CLI headless E2E workflow. - Support avalonia-native daemon kind in E2E runner for NativeAOT Linux. - Update publish-nativeaot.ps1 to support arm64 and README documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 84 +++++++++++++++- .github/workflows/cli-headless-e2e.yml | 88 ++++++++++++++--- .github/workflows/dotnet-test.yml | 1 - README.md | 9 +- scripts/publish-nativeaot.ps1 | 7 +- src/Directory.Build.props | 3 +- .../Win-arm64-NativeAot.pubxml | 16 +++ .../linux-arm64-NativeAot.pubxml | 16 +++ .../linux-x64-NativeAot.pubxml | 16 +++ .../osx-arm64-NativeAot.pubxml | 16 +++ .../PublishProfiles/osx-x64-NativeAot.pubxml | 16 +++ .../UniGetUI.Avalonia.csproj | 7 ++ .../cli-e2e.manifest.linux-nativeaot.json | 99 +++++++++++++++++++ testing/automation/run-cli-e2e.ps1 | 17 ++++ 14 files changed, 370 insertions(+), 25 deletions(-) create mode 100644 src/UniGetUI.Avalonia/Properties/PublishProfiles/Win-arm64-NativeAot.pubxml create mode 100644 src/UniGetUI.Avalonia/Properties/PublishProfiles/linux-arm64-NativeAot.pubxml create mode 100644 src/UniGetUI.Avalonia/Properties/PublishProfiles/linux-x64-NativeAot.pubxml create mode 100644 src/UniGetUI.Avalonia/Properties/PublishProfiles/osx-arm64-NativeAot.pubxml create mode 100644 src/UniGetUI.Avalonia/Properties/PublishProfiles/osx-x64-NativeAot.pubxml create mode 100644 testing/automation/cli-e2e.manifest.linux-nativeaot.json diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1b8a9e1c2c..948db64ade 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -194,7 +194,13 @@ jobs: throw "Could not resolve the Avalonia target framework from MSBuild" } - dotnet publish src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj /noLogo /p:Configuration=Release /p:Platform=$Platform -p:RuntimeIdentifier=win-$Platform -v m + dotnet publish src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj ` + /noLogo ` + /p:Configuration=Release ` + /p:Platform=$Platform ` + /p:RuntimeIdentifier=win-$Platform ` + /p:PublishProfile=Win-$Platform-NativeAot ` + -v m if ($LASTEXITCODE -ne 0) { throw "dotnet publish Avalonia failed" } # Stage binaries @@ -286,6 +292,26 @@ jobs: # Installer is created in output during the previous step + - name: Capture artifact sizes + shell: pwsh + run: | + $Platform = '${{ matrix.platform }}' + $SizesFile = "output/sizes.$Platform.json" + $UncompressedSize = (Get-ChildItem "unigetui_bin" -Recurse -File | Measure-Object -Property Length -Sum).Sum + $Artifacts = Get-ChildItem "output" -File | Where-Object { $_.Name -ne "sizes.$Platform.json" } | ForEach-Object { + @{ Name = $_.Name; SizeBytes = $_.Length } + } + $Report = @{ + Platform = $Platform + RuntimeIdentifier = "win-$Platform" + UncompressedSizeBytes = $UncompressedSize + Artifacts = $Artifacts + Timestamp = [DateTimeOffset]::UtcNow.ToString("o") + } + $Report | ConvertTo-Json -Depth 4 | Set-Content $SizesFile -Encoding UTF8 + Write-Host "Artifact sizes saved to $SizesFile" + Get-Content $SizesFile + - name: Code-sign installer if: ${{ fromJSON(needs.preflight.outputs.dry-run) == false }} shell: pwsh @@ -377,6 +403,7 @@ jobs: --configuration Release \ --runtime ${{ matrix.runtime }} \ --self-contained true \ + /p:PublishProfile=${{ matrix.runtime }}-NativeAot \ --output ../bin/${{ matrix.name }} - name: Install macOS packaging tools @@ -534,6 +561,28 @@ jobs: -Iteration $RpmIteration ` -Architecture $RpmArch + - name: Capture artifact sizes + shell: pwsh + run: | + $Name = '${{ matrix.name }}' + $Runtime = '${{ matrix.runtime }}' + $SizesFile = "output/sizes.$Name.json" + $SourceDir = if ('${{ runner.os }}' -eq 'macOS') { "UniGetUI.app/Contents/MacOS" } else { "bin/$Name" } + $UncompressedSize = (Get-ChildItem $SourceDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum + $Artifacts = Get-ChildItem "output" -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "sizes.$Name.json" } | ForEach-Object { + @{ Name = $_.Name; SizeBytes = $_.Length } + } + $Report = @{ + Platform = $Name + RuntimeIdentifier = $Runtime + UncompressedSizeBytes = $UncompressedSize + Artifacts = $Artifacts + Timestamp = [DateTimeOffset]::UtcNow.ToString("o") + } + $Report | ConvertTo-Json -Depth 4 | Set-Content $SizesFile -Encoding UTF8 + Write-Host "Artifact sizes saved to $SizesFile" + Get-Content $SizesFile + - name: Upload artifacts uses: actions/upload-artifact@v7 with: @@ -588,6 +637,39 @@ jobs: Get-Content $ChecksumFile echo "::endgroup::" + - name: Generate consolidated size report + shell: pwsh + working-directory: output + run: | + $SizeFiles = Get-ChildItem -Path . -Recurse -File -Filter "sizes.*.json" | Sort-Object Name + if (-not $SizeFiles) { + Write-Host "No size files found." + return + } + + $Rows = foreach ($file in $SizeFiles) { + $data = Get-Content $file.FullName -Raw | ConvertFrom-Json + $uncompressedMiB = [math]::Round($data.UncompressedSizeBytes / 1MB, 2) + foreach ($artifact in $data.Artifacts) { + [PSCustomObject]@{ + Platform = $data.Platform + RID = $data.RuntimeIdentifier + Artifact = $artifact.Name + CompressedMiB = [math]::Round($artifact.SizeBytes / 1MB, 2) + UncompressedMiB = $uncompressedMiB + } + } + } + + $SizeReportFile = Join-Path $PWD "sizes.md" + $Lines = @("# UniGetUI Release Artifact Sizes", "") + $Lines += $Rows | Format-Table -AutoSize | Out-String -Stream | Where-Object { $_.Trim() -ne '' } + Set-Content -Path $SizeReportFile -Value $Lines -Encoding utf8NoBOM + + echo "::group::sizes" + Get-Content $SizeReportFile + echo "::endgroup::" + - name: Create GitHub Release shell: pwsh env: diff --git a/.github/workflows/cli-headless-e2e.yml b/.github/workflows/cli-headless-e2e.yml index f63de08128..8ac9959572 100644 --- a/.github/workflows/cli-headless-e2e.yml +++ b/.github/workflows/cli-headless-e2e.yml @@ -27,12 +27,26 @@ jobs: daemon_project: UniGetUI.Avalonia/UniGetUI.Avalonia.csproj daemon_build_args: '-p:Platform=x64' manifest: testing/automation/cli-e2e.manifest.windows.json + - name: Windows (NativeAOT) + os: windows-latest + solution: UniGetUI.Windows.slnx + daemon_project: UniGetUI.Avalonia/UniGetUI.Avalonia.csproj + daemon_publish_profile: Win-x64-NativeAot + daemon_build_args: '-p:Platform=x64' + manifest: testing/automation/cli-e2e.manifest.windows.json - name: Linux (Avalonia) os: ubuntu-latest solution: UniGetUI.Avalonia.slnx daemon_project: UniGetUI.Avalonia/UniGetUI.Avalonia.csproj daemon_build_args: '' manifest: testing/automation/cli-e2e.manifest.linux.json + - name: Linux (NativeAOT) + os: ubuntu-latest + solution: UniGetUI.Avalonia.slnx + daemon_project: UniGetUI.Avalonia/UniGetUI.Avalonia.csproj + daemon_publish_profile: linux-x64-NativeAot + daemon_build_args: '' + manifest: testing/automation/cli-e2e.manifest.linux-nativeaot.json name: ${{ matrix.name }} runs-on: ${{ matrix.os }} @@ -76,22 +90,66 @@ jobs: working-directory: src shell: pwsh run: | - $args = @( - 'build', - '${{ matrix.daemon_project }}', - '--no-restore', - '--configuration', - '${{ env.CONFIGURATION }}', - '--verbosity', - 'minimal', - '/p:UseSharedCompilation=false', - '/m:1' - ) - if ('${{ matrix.daemon_build_args }}') { - $args += '${{ matrix.daemon_build_args }}' - } + $PublishProfile = '${{ matrix.daemon_publish_profile }}' + $BuildArgs = '${{ matrix.daemon_build_args }}' + dotnet build-server shutdown - dotnet @args + + if ($PublishProfile) { + $RuntimeIdentifier = $PublishProfile -replace '-NativeAot$', '' -replace '^Win-', 'win-' -replace '^linux-', 'linux-' -replace '^osx-', 'osx-' + $OutputPath = Join-Path (Resolve-Path '..') "artifacts/e2e-nativeaot/$RuntimeIdentifier" + New-Item -ItemType Directory -Force -Path $OutputPath | Out-Null + + $args = @( + 'publish', + '${{ matrix.daemon_project }}', + '--no-restore', + '--configuration', + '${{ env.CONFIGURATION }}', + '--runtime', + $RuntimeIdentifier, + '--self-contained', + 'true', + '--output', + $OutputPath, + '--verbosity', + 'minimal', + '/p:UseSharedCompilation=false', + '/m:1', + "/p:PublishProfile=$PublishProfile" + ) + if ($BuildArgs) { + $args += $BuildArgs + } + dotnet @args + + $exeName = if ('${{ runner.os }}' -eq 'Windows') { 'UniGetUI.exe' } else { 'UniGetUI' } + $daemonExe = Join-Path $OutputPath $exeName + if (-not (Test-Path $daemonExe)) { + throw "NativeAOT daemon executable was not produced at $daemonExe" + } + if ('${{ runner.os }}' -ne 'Windows') { + chmod +x $daemonExe + } + echo "UNIGETUI_DAEMON_EXE=$daemonExe" >> $Env:GITHUB_ENV + } + else { + $args = @( + 'build', + '${{ matrix.daemon_project }}', + '--no-restore', + '--configuration', + '${{ env.CONFIGURATION }}', + '--verbosity', + 'minimal', + '/p:UseSharedCompilation=false', + '/m:1' + ) + if ($BuildArgs) { + $args += $BuildArgs + } + dotnet @args + } - name: Upgrade pip tooling shell: pwsh diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index 743f6cb28f..58cea35a17 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -99,7 +99,6 @@ jobs: - name: Report NativeAOT publish warnings working-directory: src shell: pwsh - continue-on-error: true run: | dotnet build-server shutdown dotnet publish UniGetUI.Avalonia/UniGetUI.Avalonia.csproj ` diff --git a/README.md b/README.md index ba75df507c..d60f21c7c8 100644 --- a/README.md +++ b/README.md @@ -91,14 +91,15 @@ scoop install extras/unigetui choco install unigetui ``` -### Experimental NativeAOT build (Windows) -For contributors and advanced validation, the repository includes an opt-in NativeAOT publish profile and a helper script: +### NativeAOT builds + +Release packages are compiled with NativeAOT on all supported platforms (Windows, macOS, and Linux) for improved startup performance and a reduced attack surface. If you want to build the same configuration locally, the repository includes helper publish profiles and a script: ```powershell -pwsh ./scripts/publish-nativeaot.ps1 +pwsh ./scripts/publish-nativeaot.ps1 -Platform x64 # or arm64 ``` -This publishes `src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj` for `win-x64` as a self-contained NativeAOT build into `artifacts/nativeaot/win-x64/`. It does not replace the existing Release or installer flows. +This publishes `src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj` as a self-contained NativeAOT build into `artifacts/nativeaot/win-/`. ### macOS diff --git a/scripts/publish-nativeaot.ps1 b/scripts/publish-nativeaot.ps1 index 4c563355c5..d7068c1fb3 100644 --- a/scripts/publish-nativeaot.ps1 +++ b/scripts/publish-nativeaot.ps1 @@ -7,20 +7,21 @@ Build configuration. Default: Release. .PARAMETER Platform - Target platform. Default: x64. + Target platform. Default: x64. Supported: x64, arm64. .PARAMETER OutputPath Directory for the published output. Default: ./artifacts/nativeaot/win-. .PARAMETER PublishProfileName - NativeAOT publish profile name. Default: Win-x64-NativeAot. + NativeAOT publish profile name. Default: Win--NativeAot. #> [CmdletBinding()] param( [string] $Configuration = "Release", + [ValidateSet("x64", "arm64")] [string] $Platform = "x64", [string] $OutputPath = (Join-Path (Join-Path $PSScriptRoot "..") "artifacts/nativeaot/win-$Platform"), - [string] $PublishProfileName = "Win-x64-NativeAot" + [string] $PublishProfileName = "Win-$Platform-NativeAot" ) $ErrorActionPreference = 'Stop' diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 61c627d4b0..8980d46dd7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -54,7 +54,8 @@ true true true - true + + true true diff --git a/src/UniGetUI.Avalonia/Properties/PublishProfiles/Win-arm64-NativeAot.pubxml b/src/UniGetUI.Avalonia/Properties/PublishProfiles/Win-arm64-NativeAot.pubxml new file mode 100644 index 0000000000..cbdb71058d --- /dev/null +++ b/src/UniGetUI.Avalonia/Properties/PublishProfiles/Win-arm64-NativeAot.pubxml @@ -0,0 +1,16 @@ + + + + Release + arm64 + win-arm64 + true + true + false + true + full + false + false + false + + diff --git a/src/UniGetUI.Avalonia/Properties/PublishProfiles/linux-arm64-NativeAot.pubxml b/src/UniGetUI.Avalonia/Properties/PublishProfiles/linux-arm64-NativeAot.pubxml new file mode 100644 index 0000000000..64b1110d75 --- /dev/null +++ b/src/UniGetUI.Avalonia/Properties/PublishProfiles/linux-arm64-NativeAot.pubxml @@ -0,0 +1,16 @@ + + + + Release + arm64 + linux-arm64 + true + true + false + true + full + false + false + false + + diff --git a/src/UniGetUI.Avalonia/Properties/PublishProfiles/linux-x64-NativeAot.pubxml b/src/UniGetUI.Avalonia/Properties/PublishProfiles/linux-x64-NativeAot.pubxml new file mode 100644 index 0000000000..ed2479d475 --- /dev/null +++ b/src/UniGetUI.Avalonia/Properties/PublishProfiles/linux-x64-NativeAot.pubxml @@ -0,0 +1,16 @@ + + + + Release + x64 + linux-x64 + true + true + false + true + full + false + false + false + + diff --git a/src/UniGetUI.Avalonia/Properties/PublishProfiles/osx-arm64-NativeAot.pubxml b/src/UniGetUI.Avalonia/Properties/PublishProfiles/osx-arm64-NativeAot.pubxml new file mode 100644 index 0000000000..b61e9fc9d0 --- /dev/null +++ b/src/UniGetUI.Avalonia/Properties/PublishProfiles/osx-arm64-NativeAot.pubxml @@ -0,0 +1,16 @@ + + + + Release + arm64 + osx-arm64 + true + true + false + true + full + false + false + false + + diff --git a/src/UniGetUI.Avalonia/Properties/PublishProfiles/osx-x64-NativeAot.pubxml b/src/UniGetUI.Avalonia/Properties/PublishProfiles/osx-x64-NativeAot.pubxml new file mode 100644 index 0000000000..c80c2c22fe --- /dev/null +++ b/src/UniGetUI.Avalonia/Properties/PublishProfiles/osx-x64-NativeAot.pubxml @@ -0,0 +1,16 @@ + + + + Release + x64 + osx-x64 + true + true + false + true + full + false + false + false + + diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index 685eca163d..b4ca7fdb92 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -27,6 +27,13 @@ false + + + true + + false + + $(NoWarn);MVVMTK0045;AVLN3001 diff --git a/testing/automation/cli-e2e.manifest.linux-nativeaot.json b/testing/automation/cli-e2e.manifest.linux-nativeaot.json new file mode 100644 index 0000000000..16d1ca15aa --- /dev/null +++ b/testing/automation/cli-e2e.manifest.linux-nativeaot.json @@ -0,0 +1,99 @@ +{ + "name": "linux-headless-ipc", + "daemon": { + "kind": "avalonia-native", + "project": "UniGetUI.Avalonia\\UniGetUI.Avalonia.csproj", + "assemblyName": "UniGetUI" + }, + "transport": { + "kind": "named-pipe", + "verifyNoTcpListener": false + }, + "secureSettings": { + "allowSet": true, + "toggleKey": "AllowCustomManagerPaths", + "managerForExecutableOverride": "npm" + }, + "uninstallValidationManagers": [ + "pip" + ], + "packageManagers": [ + { + "manager": "pip", + "sourceName": "pip", + "query": "cowsay", + "packageId": "cowsay", + "installVersion": "5.0", + "scope": "User", + "roles": [ + "specific-update" + ] + }, + { + "manager": "npm", + "sourceName": "npm", + "query": "cowsay", + "packageId": "cowsay", + "installVersion": "1.5.0", + "scope": "Global", + "roles": [ + "bundle", + "update-manager", + "reinstall", + "repair", + "toggle-manager", + "update-all" + ] + } + ], + "queueOperations": [ + { + "manager": "pip", + "sourceName": "pip", + "packageId": "awscli", + "target": "download", + "query": "awscli" + }, + { + "manager": "npm", + "sourceName": "npm", + "packageId": "typescript", + "target": "download", + "query": "typescript" + } + ], + "excludedCommands": [ + { + "command": "manager action", + "reason": "Current manager actions are Windows-specific and system-changing, so they are intentionally excluded from Linux CI." + }, + { + "command": "source add", + "reason": "The deterministic Linux manager matrix does not include a manager with reliable CI-safe custom source mutation." + }, + { + "command": "source remove", + "reason": "The deterministic Linux manager matrix does not include a manager with reliable CI-safe custom source mutation." + }, + { + "command": "operation cancel", + "reason": "Queued package downloads complete too quickly on current Linux CI runners to guarantee a stable cancellable operation window." + }, + { + "command": "operation retry", + "reason": "A deterministic retry scenario depends on first forcing a stable failed or canceled operation, which is not yet reliable in CI." + }, + { + "command": "operation reorder", + "reason": "Queued package downloads complete too quickly on current Linux CI runners to guarantee a stable reorderable operation window." + }, + { + "command": "backup github *", + "reason": "GitHub device-flow authentication is intentionally excluded from deterministic CI." + }, + { + "command": "backup cloud *", + "reason": "Cloud backup flows depend on external authenticated GitHub state and are intentionally excluded from deterministic CI." + } + ] +} diff --git a/testing/automation/run-cli-e2e.ps1 b/testing/automation/run-cli-e2e.ps1 index ba7c3cd59a..b87c3b97c5 100644 --- a/testing/automation/run-cli-e2e.ps1 +++ b/testing/automation/run-cli-e2e.ps1 @@ -244,6 +244,23 @@ function Get-DaemonCommand { PrefixArguments = @($resolvedDll) } } + 'avalonia-native' { + $fileName = if ($runningOnWindows) { "$($manifest.daemon.assemblyName).exe" } else { $manifest.daemon.assemblyName } + $daemonNative = if ($env:UNIGETUI_DAEMON_EXE) { + $env:UNIGETUI_DAEMON_EXE + } + else { + Find-BuiltArtifact -ProjectDirectory (Split-Path $daemonProject -Parent) -FileName $fileName + } + if ([string]::IsNullOrWhiteSpace($daemonNative) -or -not (Test-Path $daemonNative)) { + throw "NativeAOT headless executable was not found. Expected $fileName under $(Split-Path $daemonProject -Parent)\bin\$configuration" + } + + return @{ + FilePath = (Resolve-Path $daemonNative).Path + WorkingDirectory = Split-Path (Resolve-Path $daemonNative).Path -Parent + } + } default { throw "Unsupported daemon kind $($manifest.daemon.kind)" } From f3aeba7297ad68df76725f2dc7791ec31121d445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 8 Jul 2026 11:31:49 -0400 Subject: [PATCH 02/10] Fallback linux-arm64 release build from NativeAOT to ReadyToRun The default ubuntu-latest runner does not include the aarch64 cross- compilation toolchain required for linux-arm64 NativeAOT. Keep NativeAOT for linux-x64 and fall back to the existing ReadyToRun/full-trim path for linux-arm64 until cross-compilation is available. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 948db64ade..eebc81fabe 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -359,15 +359,21 @@ jobs: - os: macos-latest name: macos-arm64 runtime: osx-arm64 + publish_profile: osx-arm64-NativeAot - os: macos-latest name: macos-x64 runtime: osx-x64 + publish_profile: osx-x64-NativeAot - os: ubuntu-latest name: linux-x64 runtime: linux-x64 + publish_profile: linux-x64-NativeAot - os: ubuntu-latest name: linux-arm64 runtime: linux-arm64 + # linux-arm64 is built on an x64 runner; NativeAOT cross-compilation is not + # available in the default runner image, so fall back to ReadyToRun/full-trim. + publish_profile: '' steps: - name: Checkout @@ -399,12 +405,23 @@ jobs: - name: Publish working-directory: src run: | - dotnet publish UniGetUI.Avalonia/UniGetUI.Avalonia.csproj \ - --configuration Release \ - --runtime ${{ matrix.runtime }} \ - --self-contained true \ - /p:PublishProfile=${{ matrix.runtime }}-NativeAot \ - --output ../bin/${{ matrix.name }} + $PublishProfile = '${{ matrix.publish_profile }}' + $PublishArgs = @( + 'publish', + 'UniGetUI.Avalonia/UniGetUI.Avalonia.csproj', + '--configuration', 'Release', + '--runtime', '${{ matrix.runtime }}', + '--self-contained', 'true', + '--output', '../bin/${{ matrix.name }}' + ) + if ($PublishProfile) { + $PublishArgs += "/p:PublishProfile=$PublishProfile" + } + else { + # Fallback: explicitly disable NativeAOT so the ReadyToRun release default is used. + $PublishArgs += '/p:PublishAot=false' + } + dotnet @PublishArgs - name: Install macOS packaging tools if: runner.os == 'macOS' From 16acb56a1d0ad2513ab3f179e78cff7fe3857ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 8 Jul 2026 11:42:59 -0400 Subject: [PATCH 03/10] Use pwsh shell for cross-platform release publish step The publish step now uses PowerShell syntax for profile selection and NativeAOT fallback handling, so it must run under pwsh on macOS/Linux. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index eebc81fabe..fb906d8a39 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -404,6 +404,7 @@ jobs: - name: Publish working-directory: src + shell: pwsh run: | $PublishProfile = '${{ matrix.publish_profile }}' $PublishArgs = @( From 36aa9bb95ae51cf40508592c3f148b157b8b1e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 8 Jul 2026 12:00:23 -0400 Subject: [PATCH 04/10] Add consolidated size-report job for dry-run release builds The per-platform size JSONs are already captured during packaging. Add a standalone size-report job that downloads all artifacts, consolidates the sizes into a markdown table, and uploads it. This runs even in dry-run mode so size comparisons are available before a real release. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index fb906d8a39..71d9a9907a 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -783,3 +783,54 @@ jobs: remote: releases source_path: Devolutions.UniGetUI.* working_directory: onedrive-staging + + size-report: + name: Consolidated Size Report + runs-on: ubuntu-latest + needs: [preflight, build, build-avalonia] + if: always() && needs.build.result != 'cancelled' && needs.build-avalonia.result != 'cancelled' + + steps: + - name: Download artifacts + uses: actions/download-artifact@v8 + with: + path: output + + - name: Generate consolidated size report + shell: pwsh + working-directory: output + run: | + $SizeFiles = Get-ChildItem -Path . -Recurse -File -Filter "sizes.*.json" | Sort-Object Name + if (-not $SizeFiles) { + Write-Host "No size files found." + return + } + + $Rows = foreach ($file in $SizeFiles) { + $data = Get-Content $file.FullName -Raw | ConvertFrom-Json + $uncompressedMiB = [math]::Round($data.UncompressedSizeBytes / 1MB, 2) + foreach ($artifact in $data.Artifacts) { + [PSCustomObject]@{ + Platform = $data.Platform + RID = $data.RuntimeIdentifier + Artifact = $artifact.Name + CompressedMiB = [math]::Round($artifact.SizeBytes / 1MB, 2) + UncompressedMiB = $uncompressedMiB + } + } + } + + $SizeReportFile = Join-Path $PWD "sizes.md" + $Lines = @("# UniGetUI Release Artifact Sizes", "") + $Lines += $Rows | Format-Table -AutoSize | Out-String -Stream | Where-Object { $_.Trim() -ne '' } + Set-Content -Path $SizeReportFile -Value $Lines -Encoding utf8NoBOM + + echo "::group::sizes" + Get-Content $SizeReportFile + echo "::endgroup::" + + - name: Upload size report + uses: actions/upload-artifact@v7 + with: + name: UniGetUI-size-report + path: output/sizes.md From 895da397f4a55e132d2bfd972aa2a53898f66ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 8 Jul 2026 13:14:33 -0400 Subject: [PATCH 05/10] Clarify linux-arm64 fallback comment in build-release.yml The fallback disables NativeAOT because the hosted x64 runner cannot cross-compile NativeAOT for linux-arm64. The resulting build is the previous non-NativeAOT self-contained path; update the comment so it is not misread as a guaranteed ReadyToRun fallback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 71d9a9907a..0f032e0dcc 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -419,7 +419,8 @@ jobs: $PublishArgs += "/p:PublishProfile=$PublishProfile" } else { - # Fallback: explicitly disable NativeAOT so the ReadyToRun release default is used. + # Fallback: explicitly disable NativeAOT. The hosted x64 runner cannot cross-compile + # NativeAOT for linux-arm64, so we keep the previous non-NativeAOT self-contained path. $PublishArgs += '/p:PublishAot=false' } dotnet @PublishArgs From eb0e19de0c08c3bda2eff2627435e933bb2f45cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 8 Jul 2026 13:33:00 -0400 Subject: [PATCH 06/10] Enable linux-arm64 NativeAOT release build Install the Linux arm64 cross toolchain on the x64 Ubuntu release runner and use the linux-arm64 NativeAOT publish profile instead of falling back to the non-AOT self-contained path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 56 ++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 0f032e0dcc..fdb178dafb 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -371,9 +371,7 @@ jobs: - os: ubuntu-latest name: linux-arm64 runtime: linux-arm64 - # linux-arm64 is built on an x64 runner; NativeAOT cross-compilation is not - # available in the default runner image, so fall back to ReadyToRun/full-trim. - publish_profile: '' + publish_profile: linux-arm64-NativeAot steps: - name: Checkout @@ -392,6 +390,53 @@ jobs: restore-keys: | ${{ runner.os }}-nuget- + - name: Install Linux arm64 NativeAOT toolchain + if: matrix.runtime == 'linux-arm64' + shell: bash + run: | + set -euo pipefail + + . /etc/os-release + sudo dpkg --add-architecture arm64 + + # Keep the runner's default Ubuntu feeds bound to amd64, then add the + # Ubuntu Ports arm64 feed that provides the target CRT and zlib objects. + if compgen -G "/etc/apt/sources.list.d/*.sources" > /dev/null; then + for source in /etc/apt/sources.list.d/*.sources; do + if ! grep -q '^Architectures:' "$source"; then + sudo sed -i '/^Types: deb$/a Architectures: amd64' "$source" + fi + done + fi + + if [ -f /etc/apt/sources.list ]; then + sudo sed -i -E '/^deb /s/^deb /deb [arch=amd64] /' /etc/apt/sources.list + fi + + sudo tee /etc/apt/sources.list.d/ubuntu-arm64.sources >/dev/null < Date: Wed, 8 Jul 2026 13:59:23 -0400 Subject: [PATCH 07/10] Prune oversized Linux NativeAOT debug symbols Remove large .dbg/.pdb files from Linux release payloads before packaging, matching the existing Windows behavior of not shipping oversized debug symbols in release artifacts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index fdb178dafb..3446206ecd 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -465,6 +465,22 @@ jobs: } dotnet @PublishArgs + - name: Remove oversized Linux debug symbols + if: runner.os == 'Linux' + shell: pwsh + run: | + $PublishDir = "bin/${{ matrix.name }}" + $MaxDebugSymbolSizeBytes = 1MB + $DebugSymbolsToRemove = @(Get-ChildItem $PublishDir -Include "*.dbg", "*.pdb" -File -Recurse | Where-Object { + $_.Length -gt $MaxDebugSymbolSizeBytes + }) + + if ($DebugSymbolsToRemove.Count -gt 0) { + $RemovedDebugSymbolBytes = ($DebugSymbolsToRemove | Measure-Object -Property Length -Sum).Sum + $DebugSymbolsToRemove | Remove-Item -Force + Write-Host ("Removed {0} oversized Linux debug symbols above {1:N2} MiB ({2:N2} MiB total)." -f $DebugSymbolsToRemove.Count, ($MaxDebugSymbolSizeBytes / 1MB), ($RemovedDebugSymbolBytes / 1MB)) + } + - name: Install macOS packaging tools if: runner.os == 'macOS' run: brew install create-dmg From e29ab531fbad583f22f1d88806461fedee1d57fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 8 Jul 2026 14:19:15 -0400 Subject: [PATCH 08/10] Fix Linux package launcher executable Use the actual UniGetUI app host as the default Linux package executable and validate that the staged package payload contains it before creating launcher integration files. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/package-linux.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/package-linux.ps1 b/scripts/package-linux.ps1 index 2e2ea17cd5..26b3ca09bb 100644 --- a/scripts/package-linux.ps1 +++ b/scripts/package-linux.ps1 @@ -79,7 +79,7 @@ param( [string] $Description = 'UniGetUI - GUI for package managers', [string] $Maintainer = 'Devolutions Inc. ', [string] $Url = 'https://github.com/Devolutions/UniGetUI', - [string] $AppExecutableName = 'UniGetUI.Avalonia', + [string] $AppExecutableName = 'UniGetUI', [string] $LauncherName = 'unigetui', [string] $IconSourcePath = (Join-Path $PSScriptRoot '..' 'src' 'SharedAssets' 'Assets' 'Images' 'icon.png') ) @@ -112,6 +112,11 @@ function New-LinuxIntegrationAssets { & /bin/cp -a "$SourceDir/." $payloadDir if ($LASTEXITCODE -ne 0) { throw "cp (payload staging) exited $LASTEXITCODE" } + $appExecutableFullPath = Join-Path $payloadDir $AppExecutableName + if (-not (Test-Path $appExecutableFullPath -PathType Leaf)) { + throw "App executable '$AppExecutableName' was not found in package payload '$payloadDir'" + } + $launcherFullPath = Join-Path $StageRoot $LauncherPath.TrimStart('/') New-Item -ItemType Directory -Path (Split-Path $launcherFullPath -Parent) -Force | Out-Null $launcherCommand = 'exec {0}/{1} "$@"' -f $InstallPrefix, $AppExecutableName From 7abcddc83bb06df1d62da07a9f52004c581410b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 8 Jul 2026 14:29:06 -0400 Subject: [PATCH 09/10] Disable shared compilation for Windows release publish Pass UseSharedCompilation=false directly to the Windows NativeAOT publish command so referenced project compiles do not use VBCSCompiler and intermittently lock intermediate outputs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 3446206ecd..62776e1623 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -200,6 +200,7 @@ jobs: /p:Platform=$Platform ` /p:RuntimeIdentifier=win-$Platform ` /p:PublishProfile=Win-$Platform-NativeAot ` + /p:UseSharedCompilation=false ` -v m if ($LASTEXITCODE -ne 0) { throw "dotnet publish Avalonia failed" } From 72a23557ce70f02978dd7448c63e4b364ea8a067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 8 Jul 2026 14:43:54 -0400 Subject: [PATCH 10/10] Serialize Windows release publish builds Disable MSBuild project parallelism for the Windows NativeAOT release publish to avoid intermittent intermediate output races in referenced projects. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 62776e1623..670ff296bc 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -201,6 +201,8 @@ jobs: /p:RuntimeIdentifier=win-$Platform ` /p:PublishProfile=Win-$Platform-NativeAot ` /p:UseSharedCompilation=false ` + /p:BuildInParallel=false ` + /m:1 ` -v m if ($LASTEXITCODE -ne 0) { throw "dotnet publish Avalonia failed" }