Compare commits

..

60 commits

Author SHA1 Message Date
native-api ef4905cd81
Incorporate the pyenv-link plugin (#3538)
Some checks failed
pyenv_tests / pyenv_tests (ubuntu-22.04) (push) Failing after 1s
build / discover_build_logic_change (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-14) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-15) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-15-intel) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-26) (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-22.04-arm) (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-24.04) (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-24.04-arm) (push) Has been cancelled
pyenv_tests / pyenv_tests_docker (push) Has been cancelled
build / build (push) Has been cancelled
for use in linking binary installations
2026-09-09 02:43:22 +03:00
Ivan Pozdeev 88e9ab13e8 pwsh: Fix error in shell integration if pyenv is on PATH multiple times
Fixes "The term 'pyenv pyenv' is not recognized..."
2026-09-09 02:31:49 +03:00
macayu17 798a1ad603 Incorporate the pyenv-link plugin
* Adapt test_helper for use in plugins
* Add pyenv-link tests
* Add pyenv-link tests to CI
2026-09-09 02:30:53 +03:00
macayu17 fb7f2071fb Add 'plugins/pyenv-link/' from commit '2f8cb2cf1f093e955a72babf0b65a07f84f33522'
git-subtree-dir: plugins/pyenv-link
git-subtree-mainline: ef7a9f5d14
git-subtree-split: 2f8cb2cf1f
2026-09-08 02:55:49 +05:30
Michael Newman ef7a9f5d14
docs: Add link to list of available Python distributions (#3535)
Some checks failed
pyenv_tests / pyenv_tests (ubuntu-22.04) (push) Failing after 1s
pyenv_tests / pyenv_tests (ubuntu-22.04-arm) (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-24.04) (push) Has been cancelled
build / discover_build_logic_change (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-15) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-15-intel) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-26) (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-24.04-arm) (push) Has been cancelled
pyenv_tests / pyenv_tests_docker (push) Has been cancelled
build / build (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-14) (push) Has been cancelled
Add a link to the python-build share directory in the intro section
so users can see which Python distributions are supported before
installing pyenv.
2026-09-06 22:14:47 +03:00
Ayush 265b0dcefa
python-build: add build prerequisite installer (#3532)
Some checks failed
pyenv_tests / pyenv_tests (ubuntu-22.04) (push) Failing after 1s
pyenv_tests / pyenv_tests (ubuntu-24.04-arm) (push) Has been cancelled
pyenv_tests / pyenv_tests_docker (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-22.04-arm) (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-24.04) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-15-intel) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-26) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-14) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-15) (push) Has been cancelled
build / discover_build_logic_change (push) Has been cancelled
build / build (push) Has been cancelled
2026-09-02 17:56:35 +03:00
Michael Šimáček 2548d53706
Update graalpy URLs for 25.3.4.1 (#3533) 2026-09-02 14:47:33 +03:00
Ivan Pozdeev c293de3650 2.8.5
Some checks failed
build / discover_build_logic_change (push) Waiting to run
build / build (push) Blocked by required conditions
pyenv_tests / pyenv_tests (macos-14) (push) Waiting to run
pyenv_tests / pyenv_tests (macos-15) (push) Waiting to run
pyenv_tests / pyenv_tests (macos-15-intel) (push) Waiting to run
pyenv_tests / pyenv_tests (macos-26) (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-22.04-arm) (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-24.04) (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-24.04-arm) (push) Waiting to run
pyenv_tests / pyenv_tests_docker (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-22.04) (push) Failing after 1s
2026-09-01 18:05:40 +03:00
Ayush bdfc6b03f3
pyenv-binary: support relocating macOS archives (#3528) 2026-09-01 18:01:23 +03:00
pyenv-bot[bot] 8c46508bd0
Add CPython 3.15.0rc2 (#3530)
Co-authored-by: native-api <2670332+native-api@users.noreply.github.com>
2026-09-01 17:53:59 +03:00
Michael Šimáček a00e0bc475
Add GraalPy 25.3.4.1 (#3529)
Some checks failed
build / build (push) Blocked by required conditions
build / discover_build_logic_change (push) Waiting to run
pyenv_tests / pyenv_tests (macos-14) (push) Waiting to run
pyenv_tests / pyenv_tests (macos-15) (push) Waiting to run
pyenv_tests / pyenv_tests (macos-15-intel) (push) Waiting to run
pyenv_tests / pyenv_tests (macos-26) (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-22.04-arm) (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-24.04) (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-24.04-arm) (push) Waiting to run
pyenv_tests / pyenv_tests_docker (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-22.04) (push) Failing after 1s
2026-08-31 23:28:40 +03:00
Michael Newman 981559003f
Fix typo
Some checks failed
pyenv_tests / pyenv_tests (ubuntu-22.04) (push) Failing after 0s
pyenv_tests / pyenv_tests (macos-15) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-15-intel) (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-26) (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-22.04-arm) (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-24.04) (push) Has been cancelled
pyenv_tests / pyenv_tests (ubuntu-24.04-arm) (push) Has been cancelled
pyenv_tests / pyenv_tests_docker (push) Has been cancelled
build / discover_build_logic_change (push) Has been cancelled
pyenv_tests / pyenv_tests (macos-14) (push) Has been cancelled
build / build (push) Has been cancelled
2026-08-29 15:01:30 +03:00
Michael Newman 4733cca31b
Fix typo
Some checks failed
build / discover_build_logic_change (push) Waiting to run
build / build (push) Blocked by required conditions
pyenv_tests / pyenv_tests (ubuntu-22.04-arm) (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-24.04) (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-24.04-arm) (push) Waiting to run
pyenv_tests / pyenv_tests_docker (push) Waiting to run
pyenv_tests / pyenv_tests (macos-14) (push) Waiting to run
pyenv_tests / pyenv_tests (macos-15) (push) Waiting to run
pyenv_tests / pyenv_tests (macos-15-intel) (push) Waiting to run
pyenv_tests / pyenv_tests (macos-26) (push) Waiting to run
pyenv_tests / pyenv_tests (ubuntu-22.04) (push) Failing after 1s
2026-08-28 15:36:28 +03:00
native-api ef2fb22235
Merge pull request #3522 from sujeito-operator/ci-test-everywhere-on-build-logic-change
CI: test on all runners when the build logic changes
2026-08-26 16:05:18 +03:00
sujeito-operator 97900a4aa4 CI: test on all runners when the build logic changes
Co-Authored-By: Ivan Pozdeev <vano@mail.mipt.ru>
2026-08-26 10:09:25 +03:00
sujeito-operator ecf9a7f9e6 CI: collapse macos_build and ubuntu_build into a single job
Co-Authored-By: Ivan Pozdeev <vano@mail.mipt.ru>
2026-08-26 09:40:01 +03:00
native-api 495550afc8
Merge pull request #3525 from native-api/test_improvements
Test improvements
2026-08-26 06:00:53 +03:00
Ivan Pozdeev 57f1df294b tests: add file names to TAP output
by using a customized formatter
2026-08-26 05:51:24 +03:00
Ivan Pozdeev 14830b18bf tests: skip permission tests if permission bits are being overridden
e.g. if running as root in a docker container
2026-08-26 05:50:04 +03:00
Ivan Pozdeev 032e7af407 tests: consolidate shims tests 2026-08-26 05:49:17 +03:00
native-api b0cff001ff
tests: Fix "cd: null directory" in OpenSUSE (#3524)
A workaround because tests don't use the Pyenv launcher and setting PYENV_DIR to $PWD at every run invocation is too cumbersome.
2026-08-25 22:43:59 +03:00
Michael Newman 234a87e400
Fix typo 2026-08-17 01:51:28 +03:00
Ayush 50fbc8c6cc
pyenv-binary: record direct system dependencies in Linux/FreeBSD (#3520) 2026-08-15 22:47:27 +03:00
Ivan Pozdeev 0f16606e4f 2.8.4 2026-08-13 15:08:51 +03:00
native-api 7c2226386b
Add anaconda3-2026.07-1, miniconda3 26.5.3-2 2026-08-13 14:59:25 +03:00
Ivan Pozdeev 3a98f8c1c3 add_miniforge: better logging, skip 2 faulty releases 2026-08-13 14:16:14 +03:00
native-api f99638cb82
Merge pull request #3518 from pyenv/auto_add_version/3.10.21_3.11.16_3.12.14
Add CPython 3.10.21, 3.11.16, 3.12.14
2026-08-13 14:03:45 +03:00
Ivan Pozdeev 3b9604c093 Add missing 3.10.x patches 2026-08-13 13:10:43 +03:00
native-api 920052eb0b Add CPython 3.10.21, 3.11.16, 3.12.14 2026-08-13 08:53:14 +00:00
native-api cb20e375d3
Merge pull request #3517 from native-api/ci_ubuntu_arm
CI: add ubuntu ARM images
2026-08-13 01:51:46 +03:00
Ivan Pozdeev f11926a274 Run Docker tests separately 2026-08-13 01:44:17 +03:00
Ivan Pozdeev 0fc5dfe630 refactor 2026-08-13 01:44:17 +03:00
Ivan Pozdeev 2bcc7d8fc4 Add ubuntu ARM images 2026-08-13 01:44:11 +03:00
Ivan Pozdeev 2bd4f6c4da - rbenv compatibility note
We've not been syncing with Rbenv for years
2026-08-12 15:26:35 +03:00
Ivan Pozdeev 8771e721c1 Add past CVE-2026-68939 fix to CHANGELOG
Now that Github has issued a CVE number 3 weeks late
2026-08-12 15:19:48 +03:00
native-api d92b6e1bbe
Merge pull request #3516 from macayu17/fix/2334-package-feedback
pyenv-binary-package: check patchelf early and add --verbose
2026-08-11 05:25:39 +03:00
Ivan Pozdeev 9962f8cb39 pyenv-binary-package: refactor tests
* make them more human-readable
* don't double-test external code outside of integration tests
2026-08-11 05:17:49 +03:00
Ivan Pozdeev f9ffb73a7e test_helper: support here-document to assert_success/failure; + assert_output_glob 2026-08-11 05:17:42 +03:00
Ivan Pozdeev bd7c969e7f test_helper: show diff with whitespace highlights
to be able to see tiny and whitespace differences
2026-08-11 04:22:34 +03:00
macayu17 4a73bd17e2 pyenv-binary-package: add --verbose
Co-authored-by: Ivan Pozdeev <vano@mail.mipt.ru>
2026-08-11 04:22:33 +03:00
macayu17 2c1f405180 pyenv-binary: check patchelf before installing archive 2026-08-11 04:19:39 +03:00
native-api e234812f46
Merge pull request #3515 from fudianchn/fix/version-file-relative-loop-3513
Stop infinite loop in pyenv version-file for relative paths
2026-08-10 13:08:04 +03:00
Ivan Pozdeev 1a591b3963 Avoid extra iteration for argument with a trailing slash 2026-08-10 12:56:57 +03:00
Ivan Pozdeev 48e70e1fe9 Only test for UNC paths every iteration if they are special 2026-08-10 12:55:19 +03:00
付典 195b9fe940 version-file: Fix infinite loop for relative paths
find_local_version_file reduces its target with ${root%/*}, but that leaves a slash-less relative argument such as "." unchanged, leading to an infinite loop

Signed-off-by: 付典 <fudianchn@gmail.com>
2026-08-10 12:49:53 +03:00
native-api b7e7adf4ec
Merge pull request #3511 from macayu17/fix/2334-freebsd-ldconfig
pyenv-binary: check FreeBSD system libraries
2026-08-10 03:29:33 +03:00
macayu17 a6ef1e70ba Resolve version prefixes in package names 2026-08-10 03:27:47 +03:00
macayu17 5a31fea21b pyenv-binary: support checking FreeBSD system libraries
* Handle both spaced and unspaced FreeBSD `ldconfig -r` output
2026-08-10 03:27:46 +03:00
native-api 17be308600
Merge pull request #3514 from native-api/add_macos_26_intel
CI: Add macos_26_intel
2026-08-08 00:58:10 +03:00
Ivan Pozdeev 9d6cf587b7 Run the discover job on slim 2026-08-07 23:17:31 +03:00
Ivan Pozdeev 1cc9e770ae Fix regexes 2026-08-07 23:17:31 +03:00
Ivan Pozdeev 0e2e214f56 Fix ubuntu_build_tar_gz 2026-08-07 23:17:31 +03:00
Ivan Pozdeev 340348d98d - code irrelevant to CPython in CPython-specific scenarios 2026-08-07 23:17:30 +03:00
Ivan Pozdeev 2d9a224e6c Add macos-26-intel to CI
* Fix macos_build_bundled_dependencies in macos-26-intel
  It has a different set of preinstalled OpenSSL formulae
2026-08-07 23:16:23 +03:00
Mészáros Máté Róbert 2f8cb2cf1f
Handle osx specific ln option name (#1)
On OSX `ln` does not have `--symbolic` only `-s`.

* bin/pyenv-link-version

---------

Co-authored-by: yfprojects <62463991+real-yfprojects@users.noreply.github.com>
2023-07-07 14:18:02 +00:00
real-yfprojects b8d8dee191
Fix output
* bin/pyenv-link-version
2023-06-06 16:59:25 +02:00
real-yfprojects ba9099392a
Fix completions
* bin/pyenv-link
2023-06-06 16:56:16 +02:00
real-yfprojects d428be59ec
Write README.
* README.md
2023-06-06 16:45:22 +02:00
real-yfprojects dffc750fa2
Initial implementation
* bin/pyenv-link

* bin/pyenv-link-version
2023-06-06 16:22:52 +02:00
yfprojects f46b669f1d
Initial commit 2023-06-06 11:09:59 +00:00
68 changed files with 1757 additions and 295 deletions

View file

@ -3,9 +3,6 @@ Make sure you have checked all steps below.
### Prerequisite
* [ ] Please consider implementing the feature as a hook script or plugin as a first step.
* pyenv has some powerful support for plugins and hook scripts. Please refer to [Authoring plugins](https://github.com/pyenv/pyenv/wiki/Authoring-plugins) for details and try to implement it as a plugin if possible.
* [ ] Please consider contributing the patch upstream to [rbenv](https://github.com/rbenv/rbenv), since we have borrowed most of the code from that project.
* We occasionally import the changes from rbenv. In general, you can expect changes made in rbenv will be imported to pyenv too, eventually.
* Generally speaking, we prefer not to make changes in the core in order to keep compatibility with rbenv.
* [ ] My PR addresses the following pyenv issue (if any)
- Closes https://github.com/pyenv/pyenv/issues/XXXX

44
.github/actions/build-python/action.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: Build a Python version
description: >
Install python-build's system dependencies for the current runner's OS, build
and check the requested version.
inputs:
python-version:
description: A version as accepted by `pyenv install`.
required: true
runs:
using: composite
steps:
- shell: bash
run: |
#envvars
export PYENV_ROOT="$GITHUB_WORKSPACE"
echo "PYENV_ROOT=$PYENV_ROOT" >> $GITHUB_ENV
echo "$PYENV_ROOT/shims:$PYENV_ROOT/bin" >> $GITHUB_PATH
# The two OSes differ only in this step.
- if: runner.os == 'macOS'
shell: bash
run: |
#prerequisites
brew install openssl readline sqlite3 xz tcl-tk@8 libb2 zstd
- if: runner.os == 'Linux'
shell: bash
run: |
#prerequisites
pyenv install-prerequisites
- shell: bash
run: |
#build
pyenv --debug install ${{ inputs.python-version }} && rc=$? || rc=$?
if [[ $rc -ne 0 ]]; then echo config.log:; cat "${TMPDIR:-/tmp}"/python-build*/*/config.log; false; fi
pyenv global ${{ inputs.python-version }}
pyenv rehash
- shell: bash
run: |
#print version
python --version
python -m pip --version
- shell: python # Prove that actual Python == expected Python
env:
EXPECTED_PYTHON: ${{ inputs.python-version }}
run: import os, sys ; assert sys.version.startswith(os.getenv("EXPECTED_PYTHON"))

62
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: build
on:
push:
branches: [master]
pull_request: {}
permissions:
contents: read
jobs:
discover_build_logic_change:
if: github.event_name == 'pull_request'
runs-on: ubuntu-slim
env:
FULL_OS_MATRIX: '[
"macos-14",
"macos-15",
"macos-15-intel",
"macos-26",
"macos-26-intel",
"ubuntu-22.04",
"ubuntu-22.04-arm",
"ubuntu-24.04",
"ubuntu-24.04-arm"
]'
outputs:
os: ${{ steps.detect.outputs.os }}
steps:
- uses: actions/checkout@v7
- run: git fetch origin "$GITHUB_BASE_REF"
- id: detect
shell: bash
run: |
if [[ -n $(git diff --name-only "origin/$GITHUB_BASE_REF" -- \
plugins/python-build/bin/python-build \
.github/actions/build-python/action.yml \
.github/workflows/build.yml) ]]
then
printf "%s\n" "os<<!" "$FULL_OS_MATRIX" "!" >> "$GITHUB_OUTPUT"
fi
build:
needs: discover_build_logic_change
# Run when the detection job succeeded *or was skipped* -- but not failed
if: ${{ !cancelled() && !failure() }}
strategy:
fail-fast: false
matrix:
os: ${{ fromJson(needs.discover_build_logic_change.outputs.os || '["macos-latest","ubuntu-latest"]') }}
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/build-python
with:
python-version: ${{ matrix.python-version }}

View file

@ -1,43 +0,0 @@
name: macos_build
on:
push:
branches: [master]
pull_request: {}
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
macos_build:
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- run: |
brew install openssl readline sqlite3 xz tcl-tk@8 libb2 zstd
- run: |
#envvars
export PYENV_ROOT="$GITHUB_WORKSPACE"
echo "PYENV_ROOT=$PYENV_ROOT" >> $GITHUB_ENV
echo "$PYENV_ROOT/shims:$PYENV_ROOT/bin" >> $GITHUB_PATH
- run: |
#build
pyenv --debug install ${{ matrix.python-version }} && rc=$? || rc=$?
if [[ $rc -ne 0 ]]; then echo config.log:; cat $TMPDIR/python-build*/*/config.log; false; fi
pyenv global ${{ matrix.python-version }}
- run: |
#print version
python --version
python -m pip --version
- shell: python # Prove that actual Python == expected Python
env:
EXPECTED_PYTHON: ${{ matrix.python-version }}
run: import os, sys ; assert sys.version.startswith(os.getenv("EXPECTED_PYTHON"))

View file

@ -2,7 +2,7 @@ name: modified_scripts
on: [pull_request]
jobs:
discover_modified_scripts:
runs-on: ubuntu-latest
runs-on: ubuntu-slim
outputs:
versions: ${{steps.modified-versions.outputs.versions}}
versions_cpython_only: ${{steps.modified-versions.outputs.versions_cpython_only}}
@ -43,28 +43,32 @@ jobs:
import packaging.version
result=[]
def exclude_macos_intel(result, python_version):
result.append({'os':'macos-15-intel','python-version':python_version})
result.append({'os':'macos-26-intel','python-version':python_version})
for line in os.environ['versions'].splitlines():
if m:=re.match(r'([^-]+)-(\d+\.\d+)-(\d+\.\d+.\d+)', line):
name, version = m.group(1), packaging.version.Version(m.group(3))
if m:=re.match(r'miniconda3-\d+\.\d+-(\d+\.\d+\.\d+)', line):
version = packaging.version.Version(m.group(1))
# Miniconda dropped MacOS x64 support
if (name == 'miniconda3' and version >= packaging.version.Version('25.9.1')):
result.append({'os':'macos-15-intel','python-version':line})
if version >= packaging.version.Version('25.9.1'):
exclude_macos_intel(result, line)
if m:=re.match(r'([^-]+)-(\d+\.\d+)', line):
name, version = m.group(1), packaging.version.Version(m.group(2))
if m:=re.match(r'anaconda3-(\d+\.\d+)', line):
version = packaging.version.Version(m.group(1))
# Anaconda dropped MacOS x64 support
if name == 'anaconda3' and version >= packaging.version.Version('2025.12'):
result.append({'os':'macos-15-intel','python-version':line})
if version >= packaging.version.Version('2025.12'):
exclude_macos_intel(result, line)
if m:=re.match(r'graalpy[^-]*-(community-)?(\d+\.\d+.\d+)', line):
if m:=re.match(r'graalpy[^-]*-(community-)?(\d+\.\d+\.\d+)', line):
version = packaging.version.Version(m.group(2))
# GraalPy dropped MacOS x64 support
if version >= packaging.version.Version('25.0.2'):
result.append({'os':'macos-15-intel','python-version':line})
exclude_macos_intel(result, line)
EOF = str(random.getrandbits(15*8))
@ -91,6 +95,7 @@ jobs:
- macos-15
- macos-15-intel
- macos-26
- macos-26-intel
exclude: ${{fromJson(needs.discover_modified_scripts.outputs.versions_macos_build_exclude)}}
runs-on: ${{ matrix.os }}
steps:
@ -153,7 +158,12 @@ jobs:
fail-fast: false
matrix:
python-version: ${{fromJson(needs.discover_modified_scripts.outputs.versions_cpython_only)}}
os: ["macos-14", "macos-15", "macos-15-intel"]
os:
- macos-14
- macos-15
- macos-15-intel
- macos-26
- macos-26-intel
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
@ -165,12 +175,7 @@ jobs:
- run: |
#prerequisites
brew install sqlite3 xz tcl-tk@8 libb2 zstd
"$GITHUB_WORKSPACE/.github/workflows/scripts/brew-uninstall-cascade.sh" openssl@3 openssl@1.1 readline
if [[ "${{ matrix.python-version }}" =~ pypy.*-(src|dev) ]]; then
export PYENV_BOOTSTRAP_VERSION=pypy2.7-7
echo "PYENV_BOOTSTRAP_VERSION=$PYENV_BOOTSTRAP_VERSION" >> $GITHUB_ENV
pyenv install $PYENV_BOOTSTRAP_VERSION
fi
"$GITHUB_WORKSPACE/.github/workflows/scripts/brew-uninstall-cascade.sh" $(brew list | grep -E '^openssl(@|$)') readline
- run: |
#build
pyenv --debug install ${{ matrix.python-version }} && rc=$? || rc=$?
@ -202,7 +207,9 @@ jobs:
python-version: ${{fromJson(needs.discover_modified_scripts.outputs.versions)}}
os:
- ubuntu-22.04
- ubuntu-22.04-arm
- ubuntu-24.04
- ubuntu-24.04-arm
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
@ -213,10 +220,7 @@ jobs:
echo "$PYENV_ROOT/shims:$PYENV_ROOT/bin" >> $GITHUB_PATH
- run: |
#prerequisites
sudo apt-get update -q; sudo apt-get install -yq make build-essential \
libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \
curl llvm libncurses5-dev libncursesw5-dev \
xz-utils tk-dev libffi-dev liblzma-dev
pyenv install-prerequisites
if [[ "${{ matrix.python-version }}" =~ pypy.*-(src|dev) ]]; then
export PYENV_BOOTSTRAP_VERSION=pypy2.7-7
echo "PYENV_BOOTSTRAP_VERSION=$PYENV_BOOTSTRAP_VERSION" >> $GITHUB_ENV
@ -265,8 +269,7 @@ jobs:
fail-fast: false
matrix:
python-version: ${{fromJson(needs.discover_modified_scripts.outputs.versions_cpython_only)}}
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: |
@ -274,18 +277,10 @@ jobs:
export PYENV_ROOT="$GITHUB_WORKSPACE"
echo "PYENV_ROOT=$PYENV_ROOT" >> $GITHUB_ENV
echo "$PYENV_ROOT/shims:$PYENV_ROOT/bin" >> $GITHUB_PATH
echo "_PYTHON_BUILD_FORCE_SKIP_XZ=1" >> $GITHUB_PATH
echo "_PYTHON_BUILD_FORCE_SKIP_XZ=1" >> $GITHUB_ENV
- run: |
#prerequisites
sudo apt-get update -q; sudo apt-get install -yq make build-essential \
libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \
curl llvm libncurses5-dev libncursesw5-dev \
xz-utils tk-dev libffi-dev liblzma-dev
if [[ "${{ matrix.python-version }}" =~ pypy.*-(src|dev) ]]; then
export PYENV_BOOTSTRAP_VERSION=pypy2.7-7
echo "PYENV_BOOTSTRAP_VERSION=$PYENV_BOOTSTRAP_VERSION" >> $GITHUB_ENV
pyenv install $PYENV_BOOTSTRAP_VERSION
fi
pyenv install-prerequisites
- run: |
#build
pyenv --debug install ${{ matrix.python-version }} && rc=$? || rc=$?

View file

@ -14,7 +14,9 @@ jobs:
matrix:
os:
- ubuntu-22.04
- ubuntu-22.04-arm
- ubuntu-24.04
- ubuntu-24.04-arm
- macos-14
- macos-15
- macos-15-intel
@ -22,25 +24,22 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- run: |
- name: Install prerequisites
run: |
if test "$RUNNER_OS" == "macOS"; then
brew install coreutils fish
fi
- run: pwd
- env:
PYENV_ROOT: /home/runner/work/pyenv/pyenv
run: |
echo $PYENV_ROOT
echo "$PYENV_ROOT/shims:$PYENV_ROOT/bin" >> $GITHUB_PATH
- name: Run test on the host
- name: Run tests
run: |
make test
- name: Run test with docker
if: ${{ ! contains(matrix.os, 'macos') }}
run: |
make test-docker
- env:
PYENV_NATIVE_EXT: 1
run: |
(cd src; ./configure; make)
bats/bin/bats test/{pyenv,hooks,versions}.bats
pyenv_tests_docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: |
make test-docker

View file

@ -1,43 +0,0 @@
name: ubuntu_build
on:
push:
branches: [master]
pull_request: {}
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
ubuntu_build:
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: |
sudo apt-get update -q; sudo apt install -yq make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev curl \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
# https://github.com/pyenv/pyenv#installation
- run: pwd
- env:
PYENV_ROOT: /home/runner/work/pyenv/pyenv
run: |
echo $PYENV_ROOT
echo "$PYENV_ROOT/shims:$PYENV_ROOT/bin" >> $GITHUB_PATH
bin/pyenv --debug install ${{ matrix.python-version }}
bin/pyenv global ${{ matrix.python-version }}
bin/pyenv rehash
- run: python --version
- run: python -m pip --version
- shell: python # Prove that actual Python == expected Python
env:
EXPECTED_PYTHON: ${{ matrix.python-version }}
run: import os, sys ; assert sys.version.startswith(os.getenv("EXPECTED_PYTHON"))

View file

@ -1,5 +1,23 @@
# Version History
## Release v2.8.5
* pyenv-binary: record direct system dependencies in Linux/FreeBSD by @macayu17 in https://github.com/pyenv/pyenv/pull/3520
* tests: Fix "cd: null directory" in OpenSUSE by @native-api in https://github.com/pyenv/pyenv/pull/3524
* Test improvements by @native-api in https://github.com/pyenv/pyenv/pull/3525
* CI: test on all runners when the build logic changes by @sujeito-operator in https://github.com/pyenv/pyenv/pull/3522
* Add GraalPy 25.3.4.1 by @msimacek in https://github.com/pyenv/pyenv/pull/3529
* Add CPython 3.15.0rc2 by @pyenv-bot[bot] in https://github.com/pyenv/pyenv/pull/3530
* pyenv-binary: support relocating macOS archives by @macayu17 in https://github.com/pyenv/pyenv/pull/3528
## Release v2.8.4
* CI: Add macos_26_intel by @native-api in https://github.com/pyenv/pyenv/pull/3514
* pyenv-binary: check FreeBSD system libraries by @macayu17 in https://github.com/pyenv/pyenv/pull/3511
* version-file: Fix infinite loop for a relative path argument by @fudianchn in https://github.com/pyenv/pyenv/pull/3515
* pyenv-binary: check patchelf early and add --verbose by @macayu17 in https://github.com/pyenv/pyenv/pull/3516
* CI: add ubuntu ARM images by @native-api in https://github.com/pyenv/pyenv/pull/3517
* Add CPython 3.10.21, 3.11.16, 3.12.14 by @pyenv-bot[bot] in https://github.com/pyenv/pyenv/pull/3518
* Add anaconda3-2026.07-1, miniconda3 26.5.3-2 by @native-api in https://github.com/pyenv/pyenv/pull/3519
## Release v2.8.3
* Add CPython 3.13.15, 3.14.7 by @pyenv-bot[bot] in https://github.com/pyenv/pyenv/pull/3512
@ -16,6 +34,7 @@
* Add CPython 3.15.0b4 by @pyenv-bot[bot] in https://github.com/pyenv/pyenv/pull/3495
## Release v2.8.0
* CVE-2026-68939: Fix handling of glob characters in .python-version
* CI: add_version enhancements by @native-api in https://github.com/pyenv/pyenv/pull/3480
* Add miniforge3-26.3.2-2, 26.3.2-3 by @native-api in https://github.com/pyenv/pyenv/pull/3481
* rehash: fix race condition in landlock writability check by @Sheile in https://github.com/pyenv/pyenv/pull/3483

View file

@ -10,11 +10,14 @@ TEST_PYTHON_BUILD_DOCKER_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuf
TEST_BINARY_DOCKER_PREFIX = test-binary-docker
TEST_BINARY_DOCKER_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuffix -$(bash),$(TEST_BINARY_DOCKER_PREFIX)) $(addsuffix -gnu-$(bash),$(TEST_BINARY_DOCKER_PREFIX)))
TEST_LINK_DOCKER_PREFIX = test-link-docker
TEST_LINK_DOCKER_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuffix -$(bash),$(TEST_LINK_DOCKER_PREFIX)) $(addsuffix -gnu-$(bash),$(TEST_LINK_DOCKER_PREFIX)))
TEST_BATS_IMAGE_PREFIX = test-pyenv-docker-image
TEST_BATS_IMAGE_TARGETS = $(foreach bash,$(TEST_BASH_VERSIONS),$(addsuffix -$(bash),$(TEST_BATS_IMAGE_PREFIX)) $(addsuffix -gnu-$(bash),$(TEST_BATS_IMAGE_PREFIX)))
.PHONY: test-docker
test-docker: $(TEST_UNIT_DOCKER_PREFIX) $(TEST_PYTHON_BUILD_DOCKER_PREFIX) $(TEST_BINARY_DOCKER_PREFIX)
test-docker: $(TEST_UNIT_DOCKER_PREFIX) $(TEST_PYTHON_BUILD_DOCKER_PREFIX) $(TEST_BINARY_DOCKER_PREFIX) $(TEST_LINK_DOCKER_PREFIX)
.PHONY: $(TEST_UNIT_DOCKER_PREFIX)
$(TEST_UNIT_DOCKER_PREFIX): $(TEST_UNIT_DOCKER_TARGETS)
@ -62,7 +65,8 @@ $(TEST_PYTHON_BUILD_DOCKER_TARGETS): $(TEST_PYTHON_BUILD_DOCKER_PREFIX)-% : $(TE
$${CI+-e CI="$${CI}"} \
$(INTERACTIVE) \
$(DOCKER_IMAGE):$(DOCKER_TAG) \
bats $${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} plugins/python-build/test/$${BATS_FILE_FILTER}
bats $${CI:+-F "/code/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} plugins/python-build/test/$${BATS_FILE_FILTER}
.PHONY: $(TEST_BINARY_DOCKER_PREFIX)
$(TEST_BINARY_DOCKER_PREFIX): $(TEST_BINARY_DOCKER_TARGETS)
@ -84,7 +88,31 @@ $(TEST_BINARY_DOCKER_TARGETS): $(TEST_BINARY_DOCKER_PREFIX)-% : $(TEST_BATS_IMAG
$${CI+-e CI="$${CI}"} \
$(INTERACTIVE) \
$(DOCKER_IMAGE):$(DOCKER_TAG) \
bats $${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} plugins/pyenv-binary/test/$${BATS_FILE_FILTER}
bats $${CI:+-F "/code/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} plugins/pyenv-binary/test/$${BATS_FILE_FILTER}
.PHONY: $(TEST_LINK_DOCKER_PREFIX)
$(TEST_LINK_DOCKER_PREFIX): $(TEST_LINK_DOCKER_TARGETS)
.PHONY: $(TEST_LINK_DOCKER_TARGETS)
$(TEST_LINK_DOCKER_TARGETS): DOCKER_IMAGE = $(TEST_BATS_IMAGE_PREFIX)
$(TEST_LINK_DOCKER_TARGETS): GNU = $(if $(findstring -gnu-,$@),True,False)
$(TEST_LINK_DOCKER_TARGETS): BASH = $(filter $(TEST_BASH_VERSIONS),$(subst -, ,$@))
$(TEST_LINK_DOCKER_TARGETS): DOCKER_TAG = bash-$(BASH)-gnu-$(GNU)
$(TEST_LINK_DOCKER_TARGETS): INTERACTIVE = $(if $(findstring true,$(CI)),,-ti)
$(TEST_LINK_DOCKER_TARGETS): $(TEST_LINK_DOCKER_PREFIX)-% : $(TEST_BATS_IMAGE_PREFIX)-%
$(info Running test with docker image '$(DOCKER_IMAGE):$(DOCKER_TAG)')
docker run \
--init \
-v $(PWD):/code:ro \
-v /etc/passwd:/etc/passwd:ro \
-v /etc/group:/etc/group:ro \
-u "$$(id -u $$(whoami)):$$(id -g $$(whoami))" \
$${CI+-e CI="$${CI}"} \
$(INTERACTIVE) \
$(DOCKER_IMAGE):$(DOCKER_TAG) \
bats $${CI:+-F "/code/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} plugins/pyenv-link/test/$${BATS_FILE_FILTER}
# Build all images needed for bats under docker
.PHONY: $(TEST_BATS_IMAGE_PREFIX)
@ -108,22 +136,28 @@ $(TEST_BATS_IMAGE_TARGETS):
./ ; \
fi
.PHONY: test test-unit test-python-build test-binary
.PHONY: test test-unit test-python-build test-binary test-link
# Do not pass in user flags to build tests.
unexport PYTHON_CFLAGS
unexport PYTHON_CONFIGURE_OPTS
test: test-unit test-python-build test-binary
test: test-unit test-python-build test-binary test-link
test-unit: bats
PATH="./bats/bin:$$PATH" test/run
test-python-build: bats
cd plugins/python-build && $(PWD)/bats/bin/bats $${CI:+--tap} $${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} test/$${BATS_FILE_FILTER}
cd plugins/python-build && $(PWD)/bats/bin/bats $${CI:+-F "$(PWD)/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} test/$${BATS_FILE_FILTER}
test-binary: bats
cd plugins/pyenv-binary && $(PWD)/bats/bin/bats $${CI:+--tap} $${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} test/$${BATS_FILE_FILTER}
cd plugins/pyenv-binary && $(PWD)/bats/bin/bats $${CI:+-F "$(PWD)/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} test/$${BATS_FILE_FILTER}
test-link: bats
cd plugins/pyenv-link && $(PWD)/bats/bin/bats $${CI:+-F "$(PWD)/test/libexec/bats-format-tap-suite"} \
$${BATS_TEST_FILTER:+--filter "$${BATS_TEST_FILTER}"} test/$${BATS_FILE_FILTER}
.SECONDARY: bats-$(TEST_BATS_VERSION)
bats-$(TEST_BATS_VERSION):

View file

@ -13,6 +13,8 @@ This project was forked from [rbenv](https://github.com/rbenv/rbenv) and
* Lets you **change the global Python version** on a per-user basis.
* Provides support for **per-project Python versions**.
* Supports **multiple Python distributions**: CPython, PyPy, Stackless Python, Jython, and more.
See the [full list of available versions](https://github.com/pyenv/pyenv/tree/master/plugins/python-build/share/python-build).
* Allows you to **override the Python version** with an environment
variable.
* Searches for commands from **multiple versions of Python at a time**.

View file

@ -12,7 +12,7 @@
set -e
[ -n "$PYENV_DEBUG" ] && set -x
version="2.8.3"
version="2.8.5"
git_revision=""
if cd "${BASH_SOURCE%/*}" 2>/dev/null && git remote -v 2>/dev/null | grep -q pyenv; then

View file

@ -473,12 +473,12 @@ function pyenv {
}
if ( ("${commands[*]}" -split ' ') -contains \$command ) {
\$shell_cmds = (& (get-command -commandtype application pyenv) sh-\$command \$args)
\$shell_cmds = (& (get-command -commandtype application pyenv -totalcount 1) sh-\$command \$args)
if ( \$shell_cmds.Count -gt 0 ) {
iex (\$shell_cmds -join "\`n")
}
} else {
& (get-command -commandtype application pyenv) \$command \$args
& (get-command -commandtype application pyenv -totalcount 1) \$command \$args
}
}
EOS

View file

@ -8,12 +8,28 @@ target_dir="$1"
find_local_version_file() {
local root="$1"
while ! [[ "$root" =~ ^//[^/]*$ ]]; do
if [ -f "${root}/.python-version" ]; then
echo "${root}/.python-version"
# Nonexistent paths are UB as of this writing.
# Relative ones cause a failure but absolute ones don't
[[ $root != /* ]] && root=$(CDPATH= cd -- "$root" && pwd)
# Original Rbenv code supports UNC notation for Cygwin/MinGW
# (https://github.com/rbenv/rbenv/pull/529)
# POSIX.1-2024 still allows to treat //<name> in implementation-specific manner
# (https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap04.html#tag_04_16)
# even though few UNIX variants do that
local unc; [[ $root =~ ^//[^/] && ! / -ef // ]] && unc=1
root="${root%/}"
# when testing root, $root is ""
while true; do
# don't test //.python-version if // is special
# as it's pointless and possibly very slow
# if it e.g. leads to a network search
[[ $unc && $root == / ]] && break
if [[ -f $root/.python-version ]]; then
echo "$root/.python-version"
return 0
fi
[ -n "$root" ] || break
[[ -n $root ]] || break
root="${root%/*}"
done
return 1
@ -22,7 +38,9 @@ find_local_version_file() {
if [ -n "$target_dir" ]; then
find_local_version_file "$target_dir"
else
find_local_version_file "$PYENV_DIR" || {
# In tests, PYENV_DIR is not set, ultimately leading to "cd: null directory" in OpenSUSE
# While most `cd` implementations treat an empty argument the same as missing, that's still wrong
find_local_version_file "${PYENV_DIR:=$PWD}" || {
[ "$PYENV_DIR" != "$PWD" ] && find_local_version_file "$PWD"
} || echo "${PYENV_ROOT}/version"
fi

1
plugins/.gitignore vendored
View file

@ -3,4 +3,5 @@
!/version-ext-compat
!/python-build
!/pyenv-binary
!/pyenv-link
/python-build/test/build

View file

@ -16,7 +16,7 @@ and dependency metadata exist to catch that.
## Commands
### `pyenv binary package <version>[:<entry>] --archive-base-url <url>`
### `pyenv binary package [-v|--verbose] <version>[:<entry>] --archive-base-url <url>`
Installs `<version>` from source under a separate name, packages that install
with `save`, then emits a python-build definition for it with
@ -24,6 +24,8 @@ with `save`, then emits a python-build definition for it with
current platform, platform version and architecture. An explicit entry keeps
the existing custom-build workflow.
Pass `-v` to show build progress from `pyenv install`.
```sh
pyenv binary package 3.12.7 \
--archive-base-url https://example.com/binaries

View file

@ -14,7 +14,7 @@
# into the definition; `pyenv binary save' writes the two together.
#
# <metadata-file> A .meta file written by `pyenv binary save'.
# The corresponsing binary package written by `pyenv binary save'
# The corresponding binary package written by `pyenv binary save'
# needs to be present alongside it.
# --archive-url <url> The URL for the resulting installation script to download
# the package from.
@ -55,7 +55,7 @@ if [ -z "$metadata" ] || [ -z "$archive_url" ]; then
exit 1
fi
version="" os="" arch="" distro="" libc="" archive=""
version="" os="" arch="" distro="" libc="" build_prefix="" archive=""
deps=""
while IFS='=' read -r key value; do
case "$key" in
@ -64,6 +64,7 @@ while IFS='=' read -r key value; do
arch ) arch="$value" ;;
distro ) distro="$value" ;;
libc ) libc="$value" ;;
build_prefix ) build_prefix="$value" ;;
archive ) archive="$value" ;;
dep ) deps="${deps:+$deps }$value" ;;
esac
@ -78,10 +79,9 @@ for field in version os arch archive; do
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
# Mach-O load commands contain the prefix where Python was built.
if [ "$os" = "Darwin" ] && [ -z "$build_prefix" ]; then
echo "pyenv-binary: metadata is missing \`build_prefix'" >&2
exit 1
fi
@ -122,6 +122,7 @@ emit() {
printf 'EXPECT_OS=%q\n' "$os"
printf 'EXPECT_ARCH=%q\n' "$arch"
printf 'EXPECT_LIBC=%q\n' "$libc"
printf 'BUILD_PREFIX=%q\n' "$build_prefix"
printf 'VERSION=%q\n' "$version"
printf 'ARCHIVE_URL=%q\n' "$archive_url"
printf 'SHA256=%q\n' "$sha256"
@ -155,10 +156,23 @@ case "$EXPECT_LIBC" in
;;
esac
# Required system libraries must already be present on the target. ldconfig -p
# lists each library as "<soname> (...) => <path>"; 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
# Required system libraries must already be present on the target. Linux lists
# the soname first; FreeBSD lists it in the resolved path, so normalize that
# path before matching exact names.
cache=""
if command -v ldconfig &>/dev/null; then
case "$os" in
FreeBSD )
cache="$(ldconfig -r 2>/dev/null)" &&
cache="$(printf '%s\n' "$cache" | awk -F ' *=> *' '{ sub(/^.*\//, "", $2); print $2 }')" \
|| cache=""
;;
* )
cache="$(ldconfig -p 2>/dev/null)" || cache=""
;;
esac
fi
if [ -n "$cache" ]; then
missing=""
for dep in $DEPS; do
printf '%s\n' "$cache" | awk -v d="$dep" '$1 == d { found = 1 } END { exit !found }' \
@ -169,11 +183,32 @@ if command -v ldconfig &>/dev/null && cache="$(ldconfig -p)"; then
exit 1
fi
elif [ -n "$DEPS" ]; then
echo "pyenv-binary: cannot check required system libraries (ldconfig with -p not found)" >&2
echo "pyenv-binary: cannot check required system libraries (ldconfig cache unavailable)" >&2
fi
case "$os" in
Darwin )
for tool in otool install_name_tool; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "pyenv-binary: need ${tool} to relocate the binary" >&2
exit 1
fi
done
;;
* )
if ! command -v patchelf >/dev/null 2>&1; then
echo "pyenv-binary: need patchelf to relocate the binary" >&2
exit 1
fi
;;
esac
build_package_relocate() {
pyenv binary relocate "$PREFIX_PATH"
if [ "$os" = "Darwin" ]; then
pyenv binary relocate "$PREFIX_PATH" "$BUILD_PREFIX"
else
pyenv binary relocate "$PREFIX_PATH"
fi
}
install_package "Python-${VERSION}-binary" "${ARCHIVE_URL}#${SHA256}" copy relocate

View file

@ -2,7 +2,7 @@
#
# Summary: Create an installable binary package from a Python version
#
# Usage: pyenv binary package <version>[:<entry>] --archive-base-url <url>
# Usage: pyenv binary package [-v|--verbose] <version>[:<entry>] --archive-base-url <url>
#
# Installs <version> from source under a separate entry name, saves it as a
# binary package, then emits a python-build definition for that package.
@ -13,6 +13,7 @@
# <entry> The optional name to build under and install the
# binary as. If omitted, a name is generated from the
# current platform, platform version and architecture.
# -v,--verbose Show build progress from `pyenv install'.
# --archive-base-url <url>
# Where the archive will be hosted; the definition
# downloads it from <url>/<entry>.tar.gz.
@ -23,23 +24,29 @@ set -e
# Provide pyenv completions
if [ "$1" = "--complete" ]; then
echo --archive-base-url
echo --verbose
exec pyenv-install --list --bare
fi
spec=""
archive_base_url=""
verbose=""
while [ $# -gt 0 ]; do
case "$1" in
--archive-base-url )
[ $# -ge 2 ] || { echo "pyenv-binary: --archive-base-url needs a value" >&2; exit 1; }
archive_base_url="$2"; shift 2 ;;
[ $# -lt 2 ] && { echo "pyenv-binary: --archive-base-url needs a value" >&2; exit 1; }
archive_base_url="$2"; shift ;;
-v|--verbose)
verbose=1 ;;
-* )
echo "pyenv-binary: unknown option \`$1'" >&2; exit 1 ;;
* )
[ -z "$spec" ] || { echo "pyenv-binary: unexpected argument \`$1'" >&2; exit 1; }
spec="$1"; shift ;;
spec="$1"
;;
esac
shift
done
if [ -z "$spec" ] || [ -z "$archive_base_url" ]; then
@ -68,17 +75,8 @@ latest )
;;
esac
os="$(uname -s)"
# `generate-installer' refuses a macOS archive, so the last step here cannot
# succeed on one. Give up before compiling rather than after it.
if [ "$os" = "Darwin" ]; then
echo "pyenv-binary: macOS archives are not supported yet" >&2
exit 1
fi
# `pyenv install' puts a `<version>:<alias>' build under versions/<alias>.
pyenv-install "$spec"
pyenv-install ${verbose:+--verbose} "$spec"
pyenv-binary-save "$entry" "$PWD" --name "$entry"
pyenv-binary-generate-installer "${entry}.meta" \

View file

@ -28,6 +28,8 @@ case "$version" in
;;
esac
version="$(pyenv-latest -f -k "$version")"
os="$(uname -s)"
arch="$(uname -m)"
distro=""

View file

@ -1,12 +1,13 @@
#!/usr/bin/env bash
#
# Summary: Rewrite an unpacked Python's rpaths so it runs from its prefix
# Summary: Rewrite an unpacked Python's library paths so it runs from its prefix
#
# Usage: pyenv binary relocate <prefix>
# Usage: pyenv binary relocate <prefix> [<build-prefix>]
#
# Rewrites the rpaths of a Python tree that was unpacked into <prefix> 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.
# Rewrites the library paths of a Python tree that was unpacked into <prefix>
# 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 on
# Linux and FreeBSD, or otool and install_name_tool on macOS.
#
# The definition that `pyenv binary generate-installer' produces calls this
# after `install_package ... copy' has laid the tree down.
@ -24,6 +25,84 @@ if [ -z "$prefix" ]; then
exit 1
fi
if [ "$(uname -s)" = "Darwin" ]; then
build_prefix="${2%/}"
if [ -z "$build_prefix" ]; then
echo "pyenv-binary: need the original build prefix to relocate a macOS binary" >&2
exit 1
fi
for tool in otool install_name_tool; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "pyenv-binary: need ${tool} to relocate the binary" >&2
exit 1
fi
done
relocate_macho() {
local file="$1" new_rpath="$2"
local load_commands install_ids rpaths dependency install_id rpath replacement
load_commands="$(otool -L "$file")"
install_ids="$(otool -D "$file" 2>/dev/null || true)"
rpaths="$(otool -l "$file")"
install_id="$(printf '%s\n' "$install_ids" | sed -n '2s/^[[:space:]]*//p')"
while IFS= read -r dependency; do
case "$dependency" in
"$build_prefix"/lib/* )
if [ "$dependency" != "$install_id" ]; then
replacement="@rpath/${dependency#"$build_prefix"/lib/}"
install_name_tool -change "$dependency" "$replacement" "$file"
fi
;;
esac
done < <(printf '%s\n' "$load_commands" | tail -n +2 | sed -E \
's/^[[:space:]]*//; s/[[:space:]]+\(compatibility version.*$//')
case "$install_id" in
"$build_prefix"/lib/* )
replacement="@rpath/${install_id#"$build_prefix"/lib/}"
install_name_tool -id "$replacement" "$file"
;;
esac
while IFS= read -r rpath; do
if [ "$rpath" = "$build_prefix/lib" ]; then
install_name_tool -rpath "$rpath" "$new_rpath" "$file"
fi
done < <(printf '%s\n' "$rpaths" | awk '
/cmd LC_RPATH/ { found = 1; next }
found && /^[[:space:]]*path / {
sub(/^[[:space:]]*path /, "")
sub(/ \(offset [0-9]+\)$/, "")
print
found = 0
}
')
}
found=0
for file in "$prefix"/bin/*; do
[[ -f $file && -x $file && ! -L $file ]] || continue
otool -L "$file" >/dev/null 2>&1 || continue
relocate_macho "$file" '@executable_path/../lib'
found=1
done
if [ "$found" -eq 0 ]; then
echo "pyenv-binary: found no interpreter to relocate under \`${prefix}/bin'" >&2
exit 1
fi
for file in "$prefix"/lib/*.dylib; do
[[ -f $file && ! -L $file ]] || continue
relocate_macho "$file" '@loader_path'
done
while IFS= read -r so; do
relocate_macho "$so" '@loader_path/../..'
done < <(find "$prefix" -type f -path '*/lib/python*/lib-dynload/*.so')
exit
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

View file

@ -92,12 +92,16 @@ fi
if [ "$os" = "Linux" ]; then
libc="$(getconf GNU_LIBC_VERSION 2>/dev/null || true)"
fi
if [ "$os" != "Darwin" ] && ! LC_ALL=C readelf --version &>/dev/null; then
echo "pyenv-binary: need readelf to inspect shared libraries" >&2
exit 1
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
local f needed
{
for f in "${prefix}"/bin/python*; do
[ -e "$f" ] && printf '%s\n' "$f"
@ -111,8 +115,19 @@ system_deps() {
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 }'
# GNU binutils 2.44 writes "(NEEDED) ... [name]"; FreeBSD 15.1 writes
# "NEEDED ... [name]". readelf(1) documents -d, not its text format.
needed="$(LC_ALL=C readelf -dW "$f" 2>/dev/null | awk \
'$2 == "(NEEDED)" || $2 == "NEEDED" { sub(/^.*\[/, ""); sub(/\].*$/, ""); print }' | tr '\n' ' ')"
# Linux and FreeBSD ldd write resolved libraries as
# "name => /absolute/path (address)"; entries without "=>" are ignored.
LC_ALL=C ldd "$f" 2>/dev/null | awk -v needed="$needed" -v pfx="${prefix}/" '
BEGIN {
split(needed, deps, " ")
for (i in deps) direct[deps[i]] = 1
}
$1 in direct && $2 == "=>" && $3 ~ /^\// && substr($3, 1, length(pfx)) != pfx { print $1 }
'
fi
done | sort -u
}
@ -132,6 +147,7 @@ tar -C "$(dirname "$prefix")" -czf "${output_dir}/${archive}" "$(basename "$pref
echo "platform=${platform}"
[ -n "$distro" ] && echo "distro=${distro}"
[ -n "$libc" ] && echo "libc=${libc}"
echo "build_prefix=${prefix}"
echo "archive=${archive}"
system_deps | while IFS= read -r dep; do
[ -n "$dep" ] && echo "dep=${dep}"

View file

@ -6,6 +6,7 @@ create_meta() {
local os="${1-Linux}"
local arch="${2-x86_64}"
local libc="${3-glibc 2.17}"
local build_prefix="${4-}"
local archive="${BATS_TEST_TMPDIR}/3.12.7.tar.gz"
local meta="${BATS_TEST_TMPDIR}/sample.meta"
@ -20,6 +21,7 @@ create_meta() {
echo "os=${os}"
echo "arch=${arch}"
[ -z "$libc" ] || echo "libc=${libc}"
[ -z "$build_prefix" ] || echo "build_prefix=${build_prefix}"
echo "archive=${archive##*/}"
} > "$meta"
echo "$meta"
@ -58,10 +60,20 @@ create_meta() {
assert_failure
}
@test "refuses macOS metadata" {
@test "fails when macOS metadata is missing the build prefix" {
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"
assert_failure "pyenv-binary: metadata is missing \`build_prefix'"
}
@test "quotes the macOS build prefix in the generated definition" {
local out="${BATS_TEST_TMPDIR}/definition"
pyenv-binary-generate-installer \
"$(create_meta Darwin arm64 '' '/build prefix/$name')" \
--archive-url http://example.com/a.tar.gz -o "$out"
run grep '^BUILD_PREFIX=' "$out"
assert_success 'BUILD_PREFIX=/build\ prefix/\$name'
}
@test "fails when required metadata is missing" {
@ -93,6 +105,82 @@ create_meta() {
assert_failure "pyenv-binary: archive needs glibc 99.0 or newer, but this system has 2.31"
}
@test "checks required libraries in the FreeBSD ldconfig cache" {
local out="${BATS_TEST_TMPDIR}/definition"
local meta="$(create_meta FreeBSD amd64 '')"
printf 'dep=libc.so.7\ndep=libmissing.so.1\n' >> "$meta"
pyenv-binary-generate-installer "$meta" \
--archive-url http://example.com/a.tar.gz -o "$out"
create_stub uname 'case "$1" in -s) echo FreeBSD;; -m) echo amd64;; esac'
create_stub ldconfig '[ "$1" = "-r" ] && echo "0:-lc.7=>/lib/libc.so.7"'
run bash "$out"
assert_failure "pyenv-binary: missing required system libraries: libmissing.so.1"
}
@test "checks for patchelf before installing the archive" {
local out="${BATS_TEST_TMPDIR}/definition"
pyenv-binary-generate-installer "$(create_meta)" \
--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"'
PATH="$(path_without patchelf)" run bash "$out"
assert_failure "pyenv-binary: need patchelf to relocate the binary"
}
@test "checks for otool before installing a macOS archive" {
local out="${BATS_TEST_TMPDIR}/definition"
pyenv-binary-generate-installer \
"$(create_meta Darwin arm64 '' /build/3.12.7)" \
--archive-url http://example.com/a.tar.gz -o "$out"
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_path_executable install_name_tool true
PATH="$(path_without otool)" run bash "$out"
assert_failure "pyenv-binary: need otool to relocate the binary"
}
@test "checks for install_name_tool before installing a macOS archive" {
local out="${BATS_TEST_TMPDIR}/definition"
pyenv-binary-generate-installer \
"$(create_meta Darwin arm64 '' /build/3.12.7)" \
--archive-url http://example.com/a.tar.gz -o "$out"
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_path_executable otool true
PATH="$(path_without install_name_tool)" run bash "$out"
assert_failure "pyenv-binary: need install_name_tool to relocate the binary"
}
@test "the generated macOS definition preserves both relocation arguments" {
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 Darwin arm64 '' '/build prefix/$name')" \
--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 'printf "argc=%s\narg1=%s\narg2=%s\narg3=%s\narg4=%s\n" "$#" "$1" "$2" "$3" "$4"'
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_stub sw_vers 'echo 15.5'
create_stub ldconfig true
create_path_executable otool true
create_path_executable install_name_tool true
PYTHON_BUILD_CACHE_PATH="$cache" run \
"${BATS_TEST_DIRNAME}/../../python-build/bin/python-build" "$definition" "$prefix"
assert_success
assert_line "argc=4"
assert_line "arg1=binary"
assert_line "arg2=relocate"
assert_line "arg3=${prefix}"
assert_line 'arg4=/build prefix/$name'
assert_line "Installed Python-3.12.7-binary to ${prefix}"
}
@test "fails when the archive is not beside the metadata" {
local meta="$(create_meta)"
rm "${BATS_TEST_TMPDIR}/3.12.7.tar.gz"
@ -112,6 +200,7 @@ create_meta() {
mkdir -p "$cache"
cp "$archive" "${cache}/Python-3.12.7-binary.tar.gz"
create_stub pyenv 'echo "pyenv $*"'
create_path_executable patchelf "exit 0"
create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac'
PYTHON_BUILD_CACHE_PATH="$cache" run \

View file

@ -2,6 +2,10 @@
load test_helper
_setup() {
create_stub pyenv-latest '[ "$1" = "-f" ] && [ "$2" = "-k" ] && shift 2 && echo "$*"'
}
@test "completion lists installable versions" {
create_stub pyenv-install \
'[ "$*" = "--list --bare" ] && echo 3.13.14'
@ -40,12 +44,13 @@ load test_helper
assert_success "3.13.14-macos-15.5-arm64"
}
@test "generates a package name for FreeBSD" {
@test "resolves a version prefix when generating a package name" {
create_stub pyenv-latest '[ "$*" = "-f -k 3" ] && echo 3.14.7'
create_stub uname \
'case "$1" in -s) echo FreeBSD;; -m) echo amd64;; -r) echo 14.2-RELEASE-p3;; esac'
run pyenv-binary-package-name 3.13.14
assert_success "3.13.14-freebsd-14.2-release-p3-amd64"
run pyenv-binary-package-name 3
assert_success "3.14.7-freebsd-14.2-release-p3-amd64"
}
@test "rejects an invalid version name" {

View file

@ -6,21 +6,34 @@ load test_helper
# 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 pyenv-install 'echo "${0##*/} $*"; mkdir -p "${PYENV_ROOT}/versions/${1##*:}/bin"'
create_stub pyenv-latest 'while (($#)); do case "$1" in -f|-k);; *)break;; esac; shift; done; echo "$*"'
create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac'
create_stub getconf 'echo "glibc 2.17"'
create_stub readelf true
}
@test "completion lists the option and definitions provided by another plugin" {
mkdir -p "${PYENV_ROOT}/plugins/example/share/python-build"
touch "${PYENV_ROOT}/plugins/example/share/python-build/3.12.7-example"
PATH="${BATS_TEST_DIRNAME}/../../python-build/bin:${PATH}"
@test "-v|--verbose runs pyenv install verbosely" {
stub_build_environment
create_stub pyenv-binary-save true
create_stub pyenv-binary-generate-installer true
for opt in "" -v --verbose; do
run pyenv-binary-package $opt 3.12.7:3.12.7-test \
--archive-base-url http://example.com/binaries
assert_success "pyenv-install ${opt:+--verbose }3.12.7:3.12.7-test"
done
}
@test "completions" {
create_stub pyenv-install 'echo "${0##*/} $*"'
run pyenv-binary-package --complete
assert_success
assert_line "--archive-base-url"
assert_line "3.12.7-example"
refute_line "Available versions:"
assert_success <<!
--archive-base-url
--verbose
pyenv-install --list --bare
!
}
@test "fails with no arguments" {
@ -69,13 +82,19 @@ stub_build_environment() {
assert_failure "pyenv-binary: \`latest' cannot be used as an entry name"
}
@test "refuses to package on macOS before compiling anything" {
@test "packages on macOS" {
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_stub pyenv-install 'echo install'
create_stub pyenv-binary-save 'echo save'
create_stub pyenv-binary-generate-installer 'echo generate-installer'
run pyenv-binary-package 3.12.7:3.12.7-test --archive-base-url http://x/b
assert_failure "pyenv-binary: macOS archives are not supported yet"
assert_success "install
save
generate-installer"
}
@test "writes the archive, metadata and definition under the entry name" {
@test "writes the archive, metadata and definition under the entry name (integration)" {
stub_build_environment
cd "${BATS_TEST_TMPDIR}"
@ -89,13 +108,24 @@ stub_build_environment() {
assert_success "ARCHIVE_URL=http://example.com/binaries/3.12.7-test.tar.gz"
}
@test "strips a trailing slash from the archive base url" {
@test "correctly joins archive base url with a trailing slash" {
stub_build_environment
cd "${BATS_TEST_TMPDIR}"
create_stub pyenv-binary-save true
create_stub pyenv-binary-generate-installer <<'!'
echo -n "${0##*/} "
while (($#)); do
case $1 in
--archive-url)
echo "$1 ${2:?}"
break
;;
esac
shift
done
!
run pyenv-binary-package 3.12.7:3.12.7-test \
--archive-base-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.tar.gz"
assert_line "pyenv-binary-generate-installer --archive-url http://example.com/binaries/3.12.7-test.tar.gz"
}

View file

@ -4,6 +4,7 @@ load test_helper
_setup() {
create_stub pyenv-help "echo usage"
create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac'
}
stub_patchelf() {
@ -21,6 +22,87 @@ create_interpreter() {
chmod +x "${BATS_TEST_TMPDIR}/prefix/bin/python2.7"
}
create_macos_tree() {
local prefix="${BATS_TEST_TMPDIR}/prefix"
local lib="${prefix}/lib/python3.12"
mkdir -p "${prefix}/bin" "${lib}/lib-dynload" "${lib}/site-packages/numpy"
printf '#!/bin/sh\n' > "${prefix}/bin/python3.12"
chmod +x "${prefix}/bin/python3.12"
touch "${prefix}/lib/libpython3.12.dylib"
touch "${lib}/lib-dynload/_ssl.cpython-312-darwin.so"
touch "${lib}/site-packages/numpy/_multiarray.so"
}
stub_macos_tools() {
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_path_executable install_name_tool \
'echo "$*" >> "${BATS_TEST_TMPDIR}/install-name-tool.log"'
create_path_executable otool <<'STUB'
file="${!#}"
echo "$*" >> "${BATS_TEST_TMPDIR}/otool.log"
old="/build prefix/3.12.7"
case "$1" in
-L )
echo "${file}:"
case "$file" in
*/bin/python3.12 )
if [ -n "$OTOOL_RELOCATED" ]; then
echo " @rpath/libpython3.12.dylib (compatibility version 3.12.0, current version 3.12.0)"
else
echo " ${old}/lib/libpython3.12.dylib (compatibility version 3.12.0, current version 3.12.0)"
fi
echo " ${old}-other/lib/libother.dylib (compatibility version 1.0.0, current version 1.0.0)"
echo " /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1.0.0)"
echo " /opt/homebrew/lib/libintl.8.dylib (compatibility version 1.0.0, current version 1.0.0)"
echo " @loader_path/liblocal.dylib (compatibility version 1.0.0, current version 1.0.0)"
;;
*/lib/libpython3.12.dylib )
if [ -n "$OTOOL_RELOCATED" ]; then
echo " @rpath/libpython3.12.dylib (compatibility version 3.12.0, current version 3.12.0)"
else
echo " ${old}/lib/libpython3.12.dylib (compatibility version 3.12.0, current version 3.12.0)"
fi
;;
esac
;;
-D )
case "$file" in
*/lib/libpython3.12.dylib )
echo "${file}:"
if [ -n "$OTOOL_RELOCATED" ]; then
echo '@rpath/libpython3.12.dylib'
else
echo "${old}/lib/libpython3.12.dylib"
fi
;;
* )
exit 1
;;
esac
;;
-l )
if [ -n "$OTOOL_RELOCATED" ]; then
case "$file" in
*/bin/* ) rpath='@executable_path/../lib' ;;
*/lib/*.dylib ) rpath='@loader_path' ;;
* ) rpath='@loader_path/../..' ;;
esac
else
rpath="${old}/lib"
fi
cat <<EOF
cmd LC_RPATH
path ${rpath} (offset 12)
cmd LC_RPATH
path /opt/homebrew/lib (offset 12)
cmd LC_RPATH
path @loader_path/vendor (offset 12)
EOF
;;
esac
STUB
}
@test "completion produces nothing" {
run pyenv-binary-relocate --complete
assert_success ""
@ -36,6 +118,31 @@ create_interpreter() {
assert_failure "pyenv-binary: need patchelf to relocate the binary"
}
@test "fails without the original build prefix on macOS" {
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
run pyenv-binary-relocate "${BATS_TEST_TMPDIR}/prefix"
assert_failure "pyenv-binary: need the original build prefix to relocate a macOS binary"
}
@test "fails when otool is not available on macOS" {
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_path_executable install_name_tool true
PATH="$(path_without otool)" run \
pyenv-binary-relocate "${BATS_TEST_TMPDIR}/prefix" /build/3.12.7
assert_failure "pyenv-binary: need otool to relocate the binary"
}
@test "fails when install_name_tool is not available on macOS" {
create_stub uname 'case "$1" in -s) echo Darwin;; -m) echo arm64;; esac'
create_path_executable otool true
PATH="$(path_without install_name_tool)" run \
pyenv-binary-relocate "${BATS_TEST_TMPDIR}/prefix" /build/3.12.7
assert_failure "pyenv-binary: need install_name_tool to relocate the binary"
}
@test "fails when the prefix has no interpreter" {
create_path_executable patchelf "exit 0"
mkdir -p "${BATS_TEST_TMPDIR}/prefix"
@ -70,3 +177,45 @@ create_interpreter() {
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"
}
@test "relocates macOS load commands without changing unrelated entries" {
local prefix="${BATS_TEST_TMPDIR}/prefix"
local old="/build prefix/3.12.7"
local lib="${prefix}/lib/python3.12"
create_macos_tree
stub_macos_tools
run pyenv-binary-relocate "$prefix" "$old"
assert_success
run cat "${BATS_TEST_TMPDIR}/install-name-tool.log"
assert_output <<EOF
-change ${old}/lib/libpython3.12.dylib @rpath/libpython3.12.dylib ${prefix}/bin/python3.12
-rpath ${old}/lib @executable_path/../lib ${prefix}/bin/python3.12
-id @rpath/libpython3.12.dylib ${prefix}/lib/libpython3.12.dylib
-rpath ${old}/lib @loader_path ${prefix}/lib/libpython3.12.dylib
-rpath ${old}/lib @loader_path/../.. ${lib}/lib-dynload/_ssl.cpython-312-darwin.so
EOF
run grep -F "${lib}/site-packages/numpy/_multiarray.so" "${BATS_TEST_TMPDIR}/otool.log"
assert_failure
}
@test "skips already relocated macOS load commands" {
local prefix="${BATS_TEST_TMPDIR}/prefix"
create_macos_tree
stub_macos_tools
export OTOOL_RELOCATED=1
run pyenv-binary-relocate "$prefix" "/build prefix/3.12.7"
assert_success
assert [ ! -e "${BATS_TEST_TMPDIR}/install-name-tool.log" ]
}
@test "fails when install_name_tool cannot modify a selected file" {
local prefix="${BATS_TEST_TMPDIR}/prefix"
create_macos_tree
stub_macos_tools
create_path_executable install_name_tool 'exit 1'
run pyenv-binary-relocate "$prefix" "/build prefix/3.12.7"
assert_failure ""
}

View file

@ -79,7 +79,25 @@ platform() {
assert_line "archive=3.12.7-$(platform).tar.gz"
}
@test "records only the libraries ldd resolves outside the prefix" {
@test "records the original installation prefix" {
create_version "3.12.7"
local out="${BATS_TEST_TMPDIR}/dist"
pyenv-binary-save "3.12.7" "$out" >/dev/null
run grep '^build_prefix=' "${out}/3.12.7-$(platform).meta"
assert_success "build_prefix=${PYENV_ROOT}/versions/3.12.7"
}
@test "fails when readelf is not available" {
create_version "3.12.7"
create_stub uname 'case "$1" in -s) echo Linux;; -m) echo x86_64;; esac'
PATH="$(path_without readelf)" run pyenv-binary-save "3.12.7" "${BATS_TEST_TMPDIR}/dist"
assert_failure "pyenv-binary: need readelf to inspect shared libraries"
assert [ ! -e "${BATS_TEST_TMPDIR}/dist/3.12.7-$(platform).tar.gz" ]
}
@test "records only direct libraries resolved outside the prefix" {
create_version "3.12.7"
touch "${PYENV_ROOT}/versions/3.12.7/bin/python3.12"
create_path_executable uname <<'STUB'
@ -97,13 +115,18 @@ cat <<EOF
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4a3b800000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4a3c200000)
EOF
STUB
create_path_executable readelf <<'STUB'
cat <<EOF
0x0000000000000001 (NEEDED) Shared library: [libpython3.12.so.1.0]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
EOF
STUB
run pyenv-binary-save "3.12.7" "${BATS_TEST_TMPDIR}/dist"
assert_success
run grep '^dep=' "${BATS_TEST_TMPDIR}/dist/"*.meta
assert_output "dep=libc.so.6
dep=libm.so.6"
assert_output "dep=libm.so.6"
}
@test "records only the libraries otool resolves outside the prefix" {

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 yfprojects
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,33 @@
# pyenv-link
A [pyenv](https://github.com/pyenv/pyenv) plugin for linking Python installations and virtual environments into your pyenv root.
This plugin recommends the [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv/) plugin.
## Usage
Make an arbitrary virtualenv available through pyenv. This automatically guesses a fitting name from the prompt, the directory name or the location of the venv.
```console
$ pyenv link version .venv
Linked new version named myproject
```
You can also specify a name to use for the venv.
```console
$ pyenv link version .venv myname
Linked new version named myname
```
The same command can give a platform-specific binary installation a shorter name:
```console
$ pyenv link version "$(pyenv root)/versions/3.13.14-linux-x86_64" 3.13.14
Linked new version named 3.13.14
```
Now you can make pyenv activate/use the venv automatically:
```console
$ pyenv local myproject
```

View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
#
# Summary: Link a Python environment as a pyenv version
#
# Usage: pyenv link version [--dry] [--quiet] <path> [<name>]
#
# Link a virtual python environment to pyenvs version directory
# so that the venv can be used like one created with *pyenv-virtualenv*.
#
# <path> should be a path to a Python installation or virtual environment.
set -e
[ -n "$PYENV_DEBUG" ] && set -x
# Provide pyenv completions
case "$1" in
--complete)
if [ -z "$2" ]; then
echo version
else
shift 2
pyenv-link-version --complete "$@"
fi
;;
version)
shift
pyenv-link-version "$@"
exit $?
;;
*)
pyenv-help --usage link >&2
exit 1
;;
esac

View file

@ -0,0 +1,150 @@
#!/usr/bin/env bash
#
# Summary: Link a Python environment as a pyenv version
#
# Usage: pyenv link version [--dry] [--quiet] <path> [<name>]
#
# Link a virtual python environment to pyenvs version directory
# so that the venv can be used like one created with *pyenv-virtualenv*.
#
# <path> should be a path to a Python installation or virtual environment.
# <name> should be string used as a version name it may not be present
# in the pyenv version directory yet.
# If not specified a matching name is guessed from pyvenv.cfg
# or directory name of the venv.
set -e
[ -n "$PYENV_DEBUG" ] && set -x
# functions
if ! {
enable -f "${BASH_SOURCE%/*}"/../../../libexec/pyenv-realpath.dylib realpath ||
type realpath # realpath available?
} >/dev/null 2>&1; then
# realpath requires GNU coreutils to be installed. That's why its bundled with pyenv
echo realpath not available >&2
# work-around when realpath is unavailable
realpath() {
(cd "$1" && pwd)
}
fi
guess_name() {
local venv_dir=$1
local name
# guess venv name from pyvenv.cfg
local pyvenv_cfg_path=$venv_dir/pyvenv.cfg
if [ -f "$pyvenv_cfg_path" ]; then
# if `prompt` key wasn't found the var remains empty
name=$(cut -b 1-1024 "$pyvenv_cfg_path" | sed -n '/^ *prompt *= */s///p')
case "$name" in
\"*\" | \'*\')
name=${name:1:${#name}-2}
;;
esac
fi
# guess venv name from name of venv directory
local venv_dir_name=${venv_dir##*/}
if [ -z "$name" ]; then
case "$venv_dir_name" in
venv | env | .venv | .env | ENV | VENV) ;;
*) name=$venv_dir_name ;;
esac
fi
# guess venv name from name of the parent directory of the venv directory
local venv_dir_parent=${venv_dir%/*}
local venv_dir_parent_name=${venv_dir_parent##*/}
if [ -z "$name" ]; then
name=$venv_dir_parent_name
fi
echo "$name"
}
# process args
POSARGS=()
# Provide pyenv completions
if [ "$1" = "--complete" ]; then
echo --dry
echo --quiet
exit
fi
for arg in "$@"; do
case $arg in
--dry)
dry=true # any value
;;
--quiet)
quiet=true # any value
;;
-*)
pyenv-help --usage link-version >&2
exit 1
;;
*)
POSARGS+=("$arg")
;;
esac
done
if [ ! ${#POSARGS[@]} -le 2 ] || [ ! ${#POSARGS[@]} -ge 1 ]; then
pyenv-help --usage link-version >&2
exit 1
fi
venv_dir=${POSARGS[0]%/}
venv_name=${POSARGS[1]}
# Check venv exists
if [ ! -d "$venv_dir" ]; then
echo "The virtual env you specified doesn't exist"
exit 2
fi
# make venv dir absolute
venv_dir=$(realpath "$venv_dir")
# determine venv name
if [ -z "$venv_name" ]; then
venv_name=$(guess_name "$venv_dir")
fi
case "$venv_name" in
"" | */* | . | .. | *:* | system | *[[:cntrl:]]* )
echo "pyenv-link: invalid version name \`${venv_name}'" >&2
exit 1
;;
esac
versions_dir="$PYENV_ROOT/versions"
pyenv_prefix_path="$versions_dir/$venv_name"
if [ -e "$pyenv_prefix_path" ] || [ -L "$pyenv_prefix_path" ]; then
echo Version "$venv_name" already exists >&2
exit 3
fi
case "$venv_dir" in
"$versions_dir"/*)
link_target=${venv_dir#"$versions_dir"/}
;;
*)
link_target=$venv_dir
;;
esac
# link to pyenv version directory
if [ -z "$dry" ]; then
mkdir -p "$versions_dir"
ln -s "$link_target" "$pyenv_prefix_path"
fi
# output
if [ -z "$quiet" ]; then
echo "Linked new version named $venv_name"
fi

View file

@ -0,0 +1,61 @@
#!/usr/bin/env bats
load test_helper
@test "link completions use the dispatcher" {
run pyenv completions link
assert_success
assert_output <<OUT
--help
version
OUT
run pyenv completions link version
assert_success
assert_output <<OUT
--help
--dry
--quiet
OUT
}
@test "link requires a version path" {
run pyenv-link version
assert_failure "Usage: pyenv link version [--dry] [--quiet] <path> [<name>]"
}
@test "links an installed binary under its plain version name" {
create_alt_executable_in_version "3.13.14-linux-x86_64" python 'echo linked-python'
run pyenv-link version "$PYENV_ROOT/versions/3.13.14-linux-x86_64" 3.13.14
assert_success "Linked new version named 3.13.14"
assert_equal "3.13.14-linux-x86_64" "$(readlink "$PYENV_ROOT/versions/3.13.14")"
PYENV_VERSION=3.13.14 run pyenv exec python
assert_success "linked-python"
}
@test "links external paths containing spaces into a fresh root" {
mkdir -p "$PYENV_TEST_DIR/external env/bin"
run pyenv-link version "$PYENV_TEST_DIR/external env" custom
assert_success "Linked new version named custom"
assert_equal "$PYENV_TEST_DIR/external env" "$(readlink "$PYENV_ROOT/versions/custom")"
}
@test "refuses a dangling destination link" {
mkdir -p "$PYENV_ROOT/versions" "$PYENV_TEST_DIR/source"
ln -s missing "$PYENV_ROOT/versions/dangling"
run pyenv-link version "$PYENV_TEST_DIR/source" dangling
assert_failure "Version dangling already exists"
assert_equal missing "$(readlink "$PYENV_ROOT/versions/dangling")"
}
@test "rejects invalid version names" {
mkdir -p "$PYENV_TEST_DIR/source"
for name in ../outside . .. 'foo/bar' 'foo:bar' system; do
run pyenv-link version "$PYENV_TEST_DIR/source" "$name"
assert_failure "pyenv-link: invalid version name \`$name'"
assert [ ! -e "$PYENV_ROOT/outside" ]
done
echo "prompt = '../outside'" > "$PYENV_TEST_DIR/source/pyvenv.cfg"
run pyenv-link version "$PYENV_TEST_DIR/source"
assert_failure "pyenv-link: invalid version name \`../outside'"
assert [ ! -e "$PYENV_ROOT/outside" ]
}

View file

@ -0,0 +1 @@
../../../test/test_helper.bash

View file

@ -54,8 +54,11 @@ Or, if you would like to install the latest development release:
## Usage
Before you begin, you should ensure that your build environment has the proper
system dependencies for compiling the wanted Python Version (see our [recommendations](https://github.com/pyenv/pyenv/wiki#suggested-build-environment)).
On Ubuntu, Debian, and Mint, install the recommended build dependencies with:
pyenv install-prerequisites
For other environments, see our [build environment recommendations](https://github.com/pyenv/pyenv/wiki#suggested-build-environment).
### Using `pyenv install` with pyenv
@ -361,4 +364,3 @@ git diff --name-only master \
- Filter out any which don't live where python-build keeps its build scripts
- Look only at the file name (i.e. the python version name)
- Run a new docker container for each, building that version

View file

@ -0,0 +1,47 @@
#!/usr/bin/env bash
#
# Summary: Install Python build prerequisites on Debian-based systems
#
# Usage: pyenv install-prerequisites
#
set -e
[ -n "$PYENV_DEBUG" ] && set -x
# Provide pyenv completions
if [ "$1" = "--complete" ]; then
exit
fi
usage() {
pyenv-help install-prerequisites 2>/dev/null
[ -z "$1" ] || exit "$1"
}
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
usage 0
fi
[ "$#" -eq 0 ] || usage 1 >&2
if ! command -v apt-get >/dev/null; then
echo "pyenv: installing build prerequisites is not supported on this system" >&2
exit 1
fi
prerequisites=(
make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev
libsqlite3-dev curl git llvm libncurses5-dev libncursesw5-dev xz-utils
tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev libzstd-dev
)
if [ "$(id -u)" -eq 0 ]; then
apt_get=(apt-get)
elif command -v sudo >/dev/null; then
apt_get=(sudo apt-get)
else
echo "pyenv: sudo is required to install build prerequisites" >&2
exit 1
fi
"${apt_get[@]}" update -q
"${apt_get[@]}" install -yq "${prerequisites[@]}"

View file

@ -14,7 +14,7 @@
# -g/--debug Build a debug version
#
PYTHON_BUILD_VERSION="2.8.3"
PYTHON_BUILD_VERSION="2.8.5"
OLDIFS="$IFS"

View file

@ -219,7 +219,10 @@ class CondaVersion(NamedTuple):
# since 4.8, Miniconda specifies versions explicitly in the file name
raise ValueError("Miniconda 4.8+ is supposed to specify a Python version explicitly")
if self.flavor == "anaconda":
# https://www.anaconda.com/docs/tools/anaconda-org/release-notes
# Info about the bundled Python version
# https://www.anaconda.com/docs/getting-started/anaconda/release-notes
if v >= (2026,7):
return PyVersion.PY314
if v >= (2025,6):
return PyVersion.PY313
if v >= (2024,6):

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python3
'Adds the latest miniforge and mambaforge releases.'
from pathlib import Path
import argparse
import logging
import os
import string
@ -8,7 +9,7 @@ import string
import requests
logger = logging.getLogger(__name__)
logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO'))
logging.basicConfig(level=logging.INFO)
MINIFORGE_REPO = 'conda-forge/miniforge'
DISTRIBUTIONS = ['miniforge']
@ -21,6 +22,8 @@ SKIPPED_RELEASES = [
'22.11.1-2', #MacOS packages are broken (have broken dep tarballs, downloading them fails with 403)
'25.3.0-0', #marked as prerelease, no Linux version
'25.11.0-0', #regression reported in constructor, re-released as 25.11.0-1 with hotfix bumping to constructor>=3.14 (was >=3.12, 3.13 implicit)
'26.1.1-0', #prerelease, no binary assets
'26.1.1-1', #prerelease, no binary assets
]
install_script_fmt = """
@ -136,7 +139,7 @@ def main():
if version in SKIPPED_RELEASES:
continue
logger.info(f'Looking for {version} in {out_dir}')
logger.debug(f'Looking for {version} in {out_dir}')
# mambaforge is retired https://github.com/conda-forge/miniforge/releases/tag/24.11.2-0
if version_tuple(version) >= (24, 11, 2):
@ -149,4 +152,17 @@ def main():
add_version(release, distributions)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"-d", "--dry-run", action="store_true",
help="Do not write scripts, just report them to stdout",
)
parser.add_argument(
"-v", "--verbose", action="store_true", default=0,
help="Increase verbosity of logging",
)
parsed = parser.parse_args()
if parsed.verbose: logging.getLogger.setLevel(logging.DEBUG)
main()

View file

@ -0,0 +1,9 @@
prefer_openssl3
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
install_package "openssl-3.6.3" "https://github.com/openssl/openssl/releases/download/openssl-3.6.3/openssl-3.6.3.tar.gz#243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.3" "https://ftpmirror.gnu.org/readline/readline-8.3.tar.gz#fe5383204467828cd495ee8d1d3c037a7eba1389c22bc6a041f627976f9061cc" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
install_package "Python-3.10.21" "https://www.python.org/ftp/python/3.10.21/Python-3.10.21.tar.xz#a0da1e72132e950154eca0f6f47d5db828454700de20e5113667940d81e0db04" standard verify_py310 copy_python_gdb ensurepip
else
install_package "Python-3.10.21" "https://www.python.org/ftp/python/3.10.21/Python-3.10.21.tgz#f276987f06270ae6c1fb4da620bd105edf78c31368c2f7e85e6c1d51c560b04b" standard verify_py310 copy_python_gdb ensurepip
fi

View file

@ -0,0 +1,11 @@
prefer_openssl3
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL_RPATH=1
export PYTHON_BUILD_TCLTK_USE_PKGCONFIG=1
install_package "openssl-3.6.3" "https://github.com/openssl/openssl/releases/download/openssl-3.6.3/openssl-3.6.3.tar.gz#243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.3" "https://ftpmirror.gnu.org/readline/readline-8.3.tar.gz#fe5383204467828cd495ee8d1d3c037a7eba1389c22bc6a041f627976f9061cc" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
install_package "Python-3.11.16" "https://www.python.org/ftp/python/3.11.16/Python-3.11.16.tar.xz#91bcdebfdde239a003ae93738a7fce0f9230fee5c4bc2b86f6e6e8c6f98aabe8" standard verify_py311 copy_python_gdb ensurepip
else
install_package "Python-3.11.16" "https://www.python.org/ftp/python/3.11.16/Python-3.11.16.tgz#6c0bd76ab0ec7d94ed400b1497f01ac6c7751c8822615ee0855a3eb2d893ea76" standard verify_py311 copy_python_gdb ensurepip
fi

View file

@ -0,0 +1,11 @@
prefer_openssl3
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL_RPATH=1
export PYTHON_BUILD_TCLTK_USE_PKGCONFIG=1
install_package "openssl-3.6.3" "https://github.com/openssl/openssl/releases/download/openssl-3.6.3/openssl-3.6.3.tar.gz#243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.3" "https://ftpmirror.gnu.org/readline/readline-8.3.tar.gz#fe5383204467828cd495ee8d1d3c037a7eba1389c22bc6a041f627976f9061cc" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
install_package "Python-3.12.14" "https://www.python.org/ftp/python/3.12.14/Python-3.12.14.tar.xz#5c8462af5790baf43a321a1559dbe0db06d1be4300fb85fb53c40060668e548a" standard verify_py312 copy_python_gdb ensurepip
else
install_package "Python-3.12.14" "https://www.python.org/ftp/python/3.12.14/Python-3.12.14.tgz#6c6df908d2c3fd24e6d76869e92542abd0f33aec9dfc18df8875f89660286d43" standard verify_py312 copy_python_gdb ensurepip
fi

View file

@ -1,11 +0,0 @@
prefer_openssl3
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL_RPATH=1
export PYTHON_BUILD_TCLTK_USE_PKGCONFIG=1
install_package "openssl-4.0.1" "https://github.com/openssl/openssl/releases/download/openssl-4.0.1/openssl-4.0.1.tar.gz#2db3f3a0d6ea4b59e1f094ace2c8cd536dffb87cdc39084c5afa1e6f7f37dd09" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.3" "https://ftpmirror.gnu.org/readline/readline-8.3.tar.gz#fe5383204467828cd495ee8d1d3c037a7eba1389c22bc6a041f627976f9061cc" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
install_package "Python-3.15.0rc1" "https://www.python.org/ftp/python/3.15.0/Python-3.15.0rc1.tar.xz#f84dad680ab2147417d2739355c2678f0f9acffe4ae8ef77895de1454b384b07" standard verify_py315 copy_python_gdb ensurepip
else
install_package "Python-3.15.0rc1" "https://www.python.org/ftp/python/3.15.0/Python-3.15.0rc1.tgz#32c2b9878bbd6ac3ceb138c5b6c880345ce9d33388b399378c0da6fc7cb3bfbc" standard verify_py315 copy_python_gdb ensurepip
fi

View file

@ -0,0 +1,11 @@
prefer_openssl3
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL_RPATH=1
export PYTHON_BUILD_TCLTK_USE_PKGCONFIG=1
install_package "openssl-4.0.2" "https://github.com/openssl/openssl/releases/download/openssl-4.0.2/openssl-4.0.2.tar.gz#736b467530f916737b7031310ccb21d8218c6229e61e8e160cd1d3458cd543a8" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.3" "https://ftpmirror.gnu.org/readline/readline-8.3.tar.gz#fe5383204467828cd495ee8d1d3c037a7eba1389c22bc6a041f627976f9061cc" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
install_package "Python-3.15.0rc2" "https://www.python.org/ftp/python/3.15.0/Python-3.15.0rc2.tar.xz#8d93af5eaaaea5adfd41bd786a7ba3f03f2ad1ab57c6a65e0b963deab91d5ad7" standard verify_py315 copy_python_gdb ensurepip
else
install_package "Python-3.15.0rc2" "https://www.python.org/ftp/python/3.15.0/Python-3.15.0rc2.tgz#164dbb80f5e1fdd8768387724bfa014da1000401b2015d872a2396c8b858b829" standard verify_py315 copy_python_gdb ensurepip
fi

View file

@ -0,0 +1,19 @@
case "$(anaconda_architecture 2>/dev/null || true)" in
"Linux-aarch64" )
install_script "Anaconda3-2026.07-1-Linux-aarch64" "https://repo.anaconda.com/archive/Anaconda3-2026.07-1-Linux-aarch64.sh#af2fd0025728a04408e0481d5668b92c53589cbfc66525a15119e6503742a04d" "anaconda" verify_py314
;;
"Linux-x86_64" )
install_script "Anaconda3-2026.07-1-Linux-x86_64" "https://repo.anaconda.com/archive/Anaconda3-2026.07-1-Linux-x86_64.sh#9f49042048e9a8220c1cddf9192054290724052d3b2a86f938f9f5edd911db8f" "anaconda" verify_py314
;;
"MacOSX-arm64" )
install_script "Anaconda3-2026.07-1-MacOSX-arm64" "https://repo.anaconda.com/archive/Anaconda3-2026.07-1-MacOSX-arm64.sh#6a9b63c645db4dbe293ffe9f168f7e380fe7a041fc863e67ee84db027425bce2" "anaconda" verify_py314
;;
* )
{ echo
colorize 1 "ERROR"
echo ": The binary distribution of Anaconda is not available for $(anaconda_architecture 2>/dev/null || true)."
echo
} >&2
exit 1
;;
esac

View file

@ -0,0 +1,50 @@
# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
VERSION='25.3.4.1'
RELEASE_TAG='graal-25.3.4'
colorize 1 "GraalPy 23.1 and later installed by python-build use the faster Oracle GraalVM distribution" && echo
colorize 1 "Oracle GraalVM uses the GFTC license, which is free for development and production use, see https://medium.com/graalvm/161527df3d76" && echo
colorize 1 "The GraalVM Community Edition variant of GraalPy is also available, under the name graalpy3.13-community-${VERSION}" && echo
graalpy_arch="$(graalpy_architecture 2>/dev/null || true)"
case "$graalpy_arch" in
"linux-amd64" )
checksum="23515b4e659d645960517b0ceedb90faa84fcb01f0f4497beca5868a1b1ae3ea"
;;
"linux-aarch64" )
checksum="53b80e284e444540ae7cb67349af3dd8f6c1848fbf6abb5051742ac745f1f33d"
;;
"macos-aarch64" )
checksum="8ac33b5a5d0ee872751595211508fd826f47051ff6e6614032aed1de071eebab"
;;
* )
{ echo
colorize 1 "ERROR"
echo ": No binary distribution of GraalPy is available for $(uname -sm)."
echo
} >&2
exit 1
;;
esac
install_package "graalpy3.13-${VERSION}" "https://github.com/oracle/graalpython/releases/download/${RELEASE_TAG}/graalpy3.13-${VERSION}-${graalpy_arch}.tar.gz#${checksum}" "copy" ensurepip

View file

@ -0,0 +1,45 @@
# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
VERSION='25.3.4.1'
RELEASE_TAG='graal-25.3.4'
graalpy_arch="$(graalpy_architecture 2>/dev/null || true)"
case "$graalpy_arch" in
"linux-amd64" )
checksum="5e239616c8352488585f58d33991d262e8c627483741778ad43d39a4830176ad"
;;
"linux-aarch64" )
checksum="13007ecfe8180e91f1060de60a2ba6ce88f38faa4a36574b34143a492e8c660e"
;;
"macos-aarch64" )
checksum="e2cf7ae230478ad8d100ad868f75e3addb174d81e7f550322ea5e74cab066644"
;;
* )
{ echo
colorize 1 "ERROR"
echo ": No binary distribution of GraalPy is available for $(uname -sm)."
echo
} >&2
exit 1
;;
esac
install_package "graalpy3.13-community-${VERSION}" "https://github.com/oracle/graalpython/releases/download/${RELEASE_TAG}/graalpy3.13-community-${VERSION}-${graalpy_arch}.tar.gz#${checksum}" "copy" ensurepip

View file

@ -0,0 +1,20 @@
export CONDA_PLUGINS_AUTO_ACCEPT_TOS=true
case "$(anaconda_architecture 2>/dev/null || true)" in
"Linux-aarch64" )
install_script "Miniconda3-py310_26.5.3-2-Linux-aarch64" "https://repo.anaconda.com/miniconda/Miniconda3-py310_26.5.3-2-Linux-aarch64.sh#69b30d25b1308596e39819d2eeb8d669fec8b35205254d4d38975e9a6eb525e5" "miniconda" verify_py310
;;
"Linux-x86_64" )
install_script "Miniconda3-py310_26.5.3-2-Linux-x86_64" "https://repo.anaconda.com/miniconda/Miniconda3-py310_26.5.3-2-Linux-x86_64.sh#83e53ab0246ca320db28734bfd16f9bba895d96ae3e0ccf6f9b65e1383ab6a26" "miniconda" verify_py310
;;
"MacOSX-arm64" )
install_script "Miniconda3-py310_26.5.3-2-MacOSX-arm64" "https://repo.anaconda.com/miniconda/Miniconda3-py310_26.5.3-2-MacOSX-arm64.sh#fc25d37469e162e3073093c1e02a0a21a59681af10a097f73df3f303b89b050a" "miniconda" verify_py310
;;
* )
{ echo
colorize 1 "ERROR"
echo ": The binary distribution of Miniconda is not available for $(anaconda_architecture 2>/dev/null || true)."
echo
} >&2
exit 1
;;
esac

View file

@ -0,0 +1,20 @@
export CONDA_PLUGINS_AUTO_ACCEPT_TOS=true
case "$(anaconda_architecture 2>/dev/null || true)" in
"Linux-aarch64" )
install_script "Miniconda3-py311_26.5.3-2-Linux-aarch64" "https://repo.anaconda.com/miniconda/Miniconda3-py311_26.5.3-2-Linux-aarch64.sh#e50d1c902e94b6f64a19e0a3fbc35f8cadc09c5a1cf5b0fe10da14e4ce7ff24d" "miniconda" verify_py311
;;
"Linux-x86_64" )
install_script "Miniconda3-py311_26.5.3-2-Linux-x86_64" "https://repo.anaconda.com/miniconda/Miniconda3-py311_26.5.3-2-Linux-x86_64.sh#cdca3dd8440759bb87c60b227e26946263fe2856b30b2bcfdd964c38254fb8eb" "miniconda" verify_py311
;;
"MacOSX-arm64" )
install_script "Miniconda3-py311_26.5.3-2-MacOSX-arm64" "https://repo.anaconda.com/miniconda/Miniconda3-py311_26.5.3-2-MacOSX-arm64.sh#129bee9cb8e23d0eed175439a8b41c9844bac5c75f3f2aefa0be7104b403706f" "miniconda" verify_py311
;;
* )
{ echo
colorize 1 "ERROR"
echo ": The binary distribution of Miniconda is not available for $(anaconda_architecture 2>/dev/null || true)."
echo
} >&2
exit 1
;;
esac

View file

@ -0,0 +1,20 @@
export CONDA_PLUGINS_AUTO_ACCEPT_TOS=true
case "$(anaconda_architecture 2>/dev/null || true)" in
"Linux-aarch64" )
install_script "Miniconda3-py312_26.5.3-2-Linux-aarch64" "https://repo.anaconda.com/miniconda/Miniconda3-py312_26.5.3-2-Linux-aarch64.sh#822923b30d789477964ef5589f86de5a72ac4c584bad3b9f72d97190e13d776f" "miniconda" verify_py312
;;
"Linux-x86_64" )
install_script "Miniconda3-py312_26.5.3-2-Linux-x86_64" "https://repo.anaconda.com/miniconda/Miniconda3-py312_26.5.3-2-Linux-x86_64.sh#37606f9f03ced8ef60f4ffc76b21dda01728eac8a632dcab316c891cea4fe2f5" "miniconda" verify_py312
;;
"MacOSX-arm64" )
install_script "Miniconda3-py312_26.5.3-2-MacOSX-arm64" "https://repo.anaconda.com/miniconda/Miniconda3-py312_26.5.3-2-MacOSX-arm64.sh#ba7e4dede827e98c468c2dfa3b9a859728844ee8bde72a28eb216869af2d6ce1" "miniconda" verify_py312
;;
* )
{ echo
colorize 1 "ERROR"
echo ": The binary distribution of Miniconda is not available for $(anaconda_architecture 2>/dev/null || true)."
echo
} >&2
exit 1
;;
esac

View file

@ -0,0 +1,20 @@
export CONDA_PLUGINS_AUTO_ACCEPT_TOS=true
case "$(anaconda_architecture 2>/dev/null || true)" in
"Linux-aarch64" )
install_script "Miniconda3-py313_26.5.3-2-Linux-aarch64" "https://repo.anaconda.com/miniconda/Miniconda3-py313_26.5.3-2-Linux-aarch64.sh#549176858e194a15df5c8326b9f4e1b7ddf1464df47aec4115d0e6d14204e8bb" "miniconda" verify_py313
;;
"Linux-x86_64" )
install_script "Miniconda3-py313_26.5.3-2-Linux-x86_64" "https://repo.anaconda.com/miniconda/Miniconda3-py313_26.5.3-2-Linux-x86_64.sh#66f7c434bbdc7a4c5687b7e56cde724f73954d1322ffb273c6e387f12fbcdc03" "miniconda" verify_py313
;;
"MacOSX-arm64" )
install_script "Miniconda3-py313_26.5.3-2-MacOSX-arm64" "https://repo.anaconda.com/miniconda/Miniconda3-py313_26.5.3-2-MacOSX-arm64.sh#0d4d44f5d0740dddc68d381bbc8eb8c056d4792c832a9f0e3b5c663f7dda9d67" "miniconda" verify_py313
;;
* )
{ echo
colorize 1 "ERROR"
echo ": The binary distribution of Miniconda is not available for $(anaconda_architecture 2>/dev/null || true)."
echo
} >&2
exit 1
;;
esac

View file

@ -0,0 +1,20 @@
export CONDA_PLUGINS_AUTO_ACCEPT_TOS=true
case "$(anaconda_architecture 2>/dev/null || true)" in
"Linux-aarch64" )
install_script "Miniconda3-py314_26.5.3-2-Linux-aarch64" "https://repo.anaconda.com/miniconda/Miniconda3-py314_26.5.3-2-Linux-aarch64.sh#999e8761f4dc74fb8d0dc9f74d5374f6708ff81eebbb0af677a4d5307d38b2e5" "miniconda" verify_py314
;;
"Linux-x86_64" )
install_script "Miniconda3-py314_26.5.3-2-Linux-x86_64" "https://repo.anaconda.com/miniconda/Miniconda3-py314_26.5.3-2-Linux-x86_64.sh#80bc27f13c4de90f10e387aa45e864de4f0860692c1221aef5900009a2b55302" "miniconda" verify_py314
;;
"MacOSX-arm64" )
install_script "Miniconda3-py314_26.5.3-2-MacOSX-arm64" "https://repo.anaconda.com/miniconda/Miniconda3-py314_26.5.3-2-MacOSX-arm64.sh#e830bee5b9c6ebfb67e9df924b259a0d43c451bd17b965f7aa84523b69f7c3eb" "miniconda" verify_py314
;;
* )
{ echo
colorize 1 "ERROR"
echo ": The binary distribution of Miniconda is not available for $(anaconda_architecture 2>/dev/null || true)."
echo
} >&2
exit 1
;;
esac

View file

@ -0,0 +1,27 @@
From 3fec519d6b326918f39ceb142ef762042ce8365f Mon Sep 17 00:00:00 2001
From: Ivan Pozdeev <vano@mail.mipt.ru>
Date: Fri, 26 Dec 2025 13:39:21 +0300
Subject: [PATCH] Recognize an argument to -j in MAKEFLAGS
---
setup.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/setup.py b/setup.py
index ad8fb81b218..5666deaf67b 100644
--- a/setup.py
+++ b/setup.py
@@ -307,8 +307,8 @@ def __init__(self, dist):
self.failed_on_import = []
self.missing = []
self.disabled_configure = []
- if '-j' in os.environ.get('MAKEFLAGS', ''):
- self.parallel = True
+ if (m:=re.search(r'(?:^|\s)(?:-j|--jobs)(?:\s+|=)?(\d*)',os.environ.get('MAKEFLAGS', ''))) is not None:
+ self.parallel = int(m.group(1)) if m.group(1) else True
def add(self, ext):
self.extensions.append(ext)
--
2.36.1.windows.1

View file

@ -0,0 +1,27 @@
From 3fec519d6b326918f39ceb142ef762042ce8365f Mon Sep 17 00:00:00 2001
From: Ivan Pozdeev <vano@mail.mipt.ru>
Date: Fri, 26 Dec 2025 13:39:21 +0300
Subject: [PATCH] Recognize an argument to -j in MAKEFLAGS
---
setup.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/setup.py b/setup.py
index ad8fb81b218..5666deaf67b 100644
--- a/setup.py
+++ b/setup.py
@@ -307,8 +307,8 @@ def __init__(self, dist):
self.failed_on_import = []
self.missing = []
self.disabled_configure = []
- if '-j' in os.environ.get('MAKEFLAGS', ''):
- self.parallel = True
+ if (m:=re.search(r'(?:^|\s)(?:-j|--jobs)(?:\s+|=)?(\d*)',os.environ.get('MAKEFLAGS', ''))) is not None:
+ self.parallel = int(m.group(1)) if m.group(1) else True
def add(self, ext):
self.extensions.append(ext)
--
2.36.1.windows.1

View file

@ -0,0 +1,27 @@
From 3fec519d6b326918f39ceb142ef762042ce8365f Mon Sep 17 00:00:00 2001
From: Ivan Pozdeev <vano@mail.mipt.ru>
Date: Fri, 26 Dec 2025 13:39:21 +0300
Subject: [PATCH] Recognize an argument to -j in MAKEFLAGS
---
setup.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/setup.py b/setup.py
index ad8fb81b218..5666deaf67b 100644
--- a/setup.py
+++ b/setup.py
@@ -307,8 +307,8 @@ def __init__(self, dist):
self.failed_on_import = []
self.missing = []
self.disabled_configure = []
- if '-j' in os.environ.get('MAKEFLAGS', ''):
- self.parallel = True
+ if (m:=re.search(r'(?:^|\s)(?:-j|--jobs)(?:\s+|=)?(\d*)',os.environ.get('MAKEFLAGS', ''))) is not None:
+ self.parallel = int(m.group(1)) if m.group(1) else True
def add(self, ext):
self.extensions.append(ext)
--
2.36.1.windows.1

View file

@ -0,0 +1,64 @@
From cc13eabc7ce08accf49656e258ba500f74a1dae8 Mon Sep 17 00:00:00 2001
From: Ronald Oussoren <ronaldoussoren@mac.com>
Date: Thu, 28 Dec 2023 09:42:05 +0100
Subject: [PATCH] gh-110459: Make sure --with-openssl-rpath works on macOS
(#113441)
* gh-110459: Make sure --with-openssl-rpath works on macOS
On macOS the `-rpath` linker flag is spelled differently
than on on platforms.
---
.../macOS/2023-12-23-22-41-07.gh-issue-110459.NaMBJy.rst | 2 ++
configure | 7 ++++++-
configure.ac | 7 ++++++-
3 files changed, 14 insertions(+), 2 deletions(-)
create mode 100644 Misc/NEWS.d/next/macOS/2023-12-23-22-41-07.gh-issue-110459.NaMBJy.rst
diff --git a/Misc/NEWS.d/next/macOS/2023-12-23-22-41-07.gh-issue-110459.NaMBJy.rst b/Misc/NEWS.d/next/macOS/2023-12-23-22-41-07.gh-issue-110459.NaMBJy.rst
new file mode 100644
index 00000000000..44ffd857785
--- /dev/null
+++ b/Misc/NEWS.d/next/macOS/2023-12-23-22-41-07.gh-issue-110459.NaMBJy.rst
@@ -0,0 +1,2 @@
+Running ``configure ... --with-openssl-rpath=X/Y/Z`` no longer fails to detect
+OpenSSL on macOS.
diff --git a/configure b/configure
index 7e50abc29d0..6d65d3abc18 100755
--- a/configure
+++ b/configure
@@ -27478,7 +27478,12 @@ then :
else $as_nop
- rpath_arg="-Wl,-rpath="
+ if test "$ac_sys_system" = "Darwin"
+ then
+ rpath_arg="-Wl,-rpath,"
+ else
+ rpath_arg="-Wl,-rpath="
+ fi
fi
diff --git a/configure.ac b/configure.ac
index e064848af9e..bfdabc4474e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -6808,7 +6808,12 @@ AX_CHECK_OPENSSL([have_openssl=yes],[have_openssl=no])
AS_VAR_IF([GNULD], [yes], [
rpath_arg="-Wl,--enable-new-dtags,-rpath="
], [
- rpath_arg="-Wl,-rpath="
+ if test "$ac_sys_system" = "Darwin"
+ then
+ rpath_arg="-Wl,-rpath,"
+ else
+ rpath_arg="-Wl,-rpath="
+ fi
])
AC_MSG_CHECKING([for --with-openssl-rpath])
--
2.46.2.windows.1

View file

@ -1276,6 +1276,7 @@ OUT
mkdir -p "$INSTALL_ROOT/bin"
touch "$INSTALL_ROOT/bin/package"
chmod -w "$INSTALL_ROOT/bin/package"
skip_if_nonposix_security -w "$INSTALL_ROOT/bin/package"
install_fixture definitions/without-checksum
assert_success
@ -1288,6 +1289,7 @@ OUT
export TMPDIR="${BATS_TEST_TMPDIR}/build"
mkdir -p "$TMPDIR"
chmod -w "$TMPDIR"
skip_if_nonposix_security -w "$TMPDIR"
touch "${BATS_TEST_TMPDIR}/build-definition"
run python-build "${BATS_TEST_TMPDIR}/build-definition" "$INSTALL_ROOT"
@ -1298,6 +1300,7 @@ OUT
export TMPDIR="${BATS_TEST_TMPDIR}/build"
mkdir -p "$TMPDIR"
chmod -x "$TMPDIR"
skip_if_nonposix_security -x "$TMPDIR"
touch "${BATS_TEST_TMPDIR}/build-definition"
run python-build "${BATS_TEST_TMPDIR}/build-definition" "$INSTALL_ROOT"

View file

@ -0,0 +1,83 @@
#!/usr/bin/env bats
load test_helper
_setup() {
prerequisites="make build-essential libssl-dev zlib1g-dev libbz2-dev \
libreadline-dev libsqlite3-dev curl git llvm libncurses5-dev \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev \
liblzma-dev libzstd-dev"
}
@test "completion does not install packages" {
stub apt-get
PATH="${BATS_TEST_DIRNAME}/../../../libexec:$PATH" \
run pyenv completions install-prerequisites
assert_success "--help"
unstub apt-get
}
@test "supports help" {
stub pyenv-help 'install-prerequisites : echo "Usage: pyenv install-prerequisites"'
run pyenv-install-prerequisites --help
assert_success "Usage: pyenv install-prerequisites"
unstub pyenv-help
}
@test "rejects arguments" {
stub pyenv-help 'install-prerequisites : echo "Usage: pyenv install-prerequisites"'
run pyenv-install-prerequisites unexpected
assert_failure "Usage: pyenv install-prerequisites"
unstub pyenv-help
}
@test "installs build prerequisites with apt-get as root" {
stub id '-u : echo 0'
stub apt-get \
'update -q : true' \
"install -yq ${prerequisites} : true"
run pyenv-install-prerequisites
assert_success
unstub apt-get
unstub id
}
@test "uses sudo when not running as root" {
stub id '-u : echo 1000'
stub apt-get
stub sudo \
'apt-get update -q : true' \
"apt-get install -yq ${prerequisites} : true"
run pyenv-install-prerequisites
assert_success
unstub sudo
unstub apt-get
unstub id
}
@test "fails when apt-get is unavailable" {
PATH="$(path_without apt-get)" run pyenv-install-prerequisites
assert_failure "pyenv: installing build prerequisites is not supported on this system"
}
@test "fails when sudo is unavailable for an unprivileged user" {
stub id '-u : echo 1000'
stub apt-get
PATH="$(path_without sudo)" run pyenv-install-prerequisites
assert_failure "pyenv: sudo is required to install build prerequisites"
unstub apt-get
unstub id
}

View file

@ -2,6 +2,7 @@ setup() {
export PYTHON_BUILD_CURL_OPTS=
export PYTHON_BUILD_HTTP_CLIENT="curl"
export PYENV_TEST_DIR="${BATS_TEST_TMPDIR}/pyenv"
export FIXTURE_ROOT="${BATS_TEST_DIRNAME}/fixtures"
export INSTALL_ROOT="${BATS_TEST_TMPDIR}/install"
PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
@ -148,7 +149,7 @@ path_without() {
if [ "$found" != "${PYENV_ROOT}/shims" ]; then
alt="${PYENV_TEST_DIR}/$(echo "${found#/}" | tr '/' '-')"
mkdir -p "$alt"
for util in bash head cut readlink greadlink; do
for util in bash head cut readlink greadlink tr; do
if [ -x "${found}/$util" ]; then
ln -s "${found}/$util" "${alt}/$util"
fi
@ -162,3 +163,7 @@ path_without() {
echo "$path"
}
skip_if_nonposix_security() {
true "${1:?}" "${2:?}"
test "$@" && skip "UNIX permission bits are being overridden" || true
}

View file

@ -12,7 +12,7 @@ FROM bash:$BASH
apk add sed coreutils findutils \
;fi
# Bats
RUN apk add --update parallel ncurses git \
RUN apk add --update parallel ncurses git binutils \
&& mkdir -p ~/.parallel \
&& touch ~/.parallel/will-cite
COPY --from=bats /root/bats-core /root/bats-core

View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -e
# Wraps Bats TAP formatter, adding file names to the output
_BATS_FORMATTER_LIB_RELPATH="lib/bats-core/formatter.bash"
source "$BATS_ROOT/$_BATS_FORMATTER_LIB_RELPATH"
# `bats-format-tap` calls this fn at the end
# We must stub it to be able to source and alter it
# https://stackoverflow.com/questions/1203583/how-do-i-rename-a-bash-function
eval orig_"$(declare -f bats_parse_internal_extended_tap)"
bats_parse_internal_extended_tap() { true; }
# stub sourcing lib from `bats-format-tap`
# as that would've reset the stubbed fn
FAKE_BATS_ROOT=
_trap_rm_fakeroot() { rm -rf "$FAKE_BATS_ROOT"; }
REAL_BATS_ROOT="$BATS_ROOT"
FAKE_BATS_ROOT=$(mktemp -d)
trap _trap_rm_fakeroot ERR EXIT
mkdir -p "$(dirname "$FAKE_BATS_ROOT/$_BATS_FORMATTER_LIB_RELPATH")"
touch "$FAKE_BATS_ROOT/$_BATS_FORMATTER_LIB_RELPATH"
BATS_ROOT="$FAKE_BATS_ROOT"
source "$REAL_BATS_ROOT/libexec/bats-core/bats-format-tap"
_trap_rm_fakeroot
trap - ERR EXIT
BATS_ROOT="$REAL_BATS_ROOT"
unset FAKE_BATS_ROOT REAL_BATS_ROOT _trap_rm_fakeroot
# Finally, alter `bats-format-tap`'s logic and invoke it
# print as "Anything" line
bats_tap_stream_suite() { # <file name>
# bats only passes --base-name to specific built-in formatters
# so we don't have info where the test root is
printf '%s\n' "${1##*/}"
}
orig_bats_parse_internal_extended_tap

View file

@ -13,6 +13,8 @@ load test_helper
@test "non-writable shims directory" {
mkdir -p "${PYENV_ROOT}/shims"
chmod -w "${PYENV_ROOT}/shims"
skip_if_nonposix_security -w "${PYENV_ROOT}/shims"
run pyenv-rehash
assert_failure "pyenv: cannot rehash: ${PYENV_ROOT}/shims isn't writable"
}
@ -23,7 +25,8 @@ load test_helper
touch "${PYENV_ROOT}/shims/.pyenv-shim"
#avoid failure due to a localized error message
LANG=C run pyenv-rehash
assert_failure <<!
assert_failure
assert_output_glob <<!
pyenv: cannot rehash: couldn't acquire lock ${PYENV_ROOT}/shims/.pyenv-shim for 1 seconds. Last error message:
*/pyenv-rehash: line *: ${PYENV_ROOT}/shims/.pyenv-shim: cannot overwrite existing file
!

View file

@ -6,4 +6,4 @@ if [ -n "$PYENV_NATIVE_EXT" ]; then
make -C src
fi
exec bats ${CI:+--tap} ${BATS_TEST_FILTER:+--filter "${BATS_TEST_FILTER}"} test/${BATS_FILE_FILTER}
exec bats ${CI:+-F "$PWD/test/libexec/bats-format-tap-suite"} ${BATS_TEST_FILTER:+--filter "${BATS_TEST_FILTER}"} test/${BATS_FILE_FILTER}

View file

@ -1,39 +0,0 @@
#!/usr/bin/env bats
load test_helper
@test "shims linked from elsewhere are chained, other programs at the same paths are unaffected (integration)" {
bats_require_minimum_version 1.5.0
progname=shimmed_program
called_progname=another_program
create_alt_executable_in_version "custom" "$progname" <<!
echo "called from \$0"
pyenv-exec "$called_progname"
!
pyenv-rehash
PATH="$(path_without "$progname" "$called_progname")"
for disguised_shim_path in "$BATS_TEST_TMPDIR/"{weird-location,even/weirder/location}; do
mkdir -p "$disguised_shim_path"
ln -s "${PYENV_ROOT}/shims/$progname" "$disguised_shim_path/$progname"
PATH="$PATH:$disguised_shim_path"
done
create_executable "$disguised_shim_path" "$called_progname" <<!
echo "convoluted call success!"
!
real_path="$BATS_TEST_TMPDIR/real-location"
mkdir -p "$real_path"
ln -s "${PYENV_ROOT}/versions/custom/bin/$progname" "$real_path/$progname"
PATH="$PATH:$real_path"
run "$progname"
assert_success
assert_output <<!
called from $real_path/$progname
convoluted call success!
!
rm "$real_path/$progname"
run -127 "$progname"
assert_failure
assert_line 0 "pyenv: shimmed_program: command not found"
}

View file

@ -27,3 +27,39 @@ load test_helper
assert_line "irb"
assert_line "python"
}
@test "shims linked from elsewhere are chained, other programs at the same paths are unaffected (integration)" {
bats_require_minimum_version 1.5.0
progname=shimmed_program
called_progname=another_program
create_alt_executable_in_version "custom" "$progname" <<!
echo "called from \$0"
pyenv-exec "$called_progname"
!
pyenv-rehash
PATH="$(path_without "$progname" "$called_progname")"
for disguised_shim_path in "$BATS_TEST_TMPDIR/"{weird-location,even/weirder/location}; do
mkdir -p "$disguised_shim_path"
ln -s "${PYENV_ROOT}/shims/$progname" "$disguised_shim_path/$progname"
PATH="$PATH:$disguised_shim_path"
done
create_executable "$disguised_shim_path" "$called_progname" <<!
echo "convoluted call success!"
!
real_path="$BATS_TEST_TMPDIR/real-location"
mkdir -p "$real_path"
ln -s "${PYENV_ROOT}/versions/custom/bin/$progname" "$real_path/$progname"
PATH="$PATH:$real_path"
run "$progname"
assert_success
assert_output <<!
called from $real_path/$progname
convoluted call success!
!
rm "$real_path/$progname"
run -127 "$progname"
assert_failure
assert_line 0 "pyenv: shimmed_program: command not found"
}

View file

@ -2,8 +2,14 @@ unset PYENV_VERSION
unset PYENV_DIR
setup() {
export _PYENV_INSTALL_PREFIX="${BATS_TEST_DIRNAME%/*}"
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
export _PYENV_INSTALL_PREFIX="${BATS_TEST_DIRNAME%/*}"
local PLUGIN_PREFIX
if [[ $_PYENV_INSTALL_PREFIX =~ /plugins/[^/]+$ ]]; then
PLUGIN_PREFIX="$_PYENV_INSTALL_PREFIX"
_PYENV_INSTALL_PREFIX="${_PYENV_INSTALL_PREFIX::${#_PYENV_INSTALL_PREFIX}-${#BASH_REMATCH[0]}}"
fi
if ! enable -f "${_PYENV_INSTALL_PREFIX}"/libexec/pyenv-realpath.dylib realpath 2>/dev/null; then
if [ -n "$PYENV_NATIVE_EXT" ]; then
echo "pyenv: failed to load \`realpath' builtin" >&2
@ -27,6 +33,9 @@ setup() {
PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
PATH="${PYENV_TEST_DIR}/bin:$PATH"
PATH="${_PYENV_INSTALL_PREFIX}/libexec:$PATH"
if [[ -n $PLUGIN_PREFIX ]]; then
PATH="${PLUGIN_PREFIX}/libexec:${PLUGIN_PREFIX}/bin:$PATH"
fi
PATH="${BATS_TEST_DIRNAME}/libexec:$PATH"
PATH="${PYENV_ROOT}/shims:$PATH"
PATH="${BATS_TEST_TMPDIR}/stubs:$PATH"
@ -40,6 +49,7 @@ setup() {
# even if its output is redirected, breaking the comparison logic
export NO_COLOR=1
# If test specific setup exist, run it
if [[ $(type -t _setup) == function ]];then
_setup
@ -58,34 +68,64 @@ assert_success() {
if [ "$status" -ne 0 ]; then
flunk "command failed with exit status $status" $'\n'\
"output: $output"
elif [ "$#" -gt 0 ]; then
assert_output "$1"
fi
_assert_output_if_provided "$@"
}
assert_failure() {
if [ "$status" -eq 0 ]; then
flunk "expected failed exit status" $'\n'\
"output: $output"
elif [ "$#" -gt 0 ]; then
assert_output "$1"
fi
_assert_output_if_provided "$@"
}
assert_equal() {
if [ "$1" != "$2" ]; then
{ echo "expected: \`$1'"
echo "actual: \`$2'"
_assert_equal_as text "$@"
}
_assert_equal_as() {
if case "$1" in
text)
[[ "$2" != "$3" ]];;
glob)
[[ "$3" != $2 ]];;
*)
echo "invalid comparison type: \`$1'" >&2; return 1;;
esac
then
{ diff -u --label expected --label actual <(echo "$2") <(echo "$3") | cat -te
} | flunk
fi
}
assert_output() {
local expected
if [ $# -eq 0 ]; then expected="$(cat -)"
else expected="$1"
_assert_output_if_provided() {
if [ "$#" -gt 0 ]; then
assert_output "$1"
elif [ ! -t 0 ]; then
local expected=$(cat -)
if [[ -n $expected ]]; then
assert_output "$expected"
fi
fi
assert_equal "$expected" "$output"
}
assert_output() {
_assert_output_as text "$@"
}
assert_output_glob() {
_assert_output_as glob "$@"
}
_assert_output_as() {
local kind="${1:?}"; shift
local expected
if [ $# -eq 0 ]
then expected="$(cat -)"
else expected="$1"
fi
_assert_equal_as "$kind" "$expected" "$output"
}
assert_line() {
@ -135,7 +175,7 @@ path_without() {
if [ "$found" != "${PYENV_ROOT}/shims" ]; then
alt="${PYENV_TEST_DIR}/$(echo "${found#/}" | tr '/' '-')"
mkdir -p "$alt"
for util in bash head cut readlink greadlink tr sed xargs basename sort; do
for util in basename bash cut dirname find greadlink gzip head mkdir readlink sed sort tar tr xargs; do
if [ -x "${found}/$util" ]; then
ln -s "${found}/$util" "${alt}/$util"
fi
@ -193,3 +233,8 @@ create_hook() {
cat > "${PYENV_HOOK_PATH}/$1/$2"
fi
}
skip_if_nonposix_security() {
true "${1:?}" "${2:?}"
test "$@" && skip "UNIX permission bits are being overridden" || true
}

View file

@ -90,13 +90,13 @@ IN
}
@test "skips glob path traversal" {
cat > my-version <<IN
cat > my-version <<'IN'
../*
3.9.3
IN
run pyenv-version-file-read my-version
assert_success <<OUT
pyenv: invalid version \`../\*' ignored in \`my-version'
assert_success <<'OUT'
pyenv: invalid version `../*' ignored in `my-version'
3.9.3
OUT
}

View file

@ -73,3 +73,15 @@ create_file() {
run pyenv-version-file "$PWD"
assert_failure ""
}
@test "walks up beyond cwd for a relative path" {
create_file ".python-version"
mkdir -p project/subdir
cd project
# Run with CPU-time limit because this used to loop forever
run bash -c 'ulimit -t 5; pyenv-version-file .'
assert_success "${PYENV_TEST_DIR}/.python-version"
run pyenv-version-file ./subdir
assert_success "${PYENV_TEST_DIR}/.python-version"
}

View file

@ -83,8 +83,7 @@ OUT
create_version "3.4.0/envs/bar"
create_version "3.5.2"
run pyenv-versions
assert_success
assert_output <<OUT
assert_success <<OUT
* system (set by ${PYENV_ROOT}/version)
2.7.6
3.4.0
@ -95,6 +94,7 @@ OUT
}
@test "skips envs with --skip-envs" {
stub_system_python
create_version "3.3.3"
create_version "3.4.0"
create_version "3.4.0/envs/foo"
@ -102,7 +102,7 @@ OUT
create_version "3.5.0"
run pyenv-versions --skip-envs
assert_success <<OUT
assert_success <<OUT
* system (set by ${PYENV_ROOT}/version)
3.3.3
3.4.0
@ -188,9 +188,7 @@ OUT
create_external_version "moo"
run pyenv-versions --bare --skip-aliases
assert_success
assert_output <<OUT
assert_success <<OUT
1.8.7
moo
OUT

View file

@ -64,7 +64,7 @@ load test_helper
assert_failure <<OUT
pyenv: version \`3.3' is not installed (set by PYENV_VERSION environment variable)
pyenv: py.test: command not found
The \`py.test' command exists in these Python versions:
3.4
@ -81,7 +81,7 @@ OUT
pyenv: version \`2.7' is not installed (set by PYENV_VERSION environment variable)
pyenv: version \`3.3' is not installed (set by PYENV_VERSION environment variable)
pyenv: py.test: command not found
The \`py.test' command exists in these Python versions:
3.4