diff --git a/roles/pi_musicmouse/README.md b/roles/pi_musicmouse/README.md index 6adfbfd..ab0a2af 100644 --- a/roles/pi_musicmouse/README.md +++ b/roles/pi_musicmouse/README.md @@ -5,9 +5,12 @@ Deploys the MusicMouse backend (`python-backend/`) and its built web frontend. - Checks out `musicmouse_version` from `pi_musicmouse_repo` to `/opt/musicmouse`, then **fails early** if that checkout has no `python-backend/pyproject.toml` - see "Which version" below. -- Installs a pinned `uv` release and uses it to create `/opt/musicmouse/.venv` with - Python `pi_musicmouse_python_version` (the backend needs 3.12+ for PEP 695 syntax; - Raspbian Bookworm's apt python3 is only 3.11) and `pip install` the backend into it. +- Creates `/opt/musicmouse/.venv` from the distribution's own `python3` and pip-installs + the backend into it. The backend targets the Python that Raspberry Pi OS ships (3.11 + on Bookworm) precisely so this step stays boring: apt and piwheels have prebuilt + armhf wheels for the native dependencies, so nothing has to be compiled on the Pi. + If the venv was built by a different interpreter than the current `python3`, it is + thrown away and rebuilt. - Builds the frontend (`web/`) on the *control machine* (delegated to `localhost`, not the Pi - no Node/npm is installed on the Pi) and syncs `web/dist` over. - Installs `/media/musicmouse/config.yml` from `files/config-.yml` diff --git a/roles/pi_musicmouse/defaults/main.yml b/roles/pi_musicmouse/defaults/main.yml index c44756c..d150c67 100644 --- a/roles/pi_musicmouse/defaults/main.yml +++ b/roles/pi_musicmouse/defaults/main.yml @@ -5,24 +5,6 @@ pi_musicmouse_repo: "ssh://git@git.bauer.tech:2222/martin/musicmouse.git" # `git push -f origin :deploy/musicdolphin` and re-run this role. # musicmouse_version: "deploy/musicdolphin" -pi_musicmouse_python_version: "3.13" -pi_musicmouse_uv_version: "0.12.10" - -# Which uv build to download. Raspberry Pi OS ships a 64-bit kernel with a 32-bit -# userland, so `uname -m` (and therefore ansible_architecture) says aarch64 on a box -# that can only run armhf binaries - the aarch64 build installs fine and then fails to -# exec with "No such file or directory", which is the dynamic loader missing, not the -# file. ansible_userspace_bits is the fact that tells the truth. -pi_musicmouse_uv_target: >- - {{ 'armv7-unknown-linux-gnueabihf' - if (ansible_architecture in ['aarch64', 'armv7l', 'armv6l'] and ansible_userspace_bits == '32') - else _pi_musicmouse_uv_targets[ansible_architecture] }} -_pi_musicmouse_uv_targets: - aarch64: aarch64-unknown-linux-gnu - armv7l: armv7-unknown-linux-gnueabihf - armv6l: armv7-unknown-linux-gnueabihf - x86_64: x86_64-unknown-linux-gnu - # Where to check out the repo on the *control machine* to build the frontend (npm isn't # installed on the Pi - see README). Keyed by version so different hosts on different # versions don't clobber each other's build. diff --git a/roles/pi_musicmouse/tasks/main.yml b/roles/pi_musicmouse/tasks/main.yml index 8fdf7b2..f945027 100644 --- a/roles/pi_musicmouse/tasks/main.yml +++ b/roles/pi_musicmouse/tasks/main.yml @@ -3,47 +3,15 @@ ansible.builtin.apt: name: - git - - libvlc5 # runtime lib python-vlc (installed into the venv by uv) binds against + - libvlc5 # runtime lib python-vlc binds against - vlc-plugin-base - alsa-utils - samba - # Pillow ships no armv7 wheel and Raspberry Pi OS's userland is 32-bit, so it is - # compiled from source here and needs its image headers. (piwheels would have a - # prebuilt one, but only for the system Python, not the 3.13 uv installs.) - - libjpeg-dev - - zlib1g-dev + - python3 + - python3-venv + - python3-pip cache_valid_time: 7200 -- name: Ensure uv install directory exists - ansible.builtin.file: - path: "/opt/uv-{{ pi_musicmouse_uv_version }}-{{ pi_musicmouse_uv_target }}" - state: directory - mode: "0755" - -- name: Download and extract uv {{ pi_musicmouse_uv_version }} - ansible.builtin.unarchive: - src: "https://github.com/astral-sh/uv/releases/download/{{ pi_musicmouse_uv_version }}/uv-{{ pi_musicmouse_uv_target }}.tar.gz" - dest: "/opt/uv-{{ pi_musicmouse_uv_version }}-{{ pi_musicmouse_uv_target }}" - remote_src: true - extra_opts: ["--strip-components=1"] - creates: "/opt/uv-{{ pi_musicmouse_uv_version }}-{{ pi_musicmouse_uv_target }}/uv" - -- name: Symlink uv/uvx into /usr/local/bin - ansible.builtin.file: - src: "/opt/uv-{{ pi_musicmouse_uv_version }}-{{ pi_musicmouse_uv_target }}/{{ item }}" - dest: "/usr/local/bin/{{ item }}" - state: link - force: true - loop: - - uv - - uvx - -- name: Verify uv runs on this host - ansible.builtin.command: - cmd: /usr/local/bin/uv --version - register: pi_musicmouse_uv_version_out - changed_when: false - - name: Checkout Musicmouse repo ansible.builtin.git: repo: "{{ pi_musicmouse_repo }}" @@ -73,9 +41,34 @@ buried. when: not ansible_check_mode and not pi_musicmouse_pyproject.stat.exists -- name: Create virtualenv with Python {{ pi_musicmouse_python_version }} +- name: Get the system Python version ansible.builtin.command: - cmd: "/usr/local/bin/uv venv --python {{ pi_musicmouse_python_version }} /opt/musicmouse/.venv" + 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. @@ -85,9 +78,9 @@ # 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. +# 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 @@ -104,7 +97,7 @@ - name: Install the backend into the venv ansible.builtin.command: - cmd: "/usr/local/bin/uv pip install --python /opt/musicmouse/.venv/bin/python /opt/musicmouse/python-backend" + 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