diff --git a/roles/pi_musicmouse/defaults/main.yml b/roles/pi_musicmouse/defaults/main.yml index 72ea230..c44756c 100644 --- a/roles/pi_musicmouse/defaults/main.yml +++ b/roles/pi_musicmouse/defaults/main.yml @@ -43,3 +43,7 @@ pi_musicmouse_force_config: false # section will not start without it - curriculum_file is validated as must-exist. Set to # "" for a host whose config has no tippen section. pi_musicmouse_curriculum_file: "tippen-curriculum.yml" + +# The commit the checkout task actually landed on, used to decide whether the venv +# needs rebuilding. Empty under --check, where the git task reports without writing. +pi_musicmouse_commit: "{{ pi_musicmouse_checkout.after | default('') }}" diff --git a/roles/pi_musicmouse/tasks/main.yml b/roles/pi_musicmouse/tasks/main.yml index 1b18a88..8fdf7b2 100644 --- a/roles/pi_musicmouse/tasks/main.yml +++ b/roles/pi_musicmouse/tasks/main.yml @@ -51,6 +51,7 @@ 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: @@ -77,18 +78,45 @@ cmd: "/usr/local/bin/uv venv --python {{ pi_musicmouse_python_version }} /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. +# +# uv's own output cannot answer this: `musicmouse` is a local path dependency, so every +# run rebuilds and reinstalls it and always prints "Installed 1 package". 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 - # Every run, not on a checkout change via a handler. 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. uv is fast when there - # is nothing to do, and its output says whether there was. ansible.builtin.command: cmd: "/usr/local/bin/uv pip install --python /opt/musicmouse/.venv/bin/python /opt/musicmouse/python-backend" - register: pi_musicmouse_install - changed_when: "'Installed' in pi_musicmouse_install.stderr" + 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