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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@ jobs:
path: dist/*
if-no-files-found: error

# 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:
uses: ./.github/workflows/snap.yml
secrets: inherit

# Build flatpak, see: https://github.com/flatpak/flatpak-github-actions
flatpak:
name: "Flatpak (${{ matrix.variant.arch }})"
Expand Down Expand Up @@ -586,7 +592,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:
Expand Down
136 changes: 136 additions & 0 deletions .github/workflows/snap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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"
# 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
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 }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ __pycache__/
/flatpak/*.flatpak
/flatpak/flatpak
/.flatpak-builder
/*.snap
/parts
/stage
/prime
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions snap/local/asound.conf
Original file line number Diff line number Diff line change
@@ -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"
}
178 changes: 178 additions & 0 deletions snap/snapcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
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-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
ALSA_PLUGIN_DIR: $SNAP/usr/lib/alsa-lib
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:
# 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: .
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.
- 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
stage-packages:
- libasound2t64
- libasound2-plugins
- libdbus-1-3
- libfontconfig1
- libfreetype6
- libsqlite3-0
- libvulkan1
- libwayland-client0
# 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
- 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

# 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.
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
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"
Loading