diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1b8a9e1c2c..670ff296bc 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -194,7 +194,16 @@ 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 ` + /p:UseSharedCompilation=false ` + /p:BuildInParallel=false ` + /m:1 ` + -v m if ($LASTEXITCODE -ne 0) { throw "dotnet publish Avalonia failed" } # Stage binaries @@ -286,6 +295,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 @@ -333,15 +362,19 @@ 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 + publish_profile: linux-arm64-NativeAot steps: - name: Checkout @@ -360,6 +393,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 <> $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/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 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)" }