From a12f947cc3b21e908648d063229678147c09e7be Mon Sep 17 00:00:00 2001 From: native-api Date: Sun, 30 Oct 2022 03:38:40 +0300 Subject: [PATCH] Auto-resolve prefixes to the latest version (#2487) --- .github/workflows/macos_build.yml | 8 +-- .github/workflows/ubuntu_build.yml | 8 +-- libexec/pyenv-latest | 78 +++++++++++++++++++++ libexec/pyenv-prefix | 1 + libexec/pyenv-version-name | 2 + plugins/python-build/bin/pyenv-install | 4 ++ plugins/python-build/bin/python-build | 23 +++++-- plugins/python-build/test/definitions.bats | 19 +++++ plugins/python-build/test/hooks.bats | 1 + plugins/python-build/test/pyenv.bats | 14 ++++ plugins/python-build/test/test_helper.bash | 26 +++++++ test/latest.bats | 80 ++++++++++++++++++++++ test/version-name.bats | 7 ++ 13 files changed, 258 insertions(+), 13 deletions(-) create mode 100755 libexec/pyenv-latest create mode 100644 test/latest.bats diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index 7b39ec76..c8eb0640 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -6,10 +6,10 @@ jobs: fail-fast: false matrix: python-version: - - 3.7.13 - - 3.8.13 - - 3.9.13 - - 3.10.6 + - "3.7" + - "3.8" + - "3.9" + - "3.10" runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/ubuntu_build.yml b/.github/workflows/ubuntu_build.yml index 97e1b081..59e7978c 100644 --- a/.github/workflows/ubuntu_build.yml +++ b/.github/workflows/ubuntu_build.yml @@ -6,10 +6,10 @@ jobs: fail-fast: false matrix: python-version: - - 3.7.13 - - 3.8.13 - - 3.9.13 - - 3.10.6 + - "3.7" + - "3.8" + - "3.9" + - "3.10" runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 diff --git a/libexec/pyenv-latest b/libexec/pyenv-latest new file mode 100755 index 00000000..af0b018f --- /dev/null +++ b/libexec/pyenv-latest @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Summary: Print the latest installed or known version with the given prefix +# Usage: pyenv latest [-k|--known] [-q|--quiet] +# +# -k/--known Select from all known versions instead of installed +# -q/--quiet Do not print a + +set -e +[ -n "$PYENV_DEBUG" ] && set -x + +while [[ $# -gt 0 ]] +do + case "$1" in + -k|--known) + FROM_KNOWN=1 + shift + ;; + -q|--quiet) + QUIET=1 + shift + ;; + *) + break + ;; + esac +done + +prefix=$1 +exitcode=0 + +IFS=$'\n' + + if [[ -z $FROM_KNOWN ]]; then + DEFINITION_CANDIDATES=( $(pyenv-versions --bare) ) + else + DEFINITION_CANDIDATES=( $(python-build --definitions ) ) + fi + # if grep -xFe "$prefix" <<<"${DEFINITION_CANDIDATES[@]}"; then + # echo "$prefix" + # exit $exitcode + # fi + # https://stackoverflow.com/questions/11856054/is-there-an-easy-way-to-pass-a-raw-string-to-grep/63483807#63483807 + prefix_re="$(sed 's/[^\^]/[&]/g;s/[\^]/\\&/g' <<< "$prefix")" + # FIXME: more reliable and readable would probably be to loop over them and transform in pure Bash + DEFINITION_CANDIDATES=(\ + $(printf '%s\n' "${DEFINITION_CANDIDATES[@]}" | \ + grep -Ee "^$prefix_re[-.]" || true)) + + DEFINITION_CANDIDATES=(\ + $(printf '%s\n' "${DEFINITION_CANDIDATES[@]}" | \ + sed -E -e '/-dev$/d' -e '/-src$/d' -e '/-latest$/d' -e '/(b|rc)[0-9]+$/d')); + + # Compose a sorting key, followed by | and original value + DEFINITION_CANDIDATES=(\ + $(printf '%s\n' "${DEFINITION_CANDIDATES[@]}" | \ + awk \ + '{ if (match($0,"^[[:alnum:]]+-")) + { print substr($0,0,RLENGTH-1) "." substr($0,RLENGTH+1) "..|" $0; } + else + { print $0 "...|" $0; } + }')) + DEFINITION_CANDIDATES=(\ + $(printf '%s\n' "${DEFINITION_CANDIDATES[@]}" \ + | sort -t. -k1,1r -k 2,2nr -k 3,3nr -k4,4nr \ + | cut -f2 -d $'|' \ + || true)) + DEFINITION="${DEFINITION_CANDIDATES[0]}" + + if [[ -n "$DEFINITION" ]]; then + echo "$DEFINITION" + else + if [[ -z $QUIET ]]; then + echo "pyenv: no $([[ -z $FROM_KNOWN ]] && echo installed || echo known) versions match the prefix \`$prefix'" >&2 + fi + exitcode=1 + fi + +exit $exitcode diff --git a/libexec/pyenv-prefix b/libexec/pyenv-prefix index e244e76a..38b8f823 100755 --- a/libexec/pyenv-prefix +++ b/libexec/pyenv-prefix @@ -42,6 +42,7 @@ OLDIFS="$IFS" exit 1 fi else + version="$(pyenv-latest -q "$version" || echo "$version")" PYENV_PREFIX_PATH="${PYENV_ROOT}/versions/${version}" fi if [ -d "$PYENV_PREFIX_PATH" ]; then diff --git a/libexec/pyenv-version-name b/libexec/pyenv-version-name index 65a198a4..d06357cb 100755 --- a/libexec/pyenv-version-name +++ b/libexec/pyenv-version-name @@ -34,6 +34,8 @@ OLDIFS="$IFS" versions=("${versions[@]}" "${version}") elif version_exists "${version#python-}"; then versions=("${versions[@]}" "${version#python-}") + elif resolved_version="$(pyenv-latest -q "$version")"; then + versions=("${versions[@]}" "${resolved_version}") else echo "pyenv: version \`$version' is not installed (set by $(pyenv-version-origin))" >&2 any_not_installed=1 diff --git a/plugins/python-build/bin/pyenv-install b/plugins/python-build/bin/pyenv-install index b966ff88..e7877b2d 100755 --- a/plugins/python-build/bin/pyenv-install +++ b/plugins/python-build/bin/pyenv-install @@ -145,6 +145,10 @@ IFS=$'\n' scripts=(`pyenv-hooks install`) IFS="$OLDIFS" for script in "${scripts[@]}"; do source "$script"; done +# Try to resolve a prefix if user indeed gave a prefix. +# We install the version under the resolved name +# and hooks also see the resolved name +DEFINITION="$(pyenv-latest -q -k "$DEFINITION" || echo "$DEFINITION")" # Set VERSION_NAME from $DEFINITION, if it is not already set. Then # compute the installation prefix. diff --git a/plugins/python-build/bin/python-build b/plugins/python-build/bin/python-build index 0b29c4e5..eca00319 100755 --- a/plugins/python-build/bin/python-build +++ b/plugins/python-build/bin/python-build @@ -2067,12 +2067,25 @@ DEFINITION_PATH="${ARGUMENTS[0]}" if [ -z "$DEFINITION_PATH" ]; then usage 1 >&2 elif [ ! -f "$DEFINITION_PATH" ]; then - for DEFINITION_DIR in "${PYTHON_BUILD_DEFINITIONS[@]}"; do - if [ -f "${DEFINITION_DIR}/${DEFINITION_PATH}" ]; then - DEFINITION_PATH="${DEFINITION_DIR}/${DEFINITION_PATH}" - break + + search_definitions() { + for DEFINITION_DIR in "${PYTHON_BUILD_DEFINITIONS[@]}"; do + if [ -f "${DEFINITION_DIR}/${DEFINITION_PATH}" ]; then + DEFINITION_PATH="${DEFINITION_DIR}/${DEFINITION_PATH}" + break + fi + done + } + + search_definitions + if [ ! -f "$DEFINITION_PATH" ]; then + if RESOLVED_DEFINITION_PATH="$(command -v pyenv-latest >/dev/null && pyenv-latest -k -q "$DEFINITION_PATH")"; then + DEFINITION_PATH="$RESOLVED_DEFINITION_PATH" + unset RESOLVED_DEFINITION_PATH + search_definitions fi - done + fi + unset search_definitions if [ ! -f "$DEFINITION_PATH" ]; then echo "python-build: definition not found: ${DEFINITION_PATH}" >&2 diff --git a/plugins/python-build/test/definitions.bats b/plugins/python-build/test/definitions.bats index 4f9e32a1..61d318bf 100644 --- a/plugins/python-build/test/definitions.bats +++ b/plugins/python-build/test/definitions.bats @@ -60,11 +60,30 @@ NUM_DEFINITIONS="$(find "$BATS_TEST_DIRNAME"/../share/python-build -maxdepth 1 - } @test "installing nonexistent definition" { + stub pyenv-latest false run python-build "nonexistent" "${TMP}/install" assert [ "$status" -eq 2 ] assert_output "python-build: definition not found: nonexistent" } +@test "resolves prefixes via pyenv-latest" { + stub pyenv-latest "echo 2.7.8" + export PYTHON_BUILD_ROOT="$TMP" + mkdir -p "${PYTHON_BUILD_ROOT}/share/python-build" + echo 'echo 2.7.8' > "${PYTHON_BUILD_ROOT}/share/python-build/2.7.8" + run python-build "2.7" "${TMP}/install" + assert_success "2.7.8" +} + +@test "doesn't resolve prefixes if pyenv-latest is unavailable" { + export PATH="$(path_without pyenv-latest)" + export PYTHON_BUILD_ROOT="$TMP" + mkdir -p "${PYTHON_BUILD_ROOT}/share/python-build" + echo 'echo 2.7.8' > "${PYTHON_BUILD_ROOT}/share/python-build/2.7.8" + run python-build "2.7" "${TMP}/install" + assert_failure "python-build: definition not found: 2.7" +} + @test "sorting Python versions" { export PYTHON_BUILD_ROOT="$TMP" mkdir -p "${PYTHON_BUILD_ROOT}/share/python-build" diff --git a/plugins/python-build/test/hooks.bats b/plugins/python-build/test/hooks.bats index 613c5c2e..03289524 100644 --- a/plugins/python-build/test/hooks.bats +++ b/plugins/python-build/test/hooks.bats @@ -15,6 +15,7 @@ after_install 'echo after: \$STATUS' OUT stub pyenv-hooks "install : echo '$HOOK_PATH'/install.bash" stub pyenv-rehash "echo rehashed" + stub pyenv-latest false definition="${TMP}/3.6.2" cat > "$definition" <<<"echo python-build" diff --git a/plugins/python-build/test/pyenv.bats b/plugins/python-build/test/pyenv.bats index e1b35818..90359c48 100644 --- a/plugins/python-build/test/pyenv.bats +++ b/plugins/python-build/test/pyenv.bats @@ -10,6 +10,7 @@ setup() { stub_python_build() { stub python-build "--lib : $BATS_TEST_DIRNAME/../bin/python-build --lib" "$@" + stub pyenv-latest " : false" } @test "install proper" { @@ -23,6 +24,19 @@ stub_python_build() { unstub pyenv-rehash } +@test "install resolves a prefix" { + stub_python_build 'echo python-build "$@"' + stub pyenv-latest '-q -k 3.4 : echo 3.4.2' + pyenv-latest || true # pass through the stub entry added by stub_python_build + + run pyenv-install 3.4 + assert_success "python-build 3.4.2 ${PYENV_ROOT}/versions/3.4.2" + + unstub python-build + unstub pyenv-hooks + unstub pyenv-rehash +} + @test "install pyenv local version by default" { stub_python_build 'echo python-build "$1"' stub pyenv-local 'echo 3.4.2' diff --git a/plugins/python-build/test/test_helper.bash b/plugins/python-build/test/test_helper.bash index 459ca6bc..c7a20ac2 100644 --- a/plugins/python-build/test/test_helper.bash +++ b/plugins/python-build/test/test_helper.bash @@ -139,3 +139,29 @@ assert_output_contains() { } | flunk } } + +# Output a modified PATH that ensures that the given executable is not present, +# but in which system utils necessary for pyenv operation are still available. +path_without() { + local path=":${PATH}:" + for exe; do + local found alt util + for found in $(PATH="$path" type -aP "$exe"); do + found="${found%/*}" + if [ "$found" != "${PYENV_ROOT}/shims" ]; then + alt="${PYENV_TEST_DIR}/$(echo "${found#/}" | tr '/' '-')" + mkdir -p "$alt" + for util in bash head cut readlink greadlink; do + if [ -x "${found}/$util" ]; then + ln -s "${found}/$util" "${alt}/$util" + fi + done + path="${path/:${found}:/:${alt}:}" + fi + done + done + path="${path#:}" + path="${path%:}" + echo "$path" +} + diff --git a/test/latest.bats b/test/latest.bats new file mode 100644 index 00000000..e3d2f445 --- /dev/null +++ b/test/latest.bats @@ -0,0 +1,80 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + export PATH="${PYENV_TEST_DIR}/bin:$PATH" +} + +create_executable() { + local name="$1" + local bin="${PYENV_TEST_DIR}/bin" + mkdir -p "$bin" + sed -Ee '1s/^ +//' > "${bin}/$name" + chmod +x "${bin}/$name" +} + +@test "read from installed" { + create_executable pyenv-versions <