diff --git a/doc/source/python-roles.rst b/doc/source/python-roles.rst index 93e80b7ad..66a4c8e89 100644 --- a/doc/source/python-roles.rst +++ b/doc/source/python-roles.rst @@ -13,6 +13,7 @@ Python Roles .. zuul:autorole:: ensure-sphinx .. zuul:autorole:: ensure-tox .. zuul:autorole:: ensure-twine +.. zuul:autorole:: ensure-uv .. zuul:autorole:: ensure-virtualenv .. zuul:autorole:: fetch-coverage-output .. zuul:autorole:: fetch-python-sdist-output diff --git a/roles/ensure-uv/README.rst b/roles/ensure-uv/README.rst new file mode 100644 index 000000000..66f9c6bdb --- /dev/null +++ b/roles/ensure-uv/README.rst @@ -0,0 +1,42 @@ +Ensure uv is installed + +Look for ``uv``, and if not found, install it via ``pip`` into a +virtual environment for the current user. + +**Role Variables** + +.. zuul:rolevar:: ensure_uv_version + :default: '' + + Version specifier to select the version of uv. The default is the + latest version. + +.. zuul:rolevar:: ensure_uv_venv_path + :default: {{ ansible_user_dir }}/.local/uv + + Directory for the Python venv where uv will be installed. + +.. zuul:rolevar:: ensure_uv_global_symlink + :default: False + + Install a symlink to the uv executable into ``/usr/local/bin/uv``. + This can be useful when scripts need to be run that expect to find + uv in a more standard location and plumbing through the value + of ``ensure_uv_executable`` would be onerous. + + Setting this requires root access, so should only be done in + circumstances where root access is available. + +**Output Variables** + +.. zuul:rolevar:: ensure_uv_executable + :default: uv + + After running this role, ``ensure_uv_executable`` will be set as the path + to a valid ``uv``. + + At role runtime, look for an existing ``uv`` at this specific + path. Note the default (``uv``) effectively means to find uv in + the current ``$PATH``. For example, if your base image + pre-installs uv in an out-of-path environment, set this so the + role does not attempt to install the user version. diff --git a/roles/ensure-uv/defaults/main.yaml b/roles/ensure-uv/defaults/main.yaml new file mode 100644 index 000000000..a6060f028 --- /dev/null +++ b/roles/ensure-uv/defaults/main.yaml @@ -0,0 +1,4 @@ +ensure_uv_global_symlink: false +ensure_uv_version: "" +ensure_uv_executable: uv +ensure_uv_venv_path: "{{ ansible_user_dir }}/.local/uv" diff --git a/roles/ensure-uv/tasks/main.yaml b/roles/ensure-uv/tasks/main.yaml new file mode 100644 index 000000000..87dd44e8a --- /dev/null +++ b/roles/ensure-uv/tasks/main.yaml @@ -0,0 +1,44 @@ +- name: Install pip + include_role: + name: ensure-pip + +- name: Check if uv is installed + shell: | + command -v {{ ensure_uv_executable }} {{ ensure_uv_venv_path }}/bin/uv || exit 1 + args: + executable: /bin/bash + register: uv_preinstalled + failed_when: false + +- name: Export preinstalled ensure_uv_executable + set_fact: + ensure_uv_executable: "{{ uv_preinstalled.stdout_lines[0] }}" + cacheable: true + when: uv_preinstalled.rc == 0 + +- name: Install uv to local env + when: uv_preinstalled.rc != 0 + block: + - name: Create local venv + command: "{{ ensure_pip_virtualenv_command }} {{ ensure_uv_venv_path }}" + + - name: Install uv to local venv + command: "{{ ensure_uv_venv_path }}/bin/pip install uv{{ ensure_uv_version }}" + + - name: Export installed ensure_uv_executable path + set_fact: + ensure_uv_executable: "{{ ensure_uv_venv_path }}/bin/uv" + cacheable: true + +- name: Output uv version + command: "{{ ensure_uv_executable }} --version" + +- name: Make global symlink + when: + - ensure_uv_global_symlink + - ensure_uv_executable != '/usr/local/bin/uv' + file: + state: link + src: "{{ ensure_uv_executable }}" + dest: /usr/local/bin/uv + become: yes diff --git a/test-playbooks/ensure-uv.yaml b/test-playbooks/ensure-uv.yaml new file mode 100644 index 000000000..ce20ee05e --- /dev/null +++ b/test-playbooks/ensure-uv.yaml @@ -0,0 +1,35 @@ +- hosts: all + name: Test ensure-uv installs into user environment + tasks: + - name: Verify uv is not installed + command: "uv --version" + register: result + failed_when: result.rc == 0 + - name: Run ensure-uv with uv not installed + include_role: + name: ensure-uv + - name: Verify ensure_uv_executable is set + assert: + that: + - ensure_uv_executable == ansible_user_dir + '/.local/uv/bin/uv' + - name: Verify uv is installed + command: "{{ ensure_uv_executable }} --version" + register: result + failed_when: result.rc != 0 + +- hosts: all + name: Test ensure-uv when ensure_uv_executable is set to an already installed uv + tasks: + - name: Create a virtualenv + command: "{{ ensure_pip_virtualenv_command }} {{ ansible_user_dir }}/uv-venv" + - name: Install uv to local venv + command: "{{ ansible_user_dir }}/uv-venv/bin/pip install uv" + - name: Run ensure-uv pointing to an already installed uv + include_role: + name: ensure-uv + vars: + ensure_uv_executable: "{{ ansible_user_dir }}/uv-venv/bin/uv" + - name: Verify ensure_uv_executable is set to the virtualenv uv + assert: + that: + - ensure_uv_executable == ansible_user_dir + '/uv-venv/bin/uv' diff --git a/zuul-tests.d/python-jobs.yaml b/zuul-tests.d/python-jobs.yaml index 61270afa4..a4ee1d420 100644 --- a/zuul-tests.d/python-jobs.yaml +++ b/zuul-tests.d/python-jobs.yaml @@ -429,6 +429,75 @@ - name: ubuntu-noble label: ubuntu-noble +- job: + name: zuul-jobs-test-ensure-uv + description: Test the ensure-uv role + files: + - roles/ensure-uv/.* + - test-playbooks/ensure-uv.yaml + run: test-playbooks/ensure-uv.yaml + tags: all-platforms + +- job: + name: zuul-jobs-test-ensure-uv-centos-9-stream + description: Test the ensure-uv role on centos-9-stream + parent: zuul-jobs-test-ensure-uv + tags: auto-generated + nodeset: + nodes: + - name: centos-9-stream + label: centos-9-stream + +- job: + name: zuul-jobs-test-ensure-uv-debian-bookworm + description: Test the ensure-uv role on debian-bookworm + parent: zuul-jobs-test-ensure-uv + tags: auto-generated + nodeset: + nodes: + - name: debian-bookworm + label: debian-bookworm + +- job: + name: zuul-jobs-test-ensure-uv-debian-bullseye + description: Test the ensure-uv role on debian-bullseye + parent: zuul-jobs-test-ensure-uv + tags: auto-generated + nodeset: + nodes: + - name: debian-bullseye + label: debian-bullseye + +- job: + name: zuul-jobs-test-ensure-uv-ubuntu-focal + description: Test the ensure-uv role on ubuntu-focal + parent: zuul-jobs-test-ensure-uv + tags: auto-generated + nodeset: + nodes: + - name: ubuntu-focal + label: ubuntu-focal + +- job: + name: zuul-jobs-test-ensure-uv-ubuntu-jammy + description: Test the ensure-uv role on ubuntu-jammy + parent: zuul-jobs-test-ensure-uv + tags: auto-generated + nodeset: + nodes: + - name: ubuntu-jammy + label: ubuntu-jammy + +- job: + name: zuul-jobs-test-ensure-uv-ubuntu-noble + description: Test the ensure-uv role on ubuntu-noble + parent: zuul-jobs-test-ensure-uv + tags: auto-generated + nodeset: + nodes: + - name: ubuntu-noble + label: ubuntu-noble + - job: name: zuul-jobs-test-fetch-sphinx-tarball description: Test the fetch-sphinx-tarball role @@ -652,6 +721,12 @@ - zuul-jobs-test-ensure-tox-ubuntu-focal - zuul-jobs-test-ensure-tox-ubuntu-jammy - zuul-jobs-test-ensure-tox-ubuntu-noble + - zuul-jobs-test-ensure-uv-centos-9-stream + - zuul-jobs-test-ensure-uv-debian-bookworm + - zuul-jobs-test-ensure-uv-debian-bullseye + - zuul-jobs-test-ensure-uv-ubuntu-focal + - zuul-jobs-test-ensure-uv-ubuntu-jammy + - zuul-jobs-test-ensure-uv-ubuntu-noble - zuul-jobs-test-fetch-sphinx-tarball-centos-9-stream - zuul-jobs-test-fetch-sphinx-tarball-debian-bookworm - zuul-jobs-test-fetch-sphinx-tarball-debian-bullseye