docs/build.md
Rama main · réplica de lectura del repositorio original.
# Local Build Guide
This document describes how to build `rsplayer` from source — on Linux (including cross-compilation for other Linux architectures), natively on macOS, and natively on Windows.
Common cross-compilation targets (Linux host):
- `arm-unknown-linux-gnueabihf` (ARMv6)
- `armv7-unknown-linux-gnueabihf` (ARMv7)
- `aarch64-unknown-linux-gnu` (ARM64 Linux)
- `x86_64-unknown-linux-gnu`
- `riscv64gc-unknown-linux-gnu`
## Prerequisites
Before you begin, ensure you have the following prerequisites installed on your Linux development machine.
### 1. Rust and dev tools
Install linux build tools:
```
sudo apt install build-essential pkg-config libasound2-dev
```
Install Rust using `rustup`:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
```
The Rust version is pinned in `rust-toolchain.toml` at the repository root: inside
the repository, `cargo` and `dx` use that version (rustup installs it on first use),
whatever your default toolchain is. Newer compilers can break the release web UI
build, so the pin is deliberate; `cargo make check_toolchain_pins` checks that the
CI images and the Android build use the same version.
### 2. Docker or Podman
The `cross` tool uses a container engine to manage the cross-compilation environment. You must have Docker or Podman installed and running.
- **Docker:** [Install Docker Engine](https://docs.docker.com/engine/install/)
- **Podman:** [Install Podman](https://podman.io/getting-started/installation)
### 3. Cargo Build Tools
Install the necessary cargo tools for building the application and Linux packages:
```bash
cargo install cross
cargo install cargo-deb
cargo install cargo-make
```
### 4. Dioxus CLI
Install the Dioxus CLI for building the frontend:
```bash
cargo install dioxus-cli
```
### 5. Node.js and npm
Required for frontend CSS compilation and font asset setup:
```bash
# Install Node.js via your package manager, e.g.:
sudo apt install nodejs npm
```
## Frontend Setup (one-time after clone)
The frontend lives in `web-ui/`. Run once after cloning to install npm packages and copy font assets to `public/`:
```bash
cd web-ui
npm install
```
`npm install` automatically runs a `postinstall` script that copies the Material Icons font files from `node_modules/` into `public/material-icons/`. Those font files and the generated `public/tw.css` are committed: the Android/F-Droid build uses them as they are and never runs npm (F-Droid does not allow the prebuilt native binaries Tailwind v4 installs).
## Build Process
### Frontend (Web UI)
The frontend is a Rust/WASM app built with the Dioxus framework.
All UI build commands run via `cargo make` from the repo root:
**Development** — served with hot-reload, proxies `/api` and `/artwork` to backend on `localhost:8000`:
```bash
cargo make serve_dev
```
Or manually: `cd web-ui && dx serve`
**Release** — produces a self-contained output under `dist/web-ui/`, embedded into the backend binary at compile time:
```bash
cargo make build_ui_release
```
**Updating CSS**: needed whenever `input.css` or the Tailwind classes used in `src/` change:
```bash
cargo make build_css
# commit public/tw.css after regenerating
```
The release workflow regenerates the CSS for the other platforms and fails if the committed `public/tw.css` or `public/material-icons/` differ from it, so a stale stylesheet cannot reach the Android build unnoticed.
### Backend
The build is orchestrated using `cargo-make`. Build the frontend release first, then the backend.
1. **Set the target architecture** in `Makefile.toml` (or pass it via env). For example, a 64-bit Raspberry Pi 4 target:
```
TARGET="aarch64-unknown-linux-gnu"
```
2. **Run the build:**
```bash
# Build the frontend (must be done before backend)
cargo make build_ui_release
# Build the backend application using cross-compilation
cargo make build_release
# Copy binary to target device (Optional, Linux targets)
# Adjust TARGET_HOST=xxx.xxx.xxx.xx in Makefile.toml
cargo make copy_remote
# Create the .deb package (Optional)
cargo make package_deb_release
```
### macOS targets
macOS binaries are built **natively on a Mac** (cross-compiling from Linux is not supported — the osxcross-based pipeline was removed in v4.0.0; CI uses GitHub macOS runners). On a Mac with Rust, `cargo-make`, and the Dioxus CLI installed:
```bash
cargo make build_ui_release
cargo build --package rsplayer --bin rsplayer --release --no-default-features
```
Darwin release output is binary-only (no `.deb`, `.rpm`, `.tgz` packaging).
## Windows Build (native)
Windows builds must be compiled natively on a Windows machine (or a Windows GitHub Actions runner). Cross-compiling from Linux to Windows is not supported.
### Prerequisites
1. **Rust** — install from [rustup.rs](https://rustup.rs/). The MSVC toolchain is selected by default on Windows.
2. **Visual Studio Build Tools** or Visual Studio with the "Desktop development with C++" workload (needed by some C dependencies).
3. **cargo-make** and **tauri-cli** (for the desktop app):
```powershell
cargo install cargo-make
cargo install tauri-cli --version "^2"
```
4. **Node.js** — required for the web UI CSS build step.
5. **LLVM** — required for `bindgen` (ASIO). Install from [releases.llvm.org](https://releases.llvm.org/) or `choco install llvm`, then set `LIBCLANG_PATH` to its `bin` directory (e.g. `C:\Program Files\LLVM\bin`).
6. **ASIO SDK** — required to link ASIO output. Download the [Steinberg ASIO SDK](https://www.steinberg.net/asiosdk), extract it, and set `CPAL_ASIO_DIR` to the extracted folder. The SDK is not redistributed with rsplayer; you accept Steinberg's license by downloading it.
### Build headless server
The Windows server binary is built without ALSA or LIRC (Linux-only) but **with** the `asio` feature for low-latency ASIO output:
```powershell
# Build the web UI first (required — embedded into the server binary)
cargo make build_ui_release
# Point the build at the ASIO SDK and LLVM (once per shell)
$env:CPAL_ASIO_DIR = "C:\path\to\asiosdk"
$env:LIBCLANG_PATH = "C:\Program Files\LLVM\bin"
# Build the headless server
cargo build --package rsplayer --bin rsplayer --release --no-default-features --features asio --target x86_64-pc-windows-msvc
```
The binary is at `target\x86_64-pc-windows-msvc\release\rsplayer.exe`.
### Build desktop app
```powershell
# Copy loading.html alongside the web UI dist
copy crates\desktop\loading.html dist\web-ui\loading.html
# Build and bundle as NSIS installer
cd crates\desktop
cargo tauri build --bundles nsis --ci
```
The NSIS installer is placed under `target\release\bundle\nsis\`.
The desktop app links ASIO too (via the desktop crate's Windows dependency); the same `CPAL_ASIO_DIR` and `LIBCLANG_PATH` must be set before `cargo tauri build`.
### Audio output on Windows
- **WASAPI** (default host) devices are always available.
- **ASIO** drivers appear as `… (ASIO)` entries in Settings → Audio interface when the build includes the `asio` feature and an ASIO driver is installed. ASIO gives exclusive, low-latency, bit-perfect output; the driver's own control panel sets its buffer size and sample rate.
> ASIO is a trademark and software of Steinberg Media Technologies GmbH.
### Platform limitations on Windows
- **Volume control**: software gain only (ALSA and PipeWire are unavailable). Software gain still applies to ASIO output.
- **Network mounts**: SMB/NFS mounting from the UI is unavailable.
- **Power control**: system poweroff/reboot commands are unavailable.
- **IR remote**: LIRC integration is unavailable.
- **Firmware USB**: serial integration works (cross-platform via `serialport` crate).
### Self-hosted Windows runner (CI)
The `build_desktop_windows` job runs on a self-hosted runner (`runs-on: [self-hosted, windows, x64]`). Unlike GitHub-hosted `windows-latest`, a bare machine has **nothing** pre-installed, so the runner will fail immediately (e.g. `bash: command not found`, because `dtolnay/rust-toolchain` is a bash action). Provision the machine **once**, in an elevated PowerShell:
```powershell
# Allow the runner to execute the per-step .ps1 files it generates.
# Windows defaults to Restricted, which blocks them ("running scripts is disabled").
# Elevated shell (runner as a service): use -Scope LocalMachine.
# Non-elevated shell (runner runs as the current user): use -Scope CurrentUser.
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force
# Toolchain + native build prerequisites
winget install --id Git.Git -e # git (checkout) + Git Bash
winget install --id Rustlang.Rustup -e # rustup / cargo
winget install --id LLVM.LLVM -e # libclang for bindgen (ASIO)
winget install --id Microsoft.VisualStudio.2022.BuildTools -e `
--override "--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
# rustup itself; the toolchain version comes from rust-toolchain.toml in the
# checkout and is installed on the first build
rustup target add x86_64-pc-windows-msvc
# Optional: pre-install so it isn't rebuilt every run
cargo install tauri-cli --version "^2" --locked
```
Notes:
- The workflow sets `LIBCLANG_PATH: C:\Program Files\LLVM\bin`, which matches the default LLVM install location. Adjust the env in `cd.yml` if you install LLVM elsewhere.
- **Visual Studio Build Tools** (the "Desktop development with C++" / VCTools workload) provides the MSVC linker and Windows SDK needed to compile the ASIO C++ shim and link the executables.
- **Restart the runner service after provisioning** — the runner captures `PATH`/environment when it starts, so newly installed `cargo`, `rustup`, `clang` and `git` are not visible until it restarts.
- The workflow's Windows steps use the built-in Windows PowerShell (`shell: powershell`), so neither Git Bash nor PowerShell 7 (`pwsh`) needs to be installed. Git itself is still needed for `actions/checkout`.
## Flatpak (desktop app)
The desktop app is packaged as a Flatpak from `PKGS/flatpak/` (built by the
`build_flatpak` job of the "Full release" workflow) and published to the
self-hosted repo at <https://ljufa.github.io/rsplayer-flatpak> when the
GitHub release is promoted to a full release (`flatpak-repo.yml`) — see
[`PKGS/flatpak/README.md`](https://github.com/ljufa/rsplayer/blob/master/PKGS/flatpak/README.md)
for regenerating `cargo-sources.json`, building locally with `flatpak-builder`,
and the release checklist. The manifest builds a staged copy of the workspace
offline from vendored crates plus the pre-built `dist/web-ui`.
## Arch Linux packages (pacman)
`rsplayer-bin` (server) and `rsplayer-desktop-bin` (desktop app) wrap the
release `.tgz` file trees; nothing is compiled. The `build_arch_pkgs` job of
the "Full release" workflow runs `makepkg` in an `archlinux` container for
every architecture and attaches the `.pkg.tar.zst` files to the release.
Locally, after the tgz files exist:
```bash
cargo make package_arch_pkgs # needs Docker; output in target/arch-pkg/
```
See [`PKGS/arch/README.md`](https://github.com/ljufa/rsplayer/blob/master/PKGS/arch/README.md)
for the layout.
## Android (desktop app)
The Android app is the Tauri desktop crate built for Android (Tauri mobile):
the backend runs in-process, the Kotlin host in `crates/desktop/gen/android`
provides the media session. Requirements (no Android Studio needed):
```bash
# Command-line SDK under ~/Android/Sdk (download commandlinetools-linux-*_latest.zip
# from https://developer.android.com/studio#command-line-tools-only and unzip it
# to ~/Android/Sdk/cmdline-tools/latest), then:
sdkmanager --licenses
sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0" "ndk;28.2.13676358" \
"emulator" "system-images;android-36;google_apis;x86_64"
rustup target add aarch64-linux-android armv7-linux-androideabi wasm32-unknown-unknown
rustup target add x86_64-linux-android # only for emulators (cargo make run_android_dev with ANDROID_TARGET=x86_64)
# In your shell profile (JDK 17+ required):
export ANDROID_HOME="$HOME/Android/Sdk"
export NDK_HOME="$ANDROID_HOME/ndk/28.2.13676358"
export JAVA_HOME=...
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
```
Then:
```bash
# Run on the connected phone (USB debugging) or a running emulator.
# ANDROID_TARGET=aarch64 (default) | x86_64 (emulator) | armv7
cargo make run_android_dev
# Debug-signed universal APK (arm64 + x86_64) for `adb install`
cargo make build_android_dev
# Release APK + AAB (arm64 + armv7) and one APK per ABI (arm64, armv7, x86_64) into target/android/pkg/
# (signed when crates/desktop/gen/android/keystore.properties exists — see
# https://tauri.app/distribute/sign/android/)
cargo make build_android_release
```
### Android release builds (CI)
The "Full release" workflow builds the Android app in the `build_android` job and attaches
to the draft release the universal `rsplayer_<version>_android.apk`, plus one signed APK per
ABI: `rsplayer_<version>_android_arm64-v8a.apk`, `rsplayer_<version>_android_armeabi-v7a.apk` and
`rsplayer_<version>_android_x86_64.apk` (emulators and x86 Chromebooks).
The job runs on a self-hosted runner carrying the `android` label, as a job `container:` on
`ghcr.io/ljufa/rsplayer-android-builder:latest` (built from `docker/Dockerfile.android` by
`build-images.yml`), so the runner itself needs only Docker — none of the SDK/NDK/JDK/rustup
setup above. Dispatch the workflow with target `android` to build only the APKs.
#### Reproducible builds (F-Droid)
F-Droid builds the app from source and compares the result with the per-ABI APKs above (the
`binary:` URLs in its recipe), then publishes our signed file. That only works if both builds
are byte-identical, which turned out to need more than matching tool versions: an identical
commit, `Cargo.lock`, Rust/`dx`/`wasm-bindgen`/`wasm-opt`, built on Fedora, produced a web UI
bundle with different hashed asset names than F-Droid's Debian trixie build — some part of the
OS environment itself affects the output, not just the pinned tools. So the job:
- builds inside the image built from `docker/Dockerfile.android`, Debian trixie with the same
pinned tool versions F-Droid's recipe installs — proven to reproduce F-Droid's build byte for
byte, not just assumed to,
- builds the release UI itself (`cargo make build_ui_android`), never from the shared `web_ui`
artifact, which could have been built anywhere. It uses the committed `public/tw.css` and
icon fonts (no npm), and `NO_DOWNLOADS=1` (set in `android-build-env.sh`) makes `dx` take
`wasm-bindgen`, `wasm-opt` and `esbuild` from PATH instead of downloading prebuilt ones:
cargo-installed `wasm-bindgen-cli`, Debian's `binaryen` and `esbuild`. The `wasm-opt`
version changes the output, so the image and the recipe must install the same Debian ones,
- sources `crates/desktop/android-build-env.sh` before every Rust build. It maps machine-specific
paths (`~/.cargo`, `~/.rustup`, the Rust sources) to fixed names in the binaries and keeps
`--cfg tokio_unstable`, because setting `RUSTFLAGS` replaces `build.rustflags` from
`.cargo/config.toml`,
- runs `android-check-tools.sh` before building, which stops the job if the image is stale
relative to the versions pinned in `android-build-env.sh`,
- sets `SOURCE_DATE_EPOCH` to the commit time, like F-Droid does.
When you change a tool version, change it in all three places in the same release:
`crates/desktop/android-build-env.sh`, `docker/Dockerfile.android` (then let
`build-images.yml` rebuild and push the image), and the F-Droid recipe (`PKGS/fdroid`, and its
copy in fdroiddata). Do not use `asset!()` in the web UI: it embeds the absolute source path in
the wasm.
The APK is signed with the release key from three repository secrets; without them
the job warns and signs with the runner's debug key. Create the key once and keep a
backup of it — every future update must be signed with the same key:
```bash
keytool -genkey -v -keystore rsplayer-release.jks -keyalg RSA -keysize 2048 -validity 10000 -alias rsplayer
base64 -w0 rsplayer-release.jks | gh secret set ANDROID_KEYSTORE_BASE64 --repo ljufa/rsplayer
gh secret set ANDROID_KEYSTORE_PASSWORD --repo ljufa/rsplayer # prompts for the keystore password entered above
gh secret set ANDROID_KEY_ALIAS --body rsplayer --repo ljufa/rsplayer
```
Keep the `.jks` file outside the repository (keystores are git-ignored as a safety net).
An emulator: `avdmanager create avd -n rsplayer36 -k "system-images;android-36;google_apis;x86_64" -d pixel_7`
then `emulator -avd rsplayer36`. Logs: `adb logcat -s rsplayer RustStdoutStderr`
(the Rust logger uses the `rsplayer` tag and honours `RUST_LOG`). The webview
can be inspected from desktop Chrome at `chrome://inspect`.
`cargo tauri android init` (`cargo make android_init`) regenerates the Gradle
project; the manifest, `app/build.gradle.kts` and the Kotlin sources under
`app/src/main/java/de/rsplayer/app/` are hand-maintained and
committed. The NDK version is pinned in `app/build.gradle.kts` (r28+ gives
the 16 KB page alignment Google Play requires).
### Releasing the Android app
The Android version is set by hand in `app/build.gradle.kts` (the `versionCode` and
`versionName` literals in `defaultConfig`), because F-Droid's update checker reads them with a
regex. On every release bump them together with the workspace version in `Cargo.toml`:
`versionCode = major * 1_000_000 + minor * 1_000 + patch` (5.0.0 is 5000000). The Gradle build
fails with a clear message if the values and `Cargo.toml` disagree.
The APK's own version code adds one digit for the ABI: `10 * versionCode + abi`. The F-Droid
recipe builds one APK per ABI and sets `RSPLAYER_ABI_CODE` (armeabi-v7a 1, arm64-v8a 2, x86 3,
x86_64 4; unset gives 0, e.g. the universal APK). F-Droid ships armeabi-v7a, arm64-v8a and
x86_64, so 5.0.0 becomes 50000001, 50000002 and 50000004. F-Droid takes the changelog from
`fastlane/metadata/android/en-US/changelogs/` named after the highest of these
(`50000004.txt` since 5.6.0, `…2.txt` before), so add a new file per release.
x86_64 exists for emulators and x86 Chromebooks. The arm64 build does run there under ARM
translation, but the translated code reads its own copy of the environment, so the variables
the Kotlin host sets with `Os.setenv` never arrive (the Rust side falls back to defaults for
all of them, including the data dir).
## Output
After a successful build, Linux packages and binaries are located under the target output directories:
`target/${TARGET}/debian/`
`target/${TARGET}/release/`
When local `cargo-make` cross target-dir override is active, artifacts are under:
`target/cross/${TARGET}/release/`
For example: `target/aarch64-unknown-linux-gnu/debian/rsplayer_<version>_arm64.deb`