diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b40a9fe..062f619 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,6 +33,10 @@ jobs: artifact: linux binary: dscpkg test_args: -race + - os: ubuntu-24.04-arm + artifact: linux-arm64 + binary: dscpkg + test_args: -race - os: windows-latest artifact: windows binary: dscpkg.exe diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3f71a99..ae213d0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -56,6 +56,10 @@ jobs: with: name: dscpkg-release-linux path: dist/linux + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: dscpkg-release-linux-arm64 + path: dist/linux-arm64 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: dscpkg-release-windows diff --git a/README.md b/README.md index 1e02b74..2f7487b 100644 --- a/README.md +++ b/README.md @@ -205,21 +205,21 @@ go build -trimpath -o dist/dscpkg ./cmd/dscpkg ``` The GitHub Actions workflow runs formatting, vetting, tests (with race detection on -Linux and macOS), and builds on Linux, Windows, and macOS. It uploads one executable artifact -for each runner operating system. Go build caching is enabled through -`actions/setup-go`. - -CI also builds and inspects release archives and native installers. Windows MSI, -Ubuntu DEB, and containerized Fedora RPM tests cover fresh installation, -reinstallation/repair, upgrade replacement using isolated fixtures, uninstall, -and preservation of downloaded resources and caches. Windows checks system PATH -registration/removal and rejects downgrades. Installer tests run only on +Linux and macOS), and builds on Linux AMD64/ARM64, Windows, and macOS. It uploads +one executable artifact for each runner operating system and architecture. +Go build caching is enabled through `actions/setup-go`. + +CI also builds and inspects release archives and native installers. Windows MSI +tests and Linux AMD64/ARM64 Ubuntu DEB and containerized Fedora RPM tests cover +fresh installation, reinstallation/repair, upgrade replacement using isolated +fixtures, uninstall, and preservation of downloaded resources and caches. +Windows checks system PATH registration/removal and rejects downgrades. Installer tests run only on disposable GitHub-hosted runners, never on a developer workstation. ## Release downloads and installation -Releases include Linux AMD64 and Windows AMD64 archives, a macOS archive for the -native architecture of the macOS CI runner, Windows MSI, Linux DEB/RPM, and +Releases include Linux AMD64/ARM64 and Windows AMD64 archives, a macOS archive for the +native architecture of the macOS CI runner, Windows MSI, Linux AMD64/ARM64 DEB/RPM, and `SHA256SUMS`. macOS filenames use Go's `darwin` platform name and the actual `amd64` or `arm64` build architecture. Extract an archive and put its `bin` directory on PATH, or use the appropriate native installer: @@ -230,6 +230,9 @@ sudo apt install ./dscpkg_1.2.3_amd64.deb sudo dnf install ./dscpkg-1.2.3-1.x86_64.rpm ``` +On ARM64 Linux, use `dscpkg_1.2.3_arm64.deb` or +`dscpkg-1.2.3-1.aarch64.rpm` instead. + On Windows, run `dscpkg-v1.2.3-windows-amd64.msi` as administrator, or use: ```powershell @@ -272,9 +275,9 @@ go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0 # Ensure Go's bin directory is on PATH. Go may download nFPM's required toolchain. ``` -On each native platform, build one executable with CGO disabled and pass that -same binary to the packager. The output directory must be empty. For example, -in PowerShell on Windows: +On each native platform (including native AMD64 or ARM64 Linux), build one +executable with CGO disabled and pass that same binary to the packager. The output +directory must be empty. For example, in PowerShell on Windows: ```powershell $env:CGO_ENABLED = '0' diff --git a/packaging/common.ps1 b/packaging/common.ps1 index c15af32..cf61963 100644 --- a/packaging/common.ps1 +++ b/packaging/common.ps1 @@ -25,10 +25,12 @@ function Get-AssetNames { param([string] $Version, [string] $Platform) $packageVersion = & "$PSScriptRoot/version.ps1" -Version $Version switch -Regex -CaseSensitive ($Platform) { - '^linux-amd64$' { - "dscpkg-$Version-linux-amd64.tar.gz" - "dscpkg_${packageVersion}_amd64.deb" - "dscpkg-${packageVersion}-1.x86_64.rpm" + '^linux-(amd64|arm64)$' { + $arch = $Matches[1] + $rpmArch = if ($arch -eq 'arm64') { 'aarch64' } else { 'x86_64' } + "dscpkg-$Version-$Platform.tar.gz" + "dscpkg_${packageVersion}_${arch}.deb" + "dscpkg-${packageVersion}-1.$rpmArch.rpm" } '^windows-amd64$' { "dscpkg-$Version-windows-amd64.zip" diff --git a/packaging/linux/architecture.sh b/packaging/linux/architecture.sh new file mode 100644 index 0000000..59e761a --- /dev/null +++ b/packaging/linux/architecture.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +case "$(uname -m)" in + x86_64) export PACKAGE_ARCH=amd64 RPM_ARCH=x86_64 ;; + aarch64|arm64) export PACKAGE_ARCH=arm64 RPM_ARCH=aarch64 ;; + *) echo "Unsupported Linux package architecture: $(uname -m)" >&2; exit 1 ;; +esac diff --git a/packaging/linux/build.sh b/packaging/linux/build.sh index badd10e..4d8bcbe 100644 --- a/packaging/linux/build.sh +++ b/packaging/linux/build.sh @@ -4,6 +4,8 @@ trap 'printf "ERROR %s:%s: %s\n" "$0" "$LINENO" "$BASH_COMMAND" >&2' ERR [[ $# == 3 ]] || { echo "Usage: $0 VERSION BINARY OUTPUT_DIRECTORY" >&2; exit 1; } root=$(cd "$(dirname "$0")/../.." && pwd) +# shellcheck source=packaging/linux/architecture.sh +source "$root/packaging/linux/architecture.sh" export VERSION VERSION=$(pwsh -NoProfile -File "$root/packaging/version.ps1" -Version "$1") export BINARY_PATH @@ -11,8 +13,8 @@ BINARY_PATH=$(realpath "$2") output=$(realpath "$3") cd "$root" [[ $(go version -m "$BINARY_PATH") == *'GOOS=linux'* ]] -[[ $(go version -m "$BINARY_PATH") == *'GOARCH=amd64'* ]] +[[ $(go version -m "$BINARY_PATH") == *"GOARCH=$PACKAGE_ARCH"* ]] nfpm package --config packaging/linux/nfpm.yaml --packager deb \ - --target "$output/dscpkg_${VERSION}_amd64.deb" + --target "$output/dscpkg_${VERSION}_${PACKAGE_ARCH}.deb" nfpm package --config packaging/linux/nfpm.yaml --packager rpm \ - --target "$output/dscpkg-${VERSION}-1.x86_64.rpm" + --target "$output/dscpkg-${VERSION}-1.$RPM_ARCH.rpm" diff --git a/packaging/linux/inspect.sh b/packaging/linux/inspect.sh index 8c8402b..5982233 100644 --- a/packaging/linux/inspect.sh +++ b/packaging/linux/inspect.sh @@ -4,16 +4,18 @@ trap 'printf "ERROR %s:%s: %s\n" "$0" "$LINENO" "$BASH_COMMAND" >&2' ERR [[ $# == 3 ]] || { echo "Usage: $0 VERSION BINARY OUTPUT_DIRECTORY" >&2; exit 1; } root=$(cd "$(dirname "$0")/../.." && pwd) +# shellcheck source=packaging/linux/architecture.sh +source "$root/packaging/linux/architecture.sh" version=$(pwsh -NoProfile -File "$root/packaging/version.ps1" -Version "$1") binary=$(realpath "$2") output=$(realpath "$3") -deb="$output/dscpkg_${version}_amd64.deb" -rpm="$output/dscpkg-${version}-1.x86_64.rpm" +deb="$output/dscpkg_${version}_${PACKAGE_ARCH}.deb" +rpm="$output/dscpkg-${version}-1.$RPM_ARCH.rpm" [[ $(dpkg-deb -f "$deb" Package) == dscpkg ]] [[ $(dpkg-deb -f "$deb" Version) == "$version" ]] -[[ $(dpkg-deb -f "$deb" Architecture) == amd64 ]] +[[ $(dpkg-deb -f "$deb" Architecture) == "$PACKAGE_ARCH" ]] [[ -z $(dpkg-deb -f "$deb" Depends) ]] -[[ $(rpm -qp --qf '%{NAME} %{VERSION} %{RELEASE} %{ARCH}' "$rpm") == "dscpkg $version 1 x86_64" ]] +[[ $(rpm -qp --qf '%{NAME} %{VERSION} %{RELEASE} %{ARCH}' "$rpm") == "dscpkg $version 1 $RPM_ARCH" ]] [[ -z $(rpm -qp --scripts "$rpm") ]] rpm --checksig "$rpm" stage=$(mktemp -d) diff --git a/packaging/linux/nfpm.yaml b/packaging/linux/nfpm.yaml index c6d822b..bfdcbd8 100644 --- a/packaging/linux/nfpm.yaml +++ b/packaging/linux/nfpm.yaml @@ -1,5 +1,5 @@ name: dscpkg -arch: amd64 +arch: ${PACKAGE_ARCH} platform: linux version: ${VERSION} maintainer: dscpkg contributors diff --git a/packaging/linux/test-install.sh b/packaging/linux/test-install.sh index 568adda..52c5d04 100644 --- a/packaging/linux/test-install.sh +++ b/packaging/linux/test-install.sh @@ -7,6 +7,9 @@ if [[ $# != 7 || $1 != --disposable || ${GITHUB_ACTIONS:-} != true || echo "Usage (disposable GitHub-hosted runner/container only): $0 --disposable deb|rpm VERSION ASSETS BINARY FIXTURES LOGS" >&2 exit 1 fi +root=$(cd "$(dirname "$0")/../.." && pwd) +# shellcheck source=packaging/linux/architecture.sh +source "$root/packaging/linux/architecture.sh" format=$2 version=${3#v} assets=$(realpath "$4") @@ -33,8 +36,8 @@ for file in "${sentinels[@]}"; do printf 'preserve-me\n' > "$file"; done installed=false package_path() { - if [[ $format == deb ]]; then printf '%s/dscpkg_%s_amd64.deb' "$1" "$2" - else printf '%s/dscpkg-%s-1.x86_64.rpm' "$1" "$2"; fi + if [[ $format == deb ]]; then printf '%s/dscpkg_%s_%s.deb' "$1" "$2" "$PACKAGE_ARCH" + else printf '%s/dscpkg-%s-1.%s.rpm' "$1" "$2" "$RPM_ARCH"; fi } install_package() { if [[ $format == deb ]]; then dpkg -i "$1"; else rpm -U --replacepkgs "$1"; fi diff --git a/packaging/release.ps1 b/packaging/release.ps1 index 949245f..92f7075 100644 --- a/packaging/release.ps1 +++ b/packaging/release.ps1 @@ -61,7 +61,7 @@ if ($Operation -eq 'Preflight') { if (-not $AssetDirectory) { throw 'Draft creation requires AssetDirectory.' } $downloads = (Resolve-Path -LiteralPath $AssetDirectory).Path $directories = @(Get-ChildItem -LiteralPath $downloads -Force) -Assert-Equal (($directories.Name | Sort-Object) -join ',') 'linux,macos,windows' 'Downloaded platform directories' +Assert-Equal (($directories.Name | Sort-Object) -join ',') 'linux,linux-arm64,macos,windows' 'Downloaded platform directories' foreach ($directory in $directories) { if (-not $directory.PSIsContainer) { throw "Expected a platform directory: $($directory.Name)" } } @@ -72,6 +72,7 @@ Assert-Equal $macArchives.Count 1 'macOS archive count' $macPlatform = [regex]::Match($macArchives[0].Name, $macPattern).Groups[1].Value $platforms = @( @{ Directory = 'linux'; Platform = 'linux-amd64' }, + @{ Directory = 'linux-arm64'; Platform = 'linux-arm64' }, @{ Directory = 'windows'; Platform = 'windows-amd64' }, @{ Directory = 'macos'; Platform = $macPlatform } ) diff --git a/packaging/test-release.ps1 b/packaging/test-release.ps1 index 07cd7b5..54246be 100644 --- a/packaging/test-release.ps1 +++ b/packaging/test-release.ps1 @@ -3,7 +3,7 @@ $releaseScript = Join-Path $PSScriptRoot 'release.ps1' $stage = Join-Path ([IO.Path]::GetTempPath()) ("dscpkg-release-test-" + [guid]::NewGuid()) $global:DscpkgReleaseTestState = @{ Commit = ''; TagObject = $null; Releases = '[[]]'; FailAPI = $false - Created = 0; Assets = @(); Arguments = @() + Created = 0; Assets = @(); Arguments = @(); ChecksumLines = @() } function global:gh { @@ -32,6 +32,9 @@ function global:gh { $state.Arguments = $Arguments $state.Assets = @($Arguments | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | ForEach-Object { Split-Path $_ -Leaf }) + $manifest = @($Arguments | Where-Object { (Split-Path $_ -Leaf) -ceq 'SHA256SUMS' }) + Assert-Equal $manifest.Count 1 'Combined checksum manifest count' + $state.ChecksumLines = @(Get-Content -LiteralPath $manifest[0]) return } throw "Unexpected gh command: $Arguments" @@ -54,6 +57,7 @@ function New-TestAssets { param([string] $Directory) foreach ($entry in @( @{ Name = 'linux'; Platform = 'linux-amd64' }, + @{ Name = 'linux-arm64'; Platform = 'linux-arm64' }, @{ Name = 'windows'; Platform = 'windows-amd64' }, @{ Name = 'macos'; Platform = 'darwin-arm64' } )) { @@ -142,7 +146,18 @@ try { New-TestAssets $downloads & $releaseScript -Operation Draft @parameters -AssetDirectory $downloads Assert-Equal $global:DscpkgReleaseTestState.Created 1 'Draft creation count' - Assert-Equal $global:DscpkgReleaseTestState.Assets.Count 7 'Draft asset count' + $expectedAssets = @( + 'dscpkg-v1.2.3-linux-amd64.tar.gz', 'dscpkg_1.2.3_amd64.deb', 'dscpkg-1.2.3-1.x86_64.rpm', + 'dscpkg-v1.2.3-linux-arm64.tar.gz', 'dscpkg_1.2.3_arm64.deb', 'dscpkg-1.2.3-1.aarch64.rpm', + 'dscpkg-v1.2.3-windows-amd64.zip', 'dscpkg-v1.2.3-windows-amd64.msi', + 'dscpkg-v1.2.3-darwin-arm64.tar.gz', 'SHA256SUMS' + ) + Assert-Equal (($global:DscpkgReleaseTestState.Assets | Sort-Object) -join ',') ` + (($expectedAssets | Sort-Object) -join ',') 'Draft assets for both Linux architectures' + $expectedChecksums = @(Get-ChildItem -LiteralPath $downloads -Recurse -File -Filter SHA256SUMS | + ForEach-Object { Get-Content -LiteralPath $_.FullName }) + Assert-Equal (($global:DscpkgReleaseTestState.ChecksumLines | Sort-Object) -join "`n") ` + (($expectedChecksums | Sort-Object) -join "`n") 'Combined platform checksums' foreach ($flag in @('--draft', '--verify-tag', '--generate-notes')) { if ($global:DscpkgReleaseTestState.Arguments -cnotcontains $flag) { throw "Missing release flag: $flag" } } @@ -156,6 +171,22 @@ try { Assert-Rejected { & $releaseScript -Operation Draft @parameters -AssetDirectory $downloads } 'gh failed' $global:DscpkgReleaseTestState.FailAPI = $false + $armDirectory = Join-Path $downloads 'linux-arm64' + $savedArmDirectory = Join-Path $stage 'saved-linux-arm64' + Move-Item -LiteralPath $armDirectory -Destination $savedArmDirectory + Assert-Rejected { & $releaseScript -Operation Draft @parameters -AssetDirectory $downloads } 'Downloaded platform directories' + Move-Item -LiteralPath $savedArmDirectory -Destination $armDirectory + foreach ($name in @(Get-AssetNames 'v1.2.3' 'linux-arm64')) { + $armAsset = Join-Path $armDirectory $name + $armBytes = [IO.File]::ReadAllBytes($armAsset) + Add-Content -LiteralPath $armAsset -Value corrupt + Assert-Rejected { & $releaseScript -Operation Draft @parameters -AssetDirectory $downloads } 'Checksum for' + [IO.File]::WriteAllBytes($armAsset, $armBytes) + Remove-Item -LiteralPath $armAsset + Assert-Rejected { & $releaseScript -Operation Draft @parameters -AssetDirectory $downloads } 'Files in' + [IO.File]::WriteAllBytes($armAsset, $armBytes) + } + $extra = Join-Path $downloads 'linux/unexpected.txt' Set-Content -LiteralPath $extra -Value extra Assert-Rejected { & $releaseScript -Operation Draft @parameters -AssetDirectory $downloads } 'Files in'