From 7a46257560991bed974d9ce6282c7c4ac4c3019e Mon Sep 17 00:00:00 2001 From: Makakashan Date: Sat, 19 Sep 2026 20:02:51 +0200 Subject: [PATCH 1/7] ci: build a snap for linux releases --- .github/workflows/release.yml | 111 +++++++++++++++++++++++++- .gitignore | 4 + CHANGELOG.md | 4 + README.md | 17 ++++ snap/local/asound.conf | 16 ++++ snap/snapcraft.yaml | 145 ++++++++++++++++++++++++++++++++++ 6 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 snap/local/asound.conf create mode 100644 snap/snapcraft.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b0959f5..83f8444c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -441,6 +441,115 @@ jobs: path: dist/* if-no-files-found: error + # Unlike the AppImage, the snap does not reuse the executable the build job staged: + # snapcraft compiles the workspace itself inside its own core24 container, so the + # manifest a contributor runs locally is the one that cuts the release. + snap: + name: "Snap (${{ matrix.variant.arch }})" + strategy: + fail-fast: false + matrix: + variant: + - arch: amd64 + runner: ubuntu-22.04 + - arch: arm64 + runner: ubuntu-22.04-arm + + runs-on: ${{ matrix.variant.runner }} + + env: + ARCH: ${{ matrix.variant.arch }} + # Read at step level to decide whether the store upload can run at all: a step's + # own env is not visible to its own `if`, a job's is. + STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + + steps: + - uses: actions/checkout@v5 + + - name: name the artifact + id: meta + shell: bash + run: | + set -euo pipefail + case "$GITHUB_REF" in + refs/tags/*) version="$GITHUB_REF_NAME" ;; + *) version="${GITHUB_SHA:0:7}" ;; + esac + + # A prerelease tag goes to the beta channel, everything else to stable. An + # untagged run publishes nowhere, which is what the empty channel means. + channel="" + case "$GITHUB_REF" in + refs/tags/*-*) channel="beta" ;; + refs/tags/*) channel="stable" ;; + esac + + { + echo "version=$version" + echo "channel=$channel" + } >> "$GITHUB_OUTPUT" + + - name: build the snap + id: build + uses: snapcore/action-build@v1 + + - name: inspect the snap + env: + SNAP: ${{ steps.build.outputs.snap }} + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends squashfs-tools + + list="$(unsquashfs -l "$SNAP")" + # The rust plugin installs to bin/; override-build moves it where the desktop + # entry and the app command both expect it. + grep -qx 'squashfs-root/usr/bin/sonora' <<<"$list" + grep -qx 'squashfs-root/usr/share/applications/sonora.desktop' <<<"$list" + grep -qx 'squashfs-root/etc/asound.conf' <<<"$list" + grep -qx 'squashfs-root/usr/share/doc/sonora/THIRD-PARTY.md' <<<"$list" + # The Vulkan ICD has to come from the gpu-2404 provider, never from the snap. + if grep -q 'squashfs-root/usr/share/vulkan/icd.d' <<<"$list"; then + echo "a Vulkan ICD was staged; it would shadow the gpu-2404 provider's" >&2 + exit 1 + fi + + # The snap's own version is adopted from Cargo.toml, not from the tag, so a + # release built off a stale version line is caught here rather than in the store. + unsquashfs -d snapmeta -q "$SNAP" meta/snap.yaml + cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)" + grep -qx "version: $cargo_version" snapmeta/meta/snap.yaml + grep -qx "base: core24" snapmeta/meta/snap.yaml + grep -qx "confinement: strict" snapmeta/meta/snap.yaml + rm -rf snapmeta + + - name: name the snap + env: + SNAP: ${{ steps.build.outputs.snap }} + VERSION: ${{ steps.meta.outputs.version }} + run: | + set -euo pipefail + mkdir -p dist + mv "$SNAP" "dist/sonora-$VERSION-$ARCH.snap" + + - uses: actions/upload-artifact@v4 + with: + name: release-snap-${{ matrix.variant.arch }} + path: dist/* + if-no-files-found: error + + # Publishing is optional: with no store credentials configured the release still + # carries the .snap file as a downloadable asset, the way it carries the Flatpak + # bundles, and only the Snap Store upload is skipped. + - name: publish to the Snap Store + if: steps.meta.outputs.channel != '' && env.STORE_CREDENTIALS != '' + uses: snapcore/action-publish@v1 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + with: + snap: dist/sonora-${{ steps.meta.outputs.version }}-${{ matrix.variant.arch }}.snap + release: ${{ steps.meta.outputs.channel }} + # Build flatpak, see: https://github.com/flatpak/flatpak-github-actions flatpak: name: "Flatpak (${{ matrix.variant.arch }})" @@ -586,7 +695,7 @@ jobs: git push --force "https://x-access-token:$TOKEN@github.com/$GITHUB_REPOSITORY" flatpak-repo release: - needs: [build, bundle, appimage, flatpak, flatpak-repo] + needs: [build, bundle, appimage, snap, flatpak, flatpak-repo] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest permissions: diff --git a/.gitignore b/.gitignore index 40aa13cd..954f3619 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ __pycache__/ /flatpak/*.flatpak /flatpak/flatpak /.flatpak-builder +/*.snap +/parts +/stage +/prime diff --git a/CHANGELOG.md b/CHANGELOG.md index c0effd41..cc840f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- Sonora is packaged as a snap, for x86-64 and ARM64, alongside the AppImage and the Flatpak. + ### Changed - An artist page opens as soon as their profile and popular tracks arrive, and fills its diff --git a/README.md b/README.md index a879d788..576f7740 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,23 @@ information, so [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUp AppImage manager such as [AppManager](https://github.com/kem-a/AppManager) can fetch a new release for you. +#### Snap + +```sh +sudo snap install sonora +``` + +An `arm64` build is published beside the `amd64` one, and the `.snap` files are attached to every +release for installing without the store (`snap install --dangerous ./sonora-*.snap`). Local music +on an external drive needs one connection the store does not make for you: + +```sh +sudo snap connect sonora:removable-media +``` + +Discord Rich Presence does not work under snap confinement, and neither does the Widevine module +some providers need for playback; the Flatpak has both. + ### Nix The flake packages the latest tagged release binary or builds from source if unavailable for your platform. diff --git a/snap/local/asound.conf b/snap/local/asound.conf new file mode 100644 index 00000000..7ff5afb8 --- /dev/null +++ b/snap/local/asound.conf @@ -0,0 +1,16 @@ +# cpal opens the ALSA default device, which inside the snap has to be the +# PulseAudio plugin: audio-playback hands out a PulseAudio/PipeWire socket and +# no direct access to the sound card. +pcm.!default { + type pulse + fallback "sysdefault" + hint { + show on + description "Default ALSA Output (currently PulseAudio Sound Server)" + } +} + +ctl.!default { + type pulse + fallback "sysdefault" +} diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 00000000..2426a65a --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,145 @@ +name: sonora +title: Sonora +base: core24 +adopt-info: sonora +summary: A native music streaming client, built with Rust and GPUI +description: | + Sonora is a native music streaming client built with Rust and GPUI, streaming + through librespot and ytmusic-rs. It plays a Spotify or YouTube Music account + and a local music folder from one library. + + Note: playing a Spotify account requires Spotify Premium. +license: GPL-3.0-or-later +website: https://github.com/sonorahq/sonora +source-code: https://github.com/sonorahq/sonora +issues: https://github.com/sonorahq/sonora/issues + +grade: stable +confinement: strict +compression: lzo + +platforms: + amd64: + arm64: + +apps: + sonora: + command: usr/bin/sonora + extensions: [gnome] + common-id: io.github.nolight132.sonora + desktop: usr/share/applications/sonora.desktop + environment: + # cpal reaches the sound server through ALSA, so the snap carries the ALSA + # PulseAudio plugin and a config that routes the default device to it. + ALSA_CONFIG_PATH: $SNAP/etc/asound.conf + LD_LIBRARY_PATH: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib:$LD_LIBRARY_PATH + plugs: + - audio-playback + - network + # The OAuth redirect (127.0.0.1:8989) and the scrobble callback (127.0.0.1:8990) + # are listening sockets, so a plain `network` plug is not enough. + - network-bind + - home + - removable-media + - screen-inhibit-control + # The StatusNotifierItem tray and desktop notifications. + - unity7 + slots: + - mpris + +slots: + mpris: + interface: mpris + # souvlaki publishes org.mpris.MediaPlayer2.sonora. + name: sonora + +parts: + sonora: + plugin: rust + source: . + # The workspace has several members; only the binary crate is wanted. + rust-path: [crates/sonora] + rust-channel: "1.98.1" + build-packages: + # .cargo/config.toml passes -fuse-ld=mold for x86_64-unknown-linux-gnu. + - mold + # crates/widevine compiles its CDM host from shim.cc. + - g++ + - pkg-config + - libasound2-dev + - libdbus-1-dev + - libfontconfig1-dev + - libfreetype-dev + - libsqlite3-dev + - libvulkan-dev + - libwayland-dev + - libx11-dev + - libxcb1-dev + - libxcursor-dev + - libxi-dev + - libxkbcommon-dev + - libxkbcommon-x11-dev + # A cookie sign-in (YouTube) dlopens libwebkit2gtk-4.1.so.0. + - libwebkit2gtk-4.1-dev + stage-packages: + - libasound2t64 + - libasound2-plugins + - libdbus-1-3 + - libfontconfig1 + - libfreetype6 + - libsqlite3-0 + - libvulkan1 + - libwayland-client0 + - libwebkit2gtk-4.1-0 + - libx11-6 + - libxcb1 + - libxcursor1 + - libxi6 + - libxkbcommon0 + - libxkbcommon-x11-0 + override-pull: | + craftctl default + version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)" + craftctl set version="$version" + override-build: | + craftctl default + + # The rust plugin installs to $CRAFT_PART_INSTALL/bin; the desktop file and + # the launcher both expect usr/bin. + install -Dm755 "$CRAFT_PART_INSTALL/bin/sonora" "$CRAFT_PART_INSTALL/usr/bin/sonora" + rm -f "$CRAFT_PART_INSTALL/bin/sonora" + + # Every asset is compiled into the executable, so only the launcher entry + # and the icons are installed. + for icon in assets/linux/icons/hicolor/*/apps/sonora.png; do + size="$(basename "$(dirname "$(dirname "$icon")")")" + install -Dm644 "$icon" \ + "$CRAFT_PART_INSTALL/usr/share/icons/hicolor/$size/apps/sonora.png" + done + install -Dm644 assets/linux/sonora.svg \ + "$CRAFT_PART_INSTALL/usr/share/icons/hicolor/scalable/apps/sonora.svg" + + # snapd rewrites Exec=, but never Icon=, so the icon is given as an absolute + # path inside the snap. + install -d "$CRAFT_PART_INSTALL/usr/share/applications" + sed 's|^Icon=sonora$|Icon=${SNAP}/usr/share/icons/hicolor/512x512/apps/sonora.png|' \ + assets/linux/sonora.desktop \ + > "$CRAFT_PART_INSTALL/usr/share/applications/sonora.desktop" + + install -Dm644 snap/local/asound.conf "$CRAFT_PART_INSTALL/etc/asound.conf" + + install -Dm644 COPYING "$CRAFT_PART_INSTALL/usr/share/doc/sonora/LICENSE" + install -Dm644 THIRD-PARTY.md "$CRAFT_PART_INSTALL/usr/share/doc/sonora/THIRD-PARTY.md" + install -Dm644 assets/fonts/LICENSE.txt \ + "$CRAFT_PART_INSTALL/usr/share/doc/sonora/LICENSE.Inter" + install -Dm644 assets/icons/LICENSE \ + "$CRAFT_PART_INSTALL/usr/share/doc/sonora/icons/LICENSE" + for licence in assets/icons/*/LICENSE; do + pack="$(basename "$(dirname "$licence")")" + install -Dm644 "$licence" \ + "$CRAFT_PART_INSTALL/usr/share/doc/sonora/icons/LICENSE.$pack" + done + prime: + # The Vulkan ICD comes from the gpu-2404 provider, which the gnome extension + # points the loader at; a driver staged here would only shadow it. + - -usr/share/vulkan/icd.d From c950b0b2658b1d9b658fdb78399789b859bc5d6d Mon Sep 17 00:00:00 2001 From: Makakashan Date: Sat, 19 Sep 2026 20:11:13 +0200 Subject: [PATCH 2/7] ci: find the alsa plugins in the snap by a triplet-free path --- .github/workflows/release.yml | 3 +++ snap/snapcraft.yaml | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83f8444c..f09cfeae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -507,6 +507,9 @@ jobs: grep -qx 'squashfs-root/usr/bin/sonora' <<<"$list" grep -qx 'squashfs-root/usr/share/applications/sonora.desktop' <<<"$list" grep -qx 'squashfs-root/etc/asound.conf' <<<"$list" + # ALSA_PLUGIN_DIR names one triplet-free path, so the PulseAudio plugin the + # asound.conf routes to has to have been moved there. + grep -qx 'squashfs-root/usr/lib/alsa-lib/libasound_module_pcm_pulse.so' <<<"$list" grep -qx 'squashfs-root/usr/share/doc/sonora/THIRD-PARTY.md' <<<"$list" # The Vulkan ICD has to come from the gpu-2404 provider, never from the snap. if grep -q 'squashfs-root/usr/share/vulkan/icd.d' <<<"$list"; then diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 2426a65a..4258fe57 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -31,8 +31,11 @@ apps: environment: # cpal reaches the sound server through ALSA, so the snap carries the ALSA # PulseAudio plugin and a config that routes the default device to it. + # alsa-lib dlopens its plugins out of ALSA_PLUGIN_DIR, not off the linker path, + # and snapd expands only its own $SNAP_* variables here, never a craft one, so + # the directory the plugins are moved to in override-prime carries no triplet. ALSA_CONFIG_PATH: $SNAP/etc/asound.conf - LD_LIBRARY_PATH: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib:$LD_LIBRARY_PATH + ALSA_PLUGIN_DIR: $SNAP/usr/lib/alsa-lib plugs: - audio-playback - network @@ -143,3 +146,17 @@ parts: # The Vulkan ICD comes from the gpu-2404 provider, which the gnome extension # points the loader at; a driver staged here would only shadow it. - -usr/share/vulkan/icd.d + override-prime: | + craftctl default + + # libasound2-plugins installs under an architecture triplet, which ALSA_PLUGIN_DIR + # cannot name: it is read by snapd, which expands no craft variable. Moving the + # plugins to one path spells the same thing on amd64 and arm64. + triplet="$(dirname "$(find "$CRAFT_PRIME/usr/lib" -maxdepth 3 -type d -name alsa-lib -print -quit)")" + if [ -n "$triplet" ] && [ "$triplet" != "$CRAFT_PRIME/usr/lib" ]; then + mkdir -p "$CRAFT_PRIME/usr/lib/alsa-lib" + mv "$triplet/alsa-lib"/* "$CRAFT_PRIME/usr/lib/alsa-lib/" + rmdir "$triplet/alsa-lib" + fi + test -f "$CRAFT_PRIME/usr/lib/alsa-lib/libasound_module_pcm_pulse.so" + test -f "$CRAFT_PRIME/usr/lib/alsa-lib/libasound_module_conf_pulse.so" From 6cd6ae8c41d9af4162cb5e44be431bc2a5f241c9 Mon Sep 17 00:00:00 2001 From: Makakashan Date: Mon, 21 Sep 2026 11:46:58 +0200 Subject: [PATCH 3/7] ci: build the snap in a workflow of its own --- .github/workflows/release.yml | 114 ++--------------------------- .github/workflows/snap.yml | 131 ++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 110 deletions(-) create mode 100644 .github/workflows/snap.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f09cfeae..40f4d5fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -441,117 +441,11 @@ jobs: path: dist/* if-no-files-found: error - # Unlike the AppImage, the snap does not reuse the executable the build job staged: - # snapcraft compiles the workspace itself inside its own core24 container, so the - # manifest a contributor runs locally is the one that cuts the release. + # The snap has a workflow of its own so it can be built without running the release; + # on a tag the release calls that same definition, the way it calls announce. snap: - name: "Snap (${{ matrix.variant.arch }})" - strategy: - fail-fast: false - matrix: - variant: - - arch: amd64 - runner: ubuntu-22.04 - - arch: arm64 - runner: ubuntu-22.04-arm - - runs-on: ${{ matrix.variant.runner }} - - env: - ARCH: ${{ matrix.variant.arch }} - # Read at step level to decide whether the store upload can run at all: a step's - # own env is not visible to its own `if`, a job's is. - STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} - - steps: - - uses: actions/checkout@v5 - - - name: name the artifact - id: meta - shell: bash - run: | - set -euo pipefail - case "$GITHUB_REF" in - refs/tags/*) version="$GITHUB_REF_NAME" ;; - *) version="${GITHUB_SHA:0:7}" ;; - esac - - # A prerelease tag goes to the beta channel, everything else to stable. An - # untagged run publishes nowhere, which is what the empty channel means. - channel="" - case "$GITHUB_REF" in - refs/tags/*-*) channel="beta" ;; - refs/tags/*) channel="stable" ;; - esac - - { - echo "version=$version" - echo "channel=$channel" - } >> "$GITHUB_OUTPUT" - - - name: build the snap - id: build - uses: snapcore/action-build@v1 - - - name: inspect the snap - env: - SNAP: ${{ steps.build.outputs.snap }} - run: | - set -euo pipefail - sudo apt-get update - sudo apt-get install -y --no-install-recommends squashfs-tools - - list="$(unsquashfs -l "$SNAP")" - # The rust plugin installs to bin/; override-build moves it where the desktop - # entry and the app command both expect it. - grep -qx 'squashfs-root/usr/bin/sonora' <<<"$list" - grep -qx 'squashfs-root/usr/share/applications/sonora.desktop' <<<"$list" - grep -qx 'squashfs-root/etc/asound.conf' <<<"$list" - # ALSA_PLUGIN_DIR names one triplet-free path, so the PulseAudio plugin the - # asound.conf routes to has to have been moved there. - grep -qx 'squashfs-root/usr/lib/alsa-lib/libasound_module_pcm_pulse.so' <<<"$list" - grep -qx 'squashfs-root/usr/share/doc/sonora/THIRD-PARTY.md' <<<"$list" - # The Vulkan ICD has to come from the gpu-2404 provider, never from the snap. - if grep -q 'squashfs-root/usr/share/vulkan/icd.d' <<<"$list"; then - echo "a Vulkan ICD was staged; it would shadow the gpu-2404 provider's" >&2 - exit 1 - fi - - # The snap's own version is adopted from Cargo.toml, not from the tag, so a - # release built off a stale version line is caught here rather than in the store. - unsquashfs -d snapmeta -q "$SNAP" meta/snap.yaml - cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)" - grep -qx "version: $cargo_version" snapmeta/meta/snap.yaml - grep -qx "base: core24" snapmeta/meta/snap.yaml - grep -qx "confinement: strict" snapmeta/meta/snap.yaml - rm -rf snapmeta - - - name: name the snap - env: - SNAP: ${{ steps.build.outputs.snap }} - VERSION: ${{ steps.meta.outputs.version }} - run: | - set -euo pipefail - mkdir -p dist - mv "$SNAP" "dist/sonora-$VERSION-$ARCH.snap" - - - uses: actions/upload-artifact@v4 - with: - name: release-snap-${{ matrix.variant.arch }} - path: dist/* - if-no-files-found: error - - # Publishing is optional: with no store credentials configured the release still - # carries the .snap file as a downloadable asset, the way it carries the Flatpak - # bundles, and only the Snap Store upload is skipped. - - name: publish to the Snap Store - if: steps.meta.outputs.channel != '' && env.STORE_CREDENTIALS != '' - uses: snapcore/action-publish@v1 - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} - with: - snap: dist/sonora-${{ steps.meta.outputs.version }}-${{ matrix.variant.arch }}.snap - release: ${{ steps.meta.outputs.channel }} + uses: ./.github/workflows/snap.yml + secrets: inherit # Build flatpak, see: https://github.com/flatpak/flatpak-github-actions flatpak: diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml new file mode 100644 index 00000000..3ef77f77 --- /dev/null +++ b/.github/workflows/snap.yml @@ -0,0 +1,131 @@ +name: snap + +# The snap is built by its own workflow, not inline in the release, so it can be +# exercised without running anything named release: a pull request that touches the +# manifest builds it, and it can be dispatched by hand. The release calls this same +# definition on a tag, the way it calls announce. +# +# Unlike the AppImage, the snap does not reuse the executable the build job staged: +# snapcraft compiles the workspace itself inside its own core24 container, so the +# manifest a contributor runs locally is the one that cuts the release. +on: + workflow_call: + workflow_dispatch: + pull_request: + paths: + - snap/** + - .github/workflows/snap.yml + +concurrency: + group: snap-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + snap: + name: "Snap (${{ matrix.variant.arch }})" + strategy: + fail-fast: false + matrix: + variant: + - arch: amd64 + runner: ubuntu-22.04 + - arch: arm64 + runner: ubuntu-22.04-arm + + runs-on: ${{ matrix.variant.runner }} + + env: + ARCH: ${{ matrix.variant.arch }} + # Read at step level to decide whether the store upload can run at all: a step's + # own env is not visible to its own `if`, a job's is. + STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + + steps: + - uses: actions/checkout@v5 + + - name: name the artifact + id: meta + shell: bash + run: | + set -euo pipefail + case "$GITHUB_REF" in + refs/tags/*) version="$GITHUB_REF_NAME" ;; + *) version="${GITHUB_SHA:0:7}" ;; + esac + + # A prerelease tag goes to the beta channel, everything else to stable. An + # untagged run publishes nowhere, which is what the empty channel means. + channel="" + case "$GITHUB_REF" in + refs/tags/*-*) channel="beta" ;; + refs/tags/*) channel="stable" ;; + esac + + { + echo "version=$version" + echo "channel=$channel" + } >> "$GITHUB_OUTPUT" + + - name: build the snap + id: build + uses: snapcore/action-build@v1 + + - name: inspect the snap + env: + SNAP: ${{ steps.build.outputs.snap }} + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends squashfs-tools + + list="$(unsquashfs -l "$SNAP")" + # The rust plugin installs to bin/; override-build moves it where the desktop + # entry and the app command both expect it. + grep -qx 'squashfs-root/usr/bin/sonora' <<<"$list" + grep -qx 'squashfs-root/usr/share/applications/sonora.desktop' <<<"$list" + grep -qx 'squashfs-root/etc/asound.conf' <<<"$list" + # ALSA_PLUGIN_DIR names one triplet-free path, so the PulseAudio plugin the + # asound.conf routes to has to have been moved there. + grep -qx 'squashfs-root/usr/lib/alsa-lib/libasound_module_pcm_pulse.so' <<<"$list" + grep -qx 'squashfs-root/usr/share/doc/sonora/THIRD-PARTY.md' <<<"$list" + # The Vulkan ICD has to come from the gpu-2404 provider, never from the snap. + if grep -q 'squashfs-root/usr/share/vulkan/icd.d' <<<"$list"; then + echo "a Vulkan ICD was staged; it would shadow the gpu-2404 provider's" >&2 + exit 1 + fi + + # The snap's own version is adopted from Cargo.toml, not from the tag, so a + # release built off a stale version line is caught here rather than in the store. + unsquashfs -d snapmeta -q "$SNAP" meta/snap.yaml + cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)" + grep -qx "version: $cargo_version" snapmeta/meta/snap.yaml + grep -qx "base: core24" snapmeta/meta/snap.yaml + grep -qx "confinement: strict" snapmeta/meta/snap.yaml + rm -rf snapmeta + + - name: name the snap + env: + SNAP: ${{ steps.build.outputs.snap }} + VERSION: ${{ steps.meta.outputs.version }} + run: | + set -euo pipefail + mkdir -p dist + mv "$SNAP" "dist/sonora-$VERSION-$ARCH.snap" + + - uses: actions/upload-artifact@v4 + with: + name: release-snap-${{ matrix.variant.arch }} + path: dist/* + if-no-files-found: error + + # Publishing is optional: with no store credentials configured the release still + # carries the .snap file as a downloadable asset, the way it carries the Flatpak + # bundles, and only the Snap Store upload is skipped. + - name: publish to the Snap Store + if: steps.meta.outputs.channel != '' && env.STORE_CREDENTIALS != '' + uses: snapcore/action-publish@v1 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + with: + snap: dist/sonora-${{ steps.meta.outputs.version }}-${{ matrix.variant.arch }}.snap + release: ${{ steps.meta.outputs.channel }} From 019ee57939511704bec8bb6667d0424735f12022 Mon Sep 17 00:00:00 2001 From: Makakashan Date: Mon, 21 Sep 2026 11:59:04 +0200 Subject: [PATCH 4/7] ci: install rustup for the snap build --- snap/snapcraft.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 4258fe57..1d6d98a6 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -63,6 +63,12 @@ parts: # The workspace has several members; only the binary crate is wanted. rust-path: [crates/sonora] rust-channel: "1.98.1" + # The rust plugin validates that rustup is on the build environment, but the + # craft-parts inside snapcraft 9.0.1 does not put it there itself, so the build + # fails before it starts. Asking for the snap explicitly costs nothing on a + # version that does add it: build snaps are a set. + build-snaps: + - rustup build-packages: # .cargo/config.toml passes -fuse-ld=mold for x86_64-unknown-linux-gnu. - mold From 1337455d69ca76f656b2213faee03afaf997b93c Mon Sep 17 00:00:00 2001 From: Makakashan Date: Mon, 21 Sep 2026 12:05:23 +0200 Subject: [PATCH 5/7] ci: build the snap without the rust plugin --- snap/snapcraft.yaml | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 1d6d98a6..f3b0792c 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -58,18 +58,14 @@ slots: parts: sonora: - plugin: rust + # Not the rust plugin: it validates that rustup answers in the build environment, + # which /snap/bin is not on, and it would drive cargo for a workspace this part + # already overrides the build and the prime of anyway. Installing the toolchain + # here is the whole of what the plugin would have done. + plugin: nil source: . - # The workspace has several members; only the binary crate is wanted. - rust-path: [crates/sonora] - rust-channel: "1.98.1" - # The rust plugin validates that rustup is on the build environment, but the - # craft-parts inside snapcraft 9.0.1 does not put it there itself, so the build - # fails before it starts. Asking for the snap explicitly costs nothing on a - # version that does add it: build snaps are a set. - build-snaps: - - rustup build-packages: + - curl # .cargo/config.toml passes -fuse-ld=mold for x86_64-unknown-linux-gnu. - mold # crates/widevine compiles its CDM host from shim.cc. @@ -113,10 +109,18 @@ parts: override-build: | craftctl default - # The rust plugin installs to $CRAFT_PART_INSTALL/bin; the desktop file and - # the launcher both expect usr/bin. - install -Dm755 "$CRAFT_PART_INSTALL/bin/sonora" "$CRAFT_PART_INSTALL/usr/bin/sonora" - rm -f "$CRAFT_PART_INSTALL/bin/sonora" + # rust-toolchain.toml pins the version, but it also asks for rust-analyzer and + # friends that a build has no use for; RUSTUP_TOOLCHAIN overrides the file so + # only the compiler is fetched. Keep the version in step with that file. + export RUSTUP_HOME="$CRAFT_PART_BUILD/.rustup" + export CARGO_HOME="$CRAFT_PART_BUILD/.cargo" + export RUSTUP_TOOLCHAIN=1.98.1 + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --no-modify-path --profile minimal --default-toolchain "$RUSTUP_TOOLCHAIN" + export PATH="$CARGO_HOME/bin:$PATH" + + cargo build --release --locked --package sonora + install -Dm755 target/release/sonora "$CRAFT_PART_INSTALL/usr/bin/sonora" # Every asset is compiled into the executable, so only the launcher entry # and the icons are installed. From bd1604d4a968dddb1fca4aa231e7ea83e6753c6d Mon Sep 17 00:00:00 2001 From: Makakashan Date: Mon, 21 Sep 2026 12:16:04 +0200 Subject: [PATCH 6/7] ci: install cmake and ninja for the snap build --- snap/snapcraft.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index f3b0792c..419f22bf 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -66,6 +66,11 @@ parts: source: . build-packages: - curl + # opusic-sys and aws-lc-sys build their C libraries with cmake, and the cmake + # crate asks for the Ninja generator. A bare core24 container has neither; the + # runners and the freedesktop SDK do, which is why only the snap needs this said. + - cmake + - ninja-build # .cargo/config.toml passes -fuse-ld=mold for x86_64-unknown-linux-gnu. - mold # crates/widevine compiles its CDM host from shim.cc. From ce42887ecf7fd94a1ff71cb1728e4feebc3f2a45 Mon Sep 17 00:00:00 2001 From: Makakashan Date: Mon, 21 Sep 2026 12:37:21 +0200 Subject: [PATCH 7/7] ci: take webkit from the gnome platform instead of staging it --- .github/workflows/snap.yml | 5 +++++ snap/snapcraft.yaml | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 3ef77f77..01cce3f8 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -87,6 +87,11 @@ jobs: # ALSA_PLUGIN_DIR names one triplet-free path, so the PulseAudio plugin the # asound.conf routes to has to have been moved there. grep -qx 'squashfs-root/usr/lib/alsa-lib/libasound_module_pcm_pulse.so' <<<"$list" + # webkit comes from gnome-46-2404; a staged copy is 122 MiB of nothing. + if grep -q 'libwebkit2gtk' <<<"$list"; then + echo "libwebkit2gtk was staged; the gnome platform already provides it" >&2 + exit 1 + fi grep -qx 'squashfs-root/usr/share/doc/sonora/THIRD-PARTY.md' <<<"$list" # The Vulkan ICD has to come from the gpu-2404 provider, never from the snap. if grep -q 'squashfs-root/usr/share/vulkan/icd.d' <<<"$list"; then diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 419f22bf..c28b49b3 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -89,8 +89,6 @@ parts: - libxi-dev - libxkbcommon-dev - libxkbcommon-x11-dev - # A cookie sign-in (YouTube) dlopens libwebkit2gtk-4.1.so.0. - - libwebkit2gtk-4.1-dev stage-packages: - libasound2t64 - libasound2-plugins @@ -100,7 +98,10 @@ parts: - libsqlite3-0 - libvulkan1 - libwayland-client0 - - libwebkit2gtk-4.1-0 + # No libwebkit2gtk here on purpose: gnome-46-2404 stages it and the extension + # puts the platform's library directory on the load path, so a copy of our own + # would be 122 MiB shadowing the one already there. Nothing links it either -- + # the cookie sign-in dlopens it -- so there is no build dependency to match. - libx11-6 - libxcb1 - libxcursor1