Build and install the native front-end on musicdolphin
slint-frontend/ in the app repo is a native front-end for the same backend, written because the React UI is sluggish on this device. This installs it beside the web UI, never instead of it: the systemd unit is installed but not enabled, so the device still boots into the kiosk browser and the two can be compared on the same screen. systemctl start musikdelfin # takes over the display systemctl stop musikdelfin # back to the browser underneath Unlike pi_musicmouse, which builds the web front-end on the control machine and rsyncs dist/, this builds on the device. A Rust binary is not portable, and the reason it has to be built here rather than cross-compiled is worth writing down: these Pis run 32-bit armhf Raspbian on a 64-bit kernel. `uname -m` says aarch64 and it is misleading; `dpkg --print-architecture` says armhf and that is the one that matters. Cross-compiling to armv7-unknown-linux-gnueabihf needs a sysroot, the devices carry no -dev packages to build one from, and reconstructing one from the Debian archive means owning a glibc skew risk indefinitely. Spending the device's own CPU once is the smaller liability. The build is stamped with the commit it came from, so it only runs again when that changes. The same fact rules out the Skia renderer that the app's own kiosk profile prefers: rust-skia publishes no prebuilt binaries for armhf. This builds --features desktop - winit, FemtoVG and the software renderer - and runs as an X client in the existing session. Worth noting after the fact: the FemtoVG bug that makes Skia the upstream recommendation (slint-ui/slint#11785, album covers drawn as solid black) was reported on a Pi 5 with a 64-bit userland and does not reproduce here - covers render correctly. Three things had to be found by running it rather than reading anything: The X session belongs to `kiosk`, not root, so the launcher needs that user's cookie and the unit runs as them. Without it winit fails with "could not initialize backend", which says nothing about authorization. winit and glutin dlopen every windowing and GL library they use, so none of them appear in `ldd` and the binary links happily without them - then aborts on first run with "Library libxkbcommon-x11.so could not be loaded". The runtime packages are listed explicitly for that reason. Raspbian ships no font containing the emoji the design uses as iconography, so the shelf badges rendered as empty boxes until fonts-noto-color-emoji went in. First build took 63 minutes: 570 crates, two jobs, nice'd, which bounds peak memory on musicdolphin's 1.8 GB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
44
roles/pi_slint_frontend/README.md
Normal file
44
roles/pi_slint_frontend/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# pi_slint_frontend
|
||||
|
||||
Builds and installs `slint-frontend/` from the app repo - the native front-end that
|
||||
exists because the React UI is sluggish on the kiosk. Installed *alongside* the web UI,
|
||||
never instead of it, and the systemd unit is installed but not enabled: the device still
|
||||
boots into the kiosk browser.
|
||||
|
||||
```sh
|
||||
systemctl start musikdelfin # takes over the screen
|
||||
systemctl stop musikdelfin # back to the browser underneath
|
||||
```
|
||||
|
||||
or over SSH, `DISPLAY=:0 /usr/local/bin/musikdelfin`.
|
||||
|
||||
## Why this builds on the device
|
||||
|
||||
`pi_musicmouse` builds the web front-end on the control machine and rsyncs `dist/`,
|
||||
because npm output is portable and npm is not installed on the Pis. A Rust binary is
|
||||
not portable, so this role has a harder choice.
|
||||
|
||||
The Pis run **32-bit armhf Raspbian on a 64-bit kernel** - `uname -m` says `aarch64`
|
||||
and `dpkg --print-architecture` says `armhf`, and the second one is the one that
|
||||
matters. The target is `armv7-unknown-linux-gnueabihf`. Cross-compiling to it from the
|
||||
control machine needs a sysroot, and the devices carry no `-dev` packages to build one
|
||||
out of, so it would have to be reconstructed from the Debian archive - and then kept in
|
||||
step with the devices' glibc forever. Spending the device's own CPU once is the smaller
|
||||
liability. A rebuild only happens when the commit changes.
|
||||
|
||||
The same fact rules out the Skia renderer: rust-skia publishes no prebuilt binaries for
|
||||
armhf, so `--features kiosk` would mean compiling Skia from source on a Pi. This role
|
||||
builds `--features desktop` - winit, FemtoVG and the software renderer - and runs as an
|
||||
X client in the existing session rather than taking over KMS.
|
||||
|
||||
`SLINT_BACKEND=winit-software musikdelfin` switches renderer without a rebuild, which
|
||||
is the thing to try first if the GPU path misbehaves. The FemtoVG bug that makes
|
||||
`kiosk` use Skia upstream (slint-ui/slint#11785) was reported on a Pi 5 with a 64-bit
|
||||
userland, so it may well not apply here - but it is worth checking that album covers
|
||||
actually appear before drawing conclusions about frame rate.
|
||||
|
||||
## First run
|
||||
|
||||
Roughly 40 minutes: 570 crates on two cores. Later runs are incremental and usually
|
||||
seconds. The build is `nice`d and limited to two jobs, which bounds peak memory on
|
||||
musicdolphin's 1.8 GB.
|
||||
42
roles/pi_slint_frontend/defaults/main.yml
Normal file
42
roles/pi_slint_frontend/defaults/main.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
# The native Slint front-end (slint-frontend/ in the app repo). Installed alongside the
|
||||
# web UI, not instead of it: both talk to the same backend, so the two can be compared
|
||||
# on the same screen.
|
||||
|
||||
# Built on the device rather than on the control machine, which is the opposite of what
|
||||
# pi_musicmouse does for the web front-end - and for a reason. The Pis run 32-bit armhf
|
||||
# Raspbian on a 64-bit kernel, so the target is armv7-unknown-linux-gnueabihf. Cross
|
||||
# compiling to it needs a sysroot, and the device has no -dev packages to build one
|
||||
# from, so the choice is between reconstructing one from the Debian archive (and
|
||||
# owning a glibc-version skew risk forever) or spending the device's own CPU once.
|
||||
# A rebuild only happens when the commit changes - see the stamp below.
|
||||
pi_slint_frontend_toolchain: stable
|
||||
|
||||
# Bounds peak memory during the build. musicdolphin has 4 cores but only 1.8 GB.
|
||||
pi_slint_frontend_jobs: 2
|
||||
|
||||
# winit + FemtoVG + the software renderer. Deliberately not Skia: rust-skia ships no
|
||||
# prebuilt binaries for armhf, so `kiosk` would mean compiling Skia from source on a
|
||||
# Pi, which will not finish in any reasonable time and would not fit on the disk.
|
||||
pi_slint_frontend_features: desktop
|
||||
|
||||
# Which backend the app is served by. Same machine as the kiosk browser uses.
|
||||
pi_slint_frontend_server: "http://localhost:8080"
|
||||
|
||||
# Where the checkout lives. Shared with pi_musicmouse, which owns the checkout itself.
|
||||
pi_slint_frontend_root: /opt/musicmouse/slint-frontend
|
||||
|
||||
# The user the kiosk's X session belongs to - the app has to run as them to reach the
|
||||
# display. pi_kiosk owns this name; repeated here as a default so the role still works
|
||||
# on a host where pi_kiosk has not run.
|
||||
pi_slint_frontend_x_user: "{{ pi_kiosk_user | default('kiosk') }}"
|
||||
|
||||
# The device is a kiosk, so the window takes the whole screen. Set false to get a
|
||||
# normal window - useful while pi_kiosk_mode is "debug" and you want the browser and
|
||||
# this side by side on the same display.
|
||||
pi_slint_frontend_fullscreen: true
|
||||
|
||||
# Slint's own frame-timing readout, for measuring rather than guessing. Empty to leave
|
||||
# it off; "overlay,refresh_lazy" draws the numbers on screen and only counts frames
|
||||
# that were actually rendered, which is the honest measure for a UI that idles.
|
||||
pi_slint_frontend_debug_performance: ""
|
||||
4
roles/pi_slint_frontend/handlers/main.yml
Normal file
4
roles/pi_slint_frontend/handlers/main.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
- name: Reload systemd
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
143
roles/pi_slint_frontend/tasks/main.yml
Normal file
143
roles/pi_slint_frontend/tasks/main.yml
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
- name: Packages needed to build the front-end
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- build-essential
|
||||
- pkg-config
|
||||
# The only C libraries the binary links: everything else - X11, Wayland, EGL,
|
||||
# GL - is dlopen'd at runtime by winit and glutin, so it is not needed to build.
|
||||
- libfontconfig1-dev
|
||||
- libfreetype-dev
|
||||
- curl
|
||||
# Runtime, not build: winit and glutin dlopen all of these, so they never show
|
||||
# up in `ldd` and the binary links happily without them - it then aborts on
|
||||
# first run with "Library libxkbcommon-x11.so could not be loaded".
|
||||
- libxkbcommon0
|
||||
- libxkbcommon-x11-0
|
||||
- libx11-xcb1
|
||||
- libxcb-xkb1
|
||||
- libxcursor1
|
||||
- libxi6
|
||||
- libxrandr2
|
||||
- libgl1
|
||||
- libegl1
|
||||
- libgles2
|
||||
# The design uses emoji as iconography - the shelf badges, the figurine marker,
|
||||
# the scene sparkle. Raspbian ships no font that has them, so without these they
|
||||
# render as empty boxes. Symbola is the monochrome fallback for the glyphs Noto
|
||||
# does not cover.
|
||||
- fonts-noto-color-emoji
|
||||
- fonts-symbola
|
||||
cache_valid_time: 7200
|
||||
|
||||
- name: Check for a rust toolchain
|
||||
ansible.builtin.stat:
|
||||
path: /root/.cargo/bin/cargo
|
||||
register: pi_slint_frontend_cargo
|
||||
|
||||
- name: Fetch the rustup installer
|
||||
ansible.builtin.get_url:
|
||||
url: https://sh.rustup.rs
|
||||
dest: /tmp/rustup-init.sh
|
||||
mode: "0755"
|
||||
when: not pi_slint_frontend_cargo.stat.exists
|
||||
|
||||
- name: Install the rust toolchain
|
||||
# Debian 12 ships rustc 1.63, which is far too old for Slint, so this comes from
|
||||
# rustup rather than apt.
|
||||
ansible.builtin.command:
|
||||
cmd: "/tmp/rustup-init.sh -y --profile minimal --default-toolchain {{ pi_slint_frontend_toolchain }}"
|
||||
creates: /root/.cargo/bin/cargo
|
||||
when: not pi_slint_frontend_cargo.stat.exists
|
||||
|
||||
- name: Check the checkout has a slint-frontend
|
||||
ansible.builtin.stat:
|
||||
path: "{{ pi_slint_frontend_root }}/Cargo.toml"
|
||||
register: pi_slint_frontend_manifest
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Fail on a checkout without the front-end
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
{{ musicmouse_version }} has no slint-frontend/, so it predates the native
|
||||
front-end. Point musicmouse_version at a branch that has it, or set
|
||||
mediapi_install_slint_frontend to false for this host.
|
||||
when: not ansible_check_mode and not pi_slint_frontend_manifest.stat.exists
|
||||
|
||||
- name: Fail when the checkout's commit is unknown
|
||||
# This role builds what pi_musicmouse checked out, and reuses its record of which
|
||||
# commit that was. With mediapi_install_kidsmusic false that role never ran, so
|
||||
# there is no checkout to build and no commit to stamp - and the stamp comparison
|
||||
# below would compare against an empty string and rebuild on every run.
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
pi_slint_frontend needs pi_musicmouse to have run first - it builds the
|
||||
slint-frontend/ inside the checkout that role owns. Set
|
||||
mediapi_install_kidsmusic to true for this host, or
|
||||
mediapi_install_slint_frontend to false.
|
||||
when: not ansible_check_mode and pi_musicmouse_commit | length == 0
|
||||
|
||||
- name: Read which commit the binary was built from
|
||||
# The same guard pi_musicmouse uses for the venv and the web dist, for the same
|
||||
# reason: hanging a 40-minute build off "the checkout changed" strands the device
|
||||
# when a run fails after the checkout, because the next run finds it current and
|
||||
# rebuilds nothing.
|
||||
ansible.builtin.command:
|
||||
cmd: "cat {{ pi_slint_frontend_root }}/.built-commit"
|
||||
register: pi_slint_frontend_stamp
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Build the front-end
|
||||
# First build takes roughly 40 minutes on a Pi 4 - 570 crates, two jobs. Later ones
|
||||
# are incremental. `async` keeps the SSH connection from idling out underneath it.
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
nice -n 10 /root/.cargo/bin/cargo build --release
|
||||
--no-default-features --features {{ pi_slint_frontend_features }}
|
||||
-j {{ pi_slint_frontend_jobs }}
|
||||
chdir: "{{ pi_slint_frontend_root }}"
|
||||
environment:
|
||||
# The committed release profile is tuned for a workstation. Thin LTO with
|
||||
# codegen-units=1 is a memory risk on 1.8 GB and buys nothing that matters here.
|
||||
CARGO_PROFILE_RELEASE_LTO: "false"
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "16"
|
||||
PATH: "/root/.cargo/bin:{{ ansible_env.PATH }}"
|
||||
async: 7200
|
||||
poll: 30
|
||||
changed_when: true
|
||||
when: >-
|
||||
not ansible_check_mode
|
||||
and pi_slint_frontend_stamp.stdout | trim != pi_musicmouse_commit
|
||||
|
||||
- name: Record which commit the binary was built from
|
||||
ansible.builtin.copy:
|
||||
content: "{{ pi_musicmouse_commit }}\n"
|
||||
dest: "{{ pi_slint_frontend_root }}/.built-commit"
|
||||
mode: "0644"
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Find the X session user's home directory
|
||||
ansible.builtin.getent:
|
||||
database: passwd
|
||||
key: "{{ pi_slint_frontend_x_user }}"
|
||||
|
||||
- name: Note where the X cookie lives
|
||||
ansible.builtin.set_fact:
|
||||
pi_slint_frontend_x_home: "{{ ansible_facts.getent_passwd[pi_slint_frontend_x_user][4] }}"
|
||||
|
||||
- name: Install the launcher
|
||||
ansible.builtin.template:
|
||||
src: musikdelfin.sh.j2
|
||||
dest: /usr/local/bin/musikdelfin
|
||||
mode: "0755"
|
||||
|
||||
- name: Install the systemd unit
|
||||
# Installed but deliberately not enabled: this is the thing under test, and the
|
||||
# kiosk browser is still what the device shows on boot. Start it by hand -
|
||||
# `systemctl start musikdelfin` - or run /usr/local/bin/musikdelfin over SSH.
|
||||
ansible.builtin.template:
|
||||
src: musikdelfin.service.j2
|
||||
dest: /etc/systemd/system/musikdelfin.service
|
||||
mode: "0644"
|
||||
notify: Reload systemd
|
||||
25
roles/pi_slint_frontend/templates/musikdelfin.service.j2
Normal file
25
roles/pi_slint_frontend/templates/musikdelfin.service.j2
Normal file
@@ -0,0 +1,25 @@
|
||||
# {{ ansible_managed }}
|
||||
#
|
||||
# Installed, not enabled. The kiosk browser is still what this device shows on boot;
|
||||
# this unit is here so the native front-end can be started and stopped the same way
|
||||
# while it is being evaluated:
|
||||
#
|
||||
# systemctl start musikdelfin # takes over the screen
|
||||
# systemctl stop musikdelfin # back to the browser underneath
|
||||
[Unit]
|
||||
Description=Musik Delfin (native Slint front-end)
|
||||
After=musicmouse.service graphical.target
|
||||
Wants=musicmouse.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Runs as the X session's own user, not root: the display belongs to them, and the
|
||||
# cover cache should land in their home rather than root's.
|
||||
User={{ pi_slint_frontend_x_user }}
|
||||
ExecStart=/usr/local/bin/musikdelfin
|
||||
Restart=no
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
40
roles/pi_slint_frontend/templates/musikdelfin.sh.j2
Normal file
40
roles/pi_slint_frontend/templates/musikdelfin.sh.j2
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# {{ ansible_managed }}
|
||||
#
|
||||
# Run the native front-end on the kiosk's X display. The web UI stays where it is;
|
||||
# both talk to the same backend, so they can be compared on the same screen.
|
||||
#
|
||||
# musikdelfin the display, as the kiosk uses it
|
||||
# SLINT_BACKEND=winit-software musikdelfin no GPU, for comparison
|
||||
# SLINT_FULLSCREEN=0 musikdelfin a normal window, beside the browser
|
||||
# SLINT_DEBUG_PERFORMANCE=overlay,refresh_lazy musikdelfin frame timings on screen
|
||||
set -eu
|
||||
|
||||
# The kiosk's X session belongs to {{ pi_slint_frontend_x_user }}, and its cookie is
|
||||
# the only way onto that display - without it the connection is refused before winit
|
||||
# gets far enough to report anything more useful than "could not initialize backend".
|
||||
export DISPLAY="${DISPLAY:-:0}"
|
||||
export XAUTHORITY="${XAUTHORITY:-{{ pi_slint_frontend_x_home }}/.Xauthority}"
|
||||
|
||||
# Nunito is the face the design is drawn in; without it Slint falls back to whatever
|
||||
# fontconfig hands back, which changes how this looks but not whether it runs.
|
||||
for candidate in \
|
||||
/usr/share/fonts/truetype/nunito/Nunito[wght].ttf \
|
||||
/usr/share/fonts/truetype/nunito/Nunito-Regular.ttf \
|
||||
{{ pi_slint_frontend_root }}/fonts/Nunito.ttf
|
||||
do
|
||||
if [ -f "$candidate" ]; then
|
||||
export SLINT_DEFAULT_FONT="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
{% if pi_slint_frontend_fullscreen %}
|
||||
export SLINT_FULLSCREEN="${SLINT_FULLSCREEN:-1}"
|
||||
{% endif %}
|
||||
{% if pi_slint_frontend_debug_performance %}
|
||||
export SLINT_DEBUG_PERFORMANCE="${SLINT_DEBUG_PERFORMANCE:-{{ pi_slint_frontend_debug_performance }}}"
|
||||
{% endif %}
|
||||
|
||||
exec {{ pi_slint_frontend_root }}/target/release/musicmouse-slint \
|
||||
--server {{ pi_slint_frontend_server }} "$@"
|
||||
Reference in New Issue
Block a user