Add an experimental pyenv-binary plugin with a save command (#3487)
* pyenv-binary: add experimental plugin skeleton
Adds a new plugin, decoupled from `pyenv install`, for packaging and installing
relocatable Python binaries. This commit is just the command dispatcher; the
individual subcommands follow.
`pyenv binary <command>` dispatches to the matching script under the plugin's
libexec, so subcommands stay out of the top-level `pyenv commands` list.
Groundwork for the binary distribution support discussed in #2334.
* pyenv-binary: add the `save` subcommand
`pyenv binary save <version> [<output-dir>]` 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).
* pyenv-binary: reject invalid version names and drop readlink -f in save
A version is a single directory name under versions/, so refuse names with a
slash or a dot-dot component before building the prefix path.
Also emit the python paths directly instead of `readlink -f`, which BSD
readlink on macOS does not support; ldd/otool follow the symlinks anyway, so
the resolved paths were never needed.
* pyenv-binary: test the `save` subcommand
Cover argument validation, the version-name guard, and packaging an
installed version into an archive with matching metadata.
* pyenv-binary: test dependency parsing and tighten the version guard
Add tests that feed realistic ldd and otool listings through save and
check only the libraries resolving outside the prefix end up in the
metadata, since that filtering is the fiddliest part of the command.
Narrow the version guard to reject `.' and `..' rather than any name
containing a dot-dot, now that a slash is already refused, and note why
the find still matches *.so.* even though CPython does not produce them.
2026-07-03 11:38:54 +05:30
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
#
|
|
|
|
|
# Summary: Save an installed Python version as a relocatable archive
|
|
|
|
|
#
|
2026-07-28 17:01:20 +05:30
|
|
|
# Usage: pyenv binary save <version> [<output-dir>] [--name <name>]
|
Add an experimental pyenv-binary plugin with a save command (#3487)
* pyenv-binary: add experimental plugin skeleton
Adds a new plugin, decoupled from `pyenv install`, for packaging and installing
relocatable Python binaries. This commit is just the command dispatcher; the
individual subcommands follow.
`pyenv binary <command>` dispatches to the matching script under the plugin's
libexec, so subcommands stay out of the top-level `pyenv commands` list.
Groundwork for the binary distribution support discussed in #2334.
* pyenv-binary: add the `save` subcommand
`pyenv binary save <version> [<output-dir>]` 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).
* pyenv-binary: reject invalid version names and drop readlink -f in save
A version is a single directory name under versions/, so refuse names with a
slash or a dot-dot component before building the prefix path.
Also emit the python paths directly instead of `readlink -f`, which BSD
readlink on macOS does not support; ldd/otool follow the symlinks anyway, so
the resolved paths were never needed.
* pyenv-binary: test the `save` subcommand
Cover argument validation, the version-name guard, and packaging an
installed version into an archive with matching metadata.
* pyenv-binary: test dependency parsing and tighten the version guard
Add tests that feed realistic ldd and otool listings through save and
check only the libraries resolving outside the prefix end up in the
metadata, since that filtering is the fiddliest part of the command.
Narrow the version guard to reject `.' and `..' rather than any name
containing a dot-dot, now that a slash is already refused, and note why
the find still matches *.so.* even though CPython does not produce them.
2026-07-03 11:38:54 +05:30
|
|
|
#
|
|
|
|
|
# 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.
|
|
|
|
|
#
|
|
|
|
|
# <version> An installed version, as listed by `pyenv versions --bare'.
|
|
|
|
|
# <output-dir> Where to write the archive and metadata (default: `.').
|
2026-07-28 17:01:20 +05:30
|
|
|
# --name <name> Use <name> as the archive and metadata base name instead of
|
|
|
|
|
# <version>-<platform>.
|
Add an experimental pyenv-binary plugin with a save command (#3487)
* pyenv-binary: add experimental plugin skeleton
Adds a new plugin, decoupled from `pyenv install`, for packaging and installing
relocatable Python binaries. This commit is just the command dispatcher; the
individual subcommands follow.
`pyenv binary <command>` dispatches to the matching script under the plugin's
libexec, so subcommands stay out of the top-level `pyenv commands` list.
Groundwork for the binary distribution support discussed in #2334.
* pyenv-binary: add the `save` subcommand
`pyenv binary save <version> [<output-dir>]` 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).
* pyenv-binary: reject invalid version names and drop readlink -f in save
A version is a single directory name under versions/, so refuse names with a
slash or a dot-dot component before building the prefix path.
Also emit the python paths directly instead of `readlink -f`, which BSD
readlink on macOS does not support; ldd/otool follow the symlinks anyway, so
the resolved paths were never needed.
* pyenv-binary: test the `save` subcommand
Cover argument validation, the version-name guard, and packaging an
installed version into an archive with matching metadata.
* pyenv-binary: test dependency parsing and tighten the version guard
Add tests that feed realistic ldd and otool listings through save and
check only the libraries resolving outside the prefix end up in the
metadata, since that filtering is the fiddliest part of the command.
Narrow the version guard to reject `.' and `..' rather than any name
containing a dot-dot, now that a slash is already refused, and note why
the find still matches *.so.* even though CPython does not produce them.
2026-07-03 11:38:54 +05:30
|
|
|
#
|
|
|
|
|
set -e
|
|
|
|
|
[ -n "$PYENV_DEBUG" ] && set -x
|
|
|
|
|
|
|
|
|
|
# Provide pyenv completions
|
|
|
|
|
if [ "$1" = "--complete" ]; then
|
2026-07-28 17:01:20 +05:30
|
|
|
echo --name
|
Add an experimental pyenv-binary plugin with a save command (#3487)
* pyenv-binary: add experimental plugin skeleton
Adds a new plugin, decoupled from `pyenv install`, for packaging and installing
relocatable Python binaries. This commit is just the command dispatcher; the
individual subcommands follow.
`pyenv binary <command>` dispatches to the matching script under the plugin's
libexec, so subcommands stay out of the top-level `pyenv commands` list.
Groundwork for the binary distribution support discussed in #2334.
* pyenv-binary: add the `save` subcommand
`pyenv binary save <version> [<output-dir>]` 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).
* pyenv-binary: reject invalid version names and drop readlink -f in save
A version is a single directory name under versions/, so refuse names with a
slash or a dot-dot component before building the prefix path.
Also emit the python paths directly instead of `readlink -f`, which BSD
readlink on macOS does not support; ldd/otool follow the symlinks anyway, so
the resolved paths were never needed.
* pyenv-binary: test the `save` subcommand
Cover argument validation, the version-name guard, and packaging an
installed version into an archive with matching metadata.
* pyenv-binary: test dependency parsing and tighten the version guard
Add tests that feed realistic ldd and otool listings through save and
check only the libraries resolving outside the prefix end up in the
metadata, since that filtering is the fiddliest part of the command.
Narrow the version guard to reject `.' and `..' rather than any name
containing a dot-dot, now that a slash is already refused, and note why
the find still matches *.so.* even though CPython does not produce them.
2026-07-03 11:38:54 +05:30
|
|
|
exec pyenv-versions --bare
|
|
|
|
|
fi
|
|
|
|
|
|
2026-07-28 17:01:20 +05:30
|
|
|
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}"
|
Add an experimental pyenv-binary plugin with a save command (#3487)
* pyenv-binary: add experimental plugin skeleton
Adds a new plugin, decoupled from `pyenv install`, for packaging and installing
relocatable Python binaries. This commit is just the command dispatcher; the
individual subcommands follow.
`pyenv binary <command>` dispatches to the matching script under the plugin's
libexec, so subcommands stay out of the top-level `pyenv commands` list.
Groundwork for the binary distribution support discussed in #2334.
* pyenv-binary: add the `save` subcommand
`pyenv binary save <version> [<output-dir>]` 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).
* pyenv-binary: reject invalid version names and drop readlink -f in save
A version is a single directory name under versions/, so refuse names with a
slash or a dot-dot component before building the prefix path.
Also emit the python paths directly instead of `readlink -f`, which BSD
readlink on macOS does not support; ldd/otool follow the symlinks anyway, so
the resolved paths were never needed.
* pyenv-binary: test the `save` subcommand
Cover argument validation, the version-name guard, and packaging an
installed version into an archive with matching metadata.
* pyenv-binary: test dependency parsing and tighten the version guard
Add tests that feed realistic ldd and otool listings through save and
check only the libraries resolving outside the prefix end up in the
metadata, since that filtering is the fiddliest part of the command.
Narrow the version guard to reject `.' and `..' rather than any name
containing a dot-dot, now that a slash is already refused, and note why
the find still matches *.so.* even though CPython does not produce them.
2026-07-03 11:38:54 +05:30
|
|
|
|
|
|
|
|
if [ -z "$version" ]; then
|
2026-07-28 17:01:20 +05:30
|
|
|
echo "Usage: pyenv binary save <version> [<output-dir>] [--name <name>]" >&2
|
Add an experimental pyenv-binary plugin with a save command (#3487)
* pyenv-binary: add experimental plugin skeleton
Adds a new plugin, decoupled from `pyenv install`, for packaging and installing
relocatable Python binaries. This commit is just the command dispatcher; the
individual subcommands follow.
`pyenv binary <command>` dispatches to the matching script under the plugin's
libexec, so subcommands stay out of the top-level `pyenv commands` list.
Groundwork for the binary distribution support discussed in #2334.
* pyenv-binary: add the `save` subcommand
`pyenv binary save <version> [<output-dir>]` 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).
* pyenv-binary: reject invalid version names and drop readlink -f in save
A version is a single directory name under versions/, so refuse names with a
slash or a dot-dot component before building the prefix path.
Also emit the python paths directly instead of `readlink -f`, which BSD
readlink on macOS does not support; ldd/otool follow the symlinks anyway, so
the resolved paths were never needed.
* pyenv-binary: test the `save` subcommand
Cover argument validation, the version-name guard, and packaging an
installed version into an archive with matching metadata.
* pyenv-binary: test dependency parsing and tighten the version guard
Add tests that feed realistic ldd and otool listings through save and
check only the libraries resolving outside the prefix end up in the
metadata, since that filtering is the fiddliest part of the command.
Narrow the version guard to reject `.' and `..' rather than any name
containing a dot-dot, now that a slash is already refused, and note why
the find still matches *.so.* even though CPython does not produce them.
2026-07-03 11:38:54 +05:30
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# A version is a single directory name under versions/. With no slash allowed,
|
|
|
|
|
# the only remaining names that could point elsewhere are `.' and `..'.
|
|
|
|
|
case "$version" in
|
|
|
|
|
*/* | .. | . )
|
|
|
|
|
echo "pyenv-binary: invalid version name \`${version}'" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
2026-07-28 17:01:20 +05:30
|
|
|
case "$package_name" in
|
|
|
|
|
*/* | .. | . | *[[:cntrl:]]* )
|
|
|
|
|
echo "pyenv-binary: invalid package name \`${package_name}'" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
Add an experimental pyenv-binary plugin with a save command (#3487)
* pyenv-binary: add experimental plugin skeleton
Adds a new plugin, decoupled from `pyenv install`, for packaging and installing
relocatable Python binaries. This commit is just the command dispatcher; the
individual subcommands follow.
`pyenv binary <command>` dispatches to the matching script under the plugin's
libexec, so subcommands stay out of the top-level `pyenv commands` list.
Groundwork for the binary distribution support discussed in #2334.
* pyenv-binary: add the `save` subcommand
`pyenv binary save <version> [<output-dir>]` 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).
* pyenv-binary: reject invalid version names and drop readlink -f in save
A version is a single directory name under versions/, so refuse names with a
slash or a dot-dot component before building the prefix path.
Also emit the python paths directly instead of `readlink -f`, which BSD
readlink on macOS does not support; ldd/otool follow the symlinks anyway, so
the resolved paths were never needed.
* pyenv-binary: test the `save` subcommand
Cover argument validation, the version-name guard, and packaging an
installed version into an archive with matching metadata.
* pyenv-binary: test dependency parsing and tighten the version guard
Add tests that feed realistic ldd and otool listings through save and
check only the libraries resolving outside the prefix end up in the
metadata, since that filtering is the fiddliest part of the command.
Narrow the version guard to reject `.' and `..' rather than any name
containing a dot-dot, now that a slash is already refused, and note why
the find still matches *.so.* even though CPython does not produce them.
2026-07-03 11:38:54 +05:30
|
|
|
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" ] && printf '%s\n' "$f"
|
|
|
|
|
done
|
|
|
|
|
# CPython ships its extension modules as *.so (and *.dylib on macOS). A
|
|
|
|
|
# bare *.so.* is unusual for CPython itself, but the odd build carries a
|
|
|
|
|
# versioned copy alongside, so match it too rather than miss a dependency.
|
|
|
|
|
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"
|
2026-07-28 17:01:20 +05:30
|
|
|
package_name="${package_name:-${version}-${platform}}"
|
|
|
|
|
archive="${package_name}.tar.gz"
|
|
|
|
|
metadata="${package_name}.meta"
|
Add an experimental pyenv-binary plugin with a save command (#3487)
* pyenv-binary: add experimental plugin skeleton
Adds a new plugin, decoupled from `pyenv install`, for packaging and installing
relocatable Python binaries. This commit is just the command dispatcher; the
individual subcommands follow.
`pyenv binary <command>` dispatches to the matching script under the plugin's
libexec, so subcommands stay out of the top-level `pyenv commands` list.
Groundwork for the binary distribution support discussed in #2334.
* pyenv-binary: add the `save` subcommand
`pyenv binary save <version> [<output-dir>]` 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).
* pyenv-binary: reject invalid version names and drop readlink -f in save
A version is a single directory name under versions/, so refuse names with a
slash or a dot-dot component before building the prefix path.
Also emit the python paths directly instead of `readlink -f`, which BSD
readlink on macOS does not support; ldd/otool follow the symlinks anyway, so
the resolved paths were never needed.
* pyenv-binary: test the `save` subcommand
Cover argument validation, the version-name guard, and packaging an
installed version into an archive with matching metadata.
* pyenv-binary: test dependency parsing and tighten the version guard
Add tests that feed realistic ldd and otool listings through save and
check only the libraries resolving outside the prefix end up in the
metadata, since that filtering is the fiddliest part of the command.
Narrow the version guard to reject `.' and `..' rather than any name
containing a dot-dot, now that a slash is already refused, and note why
the find still matches *.so.* even though CPython does not produce them.
2026-07-03 11:38:54 +05:30
|
|
|
|
2026-07-16 10:14:40 +03:00
|
|
|
tar -C "$(dirname "$prefix")" -czf "${output_dir}/${archive}" "$(basename "$prefix")"
|
Add an experimental pyenv-binary plugin with a save command (#3487)
* pyenv-binary: add experimental plugin skeleton
Adds a new plugin, decoupled from `pyenv install`, for packaging and installing
relocatable Python binaries. This commit is just the command dispatcher; the
individual subcommands follow.
`pyenv binary <command>` dispatches to the matching script under the plugin's
libexec, so subcommands stay out of the top-level `pyenv commands` list.
Groundwork for the binary distribution support discussed in #2334.
* pyenv-binary: add the `save` subcommand
`pyenv binary save <version> [<output-dir>]` 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).
* pyenv-binary: reject invalid version names and drop readlink -f in save
A version is a single directory name under versions/, so refuse names with a
slash or a dot-dot component before building the prefix path.
Also emit the python paths directly instead of `readlink -f`, which BSD
readlink on macOS does not support; ldd/otool follow the symlinks anyway, so
the resolved paths were never needed.
* pyenv-binary: test the `save` subcommand
Cover argument validation, the version-name guard, and packaging an
installed version into an archive with matching metadata.
* pyenv-binary: test dependency parsing and tighten the version guard
Add tests that feed realistic ldd and otool listings through save and
check only the libraries resolving outside the prefix end up in the
metadata, since that filtering is the fiddliest part of the command.
Narrow the version guard to reject `.' and `..' rather than any name
containing a dot-dot, now that a slash is already refused, and note why
the find still matches *.so.* even though CPython does not produce them.
2026-07-03 11:38:54 +05:30
|
|
|
|
|
|
|
|
{
|
|
|
|
|
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}"
|