pyenv-binary: install published CPython packages (#3567)

This commit is contained in:
Ayush 2026-09-26 02:15:13 +05:30 committed by GitHub
parent 3d1dda120b
commit 513364609d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 351 additions and 10 deletions

View File

@ -19,6 +19,20 @@ by the `xz` package).
## Commands ## Commands
### `pyenv binary install <version>`
Installs a matching published package from pyenv.github.io under `<version>`.
The command selects by OS, architecture and distribution; on macOS, an archive
built on an older major release can also be used. Available packages and their
definition checksums are recorded in `share/pyenv-binary/versions`. Add an entry
there when publishing a new package. The definition checks its system-library
requirements when an `ldconfig` cache is available; otherwise, it warns and
proceeds.
```sh
pyenv binary install 3.14.7
```
### `pyenv binary package [-v|--verbose] <version>[:<entry>] --archive-base-url <url>` ### `pyenv binary package [-v|--verbose] <version>[:<entry>] --archive-base-url <url>`
Installs `<version>` from source under a separate name, packages that install Installs `<version>` from source under a separate name, packages that install
@ -55,12 +69,13 @@ pyenv binary package-name 3.12.7
# 3.12.7-debian-12-x86_64 # 3.12.7-debian-12-x86_64
``` ```
### `pyenv binary save <version> [<output-dir>] [--name <name>]` ### `pyenv binary save <version> [<output-dir>] [--name <name>] [--source-version <version>]`
Packs an installed version into `<version>-<platform>.tar.xz` (relative paths) Packs an installed version into `<version>-<platform>.tar.xz` (relative paths)
and writes `<version>-<platform>.meta` describing the build platform (OS, arch, and writes `<version>-<platform>.meta` describing the build platform (OS, arch,
distro and libc version) and the system libraries the build links against. Use distro and libc version) and the system libraries the build links against. Use
`--name` to set a different base name for both files. `--name` to set a different base name for both files. `--source-version` records
the source version when the installed version has a different name.
```sh ```sh
pyenv binary save 3.12.7 ./dist pyenv binary save 3.12.7 ./dist
@ -87,11 +102,12 @@ pyenv binary generate-installer ./dist/3.12.7-linux-x86_64.meta \
pyenv install 3.12.7-linux-x86_64 pyenv install 3.12.7-linux-x86_64
``` ```
### `pyenv binary relocate <prefix>` ### `pyenv binary relocate <prefix> [<build-prefix>]`
Rewrites the rpaths of a Python tree unpacked into `<prefix>` so the interpreter Rewrites the rpaths of a Python tree unpacked into `<prefix>` so the interpreter
and its extension modules load the bundled libraries from there rather than from and its extension modules load the bundled libraries from there rather than from
the path the archive was built at. Uses `patchelf`. The generated definition the path the archive was built at. Linux and FreeBSD use `patchelf`; macOS
calls this; you rarely run it by hand. requires the original `<build-prefix>`. The generated definition calls this;
you rarely run it by hand.
Relocation is implemented for Linux; macOS is not wired up yet. Relocation is implemented for Linux, FreeBSD and macOS.

View File

@ -0,0 +1,124 @@
#!/usr/bin/env bash
#
# Summary: Install a published binary package
#
# Usage: pyenv binary install <version>
#
# Selects a compatible package from pyenv.github.io and installs it under
# <version>. Ordinary `pyenv install' is unchanged.
#
set -e
[ -n "$PYENV_DEBUG" ] && set -x
if [ "$1" = "--complete" ]; then
exit
fi
if [ "$#" -ne 1 ]; then
pyenv-help --usage binary-install >&2
exit 1
fi
case "$1" in
*/* | .. | . | *[[:cntrl:]]* )
echo "pyenv-binary: invalid version name \`$1'" >&2
exit 1
;;
esac
requested="$1"
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Linux )
if [ -r /etc/os-release ]; then
distro="$(. /etc/os-release && printf '%s %s' "${ID:-}" "${VERSION_ID:-}")"
elif type -p lsb_release >/dev/null; then
distro="$(lsb_release -si) $(lsb_release -sr)"
fi
distro="$(printf '%s' "$distro" | tr '[:upper:]' '[:lower:]')"
;;
Darwin )
distro="macos $(sw_vers -productVersion)"
;;
esac
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
base_url="https://pyenv.github.io/pythons/binaries"
manifest_dir="$(dirname "${BASH_SOURCE[0]}")/../share/pyenv-binary/versions"
case "$requested" in
*.*.* ) manifests=( "$manifest_dir/${requested%.*}" ) ;;
*.* ) manifests=( "$manifest_dir/$requested" ) ;;
* ) manifests=( "$manifest_dir/$requested".* ) ;;
esac
matches="$tmpdir/matches"
: > "$matches"
for manifest in "${manifests[@]}"; do
[ -f "$manifest" ] || continue
while IFS=$'\t' read -r source_version name target_os target_arch target_distro sha256; do
case "$source_version" in
"$requested" | "$requested".* ) ;;
* ) continue ;;
esac
[[ "$source_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || continue
[ "$target_os" = "$os" ] && [ "$target_arch" = "$arch" ] || continue
case "$name" in
"" | . | .. | *[!A-Za-z0-9._-]* ) continue ;;
esac
case "$os" in
Linux ) [ "$(printf '%s' "$target_distro" | tr '[:upper:]' '[:lower:]')" = "$distro" ] || continue ;;
Darwin )
target_version="${target_distro#macos }"
[ "$target_version" != "$target_distro" ] || continue
host_version="${distro#macos }"
[[ "$target_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || continue
[[ "$host_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || continue
IFS=. read -r target_major target_minor target_patch <<< "$target_version"
IFS=. read -r host_major host_minor host_patch <<< "$host_version"
target_patch="${target_patch:-0}"
host_patch="${host_patch:-0}"
[ "$host_major" -ge "$target_major" ] || continue
if [ "$host_major" -eq "$target_major" ]; then
[ "$host_minor" -ge "$target_minor" ] || continue
if [ "$host_minor" -eq "$target_minor" ]; then
[ "$host_patch" -ge "$target_patch" ] || continue
fi
fi
;;
* ) continue ;;
esac
printf '%s\t%s\t%s\n' "$source_version" "$name" "$sha256" >> "$matches"
done < "$manifest"
done
selection="$(sort -t. -k1,1nr -k2,2nr -k3,3nr "$matches" | head -n1)"
IFS=$'\t' read -r version entry expected_sha <<< "$selection"
if [ -z "$entry" ]; then
echo "pyenv-binary: no binary available for $requested on $os/$arch" >&2
exit 1
fi
if ! [[ "$expected_sha" =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "pyenv-binary: invalid checksum for $entry" >&2
exit 1
fi
expected_sha="$(printf '%s' "$expected_sha" | tr '[:upper:]' '[:lower:]')"
curl -fsSL -o "$tmpdir/$entry" "$base_url/$entry"
if command -v sha256sum >/dev/null 2>&1; then
actual_sha="$(sha256sum "$tmpdir/$entry")"; actual_sha="${actual_sha%% *}"
elif command -v shasum >/dev/null 2>&1; then
actual_sha="$(shasum -a 256 "$tmpdir/$entry")"; actual_sha="${actual_sha%% *}"
elif command -v openssl >/dev/null 2>&1; then
actual_sha="$(openssl dgst -sha256 "$tmpdir/$entry")"; actual_sha="${actual_sha##* }"
else
echo "pyenv-binary: need sha256sum, shasum or openssl to verify $entry" >&2
exit 1
fi
if [ "$actual_sha" != "$expected_sha" ]; then
echo "pyenv-binary: checksum mismatch for $entry" >&2
exit 1
fi
pyenv-install "$tmpdir/$entry:$version"

View File

@ -77,7 +77,9 @@ esac
# `pyenv install' puts a `<version>:<alias>' build under versions/<alias>. # `pyenv install' puts a `<version>:<alias>' build under versions/<alias>.
pyenv-install ${verbose:+--verbose} "$spec" pyenv-install ${verbose:+--verbose} "$spec"
pyenv-binary-save "$entry" "$PWD" --name "$entry" source_version="$(pyenv-latest -f -k "${spec%%:*}")"
source_version="${source_version##*/}"
pyenv-binary-save "$entry" "$PWD" --name "$entry" --source-version "$source_version"
pyenv-binary-generate-installer "${entry}.meta" \ pyenv-binary-generate-installer "${entry}.meta" \
--archive-url "${archive_base_url%/}/${entry}.tar.xz" -o "$entry" --archive-url "${archive_base_url%/}/${entry}.tar.xz" -o "$entry"

View File

@ -2,7 +2,7 @@
# #
# Summary: Save an installed Python version as a relocatable archive # Summary: Save an installed Python version as a relocatable archive
# #
# Usage: pyenv binary save <version> [<output-dir>] [--name <name>] # Usage: pyenv binary save <version> [<output-dir>] [--name <name>] [--source-version <version>]
# #
# Packs an installed version into a relocatable .tar.xz (relative paths) and # Packs an installed version into a relocatable .tar.xz (relative paths) and
# writes a metadata file listing the build platform and the system libraries # writes a metadata file listing the build platform and the system libraries
@ -12,6 +12,7 @@
# <output-dir> Where to write the archive and metadata (default: `.'). # <output-dir> Where to write the archive and metadata (default: `.').
# --name <name> Use <name> as the archive and metadata base name instead of # --name <name> Use <name> as the archive and metadata base name instead of
# <version>-<platform>. # <version>-<platform>.
# --source-version <version> Record the version used to build a renamed install.
# #
set -e set -e
[ -n "$PYENV_DEBUG" ] && set -x [ -n "$PYENV_DEBUG" ] && set -x
@ -19,18 +20,23 @@ set -e
# Provide pyenv completions # Provide pyenv completions
if [ "$1" = "--complete" ]; then if [ "$1" = "--complete" ]; then
echo --name echo --name
echo --source-version
exec pyenv-versions --bare exec pyenv-versions --bare
fi fi
version="" version=""
output_dir="" output_dir=""
package_name="" package_name=""
source_version=""
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--name ) --name )
[ $# -ge 2 ] && [ -n "$2" ] || { echo "pyenv-binary: --name needs a value" >&2; exit 1; } [ $# -ge 2 ] && [ -n "$2" ] || { echo "pyenv-binary: --name needs a value" >&2; exit 1; }
package_name="$2"; shift 2 ;; package_name="$2"; shift 2 ;;
--source-version )
[ $# -ge 2 ] && [ -n "$2" ] && [ "${2#-}" = "$2" ] || { echo "pyenv-binary: --source-version needs a value" >&2; exit 1; }
source_version="$2"; shift 2 ;;
-* ) -* )
echo "pyenv-binary: unknown option \`$1'" >&2; exit 1 ;; echo "pyenv-binary: unknown option \`$1'" >&2; exit 1 ;;
* ) * )
@ -49,7 +55,7 @@ done
output_dir="${output_dir:-$PWD}" output_dir="${output_dir:-$PWD}"
if [ -z "$version" ]; then if [ -z "$version" ]; then
echo "Usage: pyenv binary save <version> [<output-dir>] [--name <name>]" >&2 echo "Usage: pyenv binary save <version> [<output-dir>] [--name <name>] [--source-version <version>]" >&2
exit 1 exit 1
fi fi
@ -69,6 +75,14 @@ case "$package_name" in
;; ;;
esac esac
source_version="${source_version:-$version}"
case "$source_version" in
*/* | .. | . | *[[:cntrl:]]* )
echo "pyenv-binary: invalid source version \`${source_version}'" >&2
exit 1
;;
esac
prefix="${PYENV_ROOT}/versions/${version}" prefix="${PYENV_ROOT}/versions/${version}"
if [ ! -d "${prefix}/bin" ]; then if [ ! -d "${prefix}/bin" ]; then
echo "pyenv-binary: version \`${version}' is not installed" >&2 echo "pyenv-binary: version \`${version}' is not installed" >&2
@ -142,6 +156,7 @@ tar -C "$(dirname "$prefix")" -cJf "${output_dir}/${archive}" "$(basename "$pref
{ {
echo "# pyenv-binary metadata" echo "# pyenv-binary metadata"
echo "version=${version}" echo "version=${version}"
echo "source_version=${source_version}"
echo "os=${os}" echo "os=${os}"
echo "arch=${arch}" echo "arch=${arch}"
echo "platform=${platform}" echo "platform=${platform}"

View File

@ -0,0 +1,3 @@
source_version entry os arch distro sha256
3.14.7 3.14.7-macos-15-arm64 Darwin arm64 macos 15.7.9 8a8ff86b520c3b5da00b49e04a5345b7a28f2f9e8da5a9ae43b8ac309e8f3e55
3.14.7 3.14.7-ubuntu-24.04-x86_64 Linux x86_64 ubuntu 24.04 e63dbcb981ec9ba83a2bc9b4d8b369620cec4aa45394559d719c5606ee9ff718

View File

@ -0,0 +1,144 @@
#!/usr/bin/env bats
load test_helper
host_distro() {
if [ -r /etc/os-release ]; then
. /etc/os-release
printf '%s %s' "$ID" "$VERSION_ID" | tr '[:upper:]' '[:lower:]'
else
printf '%s %s' "$(lsb_release -si)" "$(lsb_release -sr)" | tr '[:upper:]' '[:lower:]'
fi
}
use_manifest() {
local plugin="${BATS_TEST_TMPDIR}/plugin"
mkdir -p "$plugin/libexec" "$plugin/share/pyenv-binary/versions"
cp "${BATS_TEST_DIRNAME}/../libexec/pyenv-binary-install" "$plugin/libexec/"
PATH="$plugin/libexec:$PATH"
printf 'source_version\tentry\tos\tarch\tdistro\tsha256\n' > "$plugin/share/pyenv-binary/versions/3.14"
definition_sha="4c4ed1afbfdaa1e4c3bf7bbb82d730cecb7e384da91eea4f3cc093fd545524d6"
}
stub_downloads() {
create_stub curl <<'STUB'
output=""
while [ "$#" -gt 0 ]; do
case "$1" in
-o ) output="$2"; shift ;;
http* ) url="$1" ;;
esac
shift
done
case "$url" in
*/3.14.7-ubuntu-24.04-x86_64 | */3.14.7-macos-15-arm64 | */3.14.10-ubuntu-24.04-x86_64 ) printf definition > "$output" ;;
* ) exit 1 ;;
esac
STUB
}
@test "rejects a modified published definition" {
create_stub lsb_release 'case "$1" in -si) echo Ubuntu;; -sr) echo 24.04;; esac'
use_manifest
printf '3.14.7\t3.14.7-ubuntu-24.04-x86_64\tLinux\tx86_64\t%s\t%064d\n' \
"$(host_distro)" 0 >> "$BATS_TEST_TMPDIR/plugin/share/pyenv-binary/versions/3.14"
stub_downloads
create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac'
create_stub pyenv-install 'echo installed'
run pyenv-binary-install 3.14.7
assert_failure
[[ "$output" == *'checksum mismatch'* ]]
}
@test "completion does not offer source-only versions" {
create_stub pyenv-install 'echo 3.14.7'
run pyenv-binary-install --complete
assert_success ""
}
@test "installs a matching published binary with an uppercase checksum" {
create_stub lsb_release 'case "$1" in -si) echo Ubuntu;; -sr) echo 24.04;; esac'
use_manifest
local uppercase_sha="$(printf '%s' "$definition_sha" | tr '[:lower:]' '[:upper:]')"
printf '3.14.7\t3.14.7-macos-15-arm64\tDarwin\tarm64\tmacos 15.7.9\t%s\n' \
"$definition_sha" >> "$BATS_TEST_TMPDIR/plugin/share/pyenv-binary/versions/3.14"
printf '3.14.7\t3.14.7-ubuntu-24.04-x86_64\tLinux\tx86_64\t%s\t%s\n' \
"$(host_distro | tr '[:lower:]' '[:upper:]')" "$uppercase_sha" \
>> "$BATS_TEST_TMPDIR/plugin/share/pyenv-binary/versions/3.14"
stub_downloads
create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac'
create_stub pyenv-install <<'STUB'
definition="${1%:*}"
printf '%s:%s\n' "${definition##*/}" "${1##*:}"
cat "$definition"
STUB
run pyenv-binary-install 3.14
assert_success "3.14.7-ubuntu-24.04-x86_64:3.14.7
definition"
}
@test "a version prefix selects the latest published binary" {
create_stub lsb_release 'case "$1" in -si) echo Ubuntu;; -sr) echo 24.04;; esac'
use_manifest
printf '3.14.10\t3.14.10-ubuntu-24.04-x86_64\tLinux\tx86_64\t%s\t%s\n' \
"$(host_distro)" "$definition_sha" >> "$BATS_TEST_TMPDIR/plugin/share/pyenv-binary/versions/3.14"
printf '3.14.7\t3.14.7-ubuntu-24.04-x86_64\tLinux\tx86_64\t%s\t%s\n' \
"$(host_distro)" "$definition_sha" >> "$BATS_TEST_TMPDIR/plugin/share/pyenv-binary/versions/3.14"
stub_downloads
create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac'
create_stub pyenv-install 'echo "${1##*:}"'
run pyenv-binary-install 3.14
assert_success "3.14.10"
}
@test "fails when no published binary matches the host" {
use_manifest
printf '3.14.7\t3.14.7-macos-15-arm64\tDarwin\tarm64\tmacos 15.7.9\t%s\n' \
"$definition_sha" >> "$BATS_TEST_TMPDIR/plugin/share/pyenv-binary/versions/3.14"
create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac'
run pyenv-binary-install 3.14.7
assert_failure "pyenv-binary: no binary available for 3.14.7 on Linux/x86_64"
}
@test "does not select a package for another Linux release" {
create_stub lsb_release 'case "$1" in -si) echo Ubuntu;; -sr) echo 22.04;; esac'
use_manifest
local other_release="ubuntu 24.04"
[ "$(host_distro)" = "$other_release" ] && other_release="ubuntu 22.04"
printf '3.14.7\t3.14.7-ubuntu-24.04-x86_64\tLinux\tx86_64\t%s\t%s\n' \
"$other_release" "$definition_sha" >> "$BATS_TEST_TMPDIR/plugin/share/pyenv-binary/versions/3.14"
create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac'
run pyenv-binary-install 3.14.7
assert_failure "pyenv-binary: no binary available for 3.14.7 on Linux/x86_64"
}
@test "uses a macOS binary built on an older release" {
use_manifest
printf '3.14.7\t3.14.7-macos-15-arm64\tDarwin\tarm64\tmacos 15.7.9\t%s\n' \
"$definition_sha" >> "$BATS_TEST_TMPDIR/plugin/share/pyenv-binary/versions/3.14"
stub_downloads
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_stub sw_vers 'echo 26.6.2'
create_stub pyenv-install 'echo "${1##*:}"'
run pyenv-binary-install 3.14.7
assert_success "3.14.7"
}
@test "rejects a macOS binary built on a newer patch release" {
use_manifest
printf '3.14.7\t3.14.7-macos-15-arm64\tDarwin\tarm64\tmacos 15.7.9\t%s\n' \
"$definition_sha" >> "$BATS_TEST_TMPDIR/plugin/share/pyenv-binary/versions/3.14"
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_stub sw_vers 'echo 15.7.8'
create_stub pyenv-install 'echo installed'
run pyenv-binary-install 3.14.7
assert_failure "pyenv-binary: no binary available for 3.14.7 on Darwin/arm64"
}

View File

@ -68,10 +68,24 @@ pyenv-install --list --bare
assert [ -d "${PYENV_ROOT}/versions/3.12.7-debian-12-x86_64" ] assert [ -d "${PYENV_ROOT}/versions/3.12.7-debian-12-x86_64" ]
assert [ -f "${BATS_TEST_TMPDIR}/3.12.7-debian-12-x86_64.tar.xz" ] assert [ -f "${BATS_TEST_TMPDIR}/3.12.7-debian-12-x86_64.tar.xz" ]
assert [ -f "${BATS_TEST_TMPDIR}/3.12.7-debian-12-x86_64.meta" ] assert [ -f "${BATS_TEST_TMPDIR}/3.12.7-debian-12-x86_64.meta" ]
run grep '^source_version=' "${BATS_TEST_TMPDIR}/3.12.7-debian-12-x86_64.meta"
assert_success "source_version=3.12.7"
run grep '^ARCHIVE_URL=' "${BATS_TEST_TMPDIR}/3.12.7-debian-12-x86_64" run grep '^ARCHIVE_URL=' "${BATS_TEST_TMPDIR}/3.12.7-debian-12-x86_64"
assert_success "ARCHIVE_URL=http://example.com/binaries/3.12.7-debian-12-x86_64.tar.xz" assert_success "ARCHIVE_URL=http://example.com/binaries/3.12.7-debian-12-x86_64.tar.xz"
} }
@test "records the definition name when packaging from a file" {
stub_build_environment
cd "${BATS_TEST_TMPDIR}"
printf 'install_package "foo"\n' > custom-python
run pyenv-binary-package "${BATS_TEST_TMPDIR}/custom-python:custom-build" \
--archive-base-url http://example.com/binaries
assert_success
run grep '^source_version=' custom-build.meta
assert_success "source_version=custom-python"
}
@test "rejects an entry name containing a slash" { @test "rejects an entry name containing a slash" {
run pyenv-binary-package "3.12.7:foo/bar" --archive-base-url http://x/b run pyenv-binary-package "3.12.7:foo/bar" --archive-base-url http://x/b
assert_failure "pyenv-binary: invalid entry name \`foo/bar'" assert_failure "pyenv-binary: invalid entry name \`foo/bar'"
@ -84,6 +98,7 @@ pyenv-install --list --bare
@test "packages on macOS" { @test "packages on macOS" {
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac' create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_stub pyenv-latest 'echo 3.12.7'
create_stub pyenv-install 'echo install' create_stub pyenv-install 'echo install'
create_stub pyenv-binary-save 'echo save' create_stub pyenv-binary-save 'echo save'
create_stub pyenv-binary-generate-installer 'echo generate-installer' create_stub pyenv-binary-generate-installer 'echo generate-installer'

View File

@ -12,7 +12,7 @@ platform() {
@test "fails with no version given" { @test "fails with no version given" {
run pyenv-binary-save run pyenv-binary-save
assert_failure "Usage: pyenv binary save <version> [<output-dir>] [--name <name>]" assert_failure "Usage: pyenv binary save <version> [<output-dir>] [--name <name>] [--source-version <version>]"
} }
@test "fails for a version that is not installed" { @test "fails for a version that is not installed" {
@ -55,6 +55,28 @@ platform() {
assert_success "archive=custom.tar.xz" assert_success "archive=custom.tar.xz"
} }
@test "records the source version for a renamed build" {
create_version "3.12.7-ubuntu-24.04-x86_64"
local out="${BATS_TEST_TMPDIR}/dist"
run pyenv-binary-save "3.12.7-ubuntu-24.04-x86_64" "$out" \
--name "3.12.7-ubuntu-24.04-x86_64" --source-version "3.12.7"
assert_success
run grep '^source_version=' "${out}/3.12.7-ubuntu-24.04-x86_64.meta"
assert_success "source_version=3.12.7"
}
@test "rejects an option as the source version" {
run pyenv-binary-save "3.12.7" --source-version --name custom
assert_failure "pyenv-binary: --source-version needs a value"
}
@test "rejects control characters in the default source version" {
run pyenv-binary-save $'3.12.7\nsource_version=other'
assert_failure
[[ "$output" == "pyenv-binary: invalid source version"* ]]
}
@test "fails when --name has no value" { @test "fails when --name has no value" {
run pyenv-binary-save "3.12.7" --name run pyenv-binary-save "3.12.7" --name
assert_failure "pyenv-binary: --name needs a value" assert_failure "pyenv-binary: --name needs a value"