fix: ensure pip shims are always created on install (#5) - #6
Merged
Conversation
pyenv shims produced no pip/pip3/pip3.x shims after install because of several reliability gaps in shim generation and pip install: - Invoke-Rehash: unify the version-root, Scripts and bin scans into one loop that creates a .bat + shell shim for every runnable file. Non-.exe Scripts entries (extensionless/.py console-scripts) now get real, CLI-runnable shims instead of only a .lnk. - Isolate each file in try/catch so one bad file can no longer abort the whole rehash loop and drop pip along with it. - Remove New-ShortcutShim entirely: the COM WScript.Shell call throws on non-Windows and in Constrained Language Mode, and .lnk files are not runnable by name via PATHEXT anyway. - Install-Pip: extract the duplicated ensurepip logic into one helper and verify Scripts\pip.exe exists afterwards. A non-zero ensurepip exit or a missing pip.exe now surfaces a loud, actionable error instead of a silent "install completed" with no pip. Missing ensurepip warns instead of skipping silently. Adds regression tests: pip shim family end-to-end from the Scripts scan, non-.exe Scripts entries, and rehash resilience when a single file fails.
Two more ways pip shims went missing, on top of the existing Install-Pip verification: - Import-VersionsCache kept whichever entry for a version code was last in .versions.xml. For every 64-bit code, the .zip entry is listed after the .exe entry, so a plain `pyenv install 3.11.9` silently resolved to the zip archive instead of the real installer on any 64-bit machine - Install-PythonExe/Install-PythonMsi (and therefore Install-Pip) were never even reached. An installer entry now always wins over a zip entry for the same code, regardless of XML order; same-type duplicates keep the previous last-wins behavior. - Install-PythonZip never called Install-Pip at all. Some "zip" builds (e.g. the CPython arm64 archive) ship Lib\ensurepip and a pre-seeded site-packages\pip without ever having run pip's own installer, so Scripts\pip.exe never got written. Install-PythonZip now calls Install-Pip after a successful extract, same as the exe/msi paths; genuinely ensurepip-less builds (true embeddable packages, PyPy/GraalPy) still just get the existing warn-and-skip.
`pyenv install 3.11.9` on an ARM64 machine resolves (via Get-ArchPostfix) to the `3.11.9-arm64` folder, but `pyenv uninstall 3.11.9` did exact folder-name matching only and reported the version as not installed - you had to type the full `3.11.9-arm64` back. pyenv-uninstall.ps1 now resolves each argument through Resolve-VersionPrefix, the same mechanism install/global/local already use, so uninstall accepts whatever install accepted. Falls back to the literal argument when nothing matches, so an unknown version still reports not installed. Also gives tests/TestHelper.ps1's Invoke-Pyenv the ability to simulate a non-AMD64 machine (via the existing $env:PYENV_FORCE_ARCH, honored with an AMD64 fallback) so this arch-suffixed resolution can actually be exercised end-to-end. Unrelated to the pip-shims fix in this branch; kept as a separate commit since it's a distinct install/uninstall argument-resolution bug found during verification, not something #5 describes.
nikolasd
force-pushed
the
fix/pip-shims-not-created
branch
2 times, most recently
from
August 14, 2026 20:58
1d97454 to
5b55961
Compare
Initialize-PyenvLibraries dot-sourced the isolated $TestDrive copy of lib/*.ps1 instead of the real pyenv-win/lib source. Pester's coverage tracer matches hits by the literal dot-sourced path, so none of that in-process execution ever counted toward pyenv-win/lib/*.ps1 (local coverage: 0%). Pointing it at the real source fixes this (0% -> ~70-80% on lib/*.ps1) with no behavior change to test isolation - $env:PYENV_HOME still points at the isolated $TestDrive path for runtime state. Also rescope Run-Tests.ps1's CodeCoverage.Path to lib/ only. bin/pyenv.ps1 and libexec/*.ps1 are exercised almost exclusively via Invoke-Pyenv, which runs the CLI in a separate pwsh.exe process - Pester's coverage collector can't observe execution across a process boundary regardless of tracer mode, so including them only drags the reported percentage toward 0 without measuring anything real. Lower CoveragePercentTarget from the now-unreachable 100 to 75 (Pester's own default) to match.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
pyenv shimsproduced nopip/pip3/pip3.xshims afterpyenv install. Verifying the fix end-to-end on a real Windows VM turned up four independent gaps across version selection, pip installation, and shim generation that all feed into the same symptom, plus one related gap inuninstall. All are fixed here.Why pip went missing
Wrong artifact chosen in the first place.
Import-VersionsCachekept whichever entry for a version code appeared last in.versions.xml. For every 64-bit code, the.zip(embeddable) entry is listed after the.exe/.msientry, so a plainpyenv install 3.11.9silently installed the embeddable zip instead of the real installer on any 64-bit machine —Install-PythonExe/Install-PythonMsi, and therefore pip installation, were never even reached. An installer entry now always wins over a zip entry for the same code, regardless of XML order; same-type duplicates keep the previous last-wins behavior.Zip installs never attempted pip at all.
Install-PythonZipnever called anything pip-related. Some "zip" builds (e.g. the CPython arm64 archive) shipLib\ensurepipand a pre-seededsite-packages\pipwithout ever having run pip's own installer, soScripts\pip.exenever got written. It now installs pip after a successful extract, same as the exe/msi paths below; genuinely ensurepip-less builds (true embeddable packages, PyPy/GraalPy) still just warn and skip.ensurepip failures were silent. The duplicated ensurepip-invocation code in
Install-PythonMsi/Install-PythonExeis now oneInstall-Piphelper that verifiesScripts\pip.exeactually exists afterward. ensurepip can exit non-zero, or exit 0 yet leave nopip.exe(a partially broken build) — either way rehash would then silently produce no pip shims. Now it's a loud, actionable error instead of a silent "install completed" with no pip.Rehash itself dropped pip even when it was installed.
Invoke-Rehashscanned the version root and theScripts/binsubfolders with inconsistent branches — non-.exeentries underScripts/binonly got a.lnkshortcut, never a runnable.bat/shell shim. The scan is now unified across all three locations and always emits a.bat+ shell shim for every runnable file, wrapped in try/catch so one bad file can no longer abort the loop and drop pip along with it.New-ShortcutShimis removed entirely — the COMWScript.Shellcall throws on non-Windows and in Constrained Language Mode, and.lnkfiles aren't runnable by name viaPATHEXTanyway.Related: uninstall couldn't accept what install produced
pyenv install 3.11.9on an ARM64 machine resolves (viaGet-ArchPostfix) to the3.11.9-arm64folder, butpyenv uninstall 3.11.9did exact folder-name matching only and reported the version as not installed — you had to type the full3.11.9-arm64back.pyenv-uninstall.ps1now resolves each argument throughResolve-VersionPrefix, the same mechanisminstall/global/localalready use, so uninstall accepts whatever install accepted.Tests
Regression tests for all five fixes: version-table exe/msi-over-zip precedence (both orderings, plus msi-vs-zip),
Install-PythonZipinvoking pip installation, the pip shim family end-to-end from theScriptsscan, non-.exeScriptsentries, rehash resilience when a single file fails, and uninstall resolving a bare version to its arch-suffixed folder.Closes #5
Branch Tree