From bcd73586b410ec730c2f6df5b903c571abd9f76b Mon Sep 17 00:00:00 2001 From: macayu17 Date: Tue, 30 Jun 2026 23:05:37 +0530 Subject: [PATCH] pyenv-binary: add the `save` subcommand `pyenv binary save []` packs an installed version into a relocatable .tar.gz with relative paths and writes a metadata file recording the build platform, distro, libc version and the external system libraries the build links against (via `ldd` on Linux, `otool -L` on macOS). --- plugins/pyenv-binary/README.md | 12 +++ .../pyenv-binary/libexec/pyenv-binary-save | 95 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100755 plugins/pyenv-binary/libexec/pyenv-binary-save diff --git a/plugins/pyenv-binary/README.md b/plugins/pyenv-binary/README.md index 4caa6252..15023325 100644 --- a/plugins/pyenv-binary/README.md +++ b/plugins/pyenv-binary/README.md @@ -13,3 +13,15 @@ An archive is portable across machines that share its build platform (OS, architecture and a compatible libc) and have the recorded system libraries. It is not portable across, say, glibc and musl, or to an older glibc; the platform and dependency metadata exist to catch that. + +## Commands + +### `pyenv binary save []` + +Packs an installed version into `-.tar.gz` (relative paths) +and writes `-.meta` describing the build platform (OS, arch, +distro and libc version) and the system libraries the build links against. + +```sh +pyenv binary save 3.12.7 ./dist +``` diff --git a/plugins/pyenv-binary/libexec/pyenv-binary-save b/plugins/pyenv-binary/libexec/pyenv-binary-save new file mode 100755 index 00000000..3ede5da9 --- /dev/null +++ b/plugins/pyenv-binary/libexec/pyenv-binary-save @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# +# Summary: Save an installed Python version as a relocatable archive +# +# Usage: pyenv binary save [] +# +# Packs an installed version into a relocatable .tar.gz (relative paths) and +# writes a metadata file listing the build platform and the system libraries +# it links against, so an installer can check compatibility before unpacking. +# +# An installed version, as listed by `pyenv versions --bare'. +# Where to write the archive and metadata (default: `.'). +# +set -e +[ -n "$PYENV_DEBUG" ] && set -x + +# Provide pyenv completions +if [ "$1" = "--complete" ]; then + exec pyenv-versions --bare +fi + +version="$1" +output_dir="${2:-$PWD}" + +if [ -z "$version" ]; then + echo "Usage: pyenv binary save []" >&2 + exit 1 +fi + +prefix="${PYENV_ROOT}/versions/${version}" +if [ ! -d "${prefix}/bin" ]; then + echo "pyenv-binary: version \`${version}' is not installed" >&2 + exit 1 +fi + +os="$(uname -s)" +arch="$(uname -m)" +platform="$(printf '%s' "$os" | tr '[:upper:]' '[:lower:]')-${arch}" + +# Record the distro and libc version. Platform and arch alone are too coarse to +# judge compatibility: a build is only portable to a matching libc (e.g. a +# glibc 2.36 build will not load on an older glibc, nor on musl at all). +distro="" +libc="" +if [ -r /etc/os-release ]; then + distro="$( . /etc/os-release && printf '%s %s' "${ID:-}" "${VERSION_ID:-}" )" +elif [ "$os" = "Darwin" ]; then + distro="macos $(sw_vers -productVersion 2>/dev/null)" +fi +if [ "$os" = "Linux" ]; then + libc="$(getconf GNU_LIBC_VERSION 2>/dev/null || true)" +fi + +# List the external shared libraries the install links against: those that +# resolve outside its own prefix, so they must already exist on the target. +# The interpreter plus every bundled shared object are inspected. +system_deps() { + local f + { + for f in "${prefix}"/bin/python*; do + [ -e "$f" ] && readlink -f "$f" + done + find "${prefix}" -type f \( -name '*.so' -o -name '*.so.*' -o -name '*.dylib' \) + } | sort -u | while IFS= read -r f; do + if [ "$os" = "Darwin" ]; then + otool -L "$f" 2>/dev/null | tail -n +2 | awk -v pfx="${prefix}/" \ + '$1 !~ /^@/ && substr($1, 1, length(pfx)) != pfx { print $1 }' + else + ldd "$f" 2>/dev/null | awk -v pfx="${prefix}/" \ + '$2 == "=>" && $3 ~ /^\// && substr($3, 1, length(pfx)) != pfx { print $1 }' + fi + done | sort -u +} + +mkdir -p "$output_dir" +archive="${version}-${platform}.tar.gz" +metadata="${version}-${platform}.meta" + +tar -C "$prefix" -czf "${output_dir}/${archive}" . + +{ + echo "# pyenv-binary metadata" + echo "version=${version}" + echo "os=${os}" + echo "arch=${arch}" + echo "platform=${platform}" + [ -n "$distro" ] && echo "distro=${distro}" + [ -n "$libc" ] && echo "libc=${libc}" + echo "archive=${archive}" + system_deps | while IFS= read -r dep; do + [ -n "$dep" ] && echo "dep=${dep}" + done +} > "${output_dir}/${metadata}" + +echo "Saved ${archive} and ${metadata} to ${output_dir}"