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
|
|
|
# pyenv-binary (experimental)
|
|
|
|
|
|
|
|
|
|
Package an installed Python version into a relocatable archive that can be
|
|
|
|
|
installed on another machine.
|
|
|
|
|
|
|
|
|
|
This is experimental and intentionally decoupled: it does not change
|
|
|
|
|
`pyenv install` or any other command. You drive it explicitly through
|
|
|
|
|
`pyenv binary`. Run `pyenv binary <command> --help` for details on a command.
|
|
|
|
|
|
|
|
|
|
## Portability
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-08-05 01:23:34 +05:30
|
|
|
### `pyenv binary package <version>[:<entry>] --archive-base-url <url>`
|
2026-07-28 17:01:20 +05:30
|
|
|
|
2026-08-05 01:23:34 +05:30
|
|
|
Installs `<version>` from source under a separate name, packages that install
|
|
|
|
|
with `save`, then emits a python-build definition for it with
|
|
|
|
|
`generate-installer`. With no explicit entry, the name is generated from the
|
|
|
|
|
current platform, platform version and architecture. An explicit entry keeps
|
|
|
|
|
the existing custom-build workflow.
|
2026-07-28 17:01:20 +05:30
|
|
|
|
|
|
|
|
```sh
|
2026-08-05 01:23:34 +05:30
|
|
|
pyenv binary package 3.12.7 \
|
2026-07-28 17:01:20 +05:30
|
|
|
--archive-base-url https://example.com/binaries
|
2026-08-05 01:23:34 +05:30
|
|
|
# On Debian 12 x86_64, writes 3.12.7-debian-12-x86_64.tar.gz,
|
|
|
|
|
# its .meta file and a `3.12.7-debian-12-x86_64' definition.
|
|
|
|
|
|
|
|
|
|
pyenv binary package 3.12.7:company-python \
|
|
|
|
|
--archive-base-url https://example.com/binaries
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
### `pyenv binary package-name <version>`
|
|
|
|
|
|
|
|
|
|
Prints the automatically generated entry name without building anything. Linux
|
|
|
|
|
uses the distribution name and version, macOS uses the macOS version, and other
|
|
|
|
|
systems use the name and release reported by `uname`. All names include the
|
|
|
|
|
architecture.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
pyenv binary package-name 3.12.7
|
|
|
|
|
# 3.12.7-debian-12-x86_64
|
2026-07-28 17:01:20 +05:30
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `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 `<version>-<platform>.tar.gz` (relative paths)
|
|
|
|
|
and writes `<version>-<platform>.meta` describing the build platform (OS, arch,
|
2026-07-28 17:01:20 +05:30
|
|
|
distro and libc version) and the system libraries the build links against. Use
|
|
|
|
|
`--name` to set a different base name for both files.
|
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
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
pyenv binary save 3.12.7 ./dist
|
|
|
|
|
```
|
2026-07-16 10:14:40 +03:00
|
|
|
|
|
|
|
|
### `pyenv binary generate-installer <metadata-file> --archive-url <url> [-o <output>]`
|
|
|
|
|
|
|
|
|
|
Reads a `.meta` file and emits a python-build definition. Drop it into
|
|
|
|
|
python-build's definition directory and `pyenv install <name>` 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 <prefix>`
|
|
|
|
|
|
|
|
|
|
Rewrites the rpaths of a Python tree unpacked into `<prefix>` 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.
|