Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 66 additions & 40 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ on:
type: boolean
default: true

env:
LINUX_GLIBC_BASELINE: '2.27'

jobs:
preflight:
name: Preflight
Expand Down Expand Up @@ -371,10 +374,14 @@ jobs:
name: linux-x64
runtime: linux-x64
publish_profile: linux-x64-NativeAot
cross_image_arch: amd64
cross_sysroot: /crossrootfs/x64
- os: ubuntu-latest
name: linux-arm64
runtime: linux-arm64
publish_profile: linux-arm64-NativeAot
cross_image_arch: arm64
cross_sysroot: /crossrootfs/arm64

steps:
- name: Checkout
Expand All @@ -393,52 +400,31 @@ jobs:
restore-keys: |
${{ runner.os }}-nuget-

- name: Install Linux arm64 NativeAOT toolchain
if: matrix.runtime == 'linux-arm64'
- name: Free disk space for cross-build images
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail

. /etc/os-release
sudo dpkg --add-architecture arm64
sudo rm -rf /usr/local/lib/android /opt/ghc || true
df -h /

# 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
- name: Build Linux cross-compilation image
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail

if [ -f /etc/apt/sources.list ]; then
sudo sed -i -E '/^deb /s/^deb /deb [arch=amd64] /' /etc/apt/sources.list
fi
SDK_TAG="$(jq -er '.sdk.version' global.json)"

sudo tee /etc/apt/sources.list.d/ubuntu-arm64.sources >/dev/null <<EOF
Types: deb
URIs: http://ports.ubuntu.com/ubuntu-ports/
Suites: ${VERSION_CODENAME} ${VERSION_CODENAME}-updates ${VERSION_CODENAME}-backports
Components: main restricted universe multiverse
Architectures: arm64
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

Types: deb
URIs: http://ports.ubuntu.com/ubuntu-ports/
Suites: ${VERSION_CODENAME}-security
Components: main restricted universe multiverse
Architectures: arm64
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
EOF
docker build --pull --build-arg "SDK_TAG=$SDK_TAG" -t unigetui-cross-build - <<'DOCKERFILE'
ARG SDK_TAG
FROM mcr.microsoft.com/dotnet/sdk:${SDK_TAG} AS sdk

sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang \
llvm \
binutils-aarch64-linux-gnu \
gcc-aarch64-linux-gnu \
zlib1g-dev:arm64
FROM mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-net10.0-cross-${{ matrix.cross_image_arch }}
COPY --from=sdk /usr/share/dotnet /usr/share/dotnet
ENV DOTNET_ROOT=/usr/share/dotnet
ENV PATH=/usr/share/dotnet:$PATH
ENV DOTNET_NOLOGO=true
DOCKERFILE

- name: Set version
shell: pwsh
Expand All @@ -447,10 +433,12 @@ jobs:
./scripts/set-version.ps1 -Version $PackageVersion

- name: Restore dependencies
if: runner.os != 'Linux'
working-directory: src
run: dotnet restore UniGetUI.Avalonia/UniGetUI.Avalonia.csproj --runtime ${{ matrix.runtime }}

- name: Publish
if: runner.os != 'Linux'
working-directory: src
shell: pwsh
run: |
Expand All @@ -468,6 +456,39 @@ jobs:
}
dotnet @PublishArgs

- name: Publish (Linux cross-build container)
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail

DOCKER_ARGS=(
--rm
--user "$(id -u):$(id -g)"
--volume "${{ github.workspace }}:/source"
--workdir /source/src
--env HOME=/tmp
--env DOTNET_CLI_HOME=/tmp
--env NUGET_PACKAGES=/source/.nuget/packages
--env UNIGETUI_GITHUB_CLIENT_ID
--env UNIGETUI_GITHUB_CLIENT_SECRET
--env UNIGETUI_OPENSEARCH_USERNAME
--env UNIGETUI_OPENSEARCH_PASSWORD
)

PUBLISH_ARGS=(
publish UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
--configuration Release
--runtime ${{ matrix.runtime }}
--self-contained true
--output ../bin/${{ matrix.name }}
/p:PublishProfile=${{ matrix.publish_profile }}
/p:SysRoot=${{ matrix.cross_sysroot }}
/p:LinkerFlavor=lld
)

docker run "${DOCKER_ARGS[@]}" unigetui-cross-build dotnet "${PUBLISH_ARGS[@]}"

- name: Remove oversized Linux debug symbols
if: runner.os == 'Linux'
shell: pwsh
Expand All @@ -484,6 +505,11 @@ jobs:
Write-Host ("Removed {0} oversized Linux debug symbols above {1:N2} MiB ({2:N2} MiB total)." -f $DebugSymbolsToRemove.Count, ($MaxDebugSymbolSizeBytes / 1MB), ($RemovedDebugSymbolBytes / 1MB))
}

- name: Verify Linux glibc baseline
if: runner.os == 'Linux'
shell: pwsh
run: ./scripts/check-linux-glibc.ps1 -Path "bin/${{ matrix.name }}" -MaxVersion '${{ env.LINUX_GLIBC_BASELINE }}'

- name: Install macOS packaging tools
if: runner.os == 'macOS'
run: brew install create-dmg
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ macOS builds are available from GitHub Releases. Use the `.dmg` for the standard

Linux builds are available from GitHub Releases. Use the `.deb` package for Debian/Ubuntu-based distributions, the `.rpm` package for Fedora/RHEL-based distributions, or the `.tar.gz` archive for a portable build.

The binaries require **glibc 2.27 or newer** (Ubuntu 18.04+, Debian 10+, RHEL 8+, Fedora 28+). On older distributions the app exits at startup with a `version 'GLIBC_x.y' not found` message.

| Architecture | `.deb` | `.rpm` | `.tar.gz` |
|---|---|---|---|
| x64 | [Download](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.linux-x64.deb) | [Download](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.linux-x64.rpm) | [Download](https://github.com/Devolutions/UniGetUI/releases/latest/download/UniGetUI.linux-x64.tar.gz) |
Expand Down
88 changes: 88 additions & 0 deletions scripts/check-linux-glibc.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env pwsh

[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $Path,

[string] $MaxVersion
)

$ErrorActionPreference = 'Stop'

if (-not (Get-Command readelf -ErrorAction SilentlyContinue)) {
throw "readelf was not found on PATH. Install binutils to inspect glibc requirements."
}

$ResolvedPath = (Resolve-Path -LiteralPath $Path).Path

$Candidates = if (Test-Path -LiteralPath $ResolvedPath -PathType Container) {
@(Get-ChildItem -LiteralPath $ResolvedPath -Recurse -File)
} else {
@(Get-Item -LiteralPath $ResolvedPath)
}

function Test-ElfHeader {
param(
[Parameter(Mandatory)]
[string] $FilePath
)

$Header = [byte[]]::new(4)
$Stream = [System.IO.File]::OpenRead($FilePath)
try { $Read = $Stream.Read($Header, 0, 4) } finally { $Stream.Dispose() }

return ($Read -eq 4 -and
$Header[0] -eq 0x7F -and $Header[1] -eq 0x45 -and
$Header[2] -eq 0x4C -and $Header[3] -eq 0x46)
}

$ElfFiles = @($Candidates | Where-Object { Test-ElfHeader -FilePath $_.FullName })
if ($ElfFiles.Count -eq 0) {
throw "No ELF binaries were found under '$ResolvedPath'"
}

$Ceiling = if ($MaxVersion) { [version] $MaxVersion } else { $null }
$HighestVersion = $null
$Offenders = @()

foreach ($File in $ElfFiles) {
$DynamicSymbols = (& readelf --wide --dyn-syms $File.FullName) -join "`n"
if ($LASTEXITCODE -ne 0) { throw "readelf exited $LASTEXITCODE for '$($File.FullName)'" }

$FileHighest = $null
foreach ($Match in [regex]::Matches($DynamicSymbols, '(?<symbol>[A-Za-z_][A-Za-z0-9_]*)@+GLIBC_(?<version>\d+(?:\.\d+)+)')) {
$Version = [version] $Match.Groups['version'].Value

if ($null -eq $FileHighest -or $Version -gt $FileHighest) { $FileHighest = $Version }
if ($null -eq $HighestVersion -or $Version -gt $HighestVersion) { $HighestVersion = $Version }

if ($Ceiling -and $Version -gt $Ceiling) {
$Offenders += [pscustomobject]@{
File = $File.Name
Symbol = $Match.Groups['symbol'].Value
Version = $Version
}
}
}

$Reported = if ($FileHighest) { "GLIBC_$FileHighest" } else { 'no versioned glibc symbols' }
Write-Host ("{0}: requires {1}" -f $File.Name, $Reported)
}

if ($null -eq $HighestVersion) {
throw "No versioned glibc symbols were found under '$ResolvedPath'"
}

Write-Host ("Highest glibc requirement: GLIBC_$HighestVersion")

if ($Offenders.Count -gt 0) {
Write-Host "References above the GLIBC_$Ceiling baseline:"
$Offenders |
Sort-Object -Property File, Symbol -Unique |
ForEach-Object { Write-Host " $($_.File): $($_.Symbol)@GLIBC_$($_.Version)" }

throw "Binaries require GLIBC_$HighestVersion but the supported baseline is GLIBC_$Ceiling; they will not start on older distributions."
}

Write-Output $HighestVersion.ToString()
15 changes: 14 additions & 1 deletion scripts/package-linux.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@

.PARAMETER IconSourcePath
Source path for the installed desktop icon.

.PARAMETER MinGlibcVersion
Minimum glibc version recorded in the .deb Depends field (e.g. "2.27").
When omitted, it is detected from the binaries in SourceDir.
#>

[CmdletBinding()]
Expand Down Expand Up @@ -81,7 +85,8 @@ param(
[string] $Url = 'https://github.com/Devolutions/UniGetUI',
[string] $AppExecutableName = 'UniGetUI',
[string] $LauncherName = 'unigetui',
[string] $IconSourcePath = (Join-Path $PSScriptRoot '..' 'src' 'SharedAssets' 'Assets' 'Images' 'icon.png')
[string] $IconSourcePath = (Join-Path $PSScriptRoot '..' 'src' 'SharedAssets' 'Assets' 'Images' 'icon.png'),
[string] $MinGlibcVersion = ''
)

$ErrorActionPreference = 'Stop'
Expand Down Expand Up @@ -179,13 +184,21 @@ try {
$ControlDir = Join-Path $TmpDir 'control'
New-Item -ItemType Directory -Path $ControlDir | Out-Null

$GlibcVersion = $MinGlibcVersion
if (-not $GlibcVersion) {
$GlibcVersion = & (Join-Path $PSScriptRoot 'check-linux-glibc.ps1') -Path $SourceDir |
Select-Object -Last 1
}
if (-not $GlibcVersion) { throw "Unable to determine the glibc requirement for '$SourceDir'" }

# dpkg requires LF line endings and a trailing newline in control files
$ControlLines = @(
"Package: $PackageName",
"Version: $Version",
"Architecture: $Architecture",
"Maintainer: $Maintainer",
"Installed-Size: $InstalledSizeKb",
"Depends: libc6 (>= $GlibcVersion)",
"Homepage: $Url",
"Description: $Description",
"Priority: optional",
Expand Down