Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/verify-split-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ on:
- "tests/vm-iso-storage.sh"
- "tests/vm-consoles.sh"
- "tests/vm-helpers.sh"
- "tests/uv-required-version.sh"
- ".github/workflows/verify-split-files.yml"
push:
branches: [main]
Expand All @@ -41,6 +42,7 @@ on:
- "tests/vm-iso-storage.sh"
- "tests/vm-consoles.sh"
- "tests/vm-helpers.sh"
- "tests/uv-required-version.sh"
- ".github/workflows/verify-split-files.yml"
workflow_dispatch:

Expand Down Expand Up @@ -71,6 +73,9 @@ jobs:
- name: VM helper regression tests
run: bash tests/vm-helpers.sh

- name: uv pin regression tests
run: bash tests/uv-required-version.sh

- name: Prefetch lists cover every engine file
run: |
set -euo pipefail
Expand Down
3 changes: 3 additions & 0 deletions lib/API.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ _setup_nvidia_gpu
_setup_rocm
_tools_cleanup_temp_dirs
_tools_register_temp_cleanup
_uv_pick_version
_uv_release_list
_uv_required_version
_uv_ver_ge
cache_installed_version
check_for_codeberg_release
check_for_gh_branch
Expand Down
60 changes: 56 additions & 4 deletions lib/runtime.func
Original file line number Diff line number Diff line change
Expand Up @@ -2508,13 +2508,65 @@ setup_rust() {
# uv refuses to run when a project pins a required-version it does not match,
# in either direction. Only the exact form is read; a range falls through to
# whatever the caller would have installed anyway.
# required-version from pyproject.toml or uv.toml. An exact pin ("==0.12.3")
# is returned as is; a range (">=0.12.15,<0.13") resolves to the newest
# published uv inside it, so a project that caps uv keeps getting the
# upgrades it accepts instead of the latest release it rejects.
_uv_required_version() {
local dir="${1:?dir}" f v
local dir="${1:?dir}" f spec exact='^(==)?([0-9][0-9A-Za-z.+-]*)$'
for f in "$dir/pyproject.toml" "$dir/uv.toml"; do
[[ -f "$f" ]] || continue
v="$(grep -m1 -E '^[[:space:]]*required-version[[:space:]]*=' "$f" 2>/dev/null |
sed -E 's/.*=[[:space:]]*"[[:space:]]*==[[:space:]]*([0-9][0-9A-Za-z.+-]*)"[[:space:]]*$/\1/')"
if [[ "$v" =~ ^[0-9] ]]; then
spec="$(grep -m1 -E '^[[:space:]]*required-version[[:space:]]*=' "$f" 2>/dev/null |
sed -nE 's/^[^=]*=[[:space:]]*"([^"]*)".*/\1/p' | tr -d '[:space:]')"
[[ -n "$spec" ]] || continue
if [[ "$spec" =~ $exact ]]; then
echo "${BASH_REMATCH[2]}"
return 0
fi
_uv_pick_version "$spec" < <(_uv_release_list)
return
done
return 1
}

_uv_release_list() {
local tmp
tmp=$(mktemp) || return 1
if ! github_api_call "https://api.github.com/repos/astral-sh/uv/releases?per_page=100" "$tmp"; then
rm -f "$tmp"
return 1
fi
jq -r '.[].tag_name' "$tmp" | sed 's/^v//' | sort -rV
rm -f "$tmp"
}

_uv_ver_ge() { [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" == "$1" ]]; }

# Newest version on stdin (one per line, newest first) that satisfies every
# comma-separated clause in $1: ==, !=, <, <=, >, >=, ~=.
_uv_pick_version() {
local spec="$1" v c op want ok
local version='^[0-9]+(\.[0-9]+)*$' clause='^(==|!=|<=|>=|<|>|~=)(.+)$'
local -a clauses
IFS=',' read -ra clauses <<<"$spec"
while IFS= read -r v; do
[[ "$v" =~ $version ]] || continue
ok=1
for c in "${clauses[@]}"; do
[[ "$c" =~ $clause ]] || { ok=0; break; }
op="${BASH_REMATCH[1]}" want="${BASH_REMATCH[2]}"
case "$op" in
'==') [[ "$v" == "$want" ]] || ok=0 ;;
'!=') [[ "$v" != "$want" ]] || ok=0 ;;
'>=') _uv_ver_ge "$v" "$want" || ok=0 ;;
'>') [[ "$v" != "$want" ]] && _uv_ver_ge "$v" "$want" || ok=0 ;;
'<=') _uv_ver_ge "$want" "$v" || ok=0 ;;
'<') [[ "$v" != "$want" ]] && _uv_ver_ge "$want" "$v" || ok=0 ;;
'~=') _uv_ver_ge "$v" "$want" && [[ "$v" == "${want%.*}".* ]] || ok=0 ;;
esac
((ok)) || break
done
if ((ok)); then
echo "$v"
return 0
fi
Expand Down
37 changes: 37 additions & 0 deletions tests/uv-required-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail

CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
# Only the uv pin helpers; runtime.func drags the whole engine along.
eval "$(awk '/^_uv_(required_version|release_list|ver_ge|pick_version)\(\) \{/,/^}/' "$CORE_DIR/lib/runtime.func")"

TEST_DIR="$(mktemp -d)"
trap 'rm -rf -- "$TEST_DIR"' EXIT
passed=0 failed=0

_uv_release_list() { printf '%s\n' 0.13.1 0.13.0 0.12.24 0.12.23 0.12.15 0.12.14 0.11.9; }

check() {
if [[ "$3" == "$2" ]]; then
echo "PASS $1"
passed=$((passed + 1))
else
echo "FAIL $1: expected <$2>, got <$3>" >&2
failed=$((failed + 1))
fi
}
pin() { printf '[tool.uv]\nrequired-version = "%s"\n' "$1" >"$TEST_DIR/pyproject.toml"; }
resolve() { _uv_required_version "$TEST_DIR" || true; }

pin "==0.12.3"; check "exact pin" "0.12.3" "$(resolve)"
pin "0.12.3"; check "bare version" "0.12.3" "$(resolve)"
pin ">=0.12.15,<0.13"; check "range picks the newest 0.12" "0.12.24" "$(resolve)"
pin ">=0.12.15, <0.13"; check "spaces inside the range" "0.12.24" "$(resolve)"
pin "~=0.12.15"; check "compatible release" "0.12.24" "$(resolve)"
pin "<0.12.15"; check "upper bound only" "0.12.14" "$(resolve)"
pin ">=0.12.15,<0.13,!=0.12.24"; check "exclusion" "0.12.23" "$(resolve)"
pin ">0.13.1"; check "nothing published matches" "" "$(resolve)"
rm -f "$TEST_DIR/pyproject.toml"; check "no pin" "" "$(resolve)"

printf '%s passed, %s failed\n' "$passed" "$failed"
((failed == 0))
Loading