From b3ce6bea2e6825c3f1498ae27078e42852dc37c8 Mon Sep 17 00:00:00 2001 From: Rian Stockbower <7378790+rianjs@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:40:52 -0400 Subject: [PATCH 1/2] feat: publish native Arch Linux desktop packages --- .github/workflows/arch-package.yml | 71 ++++++++++++++++++++++++++++++ .github/workflows/release.yml | 34 +++++++++++++- docs/DEVELOPMENT.md | 14 +++++- docs/INSTALL.md | 18 +++++++- docs/tauri.md | 1 + scripts/check-release.mjs | 7 +++ scripts/package-arch.py | 66 +++++++++++++++++++++++++++ 7 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/arch-package.yml create mode 100644 scripts/package-arch.py diff --git a/.github/workflows/arch-package.yml b/.github/workflows/arch-package.yml new file mode 100644 index 0000000..d0e1526 --- /dev/null +++ b/.github/workflows/arch-package.yml @@ -0,0 +1,71 @@ +name: Arch package + +on: + pull_request: + paths: + - scripts/package-arch.py + - .github/workflows/arch-package.yml + - .github/workflows/release.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + package: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - name: Package checksum-verified desktop fixtures for both architectures + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y libarchive-tools + mkdir fixtures tools packages + gh release download v0.3.18 --repo open-cli-collective/Retune \ + --pattern 'retune_0.3.18_*.deb' --pattern checksums.txt --dir fixtures + (cd fixtures; sha256sum -c checksums.txt --ignore-missing) + gh release download v2.47.0 --repo goreleaser/nfpm \ + --pattern nfpm_2.47.0_Linux_x86_64.tar.gz --pattern checksums.txt --dir tools + (cd tools; sha256sum -c checksums.txt --ignore-missing) + tar -xzf tools/nfpm_2.47.0_Linux_x86_64.tar.gz -C tools nfpm + for arch in amd64 arm64; do + python3 scripts/package-arch.py "fixtures/retune_0.3.18_${arch}.deb" \ + 0.3.18 "$arch" packages --nfpm "$PWD/tools/nfpm" + done + if python3 scripts/package-arch.py fixtures/retune_0.3.18_amd64.deb \ + 0.3.19 amd64 packages --nfpm "$PWD/tools/nfpm"; then + echo '::error::mismatched payload version accepted' + exit 1 + fi + if python3 scripts/package-arch.py fixtures/retune_0.3.18_amd64.deb \ + 0.3.18 arm64 packages --nfpm "$PWD/tools/nfpm"; then + echo '::error::mismatched payload architecture accepted' + exit 1 + fi + bsdtar -xOf packages/retune_0.3.18_linux_arm64.pkg.tar.zst .PKGINFO | grep -qx 'arch = aarch64' + - name: Verify native desktop payload and dependencies in Arch + run: | + set -euo pipefail + docker run --rm -v "$PWD/packages:/packages:ro" archlinux:base bash -c ' + set -euo pipefail + pacman -Syu --noconfirm + pacman -U --noconfirm /packages/retune_0.3.18_linux_amd64.pkg.tar.zst + pacman -Qkk retune + pacman -Qo /usr/bin/retune-desktop + test -x /usr/bin/retune-desktop + test -f /usr/share/applications/Retune.desktop + test -f /usr/share/icons/hicolor/128x128/apps/retune-desktop.png + ldd /usr/bin/retune-desktop > /tmp/retune-libraries + if grep -q "not found" /tmp/retune-libraries; then + cat /tmp/retune-libraries + exit 1 + fi + pacman -R --noconfirm retune + test ! -e /usr/bin/retune-desktop + pacman -U --noconfirm /packages/retune_0.3.18_linux_amd64.pkg.tar.zst + pacman -Qkk retune + ' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cda0ac9..58a5e79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,6 +123,7 @@ jobs: curl \ file \ libasound2-dev \ + libarchive-tools \ libayatana-appindicator3-dev \ libdbus-1-dev \ libssl-dev \ @@ -314,6 +315,33 @@ jobs: [ "$package_version" = "$VERSION" ] || { echo "::error::Debian package version $package_version does not match $VERSION"; exit 1; } mv "${files[0]}" "$bundle/retune_${VERSION}_${ARCH}.deb" + - name: Package Arch Linux payload + if: startsWith(matrix.os, 'ubuntu-') + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.prepare.outputs.version }} + ARCH: ${{ matrix.arch }} + run: | + set -euo pipefail + tool_dir="$(mktemp -d)" + tool_arch=x86_64 + if [ "$ARCH" = arm64 ]; then tool_arch=arm64; fi + gh release download v2.47.0 --repo goreleaser/nfpm \ + --pattern "nfpm_2.47.0_Linux_${tool_arch}.tar.gz" --pattern checksums.txt --dir "$tool_dir" + (cd "$tool_dir"; sha256sum -c checksums.txt --ignore-missing) + tar -xzf "$tool_dir/nfpm_2.47.0_Linux_${tool_arch}.tar.gz" -C "$tool_dir" nfpm + python3 scripts/package-arch.py \ + "target/release/bundle/deb/retune_${VERSION}_${ARCH}.deb" \ + "$VERSION" "$ARCH" target/release/bundle/arch --nfpm "$tool_dir/nfpm" + + - name: Upload Arch Linux artifact + if: startsWith(matrix.os, 'ubuntu-') + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: retune-arch-${{ matrix.arch }} + path: target/release/bundle/arch/retune_${{ needs.prepare.outputs.version }}_linux_${{ matrix.arch }}.pkg.tar.zst + if-no-files-found: error + - name: Upload macOS artifact if: matrix.os == 'macos-15' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 @@ -428,6 +456,8 @@ jobs: "Retune-${VERSION}-windows-arm64-setup.exe" "retune_${VERSION}_amd64.deb" "retune_${VERSION}_arm64.deb" + "retune_${VERSION}_linux_amd64.pkg.tar.zst" + "retune_${VERSION}_linux_arm64.pkg.tar.zst" ) for asset in "${expected[@]}"; do test -f "dist/assets/$asset" || { echo "::error::missing release asset $asset"; exit 1; } @@ -457,7 +487,7 @@ jobs: run: | set -euo pipefail cat > dist/release-notes.md </dev/null 2>&1; then diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a8f82b3..8c5af62 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -79,7 +79,9 @@ must match the configured release line and point to a commit reachable from `main`. It builds and publishes exactly these assets: `Retune--aarch64.zip`, `Retune--windows-x64-setup.exe`, `Retune--windows-arm64-setup.exe`, -`retune__amd64.deb`, `retune__arm64.deb`, and +`retune__amd64.deb`, `retune__arm64.deb`, +`retune__linux_amd64.pkg.tar.zst`, +`retune__linux_arm64.pkg.tar.zst`, and `checksums.txt`. The tag version is passed to Tauri through its `--config` override so package metadata matches the release tag. The automatic workflow's `workflow_dispatch` only evaluates the release gate and reports the @@ -191,3 +193,13 @@ Do not make CI depend on live Spotify credentials or an audio device. architectural boundary, invariant, persistence format, or external contract, update that document in the same commit. Delete completed plans after durable decisions have been incorporated. + +### Arch Linux packaging + +`scripts/package-arch.py` packages the validated Tauri Debian payload with +nFPM 2.47.0, preserving its executable, desktop launcher, icons, and file modes. +It checks payload version and architecture before assigning Arch runtime +dependencies. Release checksums cover both Arch packages; the Linux package +repository signs them and updates its pacman index. No second application build +or credentials are introduced. Test the resulting package in a clean Arch +container before shipping changes to this packaging path. diff --git a/docs/INSTALL.md b/docs/INSTALL.md index cc0926e..f83108b 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -11,8 +11,10 @@ current floor. | Windows 10/11 | x64, ARM64 | WebView2 105 (the installer updates older runtimes) | | Ubuntu 22.04 | amd64, arm64 | Distribution WebKitGTK 4.1 | | Compatible Debian/Ubuntu systems | amd64, arm64 | WebKitGTK 4.1 | +| Arch Linux / Omarchy | x86_64 | Distribution WebKitGTK 4.1 | +| Compatible Arch Linux ARM systems | aarch64 | Distribution WebKitGTK 4.1 | -Homebrew, Winget, and APT are recommended because they manage updates and verify +Homebrew, Winget, APT, and pacman are recommended because they manage updates and verify published package metadata. ## Direct downloads @@ -28,6 +30,8 @@ in each artifact name: | Windows | ARM64 | `Retune--windows-arm64-setup.exe` | | Debian/Ubuntu | amd64 | `retune__amd64.deb` | | Debian/Ubuntu | arm64 | `retune__arm64.deb` | +| Arch Linux | x86_64 | `retune__linux_amd64.pkg.tar.zst` | +| Arch Linux ARM | aarch64 | `retune__linux_arm64.pkg.tar.zst` | Download `checksums.txt` from that same latest release and verify the matching asset before installing. The macOS artifact uses the Open CLI Collective's @@ -173,3 +177,15 @@ documentation](https://developer.spotify.com/documentation/web-api/concepts/quot run `sha256sum -c checksums.txt --ignore-missing` on Linux, `shasum -a 256` on macOS, or `Get-FileHash -Algorithm SHA256` in PowerShell and compare the result. + +## Arch Linux / Omarchy + +Add the signed [Open CLI Collective pacman repository](https://github.com/open-cli-collective/linux-packages#arch-linux-pacman) once, then install: + +```sh +sudo pacman -Syu retune +``` + +Launch Retune from the desktop application menu. Pacman installs the WebKitGTK, +GTK, audio, and D-Bus runtime dependencies and upgrades Retune with the system. +Packages become available after a release finishes repository publication. diff --git a/docs/tauri.md b/docs/tauri.md index da4fa61..a651c5d 100644 --- a/docs/tauri.md +++ b/docs/tauri.md @@ -154,6 +154,7 @@ identical. Platform overlays define the supported bundle contract: | macOS arm64 | `.app` inside a `ditto`-created ZIP | macOS 15+ (Apple-supported releases) | | Windows x64/ARM64 | NSIS | WebView2 105; downgrade disabled | | Ubuntu amd64/arm64 | Debian package | Ubuntu 22.04 / WebKitGTK 4.1 | +| Arch x86_64 / Arch ARM aarch64 | `.pkg.tar.zst` from the same Linux payload | Distribution WebKitGTK 4.1 | The Windows application manifest is an explicit build input and `build.rs` emits `cargo:rerun-if-changed` for it. Tauri's build helper owns capability, diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs index a3e6f0d..16f8b31 100644 --- a/scripts/check-release.mjs +++ b/scripts/check-release.mjs @@ -225,6 +225,8 @@ for (const asset of [ 'Retune-${VERSION}-windows-arm64-setup.exe', 'retune_${VERSION}_amd64.deb', 'retune_${VERSION}_arm64.deb', + 'retune_${VERSION}_linux_amd64.pkg.tar.zst', + 'retune_${VERSION}_linux_arm64.pkg.tar.zst', 'checksums.txt', ]) required(workflow, asset, `asset contract ${asset}`) assert.doesNotMatch(workflow, /Retune-\$\{VERSION\}-aarch64\.tar\.gz|tar -czf/, 'macOS release must use Apple-compatible ZIP packaging') @@ -356,3 +358,8 @@ required(winget, 'Retune-0.0.0-windows-x64-setup.exe') required(winget, 'Retune-0.0.0-windows-arm64-setup.exe') console.log(`release contract OK (${tauri.version})`) + +required(workflow, "python3 scripts/package-arch.py", "Arch payload packaging step"); +required(workflow, "nfpm_2.47.0_Linux_${tool_arch}.tar.gz", "pinned Arch packager"); +required(workflow, "sha256sum -c checksums.txt --ignore-missing", "packager checksum verification"); +required(workflow, "name: retune-arch-${{ matrix.arch }}", "Arch artifact upload"); diff --git a/scripts/package-arch.py b/scripts/package-arch.py new file mode 100644 index 0000000..877b2c5 --- /dev/null +++ b/scripts/package-arch.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Package the validated Tauri Linux payload for the signed pacman repository.""" +import argparse +import datetime +import io +import json +import pathlib +import re +import subprocess +import tarfile +import tempfile + + +def package(deb, version, arch, output, nfpm): + if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version): + raise ValueError("Expected the release's numeric version") + if arch not in ("amd64", "arm64"): + raise ValueError("Unsupported architecture") + members = subprocess.check_output(["bsdtar", "-tf", str(deb)], text=True).splitlines() + def member(prefix): + matches = [name for name in members if name.startswith(prefix + ".tar.")] + if len(matches) != 1: + raise ValueError("Expected one " + prefix + " archive") + return subprocess.check_output(["bsdtar", "-xOf", str(deb), matches[0]]) + with tarfile.open(fileobj=io.BytesIO(member("control")), mode="r:*") as control: + entry = next(item for item in control.getmembers() if item.name.lstrip("./") == "control") + fields = dict(line.split(": ", 1) for line in control.extractfile(entry).read().decode().splitlines() if ": " in line and not line.startswith(" ")) + if fields.get("Version") != version or fields.get("Architecture") != arch: + raise ValueError("Debian payload version/architecture does not match the release") + output.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory() as directory: + staging = pathlib.Path(directory) + with tarfile.open(fileobj=io.BytesIO(member("data")), mode="r:*") as payload: + payload.extractall(staging, filter="data") + for required in ("usr/bin/retune-desktop", "usr/share/applications/Retune.desktop"): + if not (staging / required).is_file(): + raise ValueError("Missing desktop payload: " + required) + contents = [{"src": str(path), "dst": "/" + str(path.relative_to(staging))} + for path in sorted((staging / "usr").rglob("*")) if path.is_file()] + contents.append({"src": str(pathlib.Path(__file__).resolve().parents[1] / "LICENSE"), + "dst": "/usr/share/licenses/retune/LICENSE"}) + config = {"name": "retune", "arch": arch, "platform": "linux", "version": version, + "release": "1", "section": "sound", + # Whole seconds avoid tar/MTREE timestamp rounding differences. + "mtime": datetime.datetime.fromtimestamp(entry.mtime, datetime.timezone.utc).isoformat(), "maintainer": "Open CLI Collective", + "description": "Desktop music player for local files and Spotify", + "homepage": "https://github.com/open-cli-collective/Retune", "license": "MIT", + "depends": ["webkit2gtk-4.1", "gtk3", "alsa-lib", "dbus", "gcc-libs", "glibc"], + "contents": contents} + config_path = staging / "nfpm.json" + config_path.write_text(json.dumps(config)) + destination = output / f"retune_{version}_linux_{arch}.pkg.tar.zst" + subprocess.run([nfpm, "package", "--packager", "archlinux", "--config", str(config_path), + "--target", str(destination)], check=True) + return destination + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("deb", type=pathlib.Path) + parser.add_argument("version") + parser.add_argument("arch", choices=("amd64", "arm64")) + parser.add_argument("output", type=pathlib.Path) + parser.add_argument("--nfpm", default="nfpm") + args = parser.parse_args() + print(package(args.deb, args.version, args.arch, args.output, args.nfpm)) From abea5fd3ccfd65bfbfea1c3306b195d0292dc2c8 Mon Sep 17 00:00:00 2001 From: Rian Stockbower <7378790+rianjs@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:48:52 -0400 Subject: [PATCH 2/2] fix: use current Rust atomic update API to unblock release validation --- apps/desktop/src-tauri/src/provider.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/provider.rs b/apps/desktop/src-tauri/src/provider.rs index f01c908..cc08542 100644 --- a/apps/desktop/src-tauri/src/provider.rs +++ b/apps/desktop/src-tauri/src/provider.rs @@ -272,7 +272,7 @@ impl SyncRun<'_> { fn note_deadline(&self, deadline: u64) { let _ = self.earliest_cooldown - .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + .try_update(Ordering::Relaxed, Ordering::Relaxed, |current| { Some(if current == 0 { deadline } else { @@ -583,7 +583,7 @@ impl<'c, T: Transport> GenreSource<'c, T> { } let lookup = run.artist_lookups - .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |lookups| { + .try_update(Ordering::Relaxed, Ordering::Relaxed, |lookups| { (lookups < ARTIST_LOOKUPS_PER_SYNC).then_some(lookups + 1) }); let Ok(lookup) = lookup else {