diff --git a/.github/workflows/release-aur.yml b/.github/workflows/release-aur.yml index 309d768a9..706641dd1 100644 --- a/.github/workflows/release-aur.yml +++ b/.github/workflows/release-aur.yml @@ -1,29 +1,28 @@ name: release-aur on: - # Trigger after release-linux completes (it uploads amd64 tarball) workflow_run: workflows: ["release-linux"] types: [completed] workflow_dispatch: inputs: version: - description: "Version to publish (e.g. 1.0.0)" + description: "Version to prepare (e.g. 2.1.0)" required: true type: string pkgrel: - description: "Package release number (bump for same-version rebuilds)" + description: "Package release number" required: false default: "1" type: string publish: - description: "Push the generated PKGBUILD/.SRCINFO to AUR" + description: "Push all four generated packages to AUR" required: false default: false type: boolean concurrency: - group: aur-publish + group: aur-prepare-publish cancel-in-progress: false permissions: @@ -31,197 +30,268 @@ permissions: jobs: update-aur: - runs-on: ubuntu-latest - # Only run on manual dispatch, or successful stable tag releases (no -rc) - if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'v') && !contains(github.event.workflow_run.head_branch, '-')) }} + runs-on: ubuntu-24.04 + # workflow_run prepares verified metadata only. Publishing is deliberately + # manual because the maintainer's AUR key requires an interactive passkey. + # Accept only a successful tag push produced inside this repository; the + # exact tag and main ancestry are checked again after checkout. + if: > + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_repository.full_name == github.repository && + startsWith(github.event.workflow_run.head_branch, 'v') && + !contains(github.event.workflow_run.head_branch, '-')) steps: - - name: Checkout + - name: Checkout app repo uses: actions/checkout@v7 with: ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.ref }} + fetch-depth: 0 - - name: Determine version - id: version + - name: Resolve and verify release + id: release env: EVENT_NAME: ${{ github.event_name }} - WORKFLOW_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} DISPATCH_VERSION: ${{ inputs.version }} + DISPATCH_PUBLISH: ${{ inputs.publish }} + DISPATCH_PKGREL: ${{ inputs.pkgrel }} run: | - if [[ "$EVENT_NAME" == "workflow_run" ]]; then - # Extract version from the triggering workflow's head branch (tag) - REF="$WORKFLOW_HEAD_BRANCH" - if [[ ! "$REF" =~ ^v[0-9]+(\.[0-9]+){1,3}([.-][0-9A-Za-z.-]+)?$ ]]; then - echo "Invalid workflow_run head branch for release: $REF" - exit 1 - fi - VERSION="${REF#v}" + set -euo pipefail + + if [[ "$EVENT_NAME" == workflow_run ]]; then + tag="$RUN_HEAD_BRANCH" + version="${tag#v}" + pkgrel=1 + should_publish=false else - VERSION="$DISPATCH_VERSION" + version="$DISPATCH_VERSION" + tag="v${version}" + pkgrel="${DISPATCH_PKGREL:-1}" + should_publish="${DISPATCH_PUBLISH:-false}" fi - if [[ ! "$VERSION" =~ ^[0-9]+(\.[0-9]+){2}([.-][0-9A-Za-z.-]+)?$ ]]; then - echo "Invalid release version: $VERSION" + + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::Invalid release version: $version" exit 1 fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "Detected version: $VERSION" + if [[ ! "$pkgrel" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::Invalid pkgrel: $pkgrel" + exit 1 + fi + if [[ "$should_publish" == true && ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::AUR publishing is limited to stable x.y.z releases" + exit 1 + fi + + if [[ "$EVENT_NAME" == workflow_run || "$should_publish" == true ]]; then + git fetch origin main --tags + if ! git show-ref --verify --quiet "refs/tags/${tag}"; then + echo "::error::Release tag ${tag} does not exist" + exit 1 + fi + tag_sha="$(git rev-parse "refs/tags/${tag}^{commit}")" + if [[ "$EVENT_NAME" == workflow_run && "$tag_sha" != "$RUN_HEAD_SHA" ]]; then + echo "::error::workflow_run SHA does not match ${tag}" + exit 1 + fi + if ! git merge-base --is-ancestor "$tag_sha" origin/main; then + echo "::error::${tag} is not on main" + exit 1 + fi + fi + + { + echo "version=$version" + echo "tag=$tag" + echo "pkgrel=$pkgrel" + echo "should_publish=$should_publish" + } >> "$GITHUB_OUTPUT" - name: Wait for all release assets env: - VERSION: ${{ steps.version.outputs.version }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.release.outputs.version }} run: | - echo "Waiting for release assets to be available..." - echo "Note: amd64 should be ready (release-linux completed), waiting for aarch64..." - MAX_WAIT=1800 - INTERVAL=30 - ELAPSED=0 + set -euo pipefail + assets=( + "qbz_${VERSION}_amd64.tar.gz" + "qbz_${VERSION}_aarch64.tar.gz" + "qbzd-${VERSION}-linux-amd64.tar.gz" + "qbzd-${VERSION}-linux-aarch64.tar.gz" + "qbz-${VERSION}-cargo-vendor.tar.xz" + ) - while [ $ELAPSED -lt $MAX_WAIT ]; do - echo "Checking for release assets (${ELAPSED}s elapsed)..." - - # Check if both tarballs exist (follow redirects, GitHub returns 302) - AMD64_CODE=$(curl -sL -o /dev/null -w "%{http_code}" "https://github.com/vicrodh/qbz/releases/download/v${VERSION}/qbz_${VERSION}_amd64.tar.gz") - AARCH64_CODE=$(curl -sL -o /dev/null -w "%{http_code}" "https://github.com/vicrodh/qbz/releases/download/v${VERSION}/qbz_${VERSION}_aarch64.tar.gz") - AMD64_EXISTS=$([[ "$AMD64_CODE" == "200" ]] && echo "true" || echo "false") - AARCH64_EXISTS=$([[ "$AARCH64_CODE" == "200" ]] && echo "true" || echo "false") - - if [ "$AMD64_EXISTS" = "true" ] && [ "$AARCH64_EXISTS" = "true" ]; then - echo "Both release assets are available!" + for attempt in $(seq 1 60); do + missing=() + for asset in "${assets[@]}"; do + url="https://github.com/vicrodh/qbz/releases/download/v${VERSION}/${asset}" + code="$(curl -sL -o /dev/null -w '%{http_code}' "$url")" + [[ "$code" == 200 ]] || missing+=("$asset") + done + if [[ ${#missing[@]} -eq 0 ]]; then + echo "All AUR assets are available" break fi - - echo "Waiting for assets... (amd64: $AMD64_EXISTS, aarch64: $AARCH64_EXISTS)" - sleep $INTERVAL - ELAPSED=$((ELAPSED + INTERVAL)) + if [[ "$attempt" -eq 60 ]]; then + printf '::error::Timed out waiting for %s\n' "${missing[*]}" + exit 1 + fi + echo "Attempt ${attempt}/60; waiting for: ${missing[*]}" + sleep 30 done - if [ $ELAPSED -ge $MAX_WAIT ]; then - echo "Timeout waiting for release assets after 30 minutes" - exit 1 - fi - - - name: Download release tarballs and get checksums + - name: Download sources and calculate checksums id: checksums env: - VERSION: ${{ steps.version.outputs.version }} + VERSION: ${{ steps.release.outputs.version }} run: | - # Download x86_64 tarball - AMD64_URL="https://github.com/vicrodh/qbz/releases/download/v${VERSION}/qbz_${VERSION}_amd64.tar.gz" - echo "Downloading: $AMD64_URL" - curl -fsSL -o /tmp/qbz_amd64.tar.gz "$AMD64_URL" - SHA256_AMD64=$(sha256sum /tmp/qbz_amd64.tar.gz | cut -d' ' -f1) - echo "sha256_amd64=$SHA256_AMD64" >> "$GITHUB_OUTPUT" - echo "x86_64 Checksum: $SHA256_AMD64" + set -euo pipefail + mkdir -p /tmp/aur-sources - # Download aarch64 tarball - AARCH64_URL="https://github.com/vicrodh/qbz/releases/download/v${VERSION}/qbz_${VERSION}_aarch64.tar.gz" - echo "Downloading: $AARCH64_URL" - curl -fsSL -o /tmp/qbz_aarch64.tar.gz "$AARCH64_URL" - SHA256_AARCH64=$(sha256sum /tmp/qbz_aarch64.tar.gz | cut -d' ' -f1) - echo "sha256_aarch64=$SHA256_AARCH64" >> "$GITHUB_OUTPUT" - echo "aarch64 Checksum: $SHA256_AARCH64" + download_hash() { + local output_name="$1" url="$2" file="$3" sum + curl -fsSL "$url" -o "/tmp/aur-sources/$file" + sum="$(sha256sum "/tmp/aur-sources/$file" | cut -d' ' -f1)" + echo "${output_name}=${sum}" >> "$GITHUB_OUTPUT" + echo "${file}: ${sum}" + } - - name: Setup SSH for AUR - if: github.event_name == 'workflow_run' || inputs.publish + base="https://github.com/vicrodh/qbz/releases/download/v${VERSION}" + download_hash qbz_amd64 "$base/qbz_${VERSION}_amd64.tar.gz" qbz-amd64.tar.gz + download_hash qbz_aarch64 "$base/qbz_${VERSION}_aarch64.tar.gz" qbz-aarch64.tar.gz + download_hash qbzd_amd64 "$base/qbzd-${VERSION}-linux-amd64.tar.gz" qbzd-amd64.tar.gz + download_hash qbzd_aarch64 "$base/qbzd-${VERSION}-linux-aarch64.tar.gz" qbzd-aarch64.tar.gz + download_hash vendor "$base/qbz-${VERSION}-cargo-vendor.tar.xz" vendor.tar.xz + download_hash source \ + "https://github.com/vicrodh/qbz/archive/refs/tags/v${VERSION}.tar.gz" \ + source.tar.gz + + - name: Configure AUR SSH + if: steps.release.outputs.should_publish == 'true' env: AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} run: | + set -euo pipefail if [[ -z "$AUR_SSH_PRIVATE_KEY" ]]; then - echo "::error::AUR_SSH_PRIVATE_KEY is required when publishing" + echo "::error::AUR_SSH_PRIVATE_KEY is required for publishing" exit 1 fi - mkdir -p ~/.ssh - echo "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur - chmod 600 ~/.ssh/aur - echo "Host aur.archlinux.org" >> ~/.ssh/config - echo " IdentityFile ~/.ssh/aur" >> ~/.ssh/config - echo " User aur" >> ~/.ssh/config + install -d -m 700 ~/.ssh + install -m 600 /dev/null ~/.ssh/aur + printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur + { + echo "Host aur.archlinux.org" + echo " IdentityFile ~/.ssh/aur" + echo " IdentitiesOnly yes" + echo " User aur" + } >> ~/.ssh/config ssh-keyscan -t ed25519 aur.archlinux.org >> ~/.ssh/known_hosts - - name: Clone AUR repo + - name: Clone AUR package repos env: - SHOULD_PUBLISH: ${{ github.event_name == 'workflow_run' || inputs.publish }} + SHOULD_PUBLISH: ${{ steps.release.outputs.should_publish }} run: | - if [[ "$SHOULD_PUBLISH" == "true" ]]; then - git clone ssh://aur@aur.archlinux.org/qbz-bin.git /tmp/aur-qbz-bin - else - git clone https://aur.archlinux.org/qbz-bin.git /tmp/aur-qbz-bin - fi - cd /tmp/aur-qbz-bin - # Ensure we are on master (recover from detached HEAD if remote branch was corrupted) - git checkout master 2>/dev/null || git checkout -b master + set -euo pipefail + for package in qbz-bin qbz qbzd-bin qbzd; do + destination="/tmp/aur-${package}" + if [[ "$SHOULD_PUBLISH" == true ]]; then + url="ssh://aur@aur.archlinux.org/${package}.git" + else + url="https://aur.archlinux.org/${package}.git" + fi - - name: Determine pkgrel - id: pkgrel + if ! git clone "$url" "$destination"; then + if [[ "$SHOULD_PUBLISH" == true ]]; then + echo "::error::Could not clone AUR repository for ${package}" + exit 1 + fi + # qbzd packages do not exist until their first AUR push. + mkdir -p "$destination" + git -C "$destination" init -b master + git -C "$destination" remote add origin "$url" + fi + git -C "$destination" checkout master 2>/dev/null || \ + git -C "$destination" checkout -b master + done + + - name: Stamp PKGBUILDs env: - EVENT_NAME: ${{ github.event_name }} - DISPATCH_PKGREL: ${{ inputs.pkgrel }} + VERSION: ${{ steps.release.outputs.version }} + PKGREL: ${{ steps.release.outputs.pkgrel }} + QBZ_AMD64: ${{ steps.checksums.outputs.qbz_amd64 }} + QBZ_AARCH64: ${{ steps.checksums.outputs.qbz_aarch64 }} + QBZD_AMD64: ${{ steps.checksums.outputs.qbzd_amd64 }} + QBZD_AARCH64: ${{ steps.checksums.outputs.qbzd_aarch64 }} + SOURCE_SHA: ${{ steps.checksums.outputs.source }} + VENDOR_SHA: ${{ steps.checksums.outputs.vendor }} run: | - if [[ "$EVENT_NAME" == "workflow_run" ]]; then - PKGREL=1 - else - PKGREL="${DISPATCH_PKGREL:-1}" - fi - if [[ ! "$PKGREL" =~ ^[1-9][0-9]*$ ]]; then - echo "Invalid pkgrel: $PKGREL" - exit 1 - fi - echo "pkgrel=$PKGREL" >> "$GITHUB_OUTPUT" + set -euo pipefail + for package in qbz-bin qbz qbzd-bin qbzd; do + cp "packaging/aur/${package}/"* "/tmp/aur-${package}/" + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "/tmp/aur-${package}/PKGBUILD" + sed -i "s/^pkgrel=.*/pkgrel=${PKGREL}/" "/tmp/aur-${package}/PKGBUILD" + done - - name: Update PKGBUILD + sed -i "s/^sha256sums_x86_64=.*/sha256sums_x86_64=('${QBZ_AMD64}')/" /tmp/aur-qbz-bin/PKGBUILD + sed -i "s/^sha256sums_aarch64=.*/sha256sums_aarch64=('${QBZ_AARCH64}')/" /tmp/aur-qbz-bin/PKGBUILD + sed -i "s/^sha256sums_x86_64=.*/sha256sums_x86_64=('${QBZD_AMD64}')/" /tmp/aur-qbzd-bin/PKGBUILD + sed -i "s/^sha256sums_aarch64=.*/sha256sums_aarch64=('${QBZD_AARCH64}')/" /tmp/aur-qbzd-bin/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SOURCE_SHA}' '${VENDOR_SHA}')/" /tmp/aur-qbz/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SOURCE_SHA}' '${VENDOR_SHA}')/" /tmp/aur-qbzd/PKGBUILD + + - name: Generate and validate .SRCINFO files + run: | + set -euo pipefail + for package in qbz-bin qbz qbzd-bin qbzd; do + docker run --rm \ + -e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \ + -v "/tmp/aur-${package}:/pkg" archlinux:latest bash -euc ' + useradd -m builder + chown -R builder:builder /pkg + runuser -u builder -- bash -c \ + "cd /pkg && makepkg --printsrcinfo > .SRCINFO" + chown -R "${HOST_UID}:${HOST_GID}" /pkg + ' + done + + - name: Commit and push all AUR packages + if: steps.release.outputs.should_publish == 'true' env: - VERSION: ${{ steps.version.outputs.version }} - PKGREL: ${{ steps.pkgrel.outputs.pkgrel }} - SHA256_AMD64: ${{ steps.checksums.outputs.sha256_amd64 }} - SHA256_AARCH64: ${{ steps.checksums.outputs.sha256_aarch64 }} + VERSION: ${{ steps.release.outputs.version }} run: | - cd /tmp/aur-qbz-bin - - # Copy PKGBUILD from main repo (source of truth) - cp ${{ github.workspace }}/packaging/aur/PKGBUILD . - - # Update version and pkgrel - sed -i "s/^pkgver=.*/pkgver=${VERSION}/" PKGBUILD - sed -i "s/^pkgrel=.*/pkgrel=${PKGREL}/" PKGBUILD - - # Update checksums for both architectures - sed -i "s/^sha256sums_x86_64=.*/sha256sums_x86_64=('${SHA256_AMD64}')/" PKGBUILD - sed -i "s/^sha256sums_aarch64=.*/sha256sums_aarch64=('${SHA256_AARCH64}')/" PKGBUILD - - echo "=== Updated PKGBUILD ===" - cat PKGBUILD - - - name: Generate .SRCINFO - run: | - cd /tmp/aur-qbz-bin - docker run --rm -v "$PWD":/pkg -w /pkg -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) archlinux:latest bash -c " - pacman -Sy --noconfirm pacman-contrib sudo - useradd -m builder - chown -R builder:builder /pkg - sudo -u builder makepkg --printsrcinfo > .SRCINFO - chown -R \$HOST_UID:\$HOST_GID /pkg - " - cat .SRCINFO - - - name: Commit and push to AUR - if: github.event_name == 'workflow_run' || inputs.publish - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - cd /tmp/aur-qbz-bin - git config user.name "GitHub Actions" - git config user.email "actions@github.com" - git add PKGBUILD .SRCINFO - git commit -m "Update to v${VERSION}" - git push + set -euo pipefail + for package in qbz-bin qbz qbzd-bin qbzd; do + directory="/tmp/aur-${package}" + git -C "$directory" config user.name "GitHub Actions" + git -C "$directory" config user.email "actions@github.com" + files=(PKGBUILD .SRCINFO) + [[ -f "$directory/qbzd.install" ]] && files+=(qbzd.install) + git -C "$directory" add "${files[@]}" + if git -C "$directory" diff --cached --quiet; then + echo "${package}: no metadata changes" + continue + fi + git -C "$directory" commit -m "Update to v${VERSION}" + git -C "$directory" push origin master + done - name: Upload dry-run package metadata - if: github.event_name == 'workflow_dispatch' && !inputs.publish + if: steps.release.outputs.should_publish != 'true' uses: actions/upload-artifact@v7 with: - name: aur-qbz-bin-dry-run + name: aur-four-package-metadata retention-days: 7 path: | /tmp/aur-qbz-bin/PKGBUILD /tmp/aur-qbz-bin/.SRCINFO + /tmp/aur-qbz/PKGBUILD + /tmp/aur-qbz/.SRCINFO + /tmp/aur-qbzd-bin/PKGBUILD + /tmp/aur-qbzd-bin/.SRCINFO + /tmp/aur-qbzd-bin/qbzd.install + /tmp/aur-qbzd/PKGBUILD + /tmp/aur-qbzd/.SRCINFO + /tmp/aur-qbzd/qbzd.install if-no-files-found: error diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD deleted file mode 100644 index 177028fd5..000000000 --- a/packaging/aur/PKGBUILD +++ /dev/null @@ -1,50 +0,0 @@ -# Maintainer: blitzkriegfc -pkgname=qbz-bin -pkgver=1.1.11 -pkgrel=1 -pkgdesc="Native Qobuz client for Linux with bit-perfect hi-res audio, DAC passthrough, Last.fm scrobbling, and Chromecast/DLNA casting" -arch=('x86_64' 'aarch64') -url="https://github.com/vicrodh/qbz" -license=('MIT') -# v2.1+ Qt binary: no webkit/gtk/appindicator. qt6-svg is load-bearing for -# the imageformats/qsvg plugin used by the shipped SVG icon set. -depends=('qt6-base>=6.8' 'qt6-declarative>=6.8' 'qt6-svg>=6.8' 'qt6-wayland' 'alsa-lib' 'fontconfig' 'freetype2' 'libpng' 'bzip2' 'expat' 'zlib' - 'libxkbcommon' 'wayland' 'libglvnd' 'dbus') -optdepends=( - 'alsa-utils: Required for bit-perfect device detection and better ALSA device names' - 'pipewire-alsa: PipeWire audio support' - 'pipewire-pulse: PulseAudio compatibility' - 'vulkan-driver: GPU-accelerated rendering (wgpu)' -) -provides=('qbz') -conflicts=('qbz') -options=('!strip') -source_x86_64=("${pkgname}-${pkgver}-x86_64.tar.gz::https://github.com/vicrodh/qbz/releases/download/v${pkgver}/qbz_${pkgver}_amd64.tar.gz") -source_aarch64=("${pkgname}-${pkgver}-aarch64.tar.gz::https://github.com/vicrodh/qbz/releases/download/v${pkgver}/qbz_${pkgver}_aarch64.tar.gz") -sha256sums_x86_64=('SKIP') -sha256sums_aarch64=('SKIP') - -package() { - if [ "$CARCH" = "x86_64" ]; then - _arch="amd64" - elif [ "$CARCH" = "aarch64" ]; then - _arch="aarch64" - fi - - cd "${srcdir}/qbz_${pkgver}_${_arch}" - - # Install binary - install -Dm755 "qbz" "${pkgdir}/usr/bin/qbz" - - # Install desktop file. MPRIS publishes DesktopEntry=com.blitzfc.qbz: - # the installed basename MUST match or the GNOME/Plasma media widget - # loses its icon (scripts/packaging/desktop_entry_check.sh). - install -Dm644 "qbz.desktop" "${pkgdir}/usr/share/applications/com.blitzfc.qbz.desktop" - - # Install icons (freedesktop hicolor structure) - install -Dm644 "icons/hicolor/32x32/apps/qbz.png" "${pkgdir}/usr/share/icons/hicolor/32x32/apps/qbz.png" - install -Dm644 "icons/hicolor/48x48/apps/qbz.png" "${pkgdir}/usr/share/icons/hicolor/48x48/apps/qbz.png" - install -Dm644 "icons/hicolor/64x64/apps/qbz.png" "${pkgdir}/usr/share/icons/hicolor/64x64/apps/qbz.png" - install -Dm644 "icons/hicolor/128x128/apps/qbz.png" "${pkgdir}/usr/share/icons/hicolor/128x128/apps/qbz.png" - install -Dm644 "icons/hicolor/256x256/apps/qbz.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/qbz.png" -} diff --git a/packaging/aur/qbz-bin/PKGBUILD b/packaging/aur/qbz-bin/PKGBUILD new file mode 100644 index 000000000..862a90f24 --- /dev/null +++ b/packaging/aur/qbz-bin/PKGBUILD @@ -0,0 +1,44 @@ +# Maintainer: blitzkriegfc +pkgname=qbz-bin +pkgver=0.0.0 +pkgrel=1 +pkgdesc="Native hi-fi Qobuz desktop player for Linux (prebuilt binary)" +arch=('x86_64' 'aarch64') +url="https://github.com/vicrodh/qbz" +license=('MIT') +depends=('qt6-base>=6.8' 'qt6-declarative>=6.8' 'qt6-svg>=6.8' 'qt6-wayland' 'alsa-lib' 'xdg-utils') +optdepends=( + 'alsa-utils: ALSA diagnostics and device inspection' + 'pipewire: PipeWire device discovery and sample-rate control' + 'libpulse: PulseAudio device discovery and routing through pactl' + 'jack2: JACK audio backend' +) +provides=('qbz') +conflicts=('qbz' 'qbz-git') +options=('!strip') +source_x86_64=("${pkgname}-${pkgver}-x86_64.tar.gz::https://github.com/vicrodh/qbz/releases/download/v${pkgver}/qbz_${pkgver}_amd64.tar.gz") +source_aarch64=("${pkgname}-${pkgver}-aarch64.tar.gz::https://github.com/vicrodh/qbz/releases/download/v${pkgver}/qbz_${pkgver}_aarch64.tar.gz") +sha256sums_x86_64=('SKIP') +sha256sums_aarch64=('SKIP') + +package() { + local release_arch + case "$CARCH" in + x86_64) release_arch=amd64 ;; + aarch64) release_arch=aarch64 ;; + esac + + cd "${srcdir}/qbz_${pkgver}_${release_arch}" + install -Dm755 qbz "${pkgdir}/usr/bin/qbz" + install -Dm644 qbz.desktop "${pkgdir}/usr/share/applications/com.blitzfc.qbz.desktop" + + local size + for size in 32 48 64 128 256 512; do + install -Dm644 "icons/hicolor/${size}x${size}/apps/qbz.png" \ + "${pkgdir}/usr/share/icons/hicolor/${size}x${size}/apps/qbz.png" + done + + install -Dm644 com.blitzfc.qbz.metainfo.xml \ + "${pkgdir}/usr/share/metainfo/com.blitzfc.qbz.metainfo.xml" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/packaging/aur/qbz/PKGBUILD b/packaging/aur/qbz/PKGBUILD new file mode 100644 index 000000000..e84a7b553 --- /dev/null +++ b/packaging/aur/qbz/PKGBUILD @@ -0,0 +1,65 @@ +# Maintainer: blitzkriegfc +pkgname=qbz +pkgver=0.0.0 +pkgrel=1 +pkgdesc="Native hi-fi Qobuz desktop player for Linux" +arch=('x86_64' 'aarch64') +url="https://github.com/vicrodh/qbz" +license=('MIT') +depends=('qt6-base>=6.8' 'qt6-declarative>=6.8' 'qt6-svg>=6.8' 'qt6-wayland' 'alsa-lib' 'xdg-utils') +makedepends=('rust' 'cmake' 'nasm' 'jack2') +# makepkg's GCC LTO objects cannot be consumed from Rust rlibs by rustc's lld. +options=('!lto') +optdepends=( + 'alsa-utils: ALSA diagnostics and device inspection' + 'pipewire: PipeWire device discovery and sample-rate control' + 'libpulse: PulseAudio device discovery and routing through pactl' + 'jack2: JACK audio backend' +) +provides=('qbz') +conflicts=('qbz-bin' 'qbz-git') +source=( + "${pkgname}-${pkgver}.tar.gz::https://github.com/vicrodh/qbz/archive/refs/tags/v${pkgver}.tar.gz" + "${pkgname}-${pkgver}-cargo-vendor.tar.xz::https://github.com/vicrodh/qbz/releases/download/v${pkgver}/qbz-${pkgver}-cargo-vendor.tar.xz" +) +sha256sums=('SKIP' 'SKIP') + +prepare() { + cd "${srcdir}/qbz-${pkgver}" + mkdir -p .cargo + printf '%s\n' \ + '[source.crates-io]' \ + 'replace-with = "vendored-sources"' \ + '' \ + '[source.vendored-sources]' \ + "directory = \"${srcdir}/qbz-${pkgver}-cargo-vendor\"" \ + '' \ + '[net]' \ + 'offline = true' > .cargo/config.toml +} + +build() { + cd "${srcdir}/qbz-${pkgver}" + # Arch's rust toolchain uses lld; cap its parallelism so this large final + # link does not exhaust RAM on typical builder machines. + QMAKE=/usr/bin/qmake6 cargo rustc --release --locked --offline \ + --manifest-path crates/Cargo.toml -p qbz-qt --bin qbz -- \ + -C link-arg=-Wl,--threads=4 +} + +package() { + cd "${srcdir}/qbz-${pkgver}" + install -Dm755 crates/target/release/qbz "${pkgdir}/usr/bin/qbz" + install -Dm644 packaging/linux/qbz.desktop \ + "${pkgdir}/usr/share/applications/com.blitzfc.qbz.desktop" + + local size + for size in 32 48 64 128 256 512; do + install -Dm644 "packaging/icons/${size}x${size}.png" \ + "${pkgdir}/usr/share/icons/hicolor/${size}x${size}/apps/qbz.png" + done + + install -Dm644 packaging/flatpak/com.blitzfc.qbz.metainfo.xml \ + "${pkgdir}/usr/share/metainfo/com.blitzfc.qbz.metainfo.xml" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/packaging/aur/qbzd-bin/PKGBUILD b/packaging/aur/qbzd-bin/PKGBUILD new file mode 100644 index 000000000..b41d8aca4 --- /dev/null +++ b/packaging/aur/qbzd-bin/PKGBUILD @@ -0,0 +1,40 @@ +# Maintainer: blitzkriegfc +pkgname=qbzd-bin +pkgver=0.0.0 +pkgrel=1 +pkgdesc="QBZ headless Qobuz daemon for servers and appliances (prebuilt binary)" +arch=('x86_64' 'aarch64') +url="https://github.com/vicrodh/qbz" +license=('MIT') +depends=('alsa-lib') +optdepends=( + 'pipewire: PipeWire device discovery and sample-rate control' + 'libpulse: PulseAudio device discovery and routing through pactl' + 'jack2: JACK audio backend' + 'xdg-utils: Open the login URL in a browser when one is available' +) +provides=('qbzd') +conflicts=('qbzd' 'qbzd-git') +options=('!strip') +install=qbzd.install +source_x86_64=("${pkgname}-${pkgver}-x86_64.tar.gz::https://github.com/vicrodh/qbz/releases/download/v${pkgver}/qbzd-${pkgver}-linux-amd64.tar.gz") +source_aarch64=("${pkgname}-${pkgver}-aarch64.tar.gz::https://github.com/vicrodh/qbz/releases/download/v${pkgver}/qbzd-${pkgver}-linux-aarch64.tar.gz") +sha256sums_x86_64=('SKIP') +sha256sums_aarch64=('SKIP') + +package() { + local release_arch + case "$CARCH" in + x86_64) release_arch=amd64 ;; + aarch64) release_arch=aarch64 ;; + esac + + cd "${srcdir}/qbzd-${pkgver}-linux-${release_arch}" + install -Dm755 qbzd "${pkgdir}/usr/bin/qbzd" + install -Dm644 qbzd.service "${pkgdir}/usr/lib/systemd/user/qbzd.service" + install -Dm644 completions/qbzd.bash "${pkgdir}/usr/share/bash-completion/completions/qbzd" + install -Dm644 completions/qbzd.zsh "${pkgdir}/usr/share/zsh/site-functions/_qbzd" + install -Dm644 completions/qbzd.fish "${pkgdir}/usr/share/fish/vendor_completions.d/qbzd.fish" + install -Dm644 README.md "${pkgdir}/usr/share/doc/${pkgname}/README.md" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/packaging/aur/qbzd-bin/qbzd.install b/packaging/aur/qbzd-bin/qbzd.install new file mode 100644 index 000000000..5b6e56c58 --- /dev/null +++ b/packaging/aur/qbzd-bin/qbzd.install @@ -0,0 +1,11 @@ +post_install() { + printf '%s\n' \ + 'qbzd ships a systemd user unit but supports systemd, OpenRC, and runit.' \ + 'For the init running on this host, generate a definition as the target user:' \ + ' qbzd service --user YOUR_USER --bin /usr/bin/qbzd' \ + 'The command detects the active init and prints exact install/enable instructions.' +} + +post_upgrade() { + post_install +} diff --git a/packaging/aur/qbzd/PKGBUILD b/packaging/aur/qbzd/PKGBUILD new file mode 100644 index 000000000..8dd0ef3cc --- /dev/null +++ b/packaging/aur/qbzd/PKGBUILD @@ -0,0 +1,65 @@ +# Maintainer: blitzkriegfc +pkgname=qbzd +pkgver=0.0.0 +pkgrel=1 +pkgdesc="QBZ headless Qobuz daemon for servers and appliances" +arch=('x86_64' 'aarch64') +url="https://github.com/vicrodh/qbz" +license=('MIT') +depends=('alsa-lib') +makedepends=('rust' 'cmake' 'nasm' 'jack2') +# makepkg's GCC LTO objects cannot be consumed from Rust rlibs by rustc's lld. +options=('!lto') +install=qbzd.install +optdepends=( + 'pipewire: PipeWire device discovery and sample-rate control' + 'libpulse: PulseAudio device discovery and routing through pactl' + 'jack2: JACK audio backend' + 'xdg-utils: Open the login URL in a browser when one is available' +) +provides=('qbzd') +conflicts=('qbzd-bin' 'qbzd-git') +source=( + "qbz-${pkgver}.tar.gz::https://github.com/vicrodh/qbz/archive/refs/tags/v${pkgver}.tar.gz" + "qbz-${pkgver}-cargo-vendor.tar.xz::https://github.com/vicrodh/qbz/releases/download/v${pkgver}/qbz-${pkgver}-cargo-vendor.tar.xz" +) +sha256sums=('SKIP' 'SKIP') + +prepare() { + cd "${srcdir}/qbz-${pkgver}" + mkdir -p .cargo + printf '%s\n' \ + '[source.crates-io]' \ + 'replace-with = "vendored-sources"' \ + '' \ + '[source.vendored-sources]' \ + "directory = \"${srcdir}/qbz-${pkgver}-cargo-vendor\"" \ + '' \ + '[net]' \ + 'offline = true' > .cargo/config.toml +} + +build() { + cd "${srcdir}/qbz-${pkgver}" + cargo build --release --locked --offline \ + --manifest-path crates/Cargo.toml -p qbzd +} + +package() { + cd "${srcdir}/qbz-${pkgver}" + local binary=crates/target/release/qbzd + + install -Dm755 "$binary" "${pkgdir}/usr/bin/qbzd" + install -Dm644 crates/qbzd/service/qbzd.service \ + "${pkgdir}/usr/lib/systemd/user/qbzd.service" + + "$binary" completions bash > "${srcdir}/qbzd.bash" + "$binary" completions zsh > "${srcdir}/qbzd.zsh" + "$binary" completions fish > "${srcdir}/qbzd.fish" + install -Dm644 "${srcdir}/qbzd.bash" "${pkgdir}/usr/share/bash-completion/completions/qbzd" + install -Dm644 "${srcdir}/qbzd.zsh" "${pkgdir}/usr/share/zsh/site-functions/_qbzd" + install -Dm644 "${srcdir}/qbzd.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/qbzd.fish" + install -Dm644 packaging/linux/qbzd-standalone-README.md \ + "${pkgdir}/usr/share/doc/${pkgname}/README.md" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/packaging/aur/qbzd/qbzd.install b/packaging/aur/qbzd/qbzd.install new file mode 100644 index 000000000..5b6e56c58 --- /dev/null +++ b/packaging/aur/qbzd/qbzd.install @@ -0,0 +1,11 @@ +post_install() { + printf '%s\n' \ + 'qbzd ships a systemd user unit but supports systemd, OpenRC, and runit.' \ + 'For the init running on this host, generate a definition as the target user:' \ + ' qbzd service --user YOUR_USER --bin /usr/bin/qbzd' \ + 'The command detects the active init and prints exact install/enable instructions.' +} + +post_upgrade() { + post_install +} diff --git a/scripts/packaging/cargo_vendor_release.sh b/scripts/packaging/cargo_vendor_release.sh new file mode 100755 index 000000000..f40dba14a --- /dev/null +++ b/scripts/packaging/cargo_vendor_release.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Build the Cargo vendor archive consumed by the source AUR and Gentoo +# packages. Cargo needs metadata for target-conditional packages while +# resolving the locked graph, so this must include every registry entry in +# Cargo.lock even though Linux compiles only a subset. The expanded tree is +# large, but deterministic xz compression keeps the release asset reasonable. + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 VERSION OUTPUT_DIR" >&2 + exit 2 +fi + +version="$1" +output_dir="$2" +manifest="crates/Cargo.toml" + +if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "invalid version: $version" >&2 + exit 2 +fi + +mkdir -p "$output_dir" +output_dir="$(cd "$output_dir" && pwd)" +work_dir="$(mktemp -d)" +trap 'rm -rf "$work_dir"' EXIT + +vendor_dir="$work_dir/vendor" +available="$work_dir/available.txt" +archive="$output_dir/qbz-${version}-cargo-vendor.tar.xz" + +cargo vendor --quiet --locked --versioned-dirs \ + --manifest-path "$manifest" "$vendor_dir" + +find "$vendor_dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' \ + | sort > "$available" + +if [[ ! -s "$available" ]]; then + echo "vendor set is empty" >&2 + exit 1 +fi + +# Stable ownership, ordering and mtimes make reruns byte-for-byte reproducible +# for the same lockfile and source epoch. The transform gives both package +# managers one predictable directory after extraction. +source_date_epoch="${SOURCE_DATE_EPOCH:-$(git log -1 --format=%ct)}" +tar -C "$vendor_dir" \ + --sort=name \ + --mtime="@${source_date_epoch}" \ + --owner=0 --group=0 --numeric-owner \ + --transform="s,^,qbz-${version}-cargo-vendor/," \ + -cJf "$archive" \ + -T "$available" + +echo "Created $archive ($(wc -l < "$available") crates)" +sha256sum "$archive" diff --git a/scripts/packaging/test_aur_packages.sh b/scripts/packaging/test_aur_packages.sh new file mode 100755 index 000000000..d0cb0f3f1 --- /dev/null +++ b/scripts/packaging/test_aur_packages.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# Build and smoke all four AUR recipes in separate, clean Arch containers. +# The source packages consume the exact complete vendor archive emitted by the +# release workflow, so Cargo remains offline during both builds. + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $0 PATH_TO_CARGO_VENDOR_ARCHIVE" >&2 + exit 2 +fi + +vendor_archive="$(realpath "$1")" +version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' crates/Cargo.toml | head -1)" +qbz_test_bin="$(realpath "${QBZ_TEST_BIN:-crates/target/release/qbz}")" +qbzd_test_bin="$(realpath "${QBZD_TEST_BIN:-crates/target/release/qbzd}")" +if [[ -n "${AUR_TEST_WORK_DIR:-}" ]]; then + mkdir -p "$AUR_TEST_WORK_DIR" + test_root="$(realpath "$AUR_TEST_WORK_DIR")" + echo "Preserving Arch build workspace at $test_root" +else + test_root="$(mktemp -d)" + trap 'rm -rf "$test_root"' EXIT +fi + +read -r -a selected_packages <<< \ + "${AUR_TEST_PACKAGES:-qbz-bin qbz qbzd-bin qbzd}" +for package in "${selected_packages[@]}"; do + case "$package" in + qbz-bin|qbz|qbzd-bin|qbzd) ;; + *) + echo "unsupported AUR test package: $package" >&2 + exit 2 + ;; + esac +done + +if [[ "${AUR_TEST_REUSE_BUILD:-0}" == 1 ]]; then + makepkg_cleanbuild="" +else + makepkg_cleanbuild="--cleanbuild" +fi + +sources="$test_root/sources" +packages="$test_root/packages" +pacman_cache="$test_root/pacman-cache" +pacman_cache="${AUR_PACMAN_CACHE:-$pacman_cache}" +mkdir -p "$sources" "$packages" "$pacman_cache" + +git archive --format=tar.gz --prefix="qbz-${version}/" \ + -o "$sources/qbz-${version}.tar.gz" HEAD +cp "$vendor_archive" "$sources/qbz-${version}-cargo-vendor.tar.xz" + +qbz_dir="$test_root/qbz_${version}_amd64" +mkdir -p "$qbz_dir/icons/hicolor"/{32x32,48x48,64x64,128x128,256x256,512x512}/apps +install -m755 "$qbz_test_bin" "$qbz_dir/qbz" +install -m644 packaging/linux/qbz.desktop "$qbz_dir/qbz.desktop" +install -m644 packaging/flatpak/com.blitzfc.qbz.metainfo.xml \ + "$qbz_dir/com.blitzfc.qbz.metainfo.xml" +install -m644 LICENSE "$qbz_dir/LICENSE" +for size in 32 48 64 128 256 512; do + install -m644 "packaging/icons/${size}x${size}.png" \ + "$qbz_dir/icons/hicolor/${size}x${size}/apps/qbz.png" +done +tar -C "$test_root" -czf "$sources/qbz-bin-${version}-x86_64.tar.gz" \ + "qbz_${version}_amd64" + +qbzd_dir="$test_root/qbzd-${version}-linux-amd64" +mkdir -p "$qbzd_dir/completions" +install -m755 "$qbzd_test_bin" "$qbzd_dir/qbzd" +install -m644 crates/qbzd/service/qbzd.service "$qbzd_dir/qbzd.service" +install -m644 packaging/linux/qbzd-standalone-README.md "$qbzd_dir/README.md" +install -m644 LICENSE "$qbzd_dir/LICENSE" +"$qbzd_test_bin" completions bash > "$qbzd_dir/completions/qbzd.bash" +"$qbzd_test_bin" completions zsh > "$qbzd_dir/completions/qbzd.zsh" +"$qbzd_test_bin" completions fish > "$qbzd_dir/completions/qbzd.fish" +tar -C "$test_root" -czf "$sources/qbzd-bin-${version}-x86_64.tar.gz" \ + "qbzd-${version}-linux-amd64" + +for package in qbz-bin qbz qbzd-bin qbzd; do + mkdir -p "$packages/$package" + cp "packaging/aur/${package}/"* "$packages/$package/" + sed -i "s/^pkgver=.*/pkgver=${version}/" "$packages/$package/PKGBUILD" +done + +for package in "${selected_packages[@]}"; do + echo "==> Clean Arch build: ${package}" + docker run --rm \ + -e PACKAGE="$package" \ + -e VERSION="$version" \ + -e MAKEPKG_CLEANBUILD="$makepkg_cleanbuild" \ + -v "$packages/$package:/pkg" \ + -v "$sources:/sources" \ + -v "$pacman_cache:/var/cache/pacman/pkg" \ + archlinux:latest bash -euc ' + pacman -Syu --noconfirm --needed base-devel sudo + useradd -m builder + printf "builder ALL=(ALL) NOPASSWD: ALL\n" > /etc/sudoers.d/builder + chown -R builder:builder /pkg + if ! runuser -u builder -- env SRCDEST=/sources CARGO_NET_OFFLINE=true \ + MAKEPKG_CLEANBUILD="$MAKEPKG_CLEANBUILD" \ + bash -c '\''cd /pkg && makepkg -s --noconfirm --force $MAKEPKG_CLEANBUILD >makepkg.log 2>&1'\''; then + echo "makepkg failed; last 160 log lines:" >&2 + tail -n 160 /pkg/makepkg.log >&2 + exit 1 + fi + tail -n 20 /pkg/makepkg.log + package_file="$(find /pkg -maxdepth 1 -type f -name "*.pkg.tar.zst" -print -quit)" + test -n "$package_file" + pacman -U --noconfirm "$package_file" + case "$PACKAGE" in + qbz|qbz-bin) + grep -aFq "QBZ/${VERSION}" /usr/bin/qbz + set +e + QT_QPA_PLATFORM=offscreen timeout 20 qbz >/tmp/qbz-smoke.log 2>&1 + status=$? + set -e + if [[ $status -ne 124 ]]; then + cat /tmp/qbz-smoke.log + echo "qbz exited during offscreen smoke: $status" >&2 + exit 1 + fi + ;; + qbzd|qbzd-bin) + qbzd version | grep -F "qbzd ${VERSION} " + qbzd completions bash >/dev/null + test -f /usr/lib/systemd/user/qbzd.service + qbzd service systemd --user builder --bin /usr/bin/qbzd \ + >/tmp/qbzd-systemd.service + qbzd service openrc --user builder --bin /usr/bin/qbzd \ + >/tmp/qbzd-openrc + qbzd service runit --user builder --bin /usr/bin/qbzd \ + >/tmp/qbzd-runit + grep -Fq "ExecStart=/usr/bin/qbzd run" /tmp/qbzd-systemd.service + grep -Fq "#!/sbin/openrc-run" /tmp/qbzd-openrc + grep -Fq "command=\"/usr/bin/qbzd\"" /tmp/qbzd-openrc + grep -Fq "exec chpst -u builder:builder /usr/bin/qbzd run" /tmp/qbzd-runit + ;; + esac + ' + find "$packages/$package" -maxdepth 1 -type f -name '*.pkg.tar.zst' \ + -exec cp {} "$test_root/" \; +done + +cp "$test_root"/*.pkg.tar.zst "$packages/" +echo "All AUR package builds passed." +# Preserve the products when requested by CI; the default local run cleans its +# temporary directory on exit after the proof above. +if [[ -n "${AUR_TEST_OUTPUT_DIR:-}" ]]; then + mkdir -p "$AUR_TEST_OUTPUT_DIR" + cp "$packages"/*.pkg.tar.zst "$AUR_TEST_OUTPUT_DIR/" +fi