Files
ansible/roles/pi_musicmouse/tasks/main.yml
Martin Bauer 9bd23467f0 Build the venv from the distribution's Python instead of uv
The backend now targets 3.11, which is what Raspberry Pi OS ships, so there is
nothing left for uv to solve. It was only ever there to obtain a 3.13 the distro
does not have, and it brought its own problems: the download had to be matched
to the Pi's 32-bit userland by hand, and no armv7 wheel exists for a 3.13 ABI,
so Pillow was compiled from source and needed image headers installed alongside.

apt for the interpreter, python3 -m venv, pip for the rest - and piwheels then
supplies prebuilt armhf wheels for the native dependencies.

A venv cannot be migrated between interpreters (it holds absolute paths into the
one that made it), and `creates:` would keep the old one forever, so check what
built it and rebuild when it does not match the current python3. That is what
carries a device off the uv-installed 3.13 without hand-holding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:21:12 +02:00

214 lines
8.0 KiB
YAML

---
- name: Packages
ansible.builtin.apt:
name:
- git
- libvlc5 # runtime lib python-vlc binds against
- vlc-plugin-base
- alsa-utils
- samba
- python3
- python3-venv
- python3-pip
cache_valid_time: 7200
- name: Checkout Musicmouse repo
ansible.builtin.git:
repo: "{{ pi_musicmouse_repo }}"
dest: /opt/musicmouse
version: "{{ musicmouse_version }}"
accept_hostkey: true
force: true
register: pi_musicmouse_checkout
- name: Check the checkout has the layout this role knows
ansible.builtin.stat:
path: /opt/musicmouse/python-backend/pyproject.toml
register: pi_musicmouse_pyproject
# Meaningless under --check on a host that has never been deployed: the git task
# above reports "changed" without writing anything, so there is nothing to stat.
when: not ansible_check_mode
- name: Fail on a pre-rearchitecture checkout
ansible.builtin.fail:
msg: >-
{{ musicmouse_version }} has no python-backend/pyproject.toml, so it predates the
rearchitecture and this role cannot install it - the venv, the systemd unit and
the web/ build all live somewhere else on those commits. Point
musicmouse_version at a branch that has it (deploy/musicdolphin), or set
mediapi_install_kidsmusic to false for this host and leave whatever is installed
alone. Stopping here rather than failing three tasks deeper with the real reason
buried.
when: not ansible_check_mode and not pi_musicmouse_pyproject.stat.exists
- name: Get the system Python version
ansible.builtin.command:
cmd: python3 -c "import sys; print('%d.%d' % sys.version_info[:2])"
register: pi_musicmouse_system_python
changed_when: false
- name: Get the version the venv was built with
ansible.builtin.command:
cmd: /opt/musicmouse/.venv/bin/python -c "import sys; print('%d.%d' % sys.version_info[:2])"
register: pi_musicmouse_venv_python
changed_when: false
failed_when: false
- name: Discard a venv built by a different Python
# `creates:` below would otherwise keep an old interpreter forever - which is exactly
# what a switch away from a separately-installed Python leaves behind. A venv holds
# absolute paths into the interpreter that made it, so it cannot be migrated; the
# cheap and correct move is to rebuild it.
ansible.builtin.file:
path: /opt/musicmouse/.venv
state: absent
when: >-
pi_musicmouse_venv_python.rc == 0
and pi_musicmouse_venv_python.stdout | trim != pi_musicmouse_system_python.stdout | trim
- name: Create the virtualenv from the system Python
ansible.builtin.command:
cmd: "python3 -m venv /opt/musicmouse/.venv"
creates: /opt/musicmouse/.venv/bin/python
# Installing the backend is deliberately not a handler hanging off the checkout.
# Tied to the checkout, one failed run strands the device: the notification is dropped
# when the play fails, the next run finds the checkout already current and notifies
# nothing, and the venv stays empty while the service crash-loops on
# ModuleNotFoundError. Instead, decide from what is actually on the device - the commit
# it has checked out, and whether the venv can import the package at all.
#
# pip's own output cannot answer this either: `musicmouse` is a local path dependency,
# so every run rebuilds and reinstalls it. Keying off that restarts the service on
# every run and rescans the whole library for nothing.
- name: Read which commit the venv was built from
ansible.builtin.command:
cmd: cat /opt/musicmouse/.venv/.installed-commit
register: pi_musicmouse_stamp
changed_when: false
failed_when: false
- name: Check the venv can actually import the backend
ansible.builtin.command:
cmd: /opt/musicmouse/.venv/bin/python -c "import musicmouse"
register: pi_musicmouse_importable
changed_when: false
failed_when: false
- name: Install the backend into the venv
ansible.builtin.command:
cmd: "/opt/musicmouse/.venv/bin/pip install /opt/musicmouse/python-backend"
when: >-
pi_musicmouse_importable.rc != 0
or pi_musicmouse_stamp.stdout | trim != pi_musicmouse_commit
changed_when: true # guarded by `when`, so reaching this means it installed something
notify: Restart musicmouse
- name: Record which commit the venv was built from
ansible.builtin.copy:
content: "{{ pi_musicmouse_commit }}\n"
dest: /opt/musicmouse/.venv/.installed-commit
mode: "0644"
- name: Create media directory
ansible.builtin.file:
path: /media/musicmouse
state: directory
mode: "0755"
- name: Install config file
ansible.builtin.copy:
src: "{{ pi_musicmouse_config_file }}"
dest: /media/musicmouse/config.yml
mode: "0644"
# Install-once: the app writes this file itself (parent mode's volume keys, the
# remote-control mapping). See pi_musicmouse_force_config in defaults.
force: "{{ pi_musicmouse_force_config }}"
notify: Restart musicmouse
- name: Install tippen curriculum
ansible.builtin.copy:
src: "{{ pi_musicmouse_curriculum_file }}"
dest: /media/musicmouse/tippen-curriculum.yml
mode: "0644"
when: pi_musicmouse_curriculum_file | length > 0
notify: Restart musicmouse
- name: Install systemd service file (from the checked-out repo, not a copy kept here)
ansible.builtin.copy:
src: /opt/musicmouse/python-backend/musicmouse.service
dest: /etc/systemd/system/musicmouse.service
mode: "0644"
remote_src: true
notify: Restart musicmouse
- name: Add script to autostart and start now
ansible.builtin.systemd:
name: musicmouse
state: started
enabled: "yes"
daemon_reload: "yes"
- name: Ensure local frontend build cache directory exists
ansible.builtin.file:
path: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}"
state: directory
mode: "0755"
delegate_to: localhost
become: false
vars:
# `become: false` above is not enough on its own. pi_standard_setup's defaults set
# `ansible_become: true`, role defaults are play-wide host variables, and the
# `ansible_become` *variable* outranks the `become` *keyword* in Ansible's
# precedence - so every task here tried to sudo on the control machine. Those
# defaults are needed to bootstrap a fresh Pi (connect as `pi`, become root, enable
# root login), so override the variable here rather than removing them.
ansible_become: false
- name: Checkout Musicmouse repo for frontend build (control machine)
ansible.builtin.git:
repo: "{{ pi_musicmouse_repo }}"
dest: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}"
version: "{{ musicmouse_version }}"
accept_hostkey: true
force: true
delegate_to: localhost
become: false
vars:
ansible_become: false # see the first delegated task in tasks/main.yml
notify: Build frontend
- name: Check whether the device has a built frontend
ansible.builtin.stat:
path: /opt/musicmouse/web/dist/index.html
register: pi_musicmouse_dist
- name: Queue a frontend build when the device has none
# The build chain hangs off the control machine's checkout changing, which has the
# same hole the backend install had: a run that fails after that checkout leaves the
# Pi with no dist, and every later run finds the checkout current and rebuilds
# nothing. Cheap to check, and it only fires when the device really has no UI.
ansible.builtin.debug:
msg: "No frontend at /opt/musicmouse/web/dist - queueing a build"
when: not pi_musicmouse_dist.stat.exists
changed_when: true
notify: Build frontend
- name: Samba setup
ansible.builtin.copy:
src: smb.conf
dest: /etc/samba/
mode: "0644"
- name: Restart samba
ansible.builtin.systemd:
name: smbd
state: restarted
enabled: "yes"
daemon_reload: "yes"
# manual steps:
# - set samba passwords with smbpasswd
# - copy music into /media/musicmouse: scripts/sync-library.sh in the app repo, which
# also carries the cover and analysis cache over (or use the samba share, and let
# the device spend the DSP time itself)