[env] CARGO_TARGET_DIR = "target" CROSS_TARGET_DIR = { value = "target/cross", condition = { env_not_set = ["CI"] } } # TARGET_HOST = "192.168.5.3" # TARGET = "arm-unknown-linux-gnueabihf" TARGET_HOST = "192.168.5.4" TARGET = "aarch64-unknown-linux-gnu" # Version single source of truth is [workspace.package] in Cargo.toml # TARGET = "x86_64-unknown-linux-gnu" RELEASE_VERSION = { script = ["grep -m1 '^version' Cargo.toml | cut -d'\"' -f2"] } RELEASE_BRANCH = "main_public" [config] default_to_workspace = false # ═══════════════════════════════════════════ # RELEASE BUILD TASKS (used by CI workflows) # ═══════════════════════════════════════════ # ── FE (Web UI) ─────────────────────────── [tasks.npm_install] description = "Install npm dependencies for web-ui" cwd = "web-ui" command = "npm" args = ["install"] [tasks.build_css] dependencies = ["npm_install"] description = "Build Tailwind CSS + DaisyUI (minified)" cwd = "web-ui" command = "npx" args = ["tailwindcss", "-i", "input.css", "-o", "public/tw.css", "--minify"] [tasks.build_ui_release] description = "Build CSS + release WASM with Dioxus and copy to dist/" dependencies = ["build_css"] run_task = "build_ui_wasm_release" [tasks.build_ui_wasm_release] description = "Release WASM with Dioxus from the committed public/tw.css (no npm), copied to dist/" cwd = "web-ui" script = [ "# dx never removes old hashed assets, and everything in dist/ ends up embedded in the binaries", "rm -rf target/dx/rsplayer_web_ui/release", "dx build --platform web --release --debug-symbols false --package rsplayer_web_ui", "# Copy output outside target/ so cross Docker containers can see it", "rm -rf ../dist/web-ui", "mkdir -p ../dist/web-ui", "cp -r target/dx/rsplayer_web_ui/release/web/public/ ../dist/web-ui/", ] # ── BE (Backend) ────────────────────────── [tasks.build_desktop_release] description = "Build web UI + desktop release bundle" dependencies = ["build_ui_release"] run_task = "bundle_desktop_release" [tasks.bundle_desktop_release] description = "Build desktop release bundle (deb+rpm+arch tgz on Linux via Tauri). Native build: output follows the host arch (x86_64 or aarch64)." script = [ "set -e", "# Package dir and arch suffix follow the host (not ${TARGET}, which defaults to aarch64 for Pi deploys)", "HOST_ARCH=$(uname -m)", "case ${HOST_ARCH} in aarch64) ARCH_SUFFIX=arm64 ;; *) ARCH_SUFFIX=amd64 ;; esac", "cd crates/desktop", "# Copy loading.html to frontend dist (Tauri expects it alongside the WASM app)", "cp loading.html ../../dist/web-ui/loading.html", "# Build the pkexec mount helper first — tauri.conf.json bundles it into deb/rpm", "cargo build --release --package rsplayer-mount-helper", "cargo tauri build --bundles deb,rpm --ci", "PKG_DIR=$(pwd)/../../target/cross/${HOST_ARCH}-unknown-linux-gnu/pkg", "ARCH_DIR=$(pwd)/../../target/cross/${HOST_ARCH}-unknown-linux-gnu/arch-desktop", "mkdir -p ${PKG_DIR}", "mv target/release/bundle/deb/rsplayer-desktop_*.deb ${PKG_DIR}/", "mv target/release/bundle/rpm/rsplayer-desktop-*.rpm ${PKG_DIR}/", "# Build Arch Linux tgz package", "mkdir -p ${ARCH_DIR}/rootfs/usr/bin", "mkdir -p ${ARCH_DIR}/rootfs/usr/share/applications", "mkdir -p ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/32x32/apps", "mkdir -p ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/128x128/apps", "mkdir -p ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/256x256/apps", "mkdir -p ${ARCH_DIR}/rootfs/usr/libexec", "mkdir -p ${ARCH_DIR}/rootfs/usr/share/polkit-1/actions", "cp target/release/rsplayer-desktop ${ARCH_DIR}/rootfs/usr/bin/rsplayer-desktop", "cp target/release/rsplayer-mount-helper ${ARCH_DIR}/rootfs/usr/libexec/rsplayer-mount-helper", "cp ../../PKGS/desktop/io.github.ljufa.rsplayer.policy ${ARCH_DIR}/rootfs/usr/share/polkit-1/actions/io.github.ljufa.rsplayer.policy", "cp ../../PKGS/arch/desktop/rsplayer-desktop.desktop ${ARCH_DIR}/rootfs/usr/share/applications/rsplayer-desktop.desktop", "cp ../../crates/desktop/icons/32x32.png ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/32x32/apps/rsplayer-desktop.png", "cp ../../crates/desktop/icons/128x128.png ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/128x128/apps/rsplayer-desktop.png", "cp ../../crates/desktop/icons/128x128@2x.png ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/256x256/apps/rsplayer-desktop.png", "RELEASE_VERSION=$(grep -m1 '^version' ../../Cargo.toml | cut -d'\"' -f2)", "(cd ${ARCH_DIR}/rootfs && tar -czf ${PKG_DIR}/rsplayer-desktop_${RELEASE_VERSION}_${ARCH_SUFFIX}.tgz .)", "rm -rf ${ARCH_DIR}/rootfs", "echo \"Arch desktop package: ${PKG_DIR}/rsplayer-desktop_${RELEASE_VERSION}_${ARCH_SUFFIX}.tgz\"", ] [tasks.bundle_desktop_release_aarch64] description = "Cross-build the desktop app for aarch64 (RPi 4/5) with cross (same rsplayer-cross-aarch64 image as the server) and bundle deb+rpm+Arch tgz. Needs dist/web-ui (build_ui_release)." script = [ "set -e", "T=aarch64-unknown-linux-gnu", "case \"${CARGO_TARGET_DIR}\" in /*) OUT=\"${CARGO_TARGET_DIR}\" ;; *) OUT=\"$(pwd)/${CARGO_TARGET_DIR}\" ;; esac", "REL=${OUT}/${T}/release", "# Tauri embeds the frontend dist (incl. loading.html) at compile time", "cp crates/desktop/loading.html dist/web-ui/loading.html", "cross build --target ${T} --release --package rsplayer-desktop --package rsplayer-mount-helper --features rsplayer-desktop/custom-protocol", "# Package the cross-built binaries without recompiling; tauri.conf.json points the bundled mount helper at the host build, so override it", "OVERRIDE='{\"bundle\":{\"linux\":{\"deb\":{\"files\":{\"/usr/libexec/rsplayer-mount-helper\":\"'${REL}'/rsplayer-mount-helper\"}},\"rpm\":{\"files\":{\"/usr/libexec/rsplayer-mount-helper\":\"'${REL}'/rsplayer-mount-helper\"}}}}}'", "(cd crates/desktop && CARGO_TARGET_DIR=${OUT} cargo tauri bundle --target ${T} --bundles deb,rpm --ci -c \"${OVERRIDE}\")", "PKG_DIR=${OUT}/${T}/pkg", "ARCH_DIR=${OUT}/${T}/arch-desktop", "mkdir -p ${PKG_DIR}", "mv ${REL}/bundle/deb/rsplayer-desktop_*_arm64.deb ${PKG_DIR}/", "mv ${REL}/bundle/rpm/rsplayer-desktop-*.aarch64.rpm ${PKG_DIR}/", "# Arch Linux tgz (same layout as bundle_desktop_release)", "mkdir -p ${ARCH_DIR}/rootfs/usr/bin ${ARCH_DIR}/rootfs/usr/libexec ${ARCH_DIR}/rootfs/usr/share/applications ${ARCH_DIR}/rootfs/usr/share/polkit-1/actions", "mkdir -p ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/32x32/apps ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/128x128/apps ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/256x256/apps", "cp ${REL}/rsplayer-desktop ${ARCH_DIR}/rootfs/usr/bin/rsplayer-desktop", "cp ${REL}/rsplayer-mount-helper ${ARCH_DIR}/rootfs/usr/libexec/rsplayer-mount-helper", "cp PKGS/desktop/io.github.ljufa.rsplayer.policy ${ARCH_DIR}/rootfs/usr/share/polkit-1/actions/io.github.ljufa.rsplayer.policy", "cp PKGS/arch/desktop/rsplayer-desktop.desktop ${ARCH_DIR}/rootfs/usr/share/applications/rsplayer-desktop.desktop", "cp crates/desktop/icons/32x32.png ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/32x32/apps/rsplayer-desktop.png", "cp crates/desktop/icons/128x128.png ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/128x128/apps/rsplayer-desktop.png", "cp crates/desktop/icons/128x128@2x.png ${ARCH_DIR}/rootfs/usr/share/icons/hicolor/256x256/apps/rsplayer-desktop.png", "RELEASE_VERSION=$(grep -m1 '^version' Cargo.toml | cut -d'\"' -f2)", "(cd ${ARCH_DIR}/rootfs && tar -czf ${PKG_DIR}/rsplayer-desktop_${RELEASE_VERSION}_arm64.tgz .)", "rm -rf ${ARCH_DIR}/rootfs", "ls -la ${PKG_DIR}/rsplayer-desktop*", ] [tasks.bundle_desktop_release_aarch64.env] CARGO_TARGET_DIR = { value = "target/cross", condition = { env_not_set = ["CI"] } } RUST_SYSROOT_LIB = { script = ["echo $(rustc --print sysroot)/lib"] } CROSS_CONTAINER_OPTS = "-e LD_LIBRARY_PATH=${RUST_SYSROOT_LIB} -v ${RUST_SYSROOT_LIB}:${RUST_SYSROOT_LIB}" [tasks.build_release] script = [ "# Clean native build-scripts that may have been cached by rust-cache", "# from a different glibc environment than the cross containers", "cargo clean --target ${TARGET}", "cross build --target ${TARGET} --release --package rsplayer --bin rsplayer", ] [tasks.build_release.env] CARGO_TARGET_DIR = { value = "target/cross", condition = { env_not_set = ["CI"] } } RUST_SYSROOT_LIB = { script = ["echo $(rustc --print sysroot)/lib"] } CROSS_CONTAINER_OPTS = "-e LD_LIBRARY_PATH=${RUST_SYSROOT_LIB} -v ${RUST_SYSROOT_LIB}:${RUST_SYSROOT_LIB}" # ── Packaging ───────────────────────────── # Each packaging task places the final, release-ready asset (exact file name # published on GitHub releases) into ${CARGO_TARGET_DIR}/${TARGET}/pkg/ [tasks.package_deb_release.env] CARGO_TARGET_DIR = { value = "target/cross", condition = { env_not_set = ["CI"] } } [tasks.package_deb_release] script = [ "set -e", "case ${TARGET} in", " arm-unknown-linux-gnueabihf) suffix=armhfv6 ;;", " armv7-unknown-linux-gnueabihf) suffix=armhfv7 ;;", " aarch64-unknown-linux-gnu) suffix=arm64 ;;", " x86_64-unknown-linux-gnu) suffix=amd64 ;;", " riscv64gc-unknown-linux-gnu) suffix=riscv64 ;;", " *) suffix=${TARGET} ;;", "esac", "PKG_DIR=${CARGO_TARGET_DIR}/${TARGET}/pkg", "mkdir -p ${PKG_DIR}", "cargo deb --no-build -p rsplayer --target=${TARGET} -o ${PKG_DIR}/rsplayer_${RELEASE_VERSION}_${suffix}.deb", 'echo "Deb package: ${PKG_DIR}/rsplayer_${RELEASE_VERSION}_${suffix}.deb"', ] [tasks.package_rpm_release.env] CARGO_TARGET_DIR = { value = "target/cross", condition = { env_not_set = ["CI"] } } [tasks.package_rpm_release] script = [ "set -e", "case ${TARGET} in", " arm-unknown-linux-gnueabihf) rpm_arch=armv6hl ;;", " armv7-unknown-linux-gnueabihf) rpm_arch=armv7hl ;;", " aarch64-unknown-linux-gnu) rpm_arch=aarch64 ;;", " x86_64-unknown-linux-gnu) rpm_arch=x86_64 ;;", " riscv64gc-unknown-linux-gnu) rpm_arch=riscv64 ;;", " *) rpm_arch=${TARGET} ;;", "esac", "PKG_DIR=${CARGO_TARGET_DIR}/${TARGET}/pkg", "mkdir -p ${PKG_DIR}", "cargo generate-rpm -p crates/server --target ${TARGET} --target-dir ${CARGO_TARGET_DIR} --arch ${rpm_arch} --payload-compress gzip -o ${PKG_DIR}/rsplayer_${RELEASE_VERSION}_${rpm_arch}.rpm", 'echo "RPM package: ${PKG_DIR}/rsplayer_${RELEASE_VERSION}_${rpm_arch}.rpm"', ] [tasks.package_arch_release.env] CARGO_TARGET_DIR = { value = "target/cross", condition = { env_not_set = ["CI"] } } [tasks.package_arch_release] script = [ "set -e", "ARCH_DIR=${CARGO_TARGET_DIR}/${TARGET}/arch", "mkdir -p ${ARCH_DIR}", "cp ${CARGO_TARGET_DIR}/${TARGET}/release/rsplayer ${ARCH_DIR}/rsplayer", "cp PKGS/debian/etc/systemd/system/rsplayer.service ${ARCH_DIR}/rsplayer.service", "cp PKGS/debian/etc/polkit-1/rules.d/99-rsplayer.rules ${ARCH_DIR}/99-rsplayer.rules", "cp PKGS/debian/opt/rsplayer/env ${ARCH_DIR}/env", "cp PKGS/debian/opt/rsplayer/self.crt ${ARCH_DIR}/self.crt", "cp PKGS/debian/opt/rsplayer/self.key ${ARCH_DIR}/self.key", "case ${TARGET} in", " arm-unknown-linux-gnueabihf) arch_arch=armhfv6 ;;", " armv7-unknown-linux-gnueabihf) arch_arch=armhfv7 ;;", " aarch64-unknown-linux-gnu) arch_arch=arm64 ;;", " x86_64-unknown-linux-gnu) arch_arch=amd64 ;;", " riscv64gc-unknown-linux-gnu) arch_arch=riscv64 ;;", " *) arch_arch=${TARGET} ;;", "esac", "mkdir -p ${ARCH_DIR}/rootfs/usr/bin ${ARCH_DIR}/rootfs/etc/systemd/system ${ARCH_DIR}/rootfs/etc/polkit-1/rules.d ${ARCH_DIR}/rootfs/opt/rsplayer", "cp ${ARCH_DIR}/rsplayer ${ARCH_DIR}/rootfs/usr/bin/rsplayer && chmod 755 ${ARCH_DIR}/rootfs/usr/bin/rsplayer", "cp ${ARCH_DIR}/rsplayer.service ${ARCH_DIR}/rootfs/etc/systemd/system/rsplayer.service", "cp ${ARCH_DIR}/99-rsplayer.rules ${ARCH_DIR}/rootfs/etc/polkit-1/rules.d/99-rsplayer.rules", "cp ${ARCH_DIR}/env ${ARCH_DIR}/rootfs/opt/rsplayer/env", "cp ${ARCH_DIR}/self.crt ${ARCH_DIR}/rootfs/opt/rsplayer/self.crt", "cp ${ARCH_DIR}/self.key ${ARCH_DIR}/rootfs/opt/rsplayer/self.key", "PKG_DIR=${CARGO_TARGET_DIR}/${TARGET}/pkg", "mkdir -p ${PKG_DIR}", "cd ${ARCH_DIR}/rootfs && tar -czf ../../pkg/rsplayer_${RELEASE_VERSION}_${arch_arch}.tgz . && cd .. && rm -rf rootfs", 'echo "Arch package: ${PKG_DIR}/rsplayer_${RELEASE_VERSION}_${arch_arch}.tgz"', ] [tasks.stage_binary_release.env] CARGO_TARGET_DIR = { value = "target/cross", condition = { env_not_set = ["CI"] } } [tasks.stage_binary_release] description = "Copy raw release binary into pkg dir under its release asset name" script = [ "set -e", "case ${TARGET} in", " arm-unknown-linux-gnueabihf) suffix=armhfv6 ;;", " armv7-unknown-linux-gnueabihf) suffix=armhfv7 ;;", " aarch64-unknown-linux-gnu) suffix=arm64 ;;", " x86_64-unknown-linux-gnu) suffix=amd64 ;;", " riscv64gc-unknown-linux-gnu) suffix=riscv64 ;;", " aarch64-apple-darwin) suffix=darwin_arm64 ;;", " x86_64-apple-darwin) suffix=darwin_amd64 ;;", " *) suffix=${TARGET} ;;", "esac", "PKG_DIR=${CARGO_TARGET_DIR}/${TARGET}/pkg", "mkdir -p ${PKG_DIR}", "cp ${CARGO_TARGET_DIR}/${TARGET}/release/rsplayer ${PKG_DIR}/rsplayer_${suffix}", ] [tasks.package_linux_release] description = "Produce all Linux release assets (deb, rpm, arch tgz, raw binary) for ${TARGET}" dependencies = [ "package_deb_release", "package_rpm_release", "package_arch_release", "stage_binary_release", ] [tasks.package_arch_pkgs] description = "Build the Arch packages (rsplayer-bin, rsplayer-desktop-bin .pkg.tar.zst) from the release tgz files in ARCH_ASSETS (default: every target/cross/*/pkg), in an archlinux container. Output: target/arch-pkg/" script = [ "set -e", "RELEASE_VERSION=$(grep -m1 '^version' Cargo.toml | cut -d'\"' -f2)", "ASSETS=$(pwd)/target/arch-pkg/assets", "rm -rf target/arch-pkg && mkdir -p ${ASSETS}", "if [ -n \"${ARCH_ASSETS}\" ]; then cp ${ARCH_ASSETS}/*.tgz ${ASSETS}/; else cp target/cross/*/pkg/*.tgz ${ASSETS}/; fi", "docker run --rm -v $(pwd):/src:ro -v ${ASSETS}:/assets:ro -v $(pwd)/target/arch-pkg:/out archlinux:latest bash -c \"pacman -Syu --noconfirm --needed base-devel >/dev/null && bash /src/PKGS/arch/build-pkgs.sh ${RELEASE_VERSION} /assets /out && chown -R $(id -u):$(id -g) /out\"", ] # ═══════════════════════════════════════════ # LOCAL / DEV TASKS # ═══════════════════════════════════════════ # ── FE (Web UI) ─────────────────────────── [tasks.build_css_dev] description = "Build Tailwind CSS + DaisyUI (minified like the release, so the committed public/tw.css only changes with its content)" cwd = "web-ui" command = "npx" args = ["tailwindcss", "-i", "input.css", "-o", "public/tw.css", "--minify"] [tasks.watch_css] description = "Watch input.css and rebuild Tailwind on changes" cwd = "web-ui" command = "npx" args = ["tailwindcss", "-i", "input.css", "-o", "public/tw.css", "--minify", "--watch"] [tasks.dx_serve] description = "Run dx serve for dev" cwd = "web-ui" command = "dx" args = ["serve", "--platform", "web", "--package", "rsplayer_web_ui"] [tasks.serve_dev] description = "Run Tailwind watch + dx serve concurrently" cwd = "web-ui" run_task = { name = ["watch_css", "dx_serve"], parallel = true } [tasks.build_ui_dev] description = "Build dev WASM with Dioxus" cwd = "web-ui" dependencies = ["build_css_dev"] command = "dx" args = ["build", "--platform", "web", "--package", "rsplayer_web_ui"] # ── Desktop ─────────────────────────────── [tasks.run_desktop_dev] description = "Build release UI + run the desktop app locally via cargo tauri dev" dependencies = ["build_ui_release"] env = { "RUST_LOG" = "rsplayer=info,playback=debug,metadata=info,symphonia=info,warp=info", "RUST_BACKTRACE" = "full" } script = [ "cp crates/desktop/loading.html dist/web-ui/loading.html", "cd crates/desktop && cargo tauri dev", ] # ── Android (Tauri mobile) ──────────────── # Needs ANDROID_HOME, NDK_HOME and JAVA_HOME plus the android rust targets — # see docs/build.md "Android". The generated Gradle project lives in # crates/desktop/gen/android and is committed (Kotlin media service, manifest). [tasks.android_init] description = "One-time: (re)generate crates/desktop/gen/android via cargo tauri android init" script = ["cd crates/desktop && cargo tauri android init"] [tasks.run_android_dev] description = "Build release UI + run the Android app on the connected device/emulator (ANDROID_TARGET=aarch64|x86_64|armv7)" dependencies = ["build_ui_release"] env = { "ANDROID_TARGET" = { value = "aarch64", condition = { env_not_set = ["ANDROID_TARGET"] } } } script = [ "cp crates/desktop/loading.html dist/web-ui/loading.html", "(cd crates/desktop && cargo tauri android dev --target ${ANDROID_TARGET})", ] [tasks.build_android_dev] description = "Debug-signed universal APK for sideloading (adb install), arm64 + x86_64" dependencies = ["build_ui_release"] script = [ "cp crates/desktop/loading.html dist/web-ui/loading.html", "(cd crates/desktop && cargo tauri android build --debug --apk --target aarch64 --target x86_64)", "ls -la crates/desktop/gen/android/app/build/outputs/apk/universal/debug/", ] [tasks.build_ui_android] description = "Release UI built with the Android reproducible-build environment (see crates/desktop/android-build-env.sh): no npm, committed CSS and fonts, dx without downloads" script = [". ./crates/desktop/android-build-env.sh && cargo make build_ui_wasm_release"] [tasks.build_android_release] description = "Build release UI + universal APK/AAB (arm64 + armv7) + one APK per ABI (arm64, armv7, x86_64) into target/android/pkg" dependencies = ["build_ui_android", "bundle_android_release", "bundle_android_abi_release"] [tasks.bundle_android_release] description = "Release APK + AAB from the existing dist/web-ui (which must come from build_ui_android to be reproducible), signed when crates/desktop/gen/android/keystore.properties exists" script = [ "set -e", "cp crates/desktop/loading.html dist/web-ui/loading.html", "(cd crates/desktop && . ./android-build-env.sh && cargo tauri android build --apk --aab --target aarch64 --target armv7 --ci)", "PKG_DIR=target/android/pkg && rm -rf ${PKG_DIR} && mkdir -p ${PKG_DIR}", "cp crates/desktop/gen/android/app/build/outputs/bundle/universalRelease/app-universal-release.aab ${PKG_DIR}/rsplayer_${RELEASE_VERSION}_android.aab", "cp crates/desktop/gen/android/app/build/outputs/apk/universal/release/app-universal-release*.apk ${PKG_DIR}/rsplayer_${RELEASE_VERSION}_android.apk", "ls -la ${PKG_DIR}", ] [tasks.bundle_android_abi_release] description = "One APK per ABI (the inputs F-Droid's reproducible-build check compares against); run after bundle_android_release, which recreates target/android/pkg" script = [ "set -e", "cp crates/desktop/loading.html dist/web-ui/loading.html", "mkdir -p target/android/pkg", "# RSPLAYER_ABI_CODE (1 armv7, 2 arm64, 4 x86_64) is the last digit of the APK versionCode, see app/build.gradle.kts.", "# x86_64 is for emulators and x86 Chromebooks: under ARM translation the arm64 library cannot see the environment the Kotlin host sets.", "for spec in aarch64:2:arm64-v8a armv7:1:armeabi-v7a x86_64:4:x86_64; do", " target=${spec%%:*}; rest=${spec#*:}; code=${rest%%:*}; abi=${rest#*:}", " (cd crates/desktop && . ./android-build-env.sh && RSPLAYER_ABI_CODE=$code cargo tauri android build --apk --target $target --ci)", " cp crates/desktop/gen/android/app/build/outputs/apk/universal/release/app-universal-release*.apk target/android/pkg/rsplayer_${RELEASE_VERSION}_android_${abi}.apk", "done", "ls -la target/android/pkg", ] # ── Flatpak (local build & run) ─────────── # Builds the desktop app as a flatpak from the *local working tree* # (including uncommitted changes) and installs it into the user flatpak # installation. Releases are built the same way by the build_flatpak job in # .github/workflows/cd.yml and published to the self-hosted repo at # https://ljufa.github.io/rsplayer-flatpak; see PKGS/flatpak/README.md. [tasks.flatpak_cargo_sources] description = "Generate PKGS/flatpak/cargo-sources.json from Cargo.lock (skipped when up to date)" script = [ "set -e", "OUT=PKGS/flatpak/cargo-sources.json", "if [ -f $OUT ] && [ $OUT -nt Cargo.lock ]; then echo 'cargo-sources.json is up to date'; exit 0; fi", "# Self-contained tooling under target/flatpak: generator script + python venv", "TOOLS=target/flatpak/tools", "mkdir -p $TOOLS", "if [ ! -f $TOOLS/flatpak-cargo-generator.py ]; then curl -sfLo $TOOLS/flatpak-cargo-generator.py https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py; fi", "if [ ! -x $TOOLS/venv/bin/python3 ]; then python3 -m venv $TOOLS/venv && $TOOLS/venv/bin/pip -q install aiohttp toml tomlkit; fi", "$TOOLS/venv/bin/python3 $TOOLS/flatpak-cargo-generator.py Cargo.lock -o $OUT", ] [tasks.build_flatpak] description = "Build the desktop app flatpak from the working tree and install it (user installation)" dependencies = ["flatpak_cargo_sources"] script = [ "set -e", "# Stage tracked + untracked (non-ignored) files plus the gitignored web UI dist", "bash PKGS/flatpak/stage-sources.sh target/flatpak/src", "cp PKGS/flatpak/io.github.ljufa.rsplayer.yml PKGS/flatpak/cargo-sources.json target/flatpak/", "# Use native flatpak-builder when present, else the org.flatpak.Builder flatpak.", "# FLATPAK_USER_DIR works around the sandboxed builder not finding user-installed SDKs.", "if command -v flatpak-builder >/dev/null 2>&1; then BUILDER='flatpak-builder'; else BUILDER=\"flatpak run --env=FLATPAK_USER_DIR=$HOME/.local/share/flatpak --command=flatpak-builder org.flatpak.Builder\"; fi", "# Export to a local repo and install from the HOST flatpak (not --install from", "# inside the builder sandbox): the desktop-file export rewrites Exec to the", "# path of the installing flatpak binary, and from inside the sandbox that is", "# /app/bin/flatpak — which doesn't exist on the host, breaking menu launches.", "cd target/flatpak && $BUILDER --force-clean --disable-rofiles-fuse --repo=repo build-dir io.github.ljufa.rsplayer.yml", "flatpak --user remote-add --if-not-exists --no-gpg-verify rsplayer-local \"file://$PWD/repo\"", "flatpak --user install --reinstall --noninteractive rsplayer-local io.github.ljufa.rsplayer || flatpak --user install --noninteractive rsplayer-local io.github.ljufa.rsplayer", "# Refresh the cached AppStream catalog so software stores (COSMIC Store,", "# GNOME Software) see the rebuilt metadata/screenshots immediately —", "# flatpak otherwise refreshes remote catalogs only about once a day.", "flatpak --user update --appstream rsplayer-local", ] [tasks.run_flatpak] description = "Run the locally installed flatpak desktop app" command = "flatpak" args = ["run", "io.github.ljufa.rsplayer"] [tasks.flatpak_bundle] description = "Create a single-file .flatpak bundle (for testing on other machines/VMs) from the repo produced by build_flatpak" script = [ "set -e", "test -d target/flatpak/repo || { echo 'No target/flatpak/repo — run: cargo make build_flatpak'; exit 1; }", "flatpak build-bundle target/flatpak/repo target/flatpak/rsplayer-${RELEASE_VERSION}.flatpak io.github.ljufa.rsplayer master", "echo \"Bundle ready: target/flatpak/rsplayer-${RELEASE_VERSION}.flatpak\"", ] # ── Snap (local build & install) ────────── # Builds the desktop app snap from the working tree with snapcraft (LXD # backend). Like the flatpak, the web UI must be pre-built: snapcraft packs # the tree with dist/web-ui in place. See PKGS/snap/README.md. [tasks.build_snap] description = "Build the desktop app snap from the working tree (snapcraft/LXD)" script = [ "set -e", "test -f dist/web-ui/public/index.html || { echo 'No dist/web-ui — run: cargo make build_ui_release'; exit 1; }", "# Stage a clean tree (same trick as build_flatpak): snapcraft copies the", "# whole source dir into the LXD container and does not respect .gitignore,", "# so building from the repo root would ship the multi-GB target/ caches", "# (and choke on root-owned files left by docker builds).", "rm -rf target/snap/src", "mkdir -p target/snap/src", "git ls-files -co --exclude-standard -z | tar --null -T - -cf - | tar -xf - -C target/snap/src", "cp -r dist target/snap/src/dist", "(cd target/snap/src && snapcraft pack)", "mv target/snap/src/rsplayer_*.snap .", "ls -1 rsplayer_*.snap", ] [tasks.install_snap] description = "Install the locally built snap (dangerous mode, unsigned)" script = [ "set -e", "snap_file=$(ls -1t rsplayer_*.snap | head -1)", "test -n \"$snap_file\" || { echo 'No rsplayer_*.snap — run: cargo make build_snap'; exit 1; }", "sudo snap install --dangerous \"$snap_file\"", ] [tasks.run_snap] description = "Run the locally installed snap desktop app" command = "snap" args = ["run", "rsplayer"] # ── BE (Backend) ────────────────────────── [tasks.build_dev] description = "Compile backend in dev mode" command = "cargo" args = ["build", "--package", "rsplayer", "--bin", "rsplayer"] [tasks.kill_local] description = "Kill running rsplayer process" script = "sudo pkill -9 rsplayer || true" [tasks.run_local] description = "Build backend, set capabilities, and run locally" dependencies = ["kill_local", "build_dev"] env = { "RUST_LOG" = "rsplayer=info,playback=debug,metadata=info,symphonia=info,warp=info", "RUST_BACKTRACE" = "full"} script = [ "set -e", "sudo /usr/bin/setcap 'cap_net_bind_service,cap_sys_boot,cap_sys_nice,cap_sys_admin,cap_dac_read_search+ep' target/debug/rsplayer", "cd .run && exec ../target/debug/rsplayer", ] # ── Deploy ──────────────────────────────── # Runs copy_remote_3 and copy_remote_4 as two *separate* cargo-make # processes. `run_task = { parallel = true }` cannot be used here: parallel # tasks share one process and cargo-make applies task env blocks as # process-global variables, so TARGET/TARGET_HOST/CARGO_TARGET_DIR of the # two targets race and clobber each other. [tasks.copy_remote] description = "Build and deploy to targets .3 and .4 in parallel" script = ''' RC_DIR=$(mktemp -d) ( cargo make copy_remote_3 2>&1; echo $? > "$RC_DIR/rc3" ) | sed -u 's/^/[.3 arm ] /' & P3=$! ( cargo make copy_remote_4 2>&1; echo $? > "$RC_DIR/rc4" ) | sed -u 's/^/[.4 arm64] /' & P4=$! wait $P3 wait $P4 RC3=$(cat "$RC_DIR/rc3" 2>/dev/null || echo 1) RC4=$(cat "$RC_DIR/rc4" 2>/dev/null || echo 1) rm -rf "$RC_DIR" echo "copy_remote_3 exit=$RC3, copy_remote_4 exit=$RC4" [ "$RC3" = "0" ] && [ "$RC4" = "0" ] ''' [tasks.build_release_3.env] TARGET = "arm-unknown-linux-gnueabihf" # Dedicated per-target dir: parallel builds don't fight over cargo's # build-dir lock, and since no host build ever touches it, the defensive # `cargo clean` is unnecessary — deploys build incrementally. CARGO_TARGET_DIR = { value = "target/cross-arm", condition = { env_not_set = ["CI"] } } RUST_SYSROOT_LIB = { script = ["echo $(rustc --print sysroot)/lib"] } CROSS_CONTAINER_OPTS = "-e LD_LIBRARY_PATH=${RUST_SYSROOT_LIB} -v ${RUST_SYSROOT_LIB}:${RUST_SYSROOT_LIB}" [tasks.build_release_3] description = "Cross-build release for target .3 (arm)" script = [ "cross build --target ${TARGET} --release --package rsplayer --bin rsplayer", ] [tasks.copy_remote_3.env] TARGET_HOST = "192.168.5.3" TARGET = "arm-unknown-linux-gnueabihf" CARGO_TARGET_DIR = { value = "target/cross-arm", condition = { env_not_set = ["CI"] } } [tasks.copy_remote_3] description = "Cross-build release and rsync to target .3" dependencies = ["build_release_3"] script = [ "rsync -avvP --rsync-path=\"sudo rsync\" ${CARGO_TARGET_DIR}/${TARGET}/release/rsplayer pi@${TARGET_HOST}:/usr/bin", "ssh -x pi@${TARGET_HOST} \"sudo systemctl restart rsplayer\"", ] [tasks.build_release_4.env] TARGET = "aarch64-unknown-linux-gnu" CARGO_TARGET_DIR = { value = "target/cross-aarch64", condition = { env_not_set = ["CI"] } } RUST_SYSROOT_LIB = { script = ["echo $(rustc --print sysroot)/lib"] } CROSS_CONTAINER_OPTS = "-e LD_LIBRARY_PATH=${RUST_SYSROOT_LIB} -v ${RUST_SYSROOT_LIB}:${RUST_SYSROOT_LIB}" [tasks.build_release_4] description = "Cross-build release for target .4 (aarch64)" script = [ "cross build --target ${TARGET} --release --package rsplayer --bin rsplayer", ] [tasks.copy_remote_4.env] TARGET_HOST = "192.168.5.4" TARGET = "aarch64-unknown-linux-gnu" CARGO_TARGET_DIR = { value = "target/cross-aarch64", condition = { env_not_set = ["CI"] } } [tasks.copy_remote_4] description = "Cross-build release and rsync to target .4" dependencies = ["build_release_4"] script = [ "rsync -avvP --rsync-path=\"sudo rsync\" ${CARGO_TARGET_DIR}/${TARGET}/release/rsplayer pi@${TARGET_HOST}:/usr/bin", "ssh -x pi@${TARGET_HOST} \"sudo systemctl restart rsplayer\"", ] [tasks.copy_remote_dev] description = "Build dev and rsync to target device" dependencies = ["build_dev"] script = [ "rsync -avvP --rsync-path=\"sudo rsync\" target/debug/rsplayer pi@${TARGET_HOST}:/usr/bin", ] [tasks.package_deb_local.env] CARGO_TARGET_DIR = { value = "target/cross" } [tasks.package_deb_local] description = "Build UI + BE release, package deb, and rsync to target" dependencies = ["build_ui_release", "build_release", "package_deb_release"] script = "rsync -avvP ${CARGO_TARGET_DIR}/${TARGET}/debian/*.deb pi@${TARGET_HOST}:~" [tasks.run_as_rsplayer] description = "Build release locally and restart systemd service" script = [ "sudo systemctl stop rsplayer", "cargo make build_release", "sudo cp target/x86_64-unknown-linux-gnu/release/rsplayer /usr/bin", "sudo systemctl start rsplayer", ] # ═══════════════════════════════════════════ # LINT / TEST / MISC # ═══════════════════════════════════════════ [tasks.check_toolchain_pins] description = "Fail when the pinned Rust / dx / wasm-bindgen versions disagree (rust-toolchain.toml, builder + android images, android-build-env.sh, macOS job, Cargo.lock)" script = ''' status=0 check() { # $1 = where, $2 = wanted, $3 = found (may be several lines) if [ -z "$3" ]; then echo "::error::$1: no version found (pattern out of date?)"; status=1; return fi for v in $3; do if [ "$v" = "$2" ]; then echo "ok $1 $v" else echo "::error::$1 pins $v, expected $2"; status=1; fi done } rust=$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml) echo "rust-toolchain.toml: $rust" check docker/Dockerfile.builder "$rust" "$(sed -n 's/^ARG RUST_VERSION=//p' docker/Dockerfile.builder)" check docker/Dockerfile.android "$rust" "$(sed -n 's/^ARG RUST_VERSION=//p' docker/Dockerfile.android)" check crates/desktop/android-build-env.sh "$rust" "$(sed -n 's/^ANDROID_RUST_VERSION=//p' crates/desktop/android-build-env.sh)" check ".github/workflows/cd.yml (macOS)" "$rust" "$(sed -n 's/^ *toolchain: //p' .github/workflows/cd.yml)" dx=$(awk '/^name = "dioxus"$/ {getline; gsub(/version = |"/, ""); print; exit}' Cargo.lock) echo "dioxus (Cargo.lock): $dx" check docker/Dockerfile.builder "$dx" "$(sed -n 's/^ARG DX_VERSION=//p' docker/Dockerfile.builder)" check crates/desktop/android-build-env.sh "$dx" "$(sed -n 's/^ANDROID_DX_VERSION=//p' crates/desktop/android-build-env.sh)" # The Android image installs wasm-bindgen-cli itself (NO_DOWNLOADS); it must equal the crate exactly. wb=$(awk '/^name = "wasm-bindgen"$/ {getline; gsub(/version = |"/, ""); print; exit}' Cargo.lock) echo "wasm-bindgen (Cargo.lock): $wb" check docker/Dockerfile.android "$wb" "$(sed -n 's/^ARG WASM_BINDGEN_VERSION=//p' docker/Dockerfile.android)" exit $status ''' [tasks.test_ci] description = "Run all tests (creates stub dist/web-ui/public so RustEmbed compiles without a UI build)" script = [ "mkdir -p dist/web-ui/public", "touch dist/web-ui/public/index.html", "cargo test --all-features", ] [tasks.clippy] description = "Lint with Clippy (fix + deny warnings)" install_crate = { rustup_component_name = "clippy", binary = "cargo-clippy", test_arg = "--help" } command = "cargo" args = [ "clippy", "--fix", "--allow-dirty", "--allow-staged", "--all-features", "--", "--deny", "warnings", "--deny", "clippy::pedantic", "--deny", "clippy::nursery", "--allow", "clippy::use_self", "--allow", "clippy::module_name_repetitions", "--allow", "clippy::must_use_candidate", "--allow", "let_underscore_drop", "--allow", "clippy::missing_panics_doc", "--allow", "clippy::missing_errors_doc", "--allow", "clippy::use_self", "--allow", "clippy::significant-drop-tightening", "--allow", "clippy::similar_names", ] [tasks.test_in_cross] command = "cross" args = ["test", "--target", "${TARGET}"] [tasks.test_in_cross.env] RUST_SYSROOT_LIB = { script = ["echo $(rustc --print sysroot)/lib"] } CROSS_CONTAINER_OPTS = "-e LD_LIBRARY_PATH=${RUST_SYSROOT_LIB} -v ${RUST_SYSROOT_LIB}:${RUST_SYSROOT_LIB}" [tasks.tag] script = [ "git tag ${RELEASE_VERSION} ${RELEASE_BRANCH}", "git push originpublic ${RELEASE_VERSION}", ]