#!/usr/bin/env bash set -euo pipefail TARGET_VERSION="${1:-3.11}" IFS="." read -r PYTHON_MAJOR PYTHON_MINOR _ <<< "${TARGET_VERSION}" if [[ -n "${PYTHON_MINOR:-}" ]]; then PYTHON_BIN="python${PYTHON_MAJOR}.${PYTHON_MINOR}" else PYTHON_BIN="python${PYTHON_MAJOR}" fi python_matches() { local candidate="$1" [[ -x "${candidate}" ]] || return 1 "${candidate}" - "${PYTHON_MAJOR}" "${PYTHON_MINOR:-}" <<'PY' import sys expected_major = int(sys.argv[1]) expected_minor = sys.argv[2] if sys.version_info.major != expected_major: raise SystemExit(1) if expected_minor and sys.version_info.minor != int(expected_minor): raise SystemExit(1) PY } python_has_venv() { local candidate="$1" "${candidate}" -m venv --help >/dev/null 2>&1 } print_if_usable() { local candidate="$1" case "${candidate}" in "${PWD}/.venv/"*) return 1 ;; esac if python_matches "${candidate}" && python_has_venv "${candidate}"; then printf '%s\n' "${candidate}" exit 0 fi } if command -v "${PYTHON_BIN}" >/dev/null 2>&1; then print_if_usable "$(command -v "${PYTHON_BIN}")" fi TOOLCACHE_DIR="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" HOME_DIR="${HOME:-/root}" shopt -s nullglob # shellcheck disable=SC2140 for candidate in \ "${TOOLCACHE_DIR}"/Python/"${PYTHON_MAJOR}.${PYTHON_MINOR}"*/x64/bin/python \ "${TOOLCACHE_DIR}"/Python/"${PYTHON_MAJOR}.${PYTHON_MINOR}"*/x64/bin/python3 \ "${TOOLCACHE_DIR}"/uv-python/cpython-"${PYTHON_MAJOR}.${PYTHON_MINOR}"*/bin/python \ "${HOME_DIR}"/.local/share/uv/python/cpython-"${PYTHON_MAJOR}.${PYTHON_MINOR}"*/bin/python \ "${HOME_DIR}"/.cache/uv/python/cpython-"${PYTHON_MAJOR}.${PYTHON_MINOR}"*/bin/python; do print_if_usable "${candidate}" done shopt -u nullglob APT_RUNNER=() if [[ "$(id -u)" -ne 0 ]]; then APT_RUNNER=(sudo) fi export DEBIAN_FRONTEND=noninteractive if "${APT_RUNNER[@]}" apt-get update >&2 \ && "${APT_RUNNER[@]}" apt-get install -y "${PYTHON_BIN}" "${PYTHON_BIN}-venv" >&2; then print_if_usable "$(command -v "${PYTHON_BIN}")" fi install_uv_standalone() { if command -v uv >/dev/null 2>&1; then command -v uv return 0 fi local uv_dir uv_dir="$(mktemp -d)" local install_url="https://astral.sh/uv/${UV_VERSION:-0.7.2}/install.sh" if ! curl -LsSf "${install_url}" | env UV_INSTALL_DIR="${uv_dir}/bin" sh >&2; then curl -LsSf "https://astral.sh/uv/install.sh" | env UV_INSTALL_DIR="${uv_dir}/bin" sh >&2 fi printf '%s\n' "${uv_dir}/bin/uv" } UV_BIN="$(install_uv_standalone)" UV_PYTHON_INSTALL_DIR="${TOOLCACHE_DIR:-${HOME_DIR}/.cache}/uv-python" \ "${UV_BIN}" python install "${TARGET_VERSION}" --no-progress >&2 UV_PYTHON_CANDIDATE="$( UV_PYTHON_INSTALL_DIR="${TOOLCACHE_DIR:-${HOME_DIR}/.cache}/uv-python" \ "${UV_BIN}" python find "${TARGET_VERSION}" --managed-python )" print_if_usable "${UV_PYTHON_CANDIDATE}" if "${APT_RUNNER[@]}" apt-get install -y software-properties-common >&2 \ && "${APT_RUNNER[@]}" add-apt-repository -y ppa:deadsnakes/ppa >&2 \ && "${APT_RUNNER[@]}" apt-get update >&2 \ && "${APT_RUNNER[@]}" apt-get install -y "${PYTHON_BIN}" "${PYTHON_BIN}-venv" >&2; then print_if_usable "$(command -v "${PYTHON_BIN}")" fi printf 'Unable to install a usable Python %s with venv support\n' "${TARGET_VERSION}" >&2 exit 1