diff --git a/.github/workflows/ci-alpine.yml b/.github/workflows/ci-alpine.yml new file mode 100644 index 00000000000..7ebc920ab79 --- /dev/null +++ b/.github/workflows/ci-alpine.yml @@ -0,0 +1,173 @@ +--- +name: CI-Alpine +permissions: {} + +on: + workflow_call: + inputs: + release_commit: + required: true + type: string + release_version: + required: true + type: string + +jobs: + build_alpine: + name: ${{ matrix.arch }} + env: + ALPINE_PACKAGE_DIR: cmake-build-alpine-package + BRANCH: ${{ github.head_ref || github.ref_name }} + BUILD_VERSION: ${{ inputs.release_version }} + CLONE_URL: >- + ${{ + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.clone_url || + github.event.repository.clone_url + }} + COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || inputs.release_commit }} + permissions: + contents: read + runs-on: ${{ matrix.runner }} + container: + image: alpine:${{ matrix.alpine_version }} + strategy: + fail-fast: false + matrix: + alpine_version: + - '3.24' + arch: + - x86_64 + - aarch64 + include: + - arch: x86_64 + runner: ubuntu-latest + - arch: aarch64 + runner: ubuntu-24.04-arm + steps: + - name: Fix arm64 Alpine container + if: runner.arch == 'ARM64' + uses: laverdet/alpine-arm64@7f0f72ee2f71eb2324e5888e8b6e42b1b53e6160 # v1.0.0 + + - name: Install packaging tools + run: | + set -eux + apk add --no-cache \ + alpine-sdk \ + atools \ + bash \ + cmake \ + file \ + git \ + libcap-utils \ + pax-utils \ + sudo + apk update + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + + - name: Setup builder user + run: | + set -eux + adduser -D builder + addgroup builder abuild + mkdir -p /etc/sudoers.d + echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder + chmod 440 /etc/sudoers.d/builder + chown -R builder:abuild "${GITHUB_WORKSPACE}" + sudo -u builder abuild-keygen -a -i -n + + - name: Configure APKBUILD + run: | + set -eux + sub_version="" + if [ "${BRANCH}" != "master" ]; then + # Alpine development-version suffixes accept a numeric component, + # while hexadecimal commit IDs are rejected by abuild. + sub_version="_git${GITHUB_RUN_NUMBER}" + fi + + cmake -S . -B "${ALPINE_PACKAGE_DIR}" \ + -DSUNSHINE_CONFIGURE_ONLY=ON \ + -DSUNSHINE_CONFIGURE_APKBUILD=ON \ + -DSUNSHINE_SUB_VERSION="${sub_version}" + + mkdir -p apk + mv "${ALPINE_PACKAGE_DIR}/APKBUILD" apk/ + mv "${ALPINE_PACKAGE_DIR}/sunshine.post-install" apk/ + mv "${ALPINE_PACKAGE_DIR}/sunshine.post-upgrade" apk/ + chown -R builder:abuild apk + + cd apk + sh -n sunshine.post-install + sh -n sunshine.post-upgrade + sudo -u builder apkbuild-lint APKBUILD + + - name: Build Alpine package + id: build + working-directory: apk + run: | + set -eux + echo "::add-matcher::.github/matchers/gcc.json" + sudo -u builder env \ + _coverage_dir="${GITHUB_WORKSPACE}/coverage" \ + _run_unit_tests=true \ + _source_dir="${GITHUB_WORKSPACE}" \ + abuild -r + echo "::remove-matcher owner=gcc::" + + - name: Install and verify package + id: package + run: | + set -eux + package_file="$(find /home/builder/packages -type f -name 'sunshine-*.apk' -print -quit)" + test -n "${package_file}" + + apk add --allow-untrusted "${package_file}" + + file /usr/bin/sunshine | tee sunshine-file.txt + grep -Fq 'dynamically linked' sunshine-file.txt + ldd /usr/bin/sunshine | tee sunshine-ldd.txt + grep -Eq 'libc\.musl|ld-musl' sunshine-ldd.txt + scanelf -n /usr/bin/sunshine + /usr/bin/sunshine --version + + rule_file="$(find /lib /usr/lib -path '*/udev/rules.d/60-sunshine.rules' -print -quit)" + test -n "${rule_file}" + grep -Fq 'IMPORT{parent}="HID_*"' "${rule_file}" + grep -Fq 'ENV{HID_PHYS}=="libvirtualhid/uhid/*"' "${rule_file}" + test -e /sys/class/mem/null + if ! udevadm test --action=add /sys/class/mem/null > udevadm-test.txt 2>&1; then + cat udevadm-test.txt + exit 1 + fi + test -f /etc/modules-load.d/60-sunshine.conf + grep -Fxq 'uhid' /etc/modules-load.d/60-sunshine.conf + getcap /usr/bin/sunshine | grep -Eq 'cap_sys_admin,cap_sys_nice[=+]p' + + mkdir -p artifacts + cp "${package_file}" \ + "artifacts/sunshine_${BUILD_VERSION}_alpine${{ matrix.alpine_version }}_${{ matrix.arch }}.apk" + + - name: Upload coverage artifact + if: >- + always() && + (steps.build.outcome == 'success' || steps.build.outcome == 'failure') + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-Alpine-${{ matrix.arch }} + path: | + coverage/coverage.xml + coverage/tests/test_results.xml + if-no-files-found: error + + - name: Upload artifacts + if: steps.package.outcome == 'success' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-Alpine-${{ matrix.arch }} + path: artifacts/ + if-no-files-found: error diff --git a/.github/workflows/ci-cloudsmith.yml b/.github/workflows/ci-cloudsmith.yml index 313a33df88d..36362e4097f 100644 --- a/.github/workflows/ci-cloudsmith.yml +++ b/.github/workflows/ci-cloudsmith.yml @@ -6,7 +6,7 @@ on: workflow_call: inputs: release_tag: - description: GitHub release tag containing the DEB assets. + description: GitHub release tag containing the package assets. required: true type: string repository: @@ -33,6 +33,7 @@ jobs: mkdir -p artifacts/packages gh release download "${RELEASE_TAG}" \ --dir artifacts/packages \ + --pattern '*.apk' \ --pattern '*.deb' \ --pattern '*.rpm' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 885b8dcb8bf..5b65d0d15af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,6 +129,16 @@ jobs: release_commit: ${{ needs.release-setup.outputs.release_commit }} release_version: ${{ needs.release-setup.outputs.release_version }} + build-alpine: + name: Alpine + needs: release-setup + permissions: + contents: read + uses: ./.github/workflows/ci-alpine.yml + with: + release_commit: ${{ needs.release-setup.outputs.release_commit }} + release_version: ${{ needs.release-setup.outputs.release_version }} + build-linux-copr: name: Linux Copr if: github.event_name != 'push' # releases are handled directly in ci-release.yml @@ -189,6 +199,7 @@ jobs: - build-freebsd - build-linux - build-archlinux + - build-alpine - build-linux-flatpak - build-macos - build-homebrew @@ -208,6 +219,10 @@ jobs: coverage: true - name: Archlinux coverage: true + - name: Alpine-x86_64 + coverage: true + - name: Alpine-aarch64 + coverage: true - name: macOS-arm64 coverage: true - name: macOS-x86_64 @@ -268,6 +283,7 @@ jobs: needs: - release-setup - build-archlinux + - build-alpine - build-docker - build-freebsd - build-homebrew diff --git a/cmake/dependencies/ffmpeg.cmake b/cmake/dependencies/ffmpeg.cmake index 291a6e5e84a..fe777fc3b0f 100644 --- a/cmake/dependencies/ffmpeg.cmake +++ b/cmake/dependencies/ffmpeg.cmake @@ -68,7 +68,11 @@ if(NOT DEFINED FFMPEG_PREPARED_BINARIES) set(FFMPEG_PREPARED_BINARIES "${FFMPEG_EXTRACT_DIR}/ffmpeg") # Set the archive filename based on architecture - set(FFMPEG_ARCHIVE_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}-ffmpeg.tar.gz") + if(NOT DEFINED FFMPEG_RELEASE_ASSET_NAME) + set(FFMPEG_ARCHIVE_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}-ffmpeg.tar.gz") + else() + set(FFMPEG_ARCHIVE_NAME "${FFMPEG_RELEASE_ASSET_NAME}") + endif() set(FFMPEG_ARCHIVE_PATH "${FFMPEG_VERSION_DIR}/${FFMPEG_ARCHIVE_NAME}") set(FFMPEG_DOWNLOAD_URL "${FFMPEG_RELEASE_URL}/${FFMPEG_ARCHIVE_NAME}") diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake index 4b2455bf295..a7f2e901c3a 100644 --- a/cmake/prep/options.cmake +++ b/cmake/prep/options.cmake @@ -54,6 +54,8 @@ elseif(UNIX) # Linux "Enable a Flatpak build." OFF) option(SUNSHINE_CONFIGURE_PKGBUILD "Configure files required for AUR. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) + option(SUNSHINE_CONFIGURE_APKBUILD + "Configure files required for an Alpine Linux package. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) option(SUNSHINE_CONFIGURE_FLATPAK_MAN "Configure manifest file required for Flatpak build. Recommended to use with SUNSHINE_CONFIGURE_ONLY" OFF) diff --git a/cmake/prep/special_package_configuration.cmake b/cmake/prep/special_package_configuration.cmake index 01b9f9b6fa0..a46a7f11d7d 100644 --- a/cmake/prep/special_package_configuration.cmake +++ b/cmake/prep/special_package_configuration.cmake @@ -37,6 +37,13 @@ elseif(UNIX) configure_file(packaging/linux/Arch/sunshine.install sunshine.install @ONLY) endif() + # configure the Alpine Linux APKBUILD + if(${SUNSHINE_CONFIGURE_APKBUILD}) + configure_file(packaging/linux/Alpine/APKBUILD APKBUILD @ONLY) + configure_file(packaging/linux/Alpine/sunshine.post-install sunshine.post-install COPYONLY) + configure_file(packaging/linux/Alpine/sunshine.post-install sunshine.post-upgrade COPYONLY) + endif() + # configure the flatpak manifest if(${SUNSHINE_CONFIGURE_FLATPAK_MAN}) configure_file(packaging/linux/flatpak/${PROJECT_FQDN}.yml ${PROJECT_FQDN}.yml @ONLY) diff --git a/docs/getting_started.md b/docs/getting_started.md index e0ecf4ec04b..a3591fa710e 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -92,6 +92,27 @@ CUDA is used for NVFBC capture. +#### Alpine Linux + +> [!IMPORTANT] +> The Alpine package is dynamically linked against Alpine 3.24 libraries. CUDA and NVFBC capture are not available in +> this package, but the other capture and encoding backends supported by the system remain enabled. + +##### Install + +1. Download `sunshine_{version}_alpine{distro-version}_{arch}.apk` from the [latest release][latest-release]. +2. Install the package as root. Release APKs use a per-build signing key, so explicitly allow the downloaded package. + + ```sh + apk add --allow-untrusted ./sunshine_{version}_alpine{distro-version}_{arch}.apk + ``` + +##### Uninstall + +```sh +apk del sunshine +``` + #### AppImage > [!CAUTION] diff --git a/packaging/linux/Alpine/APKBUILD b/packaging/linux/Alpine/APKBUILD new file mode 100644 index 00000000000..2b302987ef0 --- /dev/null +++ b/packaging/linux/Alpine/APKBUILD @@ -0,0 +1,166 @@ +# Edit on GitHub: https://github.com/LizardByte/Sunshine/blob/master/packaging/linux/Alpine/APKBUILD +# Reference: https://wiki.alpinelinux.org/wiki/APKBUILD_Reference + +: "${_run_unit_tests:=false}" # Run the test suite and generate coverage; useful in CI. +: "${_coverage_dir:=}" # Copy CI coverage artifacts to this directory when set. +: "${_source_dir:=}" # Build an existing checkout instead of cloning; useful in CI. +: "${_commit:=@GITHUB_COMMIT@}" + +pkgname=sunshine +pkgver=@PROJECT_VERSION@@SUNSHINE_SUB_VERSION@ +pkgrel=0 +pkgdesc="@PROJECT_DESCRIPTION@" +url="@PROJECT_HOMEPAGE_URL@" +arch="x86_64 aarch64" +license="GPL-3.0-only" +install="$pkgname.post-install $pkgname.post-upgrade" +options="net" + +# Shared-library dependencies are added automatically by abuild. These packages +# provide runtime tools and Qt platform plugins that ELF dependency scanning +# cannot discover. +depends=" + eudev + kmod + libcap-utils + qt6-qtbase-x11 + xrandr +" +makedepends=" + appstream + appstream-glib + build-base + cmake + curl-dev + desktop-file-utils + eudev-dev + freetype-dev + git + glib-dev + glslang + gnutls-dev + lame-dev + libass-dev + libcap-dev + libdrm-dev + libevdev-dev + libtool + libva-dev + libvorbis-dev + libx11-dev + libxcb-dev + libxfixes-dev + libxrandr-dev + libxtst-dev + linux-headers + mesa-dev + meson + miniupnpc-dev + nasm + nodejs + npm + numactl-dev + openssl-dev + opus-dev + pipewire-dev + pkgconf + pulseaudio-dev + py3-jinja2 + qt6-qtbase-dev + qt6-qtsvg-dev + samurai + sdl2-dev + shaderc-dev + vulkan-loader-dev + wayland-dev + zlib-dev +" +checkdepends=" + gcovr + imagemagick + xvfb-run +" + +case "$_run_unit_tests" in + [tT]*) ;; + *) options="$options !check" ;; +esac + +if [ -n "$_source_dir" ]; then + builddir="$_source_dir" +else + builddir="$srcdir/$pkgname" + source="$pkgname::git+@GITHUB_CLONE_URL@#commit=$_commit" + sha512sums="SKIP" +fi + +_build_dir="$srcdir/cmake-build-alpine" + +prepare() { + default_prepare + if [ -z "$_source_dir" ]; then + git -C "$builddir" submodule update --recursive --init --depth 1 + fi +} + +build() { + export BRANCH="@GITHUB_BRANCH@" + export BUILD_VERSION="@BUILD_VERSION@" + export COMMIT="$_commit" + + _build_tests=OFF + case "$_run_unit_tests" in + [tT]*) _build_tests=ON ;; + esac + + cmake -S "$builddir" -B "$_build_dir" -G Ninja -Wno-dev \ + -DBUILD_DOCS=OFF \ + -DBUILD_TESTS="$_build_tests" \ + -DBUILD_WERROR=ON \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DFFMPEG_RELEASE_ASSET_NAME="Alpine-$CARCH-ffmpeg.tar.gz" \ + -DSUNSHINE_ASSETS_DIR=share/sunshine \ + -DSUNSHINE_ENABLE_CUDA=OFF \ + -DCUDA_FAIL_ON_MISSING=OFF \ + -DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ + -DSUNSHINE_PUBLISHER_NAME=LizardByte \ + -DSUNSHINE_PUBLISHER_WEBSITE=https://app.lizardbyte.dev \ + -DSUNSHINE_PUBLISHER_ISSUE_URL=https://app.lizardbyte.dev/support + + appstreamcli validate --no-net "$_build_dir/dev.lizardbyte.app.Sunshine.metainfo.xml" + appstream-util validate "$_build_dir/dev.lizardbyte.app.Sunshine.metainfo.xml" + desktop-file-validate "$_build_dir/dev.lizardbyte.app.Sunshine.desktop" + desktop-file-validate "$_build_dir/dev.lizardbyte.app.Sunshine.terminal.desktop" + + cmake --build "$_build_dir" +} + +check() { + cd "$_build_dir/tests" + GCOV_EXIT_AT_ERROR=1 xvfb-run -a ./test_sunshine \ + --gtest_color=yes \ + --gtest_output=xml:test_results.xml + + cd "$_build_dir" + gcovr --gcov-executable gcov . -r "$builddir/src" \ + --exclude-noncode-lines \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --xml-pretty \ + -o coverage.xml + + if [ -n "$_coverage_dir" ]; then + mkdir -p "$_coverage_dir/tests" + cp coverage.xml "$_coverage_dir/coverage.xml" + cp tests/test_results.xml "$_coverage_dir/tests/test_results.xml" + fi + + ./sunshine --version +} + +package() { + DESTDIR="$pkgdir" cmake --install "$_build_dir" + install -Dm644 "$builddir/src_assets/linux/misc/60-sunshine.conf" \ + "$pkgdir/etc/modules-load.d/60-sunshine.conf" +} diff --git a/packaging/linux/Alpine/sunshine.post-install b/packaging/linux/Alpine/sunshine.post-install new file mode 100644 index 00000000000..0c893138caa --- /dev/null +++ b/packaging/linux/Alpine/sunshine.post-install @@ -0,0 +1,29 @@ +#!/bin/sh + +set_sunshine_capabilities() { + if command -v setcap >/dev/null 2>&1; then + setcap cap_sys_admin,cap_sys_nice+p /usr/bin/sunshine || \ + echo "Warning: unable to set capabilities on /usr/bin/sunshine" >&2 + fi +} + +reload_udev_rules() { + if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload-rules || true + udevadm trigger --property-match=DEVNAME=/dev/uinput || true + udevadm trigger --property-match=DEVNAME=/dev/uhid || true + udevadm trigger --subsystem-match=hidraw || true + udevadm trigger --subsystem-match=input || true + fi +} + +load_input_modules() { + if command -v modprobe >/dev/null 2>&1; then + modprobe uinput || true + modprobe uhid || true + fi +} + +set_sunshine_capabilities +reload_udev_rules +load_input_modules