From 094c0a80ed1578e7501ed88e9f22a75900113bf4 Mon Sep 17 00:00:00 2001 From: macayu17 Date: Wed, 22 Jul 2026 13:09:07 +0530 Subject: [PATCH] pyenv-binary: add the `build` subcommand Packaging a version by hand means running `pyenv install`, `pyenv binary save` and `pyenv binary generate-installer` in turn and keeping the names they produce in step. `build` runs the three in one pass, so the archive, its metadata and the definition all come out named after the entry. * The entry name is given explicitly as `:`, reusing the syntax `pyenv install` already accepts, so a packaged build can sit beside a plain source install of the same version * Entry names that `pyenv install` would not install under (`latest`), or that would point outside versions/, are refused before anything is compiled rather than after * Linux only for now, matching `save` and `generate-installer` --- plugins/pyenv-binary/README.md | 16 ++++ .../pyenv-binary/libexec/pyenv-binary-build | 83 +++++++++++++++++++ plugins/pyenv-binary/test/build.bats | 79 ++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100755 plugins/pyenv-binary/libexec/pyenv-binary-build create mode 100644 plugins/pyenv-binary/test/build.bats diff --git a/plugins/pyenv-binary/README.md b/plugins/pyenv-binary/README.md index ac01be58..4c0b2bc0 100644 --- a/plugins/pyenv-binary/README.md +++ b/plugins/pyenv-binary/README.md @@ -16,6 +16,22 @@ and dependency metadata exist to catch that. ## Commands +### `pyenv binary build : --archive-url ` + +Builds `` from source under the separate name ``, packages it +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 `` and drop the +definition into python-build's definition directory. Keeping the entry name +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 +# a `3.12.7-debian-12' definition +``` + ### `pyenv binary save []` Packs an installed version into `-.tar.gz` (relative paths) diff --git a/plugins/pyenv-binary/libexec/pyenv-binary-build b/plugins/pyenv-binary/libexec/pyenv-binary-build new file mode 100755 index 00000000..216e9198 --- /dev/null +++ b/plugins/pyenv-binary/libexec/pyenv-binary-build @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# Summary: Build a Python version and package it as an installable binary +# +# Usage: pyenv binary build : --archive-url +# +# Builds from source under the separate name , 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 . +# +# A version `pyenv install' knows how to build. +# The name to build under and to install the binary +# as, e.g. `3.13.14-debian-12'. Keeping it distinct +# from lets the binary sit alongside a +# normal source install of the same version. +# --archive-url Where the archive will be hosted; the definition +# downloads it from /. +# +set -e +[ -n "$PYENV_DEBUG" ] && set -x + +# Provide pyenv completions. Only the definitions are useful here: the options +# `pyenv install' takes are not accepted. +if [ "$1" = "--complete" ]; then + echo --archive-url + exec python-build --definitions +fi + +spec="" +archive_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 ;; + -* ) + echo "pyenv-binary: unknown option \`$1'" >&2; exit 1 ;; + * ) + [ -z "$spec" ] || { echo "pyenv-binary: unexpected argument \`$1'" >&2; exit 1; } + spec="$1"; shift ;; + esac +done + +if [ -z "$spec" ] || [ -z "$archive_url" ]; then + pyenv-help --usage binary-build >&2 + exit 1 +fi + +case "$spec" in +*?:?* ) ;; +* ) + echo "pyenv-binary: expected :, e.g. \`3.13.14:3.13.14-debian-12'" >&2 + exit 1 + ;; +esac +entry="${spec##*:}" + +case "$entry" in +# `pyenv install' reads a trailing `:latest' as part of the version rather than +# as an alias, so nothing would end up installed under that name. +latest ) + echo "pyenv-binary: \`latest' cannot be used as an entry name" >&2 + exit 1 + ;; +# The entry becomes a directory name under versions/. `pyenv install' does not +# validate the alias, and `save' only checks once the build is done, so refuse +# a name that could point elsewhere before compiling anything. +*/* | .. | . ) + echo "pyenv-binary: invalid entry name \`${entry}'" >&2 + exit 1 + ;; +esac + +# `pyenv install' puts a `:' build under versions/. +pyenv-install "$spec" +pyenv-binary-save "$entry" + +# Name the metadata and the archive the way `save' just named them. +platform="$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m)" +pyenv-binary-generate-installer "${entry}-${platform}.meta" \ + --archive-url "${archive_url%/}/${entry}-${platform}.tar.gz" -o "$entry" diff --git a/plugins/pyenv-binary/test/build.bats b/plugins/pyenv-binary/test/build.bats new file mode 100644 index 00000000..5fe9df94 --- /dev/null +++ b/plugins/pyenv-binary/test/build.bats @@ -0,0 +1,79 @@ +#!/usr/bin/env bats + +load test_helper + +# Make the build deterministic: `pyenv-install' just creates the prefix, and +# the platform tools report a fixed Linux target so the real `save' and +# `generate-installer' behave the same on any test host. +stub_build_environment() { + create_stub pyenv-install 'mkdir -p "${PYENV_ROOT}/versions/${1##*:}/bin"' + create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac' + create_stub getconf 'echo "glibc 2.17"' +} + +@test "completion lists the option and the buildable versions" { + create_stub python-build 'echo 3.12.7' + run pyenv-binary-build --complete + assert_success "--archive-url +3.12.7" +} + +@test "fails with no arguments" { + create_stub pyenv-help "echo usage" + run pyenv-binary-build + assert_failure "usage" +} + +@test "fails without an archive url" { + create_stub pyenv-help "echo usage" + run pyenv-binary-build 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 "rejects a second positional argument" { + run pyenv-binary-build 3.12.7:3.12.7-test extra --archive-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 + assert_failure "pyenv-binary: expected :, 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 + 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 + assert_failure "pyenv-binary: \`latest' cannot be used as an entry name" +} + +@test "builds under the entry name and writes the archive and definition" { + stub_build_environment + cd "${BATS_TEST_TMPDIR}" + + run pyenv-binary-build 3.12.7:3.12.7-test --archive-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" ] + 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" +} + +@test "strips a trailing slash from the archive 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/ + 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" +}