pyenv-binary: rename build to package

Delegate completion to `pyenv install --list`, use the explicit entry for the archive and metadata names, and distinguish the base URL from `generate-installer`'s full archive URL.
This commit is contained in:
macayu17 2026-07-28 11:43:49 +05:30
parent bcdc61bdf8
commit 94fcf1e269
5 changed files with 122 additions and 69 deletions

View file

@ -16,10 +16,10 @@ and dependency metadata exist to catch that.
## Commands
### `pyenv binary build <version>:<entry> --archive-url <url>`
### `pyenv binary package <version>:<entry> --archive-base-url <url>`
Builds `<version>` from source under the separate name `<entry>`, packages it
with `save`, then emits a python-build definition for it with
Installs `<version>` from source under the separate name `<entry>`, packages
that install with `save`, then emits a python-build definition for it with
`generate-installer`. The archive, metadata and definition land in the current
directory, named after the entry; host the archive under `<url>` and drop the
definition into python-build's definition directory. Keeping the entry name
@ -27,16 +27,18 @@ distinct from the version lets the binary sit alongside a normal source
install of the same version.
```sh
pyenv binary build 3.12.7:3.12.7-debian-12 --archive-url https://example.com/binaries
# writes 3.12.7-debian-12-linux-x86_64.tar.gz, its .meta file and
pyenv binary package 3.12.7:3.12.7-debian-12 \
--archive-base-url https://example.com/binaries
# writes 3.12.7-debian-12.tar.gz, its .meta file and
# a `3.12.7-debian-12' definition
```
### `pyenv binary save <version> [<output-dir>]`
### `pyenv binary save <version> [<output-dir>] [--name <name>]`
Packs an installed version into `<version>-<platform>.tar.gz` (relative paths)
and writes `<version>-<platform>.meta` describing the build platform (OS, arch,
distro and libc version) and the system libraries the build links against.
distro and libc version) and the system libraries the build links against. Use
`--name` to set a different base name for both files.
```sh
pyenv binary save 3.12.7 ./dist

View file

@ -1,48 +1,41 @@
#!/usr/bin/env bash
#
# Summary: Build a Python version and package it as an installable binary
# Summary: Create an installable binary package from a Python version
#
# Usage: pyenv binary build <version>:<entry> --archive-url <url>
# Usage: pyenv binary package <version>:<entry> --archive-base-url <url>
#
# Builds <version> from source under the separate name <entry>, packages the
# result with `pyenv binary save', then emits a python-build definition for
# it with `pyenv binary generate-installer'. The archive, metadata and
# definition are written to the current directory, named after <entry>.
# Installs <version> from source under the separate name <entry>, saves it as
# a binary package, then emits a python-build definition for that package.
# The archive, metadata and definition are written to the current directory
# as <entry>.tar.gz, <entry>.meta and <entry>.
#
# <version> A version `pyenv install' knows how to build.
# <entry> The name to build under and to install the binary
# as, e.g. `3.13.14-debian-12'. Keeping it distinct
# from <version> lets the binary sit alongside a
# normal source install of the same version.
# --archive-url <url> Where the archive will be hosted; the definition
# downloads it from <url>/<archive-name>.
# --archive-base-url <url>
# Where the archive will be hosted; the definition
# downloads it from <url>/<entry>.tar.gz.
#
set -e
[ -n "$PYENV_DEBUG" ] && set -x
# Provide pyenv completions. Only the definitions are useful here: the options
# `pyenv install' takes are not accepted. Include definitions from other
# plugins just as `pyenv install' does.
# Provide pyenv completions
if [ "$1" = "--complete" ]; then
shopt -s nullglob
for plugin_path in "$PYENV_ROOT"/plugins/*/share/python-build; do
PYTHON_BUILD_DEFINITIONS="${PYTHON_BUILD_DEFINITIONS}:${plugin_path}"
done
export PYTHON_BUILD_DEFINITIONS
shopt -u nullglob
echo --archive-url
exec python-build --definitions
echo --archive-base-url
pyenv-install --list | sed '1d; s/^ //'
exit
fi
spec=""
archive_url=""
archive_base_url=""
while [ $# -gt 0 ]; do
case "$1" in
--archive-url )
[ $# -ge 2 ] || { echo "pyenv-binary: --archive-url needs a value" >&2; exit 1; }
archive_url="$2"; shift 2 ;;
--archive-base-url )
[ $# -ge 2 ] || { echo "pyenv-binary: --archive-base-url needs a value" >&2; exit 1; }
archive_base_url="$2"; shift 2 ;;
-* )
echo "pyenv-binary: unknown option \`$1'" >&2; exit 1 ;;
* )
@ -51,8 +44,8 @@ while [ $# -gt 0 ]; do
esac
done
if [ -z "$spec" ] || [ -z "$archive_url" ]; then
pyenv-help --usage binary-build >&2
if [ -z "$spec" ] || [ -z "$archive_base_url" ]; then
pyenv-help --usage binary-package >&2
exit 1
fi
@ -84,7 +77,7 @@ esac
os="$(uname -s)"
# `generate-installer' refuses a macOS archive, so the last step here cannot
# succeed on one. Give up before the build rather than after it.
# succeed on one. Give up before compiling rather than after it.
if [ "$os" = "Darwin" ]; then
echo "pyenv-binary: macOS archives are not supported yet" >&2
exit 1
@ -92,9 +85,7 @@ fi
# `pyenv install' puts a `<version>:<alias>' build under versions/<alias>.
pyenv-install "$spec"
pyenv-binary-save "$entry"
pyenv-binary-save "$entry" "$PWD" --name "$entry"
# Name the metadata and the archive the way `save' just named them.
platform="$(printf '%s' "$os" | tr '[:upper:]' '[:lower:]')-$(uname -m)"
pyenv-binary-generate-installer "${entry}-${platform}.meta" \
--archive-url "${archive_url%/}/${entry}-${platform}.tar.gz" -o "$entry"
pyenv-binary-generate-installer "${entry}.meta" \
--archive-url "${archive_base_url%/}/${entry}.tar.gz" -o "$entry"

View file

@ -2,7 +2,7 @@
#
# Summary: Save an installed Python version as a relocatable archive
#
# Usage: pyenv binary save <version> [<output-dir>]
# Usage: pyenv binary save <version> [<output-dir>] [--name <name>]
#
# Packs an installed version into a relocatable .tar.gz (relative paths) and
# writes a metadata file listing the build platform and the system libraries
@ -10,20 +10,46 @@
#
# <version> An installed version, as listed by `pyenv versions --bare'.
# <output-dir> Where to write the archive and metadata (default: `.').
# --name <name> Use <name> as the archive and metadata base name instead of
# <version>-<platform>.
#
set -e
[ -n "$PYENV_DEBUG" ] && set -x
# Provide pyenv completions
if [ "$1" = "--complete" ]; then
echo --name
exec pyenv-versions --bare
fi
version="$1"
output_dir="${2:-$PWD}"
version=""
output_dir=""
package_name=""
while [ $# -gt 0 ]; do
case "$1" in
--name )
[ $# -ge 2 ] && [ -n "$2" ] || { echo "pyenv-binary: --name needs a value" >&2; exit 1; }
package_name="$2"; shift 2 ;;
-* )
echo "pyenv-binary: unknown option \`$1'" >&2; exit 1 ;;
* )
if [ -z "$version" ]; then
version="$1"
elif [ -z "$output_dir" ]; then
output_dir="$1"
else
echo "pyenv-binary: unexpected argument \`$1'" >&2
exit 1
fi
shift ;;
esac
done
output_dir="${output_dir:-$PWD}"
if [ -z "$version" ]; then
echo "Usage: pyenv binary save <version> [<output-dir>]" >&2
echo "Usage: pyenv binary save <version> [<output-dir>] [--name <name>]" >&2
exit 1
fi
@ -36,6 +62,13 @@ case "$version" in
;;
esac
case "$package_name" in
*/* | .. | . )
echo "pyenv-binary: invalid package name \`${package_name}'" >&2
exit 1
;;
esac
prefix="${PYENV_ROOT}/versions/${version}"
if [ ! -d "${prefix}/bin" ]; then
echo "pyenv-binary: version \`${version}' is not installed" >&2
@ -85,8 +118,9 @@ system_deps() {
}
mkdir -p "$output_dir"
archive="${version}-${platform}.tar.gz"
metadata="${version}-${platform}.meta"
package_name="${package_name:-${version}-${platform}}"
archive="${package_name}.tar.gz"
metadata="${package_name}.meta"
tar -C "$(dirname "$prefix")" -czf "${output_dir}/${archive}" "$(basename "$prefix")"

View file

@ -11,79 +11,82 @@ stub_build_environment() {
create_stub getconf 'echo "glibc 2.17"'
}
@test "completion lists definitions provided by another plugin" {
@test "completion lists the option and definitions provided by another plugin" {
mkdir -p "${PYENV_ROOT}/plugins/example/share/python-build"
touch "${PYENV_ROOT}/plugins/example/share/python-build/3.12.7-example"
PATH="${BATS_TEST_DIRNAME}/../../python-build/bin:${PATH}"
run pyenv-binary-build --complete
run pyenv-binary-package --complete
assert_success
assert_line "--archive-url"
assert_line "--archive-base-url"
assert_line "3.12.7-example"
refute_line "Available versions:"
}
@test "fails with no arguments" {
create_stub pyenv-help "echo usage"
run pyenv-binary-build
run pyenv-binary-package
assert_failure "usage"
}
@test "fails without an archive url" {
@test "fails without an archive base url" {
create_stub pyenv-help "echo usage"
run pyenv-binary-build 3.12.7:3.12.7-test
run pyenv-binary-package 3.12.7:3.12.7-test
assert_failure "usage"
}
@test "fails when --archive-url has no value" {
run pyenv-binary-build 3.12.7:3.12.7-test --archive-url
assert_failure "pyenv-binary: --archive-url needs a value"
@test "fails when --archive-base-url has no value" {
run pyenv-binary-package 3.12.7:3.12.7-test --archive-base-url
assert_failure "pyenv-binary: --archive-base-url needs a value"
}
@test "rejects a second positional argument" {
run pyenv-binary-build 3.12.7:3.12.7-test extra --archive-url http://x/b
run pyenv-binary-package 3.12.7:3.12.7-test extra --archive-base-url http://x/b
assert_failure "pyenv-binary: unexpected argument \`extra'"
}
@test "rejects a bare version without an entry name" {
run pyenv-binary-build 3.12.7 --archive-url http://x/b
run pyenv-binary-package 3.12.7 --archive-base-url http://x/b
assert_failure "pyenv-binary: expected <version>:<entry>, e.g. \`3.13.14:3.13.14-debian-12'"
}
@test "rejects an entry name containing a slash" {
run pyenv-binary-build "3.12.7:foo/bar" --archive-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'"
}
@test "rejects \`latest' as an entry name" {
run pyenv-binary-build 3.12:latest --archive-url http://x/b
run pyenv-binary-package 3.12:latest --archive-base-url http://x/b
assert_failure "pyenv-binary: \`latest' cannot be used as an entry name"
}
@test "refuses to build on macOS before compiling anything" {
@test "refuses to package on macOS before compiling anything" {
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
run pyenv-binary-build 3.12.7:3.12.7-test --archive-url http://x/b
run pyenv-binary-package 3.12.7:3.12.7-test --archive-base-url http://x/b
assert_failure "pyenv-binary: macOS archives are not supported yet"
}
@test "builds under the entry name and writes the archive and definition" {
@test "writes the archive, metadata and definition under the entry name" {
stub_build_environment
cd "${BATS_TEST_TMPDIR}"
run pyenv-binary-build 3.12.7:3.12.7-test --archive-url http://example.com/binaries
run pyenv-binary-package 3.12.7:3.12.7-test \
--archive-base-url http://example.com/binaries
assert_success
assert [ -d "${PYENV_ROOT}/versions/3.12.7-test" ]
assert [ -f "${BATS_TEST_TMPDIR}/3.12.7-test-linux-x86_64.tar.gz" ]
assert [ -f "${BATS_TEST_TMPDIR}/3.12.7-test-linux-x86_64.meta" ]
assert [ -f "${BATS_TEST_TMPDIR}/3.12.7-test.tar.gz" ]
assert [ -f "${BATS_TEST_TMPDIR}/3.12.7-test.meta" ]
run grep '^ARCHIVE_URL=' "${BATS_TEST_TMPDIR}/3.12.7-test"
assert_success "ARCHIVE_URL=http://example.com/binaries/3.12.7-test-linux-x86_64.tar.gz"
assert_success "ARCHIVE_URL=http://example.com/binaries/3.12.7-test.tar.gz"
}
@test "strips a trailing slash from the archive url" {
@test "strips a trailing slash from the archive base url" {
stub_build_environment
cd "${BATS_TEST_TMPDIR}"
run pyenv-binary-build 3.12.7:3.12.7-test --archive-url http://example.com/binaries/
run pyenv-binary-package 3.12.7:3.12.7-test \
--archive-base-url http://example.com/binaries/
assert_success
run grep '^ARCHIVE_URL=' "${BATS_TEST_TMPDIR}/3.12.7-test"
assert_success "ARCHIVE_URL=http://example.com/binaries/3.12.7-test-linux-x86_64.tar.gz"
assert_success "ARCHIVE_URL=http://example.com/binaries/3.12.7-test.tar.gz"
}

View file

@ -12,7 +12,7 @@ platform() {
@test "fails with no version given" {
run pyenv-binary-save
assert_failure "Usage: pyenv binary save <version> [<output-dir>]"
assert_failure "Usage: pyenv binary save <version> [<output-dir>] [--name <name>]"
}
@test "fails for a version that is not installed" {
@ -44,6 +44,29 @@ platform() {
assert_line 0 "3.12.7/"
}
@test "uses an explicit package name" {
create_version "3.12.7"
local out="${BATS_TEST_TMPDIR}/dist"
run pyenv-binary-save "3.12.7" "$out" --name "custom"
assert_success "Saved custom.tar.gz and custom.meta to $out"
assert [ -f "${out}/custom.tar.gz" ]
run grep '^archive=' "${out}/custom.meta"
assert_success "archive=custom.tar.gz"
}
@test "fails when --name has no value" {
run pyenv-binary-save "3.12.7" --name
assert_failure "pyenv-binary: --name needs a value"
}
@test "rejects an invalid package name" {
create_version "3.12.7"
run pyenv-binary-save "3.12.7" --name "foo/bar"
assert_failure "pyenv-binary: invalid package name \`foo/bar'"
}
@test "records the platform in the metadata" {
create_version "3.12.7"
local out="${BATS_TEST_TMPDIR}/dist"