diff --git a/.github/workflows/build-notebooks-TEMPLATE.yaml b/.github/workflows/build-notebooks-TEMPLATE.yaml index fe50f0bdd2..1930a9bd3d 100644 --- a/.github/workflows/build-notebooks-TEMPLATE.yaml +++ b/.github/workflows/build-notebooks-TEMPLATE.yaml @@ -242,13 +242,17 @@ jobs: # region Pytest image tests # https://github.com/astral-sh/setup-uv - - name: Install the latest version of uv + - name: Install uv uses: astral-sh/setup-uv@v7 with: - version: "latest" - python-version: "3.14" + version-file: uv.toml enable-cache: true cache-dependency-glob: "uv.lock" + + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" - name: Check uv is installed correctly run: uv version diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml index 0658762d23..17c19087b8 100644 --- a/.github/workflows/code-quality.yaml +++ b/.github/workflows/code-quality.yaml @@ -15,13 +15,17 @@ jobs: - uses: actions/checkout@v6 # https://github.com/astral-sh/setup-uv - - name: Install the latest version of uv + - name: Install uv uses: astral-sh/setup-uv@v7 with: - version: "latest" - python-version: "3.14" + version-file: uv.toml enable-cache: true cache-dependency-glob: "uv.lock" + + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" - name: Rerun all code generators we have run: bash ci/generate_code.sh @@ -44,10 +48,10 @@ jobs: - uses: actions/checkout@v6 # https://github.com/astral-sh/setup-uv - - name: Install the latest version of uv + - name: Install uv uses: astral-sh/setup-uv@v7 with: - version: "latest" + version-file: uv.toml python-version: "3.14" enable-cache: true cache-dependency-glob: "uv.lock" diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 4519bc4b70..8730be8cee 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -19,13 +19,17 @@ jobs: - uses: actions/checkout@v6 # https://github.com/astral-sh/setup-uv - - name: Install the latest version of uv + - name: Install uv uses: astral-sh/setup-uv@v7 with: - version: "latest" - python-version: "3.14" + version-file: uv.toml enable-cache: true cache-dependency-glob: "uv.lock" + + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" - name: Run the release notes script run: | diff --git a/.github/workflows/piplock-renewal.yaml b/.github/workflows/piplock-renewal.yaml index 2be6a8af46..a6adee7ad5 100644 --- a/.github/workflows/piplock-renewal.yaml +++ b/.github/workflows/piplock-renewal.yaml @@ -80,7 +80,9 @@ jobs: run: pip install "pipenv==2025.0.4" - name: Install uv - run: pip install "uv==0.8.12" + uses: astral-sh/setup-uv@v7 + with: + version-file: uv.toml - name: Run make refresh-pipfilelock-files run: | diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 3f05444530..541242ff2c 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -16,18 +16,18 @@ jobs: security-events: write steps: + - name: Checkout code + uses: actions/checkout@v6 + # https://github.com/astral-sh/setup-uv - - name: Install the latest version of uv - uses: astral-sh/setup-uv@v6 + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - version: "latest" + version-file: uv.toml activate-environment: false ignore-empty-workdir: true enable-cache: false - - name: Checkout code - uses: actions/checkout@v6 - # Trivy does not support pylock.toml https://github.com/aquasecurity/trivy/discussions/9408 - run: find . -name pyproject.toml -execdir uv lock \; diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c00ac37b9b..712ee933c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,15 +2,21 @@ # https://github.com/pre-commit/pre-commit-hooks?tab=readme-ov-file#hooks-available repos: # https://docs.astral.sh/uv/guides/integration/pre-commit/ - - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.9.13 + # Using a local hook instead of uv-pre-commit so it goes through ./uv, + # which handles version mismatches without requiring the exact system uv. + - repo: local hooks: - id: uv-lock + name: uv-lock + entry: ./uv lock --locked + language: system + files: '(^uv\.lock$|^pyproject\.toml$|^uv\.toml$)' + pass_filenames: false # https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.6 + rev: v0.15.4 hooks: - - id: ruff + - id: ruff-check types_or: [python, pyi] args: [--fix] files: 'ci/.*|tests/.*' @@ -25,7 +31,7 @@ repos: - id: pyright name: Run Pyright on all files # entry: /bin/bash -c 'find. -name "*.py" | xargs pyright --pythonversion 3.12' - entry: uv run pyright --pythonversion 3.12 + entry: ./uv run pyright --pythonversion 3.14 pass_filenames: true types_or: [python, pyi] language: system diff --git a/Makefile b/Makefile index d5ee9bae98..d6db6761d5 100644 --- a/Makefile +++ b/Makefile @@ -461,7 +461,7 @@ refresh-pipfilelock-files: echo "Updating $(PYTHON_VERSION) uv.lock in $$dir" cd $$dir if [ -f "pyproject.toml" ]; then - uv lock && rm uv.lock + $(ROOT_DIR)/uv lock && rm uv.lock else echo "No pyproject.toml found in $$dir, skipping." fi @@ -526,4 +526,4 @@ print-release: .PHONY: test test: @echo "Running quick static tests" - uv run pytest -m 'not buildonlytest' + ./uv run pytest -m 'not buildonlytest' diff --git a/README.md b/README.md index d971731f19..aabcc24ebe 100644 --- a/README.md +++ b/README.md @@ -62,17 +62,49 @@ Note: To ensure the GitHub Action runs successfully, users must add a `GH_ACCESS #### Prepare Python + uv + pytest env +This project pins its uv version in `uv.toml` (`required-version`). +Use the `./uv` wrapper script at the repo root — it reads the pinned +version and runs it via `uvx`, so your system uv version doesn't matter: + ```shell # Linux sudo dnf install python3.14 pip install --user uv -# MacOS +# macOS brew install python@3.14 uv -uv venv --python $(which python3.14) -uv sync --locked +./uv venv --python $(which python3.14) +./uv sync --locked ``` +
+Alternatives to ./uv + +The `./uv` wrapper is the recommended way, but you can also +(replace `0.10.6` below with the version from `uv.toml`): + +- **Use `uvx` directly** with an explicit version: + ```shell + uvx uv@0.10.6 sync --locked + ``` +- **Use `uv tool run`** (equivalent, longer form): + ```shell + uv tool run uv@0.10.6 sync --locked + ``` +- **Install the exact version** so `uv` works directly: + ```shell + # Standalone installer (any OS) + curl -LsSf https://astral.sh/uv/0.10.6/install.sh | sh + # Or with pip + pip install uv==0.10.6 + ``` + +If your system uv matches the pinned version, you can use `uv` directly — +`required-version` in `uv.toml` will let it through. If it doesn't match, +uv exits with a clear error telling you which version is required. + +
+ #### Running Python selftests in Pytest By completing configuration in previous section, you are able to run any tests that don't need to start a container using following command: @@ -106,7 +138,7 @@ sudo dnf install podman systemctl --user start podman.service systemctl --user status podman.service systemctl --user status podman.socket -DOCKER_HOST=unix:///run/user/$UID/podman/podman.sock uv run pytest tests/containers -m 'not openshift and not cuda and not rocm' --image quay.io/opendatahub/workbench-images@sha256:e98d19df346e7abb1fa3053f6d41f0d1fa9bab39e49b4cb90b510ca33452c2e4 +DOCKER_HOST=unix:///run/user/$UID/podman/podman.sock ./uv run pytest tests/containers -m 'not openshift and not cuda and not rocm' --image quay.io/opendatahub/workbench-images@sha256:e98d19df346e7abb1fa3053f6d41f0d1fa9bab39e49b4cb90b510ca33452c2e4 # Mac OS brew install podman @@ -114,7 +146,7 @@ podman machine init podman machine set --rootful=false sudo podman-mac-helper install podman machine start -uv run pytest tests/containers -m 'not openshift' --image quay.io/opendatahub/workbench-images@sha256:e98d19df346e7abb1fa3053f6d41f0d1fa9bab39e49b4cb90b510ca33452c2e4 +./uv run pytest tests/containers -m 'not openshift' --image quay.io/opendatahub/workbench-images@sha256:e98d19df346e7abb1fa3053f6d41f0d1fa9bab39e49b4cb90b510ca33452c2e4 ``` When using lima on macOS, it might be useful to give yourself access to rootful podman socket diff --git a/ci/generate_code.sh b/ci/generate_code.sh index 2568979274..39ccb5d11b 100755 --- a/ci/generate_code.sh +++ b/ci/generate_code.sh @@ -1,7 +1,11 @@ #!/usr/bin/env bash set -Eeuxo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + uv --version || pip install "uv==0.8.12" -uv run scripts/dockerfile_fragments.py +"${REPO_ROOT}/uv" run scripts/dockerfile_fragments.py +"${REPO_ROOT}/uv" run manifests/tools/generate_kustomization.py bash scripts/pylocks_generator.sh diff --git a/scripts/pylocks_generator.sh b/scripts/pylocks_generator.sh index e24941fef8..59cd126aaf 100755 --- a/scripts/pylocks_generator.sh +++ b/scripts/pylocks_generator.sh @@ -52,6 +52,7 @@ MAIN_DIRS=("jupyter" "runtimes" "rstudio" "codeserver") # CVE constraints file - applied to all lock file generations SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(dirname "$SCRIPT_DIR")" +UV="${ROOT_DIR}/uv" CVE_CONSTRAINTS_FILE="$ROOT_DIR/dependencies/cve-constraints.txt" # ---------------------------- @@ -89,13 +90,18 @@ read_conf_value() { # ---------------------------- # PRE-FLIGHT CHECK # ---------------------------- +if [[ ! -x "$UV" ]]; then + error "Expected uv wrapper at '$UV' but it is missing or not executable." + exit 1 +fi + if ! command -v uv &>/dev/null; then error "uv command not found. Please install uv: https://github.com/astral-sh/uv" exit 1 fi UV_MIN_VERSION="0.4.0" -UV_VERSION=$(uv --version 2>/dev/null | awk '{print $2}' || echo "0.0.0") +UV_VERSION=$("$UV" --version 2>/dev/null | awk '{print $2}' || echo "0.0.0") version_ge() { [ "$(printf '%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ] @@ -273,7 +279,9 @@ for TARGET_DIR in "${TARGET_DIRS[@]}"; do echo "➡️ Generating $(uppercase "$flavor") lock file..." fi - # The behavior has changed in uv 0.9.17 (https://github.com/astral-sh/uv/pull/16956) + # Tag filtering was added in uv 0.9.16 (https://github.com/astral-sh/uv/pull/16956) + # but bypassed in --universal mode. uv 0.10.5 (https://github.com/astral-sh/uv/pull/18081) + # now filters wheels by requires-python and marker disjointness even in --universal mode. # Documentation at https://docs.astral.sh/uv/reference/cli/#uv-pip-compile--python-platform says that # `--python-platform linux` is alias for `x86_64-unknown-linux-gnu`; we cannot use this to get a multiarch pylock # Let's use --universal temporarily, and in the future we can switch to using uv.lock @@ -285,17 +293,17 @@ for TARGET_DIR in "${TARGET_DIRS[@]}"; do # Build constraints flag if CVE constraints file exists # Use relative path to avoid absolute paths in pylock.toml headers # (which would differ between CI and local environments) - local constraints_flag="" + local -a constraints_flag=() if [[ -f "$CVE_CONSTRAINTS_FILE" ]]; then local relative_constraints # Use Python for cross-platform relative path computation (realpath --relative-to is GNU-only) relative_constraints=$(python3 -c "import os; print(os.path.relpath('$CVE_CONSTRAINTS_FILE', '$PWD'))") - constraints_flag="--constraints=$relative_constraints" + constraints_flag=(--constraints "$relative_constraints") fi set +e # shellcheck disable=SC2086 - uv pip compile pyproject.toml \ + "$UV" pip compile pyproject.toml \ --output-file "$output" \ --format pylock.toml \ --generate-hashes \ @@ -305,7 +313,7 @@ for TARGET_DIR in "${TARGET_DIRS[@]}"; do --no-annotate \ --quiet \ $UPGRADE_FLAG \ - $constraints_flag \ + "${constraints_flag[@]}" \ $index local status=$? set -e diff --git a/scripts/sync-python-lockfiles.sh b/scripts/sync-python-lockfiles.sh index ff8a4564ae..68d6a0b78b 100755 --- a/scripts/sync-python-lockfiles.sh +++ b/scripts/sync-python-lockfiles.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -Eeuxo pipefail +error() { + printf '%s\n' "$*" >&2 +} + # Red Hat's build tooling depends on requirements.txt files with hashes # Namely, Konflux (https://konflux-ci.dev/), and Cachi2 (https://github.com/containerbuildsystem/cachi2). @@ -9,6 +13,12 @@ set -Eeuxo pipefail # ground-up relock and force upgrades using `uv pip compile --upgrade`. # This is intended for scheduled runs, while manual runs should default to off. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +# Default to the repo-root ./uv wrapper; override with UV=/path/to/your/wrapper +UV="${UV:-$ROOT_DIR/uv}" +export UV + ADDITIONAL_UV_FLAGS="" for arg in "$@"; do case "$arg" in @@ -23,12 +33,35 @@ if [[ "${FORCE_LOCKFILES_UPGRADE:-0}" == "1" ]]; then fi export ADDITIONAL_UV_FLAGS +if [[ ! -x "$UV" ]]; then + error "Expected uv wrapper at '$UV' but it is missing or not executable." + exit 1 +fi + +if ! command -v uv &>/dev/null; then + error "uv command not found. Please install uv: https://github.com/astral-sh/uv" + exit 1 +fi + +UV_MIN_VERSION="0.4.0" +UV_VERSION=$("$UV" --version 2>/dev/null | awk '{print $2}' || echo "0.0.0") + +version_ge() { + [ "$(printf '%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ] +} + +if ! version_ge "$UV_VERSION" "$UV_MIN_VERSION"; then + error "uv version $UV_VERSION found, but >= $UV_MIN_VERSION is required." + error "Please upgrade uv: https://github.com/astral-sh/uv" + exit 1 +fi + # The following will create a pylock.toml file for every pyproject.toml we have. -uv --version || pip install "uv==0.8.12" +${UV} --version find . -name pylock.toml -execdir bash -c ' pwd # derives python-version from directory suffix (e.g., "ubi9-python-3.12") - uv pip compile pyproject.toml \ + ${UV} pip compile pyproject.toml \ --output-file pylock.toml \ --format pylock.toml \ --generate-hashes \ diff --git a/uv b/uv new file mode 100755 index 0000000000..145dfea0a7 --- /dev/null +++ b/uv @@ -0,0 +1,40 @@ +#!/usr/bin/env -S bash --norc --noprofile +# ./uv — run the project-pinned version of uv. +# +# Reads required-version from uv.toml and delegates via `uv tool run`. +# This avoids version mismatch errors when your system uv (e.g. Homebrew) +# differs from the version pinned for this project. +# +# Usage: +# ./uv sync +# ./uv run pytest +# ./uv pip compile ... +# +# The pinned version is cached by uvx after the first run. +# Parsing uses bash =~ instead of forking sed to avoid a subprocess. +# +# Bash with built-in regex is ~3x faster than Python for this task (hyperfine, +# 50 runs): bash+builtin 18.7ms, bash+sed 25.0ms, python 55.4ms. +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +while IFS= read -r line; do + if [[ "$line" =~ ^required-version\ *=\ *\"==([^\"]*)\" ]]; then + version="${BASH_REMATCH[1]}" + break + fi +done < "${SCRIPT_DIR}/uv.toml" + +if [[ -z "${version:-}" ]]; then + echo "error: could not read required-version from ${SCRIPT_DIR}/uv.toml" >&2 + exit 1 +fi + +# Fast path: use the system uv directly if it already matches the pinned version +if current=$(uv --version 2>/dev/null) && [[ "$current" == "uv $version" || "$current" == "uv $version "* ]]; then + exec uv "$@" +fi + +# Slow path: run the pinned version via uvx (downloaded and cached on first use) +exec uv tool run "uv@${version}" "$@" diff --git a/uv.toml b/uv.toml new file mode 100644 index 0000000000..d9d2d3f530 --- /dev/null +++ b/uv.toml @@ -0,0 +1 @@ +required-version = "==0.8.12"