From 6188e9efa9c04c60a82398272b5a59ea0fe50110 Mon Sep 17 00:00:00 2001 From: macayu17 Date: Thu, 16 Jul 2026 10:14:40 +0300 Subject: [PATCH] pyenv-binary: add the `generate-installer` subcommand * Relocation only implemented for Linux (requires patchelf + ldconfig -p) * Shell-completing file name is not possible with current Pyenv completion logic: `pyenv-complete` does not pass info about the word being completed * Fix `save`-produced tarball to be compatible with Python-Build fetch_tarball() * Reuse test_helper from core -- Co-authored-by: Ivan Pozdeev --- plugins/pyenv-binary/README.md | 30 +++ plugins/pyenv-binary/bin/pyenv-binary | 8 +- .../libexec/pyenv-binary-generate-installer | 189 ++++++++++++++++++ .../libexec/pyenv-binary-relocate | 58 ++++++ .../pyenv-binary/libexec/pyenv-binary-save | 2 +- .../pyenv-binary/test/generate-installer.bats | 123 ++++++++++++ plugins/pyenv-binary/test/relocate.bats | 72 +++++++ plugins/pyenv-binary/test/save.bats | 46 ++--- plugins/pyenv-binary/test/test_helper.bash | 69 +------ 9 files changed, 499 insertions(+), 98 deletions(-) create mode 100755 plugins/pyenv-binary/libexec/pyenv-binary-generate-installer create mode 100755 plugins/pyenv-binary/libexec/pyenv-binary-relocate create mode 100644 plugins/pyenv-binary/test/generate-installer.bats create mode 100644 plugins/pyenv-binary/test/relocate.bats mode change 100644 => 120000 plugins/pyenv-binary/test/test_helper.bash diff --git a/plugins/pyenv-binary/README.md b/plugins/pyenv-binary/README.md index 15023325..ac01be58 100644 --- a/plugins/pyenv-binary/README.md +++ b/plugins/pyenv-binary/README.md @@ -25,3 +25,33 @@ distro and libc version) and the system libraries the build links against. ```sh pyenv binary save 3.12.7 ./dist ``` + +### `pyenv binary generate-installer --archive-url [-o ]` + +Reads a `.meta` file and emits a python-build definition. Drop it into +python-build's definition directory and `pyenv install ` installs the +archive like any other version. The archive location is a parameter, so you can +host it anywhere (it does not have to be a pyenv location); the archive itself +must sit next to the `.meta` file so its checksum can be baked into the +definition. + +The definition refuses to install on a different OS/architecture, or an older +glibc, than the archive was built for, and checks that the system libraries it +needs are present. + +```sh +pyenv binary generate-installer ./dist/3.12.7-linux-x86_64.meta \ + --archive-url https://example.com/3.12.7-linux-x86_64.tar.gz \ + -o "$(pyenv root)/plugins/python-build/share/python-build/3.12.7-linux-x86_64" + +pyenv install 3.12.7-linux-x86_64 +``` + +### `pyenv binary relocate ` + +Rewrites the rpaths of a Python tree unpacked into `` so the interpreter +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 +calls this; you rarely run it by hand. + +Relocation is implemented for Linux; macOS is not wired up yet. diff --git a/plugins/pyenv-binary/bin/pyenv-binary b/plugins/pyenv-binary/bin/pyenv-binary index 9cd29950..36e20532 100755 --- a/plugins/pyenv-binary/bin/pyenv-binary +++ b/plugins/pyenv-binary/bin/pyenv-binary @@ -16,6 +16,11 @@ set -e libexec="${BASH_SOURCE%/*}/../libexec" +# The pyenv launcher only puts a plugin's bin on PATH, but `pyenv-help' looks a +# command up there. Add our libexec so the subcommands, and the help text they +# print, can be found. +export PATH="${libexec}:${PATH}" + list_commands() { local path for path in "$libexec"/pyenv-binary-*; do @@ -54,8 +59,7 @@ if [ ! -x "$command_path" ]; then fi if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then - # `pyenv help' looks the command up on PATH; the subcommands live in libexec. - PATH="${libexec}:${PATH}" exec pyenv-help "binary-${subcommand}" + exec pyenv-help "binary-${subcommand}" fi exec "$command_path" "$@" diff --git a/plugins/pyenv-binary/libexec/pyenv-binary-generate-installer b/plugins/pyenv-binary/libexec/pyenv-binary-generate-installer new file mode 100755 index 00000000..3a3e8445 --- /dev/null +++ b/plugins/pyenv-binary/libexec/pyenv-binary-generate-installer @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# +# Summary: Generate a python-build definition for a saved binary archive +# +# Usage: pyenv binary generate-installer --archive-url [-o ] +# +# Reads a metadata file written by `pyenv binary save' and emits a python-build +# definition. Dropped into python-build's definition directory, it installs the +# archive like any other version: `pyenv install' downloads it from , +# checks the platform and the required system libraries, unpacks it, then +# rewrites rpaths so the copy runs from its prefix. +# +# The archive must sit next to the metadata file so its checksum can be baked +# into the definition; `pyenv binary save' writes the two together. +# +# A .meta file written by `pyenv binary save'. +# The corresponsing binary package written by `pyenv binary save' +# needs to be present alongside it. +# --archive-url The URL for the resulting installation script to download +# the package from. +# -o Write the definition here (default: stdout). +# +set -e +[ -n "$PYENV_DEBUG" ] && set -x + +# Provide pyenv completions +if [ "$1" = "--complete" ]; then + echo --archive-url + echo -o + exit +fi + +metadata="" +archive_url="" +output="" + +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 ;; + -o ) + [ $# -ge 2 ] || { echo "pyenv-binary: -o needs a value" >&2; exit 1; } + output="$2"; shift 2 ;; + -* ) + echo "pyenv-binary: unknown option \`$1'" >&2; exit 1 ;; + * ) + [ -z "$metadata" ] || { echo "pyenv-binary: unexpected argument \`$1'" >&2; exit 1; } + metadata="$1"; shift ;; + esac +done + +if [ -z "$metadata" ] || [ -z "$archive_url" ]; then + pyenv-help --usage binary-generate-installer >&2 + exit 1 +fi + +version="" os="" arch="" distro="" libc="" archive="" +deps="" +while IFS='=' read -r key value; do + case "$key" in + version ) version="$value" ;; + os ) os="$value" ;; + arch ) arch="$value" ;; + distro ) distro="$value" ;; + libc ) libc="$value" ;; + archive ) archive="$value" ;; + dep ) deps="${deps:+$deps }$value" ;; + esac +done < "$metadata" + +# Fail here if the metadata is incomplete, rather than bake a blank into the +# definition where a missing arch would give a confusing platform error. +for field in version os arch archive; do + if [ -z "${!field}" ]; then + echo "pyenv-binary: metadata is missing \`${field}'" >&2 + exit 1 + fi +done + +# macOS relocation needs install_name_tool and a different rpath scheme, so it is +# not supported yet. Other systems relocate with patchelf. +if [ "$os" = "Darwin" ]; then + echo "pyenv-binary: macOS archives are not supported yet" >&2 + exit 1 +fi + +# The glibc floor only applies on Linux; require it there so the definition can +# tell whether the target is new enough to run the binaries. +if [ "$os" = "Linux" ] && [ -z "$libc" ]; then + echo "pyenv-binary: metadata is missing \`libc'" >&2 + exit 1 +fi + +# python-build downloads the archive itself, so the checksum has to be baked in. +# The archive sits next to the metadata `save' wrote it alongside. +archive_path="$(dirname "$metadata")/${archive}" +if [ ! -r "$archive_path" ]; then + echo "pyenv-binary: cannot read the archive \`${archive_path}' to checksum it" >&2 + exit 1 +fi +if command -v sha256sum >/dev/null 2>&1; then + sha256="$(sha256sum "$archive_path")"; sha256="${sha256%% *}" +elif command -v shasum >/dev/null 2>&1; then + sha256="$(shasum -a 256 "$archive_path")"; sha256="${sha256%% *}" +elif command -v openssl >/dev/null 2>&1; then + sha256="$(openssl dgst -sha256 "$archive_path")"; sha256="${sha256##* }" +else + echo "pyenv-binary: need sha256sum, shasum or openssl to checksum the archive" >&2 + exit 1 +fi + +emit() { + # The values known now go in as %q-quoted assignments so a stray quote or space + # in the URL or metadata cannot break the definition or inject into it. The body + # below is a quoted here-doc, verbatim. + { + printf '# python-build definition for prebuilt Python %s (%s/%s%s).\n' \ + "$version" "$os" "$arch" "${distro:+, $distro}" + echo "# Generated by \`pyenv binary generate-installer'." + echo + printf 'EXPECT_OS=%q\n' "$os" + printf 'EXPECT_ARCH=%q\n' "$arch" + printf 'EXPECT_LIBC=%q\n' "$libc" + printf 'VERSION=%q\n' "$version" + printf 'ARCHIVE_URL=%q\n' "$archive_url" + printf 'SHA256=%q\n' "$sha256" + printf 'DEPS=%q\n' "$deps" + } + cat <<'EOF' + +os="$(uname -s)" +arch="$(uname -m)" +if [ "$os" != "$EXPECT_OS" ] || [ "$arch" != "$EXPECT_ARCH" ]; then + echo "pyenv-binary: this archive is for ${EXPECT_OS}/${EXPECT_ARCH}, not ${os}/${arch}" >&2 + exit 1 +fi + +# Refuse a target whose glibc is older than the one the archive was built on; +# the dynamic loader would reject the binaries. +case "$EXPECT_LIBC" in +"glibc "* ) + build_libc="${EXPECT_LIBC#glibc }" + target_libc="$(getconf GNU_LIBC_VERSION 2>/dev/null || true)" + case "$target_libc" in + "glibc "* ) + target_libc="${target_libc#glibc }" + older="$(printf '%s\n%s\n' "$build_libc" "$target_libc" | sort -V | head -n1)" + if [ "$older" = "$target_libc" ] && [ "$target_libc" != "$build_libc" ]; then + echo "pyenv-binary: archive needs glibc ${build_libc} or newer, but this system has ${target_libc}" >&2 + exit 1 + fi + ;; + esac + ;; +esac + +# Required system libraries must already be present on the target. ldconfig -p +# lists each library as " (...) => "; match the soname column +# exactly so a longer name like libc.so.6.1 does not satisfy libc.so.6. +if command -v ldconfig &>/dev/null && cache="$(ldconfig -p)"; then + missing="" + for dep in $DEPS; do + printf '%s\n' "$cache" | awk -v d="$dep" '$1 == d { found = 1 } END { exit !found }' \ + || missing="${missing} ${dep}" + done + if [ -n "$missing" ]; then + echo "pyenv-binary: missing required system libraries:${missing}" >&2 + exit 1 + fi +elif [ -n "$DEPS" ]; then + echo "pyenv-binary: cannot check required system libraries (ldconfig with -p not found)" >&2 +fi + +build_package_relocate() { + pyenv binary relocate "$PREFIX_PATH" +} + +install_package "Python-${VERSION}-binary" "${ARCHIVE_URL}#${SHA256}" copy relocate + +EOF +} + +if [ -n "$output" ]; then + emit > "$output" + echo "Wrote definition to ${output}" +else + emit +fi diff --git a/plugins/pyenv-binary/libexec/pyenv-binary-relocate b/plugins/pyenv-binary/libexec/pyenv-binary-relocate new file mode 100755 index 00000000..6b977893 --- /dev/null +++ b/plugins/pyenv-binary/libexec/pyenv-binary-relocate @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# +# Summary: Rewrite an unpacked Python's rpaths so it runs from its prefix +# +# Usage: pyenv binary relocate +# +# Rewrites the rpaths of a Python tree that was unpacked into so the +# interpreter and its extension modules load the bundled libraries from their +# new location rather than the path it was built at. Requires patchelf. +# +# The definition that `pyenv binary generate-installer' produces calls this +# after `install_package ... copy' has laid the tree down. +# +set -e +[ -n "$PYENV_DEBUG" ] && set -x + +if [ "$1" = "--complete" ]; then + exit +fi + +prefix="$1" +if [ -z "$prefix" ]; then + pyenv-help --usage binary-relocate >&2 + exit 1 +fi + +# patchelf can add an rpath and write one of any length; chrpath can only shorten +# an existing one, which is not enough to relocate every build, so require it. +if ! command -v patchelf >/dev/null 2>&1; then + echo "pyenv-binary: need patchelf to relocate the binary" >&2 + exit 1 +fi + +# The interpreter loads libpython from ../lib. Patch every ELF binary in bin/ so +# this holds whatever the interpreter is named (python3, python2.7, ...); the +# scripts alongside it (pip, idle) are not ELF, so `--print-rpath' fails on them +# and they are skipped. A tree with no interpreter at all did not unpack the way +# we expect, so treat that as an error rather than quietly relocating nothing. +found=0 +for file in "$prefix"/bin/*; do + [[ -f $file && -x $file && ! -L $file ]] || continue + patchelf --print-rpath "$file" >/dev/null 2>&1 || continue + patchelf --set-rpath "$prefix/lib" "$file" + found=1 +done +if [ "$found" -eq 0 ]; then + echo "pyenv-binary: found no interpreter to relocate under \`${prefix}/bin'" >&2 + exit 1 +fi + +# The extension modules CPython built are the only other objects with an rpath +# into the old prefix, so point them at lib/ as well. Anything a wheel installed +# carries an rpath of its own, often into a bundled library directory beside it, +# and would stop loading if we overwrote it. Each match is an ELF object we expect +# to patch, so let a failure stop us rather than swallow it. +while IFS= read -r so; do + patchelf --set-rpath "$prefix/lib" "$so" +done < <(find "$prefix" -path '*/lib-dynload/*.so') diff --git a/plugins/pyenv-binary/libexec/pyenv-binary-save b/plugins/pyenv-binary/libexec/pyenv-binary-save index e4273681..7a20b5c4 100755 --- a/plugins/pyenv-binary/libexec/pyenv-binary-save +++ b/plugins/pyenv-binary/libexec/pyenv-binary-save @@ -88,7 +88,7 @@ mkdir -p "$output_dir" archive="${version}-${platform}.tar.gz" metadata="${version}-${platform}.meta" -tar -C "$prefix" -czf "${output_dir}/${archive}" . +tar -C "$(dirname "$prefix")" -czf "${output_dir}/${archive}" "$(basename "$prefix")" { echo "# pyenv-binary metadata" diff --git a/plugins/pyenv-binary/test/generate-installer.bats b/plugins/pyenv-binary/test/generate-installer.bats new file mode 100644 index 00000000..e7e36c51 --- /dev/null +++ b/plugins/pyenv-binary/test/generate-installer.bats @@ -0,0 +1,123 @@ +#!/usr/bin/env bats + +load test_helper + +create_meta() { + local os="${1-Linux}" + local arch="${2-x86_64}" + local libc="${3-glibc 2.17}" + local archive="${BATS_TEST_TMPDIR}/3.12.7.tar.gz" + local meta="${BATS_TEST_TMPDIR}/sample.meta" + + rm -rf "${BATS_TEST_TMPDIR}/archive" + mkdir -p "${BATS_TEST_TMPDIR}/archive/3.12.7/bin" + printf '#!/bin/sh\n' > "${BATS_TEST_TMPDIR}/archive/3.12.7/bin/python" + chmod +x "${BATS_TEST_TMPDIR}/archive/3.12.7/bin/python" + tar -C "${BATS_TEST_TMPDIR}/archive" -czf "$archive" 3.12.7 + + { + echo "version=3.12.7" + echo "os=${os}" + echo "arch=${arch}" + [ -z "$libc" ] || echo "libc=${libc}" + echo "archive=${archive##*/}" + } > "$meta" + echo "$meta" +} + +@test "completion lists the options" { + run pyenv-binary-generate-installer --complete + assert_success "--archive-url +-o" +} + +@test "fails with no arguments" { + create_stub pyenv-help "echo usage" + run pyenv-binary-generate-installer + assert_failure "usage" +} + +@test "fails without an archive url" { + create_stub pyenv-help "echo usage" + run pyenv-binary-generate-installer "$(create_meta)" + assert_failure "usage" +} + +@test "fails when --archive-url has no value" { + run pyenv-binary-generate-installer "$(create_meta)" --archive-url + assert_failure "pyenv-binary: --archive-url needs a value" +} + +@test "rejects a second positional argument" { + run pyenv-binary-generate-installer "$(create_meta)" extra --archive-url http://x/a.tar.gz + assert_failure "pyenv-binary: unexpected argument \`extra'" +} + +@test "fails for a metadata file that cannot be read" { + run pyenv-binary-generate-installer /no/such.meta --archive-url http://x/a.tar.gz + assert_failure +} + +@test "refuses macOS metadata" { + run pyenv-binary-generate-installer "$(create_meta Darwin arm64 '')" \ + --archive-url http://x/a.tar.gz + assert_failure "pyenv-binary: macOS archives are not supported yet" +} + +@test "fails when required metadata is missing" { + local field meta + for field in version os arch archive; do + meta="$(create_meta)" + #cannot use -i: GNU sed requires -i[suf], BSD sed required -i + sed "/^${field}=/d" "$meta" > "${meta}.tmp"; mv "${meta}"{.tmp,} + + run pyenv-binary-generate-installer "$meta" --archive-url http://x/a.tar.gz + assert_failure "pyenv-binary: metadata is missing \`${field}'" + done +} + +@test "fails when Linux metadata is missing libc" { + run pyenv-binary-generate-installer "$(create_meta Linux x86_64 '')" \ + --archive-url http://x/a.tar.gz + assert_failure "pyenv-binary: metadata is missing \`libc'" +} + +@test "refuses a host whose glibc is older than the archive" { + local out="${BATS_TEST_TMPDIR}/definition" + pyenv-binary-generate-installer "$(create_meta Linux x86_64 'glibc 99.0')" \ + --archive-url http://example.com/a.tar.gz -o "$out" + create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac' + create_stub getconf 'echo "glibc 2.31"' + + run bash "$out" + assert_failure "pyenv-binary: archive needs glibc 99.0 or newer, but this system has 2.31" +} + +@test "fails when the archive is not beside the metadata" { + local meta="$(create_meta)" + rm "${BATS_TEST_TMPDIR}/3.12.7.tar.gz" + + run pyenv-binary-generate-installer "$meta" --archive-url http://x/a.tar.gz + assert_failure "pyenv-binary: cannot read the archive \`${BATS_TEST_TMPDIR}/3.12.7.tar.gz' to checksum it" +} + +@test "the generated definition is installable with python-build" { + ldconfig -p &>/dev/null || skip "ldconfig with -p is not present" + local archive="${BATS_TEST_TMPDIR}/3.12.7.tar.gz" + local cache="${BATS_TEST_TMPDIR}/cache" + local definition="${BATS_TEST_TMPDIR}/definition" + local prefix="${BATS_TEST_TMPDIR}/install" + pyenv-binary-generate-installer "$(create_meta)" \ + --archive-url http://example.com/3.12.7.tar.gz -o "$definition" + mkdir -p "$cache" + cp "$archive" "${cache}/Python-3.12.7-binary.tar.gz" + create_stub pyenv 'echo "pyenv $*"' + create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac' + + PYTHON_BUILD_CACHE_PATH="$cache" run \ + "${BATS_TEST_DIRNAME}/../../python-build/bin/python-build" "$definition" "$prefix" + assert_success + assert_line "pyenv binary relocate ${prefix}" + assert_line "Installed Python-3.12.7-binary to ${prefix}" + assert [ -x "${prefix}/bin/python" ] +} diff --git a/plugins/pyenv-binary/test/relocate.bats b/plugins/pyenv-binary/test/relocate.bats new file mode 100644 index 00000000..0051a42c --- /dev/null +++ b/plugins/pyenv-binary/test/relocate.bats @@ -0,0 +1,72 @@ +#!/usr/bin/env bats + +load test_helper + +_setup() { + create_stub pyenv-help "echo usage" +} + +stub_patchelf() { + create_path_executable patchelf <> "${BATS_TEST_TMPDIR}/patchelf.log" +STUB +} + +create_interpreter() { + mkdir -p "${BATS_TEST_TMPDIR}/prefix/bin" + printf '#!/bin/sh\n' > "${BATS_TEST_TMPDIR}/prefix/bin/python2.7" + chmod +x "${BATS_TEST_TMPDIR}/prefix/bin/python2.7" +} + +@test "completion produces nothing" { + run pyenv-binary-relocate --complete + assert_success "" +} + +@test "fails without a prefix" { + run pyenv-binary-relocate + assert_failure "usage" +} + +@test "fails when patchelf is not available" { + PATH="$(path_without patchelf)" run pyenv-binary-relocate "${BATS_TEST_TMPDIR}/prefix" + assert_failure "pyenv-binary: need patchelf to relocate the binary" +} + +@test "fails when the prefix has no interpreter" { + create_path_executable patchelf "exit 0" + mkdir -p "${BATS_TEST_TMPDIR}/prefix" + + run pyenv-binary-relocate "${BATS_TEST_TMPDIR}/prefix" + assert_failure "pyenv-binary: found no interpreter to relocate under \`${BATS_TEST_TMPDIR}/prefix/bin'" +} + +@test "relocates an executable interpreter" { + stub_patchelf + create_interpreter + + run pyenv-binary-relocate "${BATS_TEST_TMPDIR}/prefix" + assert_success + run cat "${BATS_TEST_TMPDIR}/patchelf.log" + assert_output "--set-rpath ${BATS_TEST_TMPDIR}/prefix/lib ${BATS_TEST_TMPDIR}/prefix/bin/python2.7" +} + +@test "relocates the extension modules but not what a wheel installed" { + local lib="${BATS_TEST_TMPDIR}/prefix/lib/python3.12" + stub_patchelf + create_interpreter + mkdir -p "${lib}/lib-dynload" "${lib}/site-packages/numpy" + touch "${lib}/lib-dynload/_ssl.cpython-312-x86_64-linux-gnu.so" + # A wheel points its extensions at the libraries it bundles alongside them, so + # its rpath is its own business and must survive relocation. + touch "${lib}/site-packages/numpy/_multiarray.so" + + run pyenv-binary-relocate "${BATS_TEST_TMPDIR}/prefix" + assert_success + run cat "${BATS_TEST_TMPDIR}/patchelf.log" + assert_line "--set-rpath ${BATS_TEST_TMPDIR}/prefix/lib ${lib}/lib-dynload/_ssl.cpython-312-x86_64-linux-gnu.so" + refute_line "--set-rpath ${BATS_TEST_TMPDIR}/prefix/lib ${lib}/site-packages/numpy/_multiarray.so" +} diff --git a/plugins/pyenv-binary/test/save.bats b/plugins/pyenv-binary/test/save.bats index 1e1188ef..97c9f12a 100644 --- a/plugins/pyenv-binary/test/save.bats +++ b/plugins/pyenv-binary/test/save.bats @@ -33,40 +33,39 @@ platform() { @test "packages an installed version" { create_version "3.12.7" local out="${BATS_TEST_TMPDIR}/dist" + local archive="${out}/3.12.7-$(platform).tar.gz" + run pyenv-binary-save "3.12.7" "$out" assert_success "Saved 3.12.7-$(platform).tar.gz and 3.12.7-$(platform).meta to $out" - assert [ -f "${out}/3.12.7-$(platform).tar.gz" ] + assert [ -f "$archive" ] assert [ -f "${out}/3.12.7-$(platform).meta" ] + run tar -tzf "$archive" + assert_success + assert_line 0 "3.12.7/" } @test "records the platform in the metadata" { create_version "3.12.7" local out="${BATS_TEST_TMPDIR}/dist" pyenv-binary-save "3.12.7" "$out" >/dev/null + run cat "${out}/3.12.7-$(platform).meta" assert_success - assert_output_contains "version=3.12.7" - assert_output_contains "platform=$(platform)" - assert_output_contains "archive=3.12.7-$(platform).tar.gz" -} - -# Put an executable on PATH that stands in for a system tool during the test. -stub() { - local name="$1" - local dir="${BATS_TEST_TMPDIR}/stubs" - mkdir -p "$dir" - { echo "#!/usr/bin/env bash"; cat -; } > "${dir}/${name}" - chmod +x "${dir}/${name}" - export PATH="${dir}:$PATH" + assert_line "version=3.12.7" + assert_line "platform=$(platform)" + assert_line "archive=3.12.7-$(platform).tar.gz" } @test "records only the libraries ldd resolves outside the prefix" { create_version "3.12.7" touch "${PYENV_ROOT}/versions/3.12.7/bin/python3.12" - - # A realistic ldd listing: the vdso and the loader have no `=>' mapping, - # libpython resolves inside the prefix, and libc/libm are external. - stub ldd <<'STUB' + create_path_executable uname <<'STUB' +case "$1" in + -s) echo Linux ;; + -m) echo x86_64 ;; +esac +STUB + create_path_executable ldd <<'STUB' prefix="${PYENV_ROOT}/versions/3.12.7" cat <