Skip to content

fix: ensure pip shims are always created on install (#5) - #6

Merged
nikolasd merged 7 commits into
masterfrom
fix/pip-shims-not-created
Aug 18, 2026
Merged

fix: ensure pip shims are always created on install (#5)#6
nikolasd merged 7 commits into
masterfrom
fix/pip-shims-not-created

Conversation

@nikolasd

@nikolasd nikolasd commented Jul 20, 2026

Copy link
Copy Markdown
Member

pyenv shims produced no pip/pip3/pip3.x shims after pyenv 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 in uninstall. All are fixed here.

Why pip went missing

  1. Wrong artifact chosen in the first place. Import-VersionsCache kept 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/.msi entry, so a plain pyenv install 3.11.9 silently 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.

  2. Zip installs never attempted pip at all. Install-PythonZip never called anything pip-related. 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. 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.

  3. ensurepip failures were silent. The duplicated ensurepip-invocation code in Install-PythonMsi/Install-PythonExe is now one Install-Pip helper that verifies Scripts\pip.exe actually exists afterward. ensurepip can exit non-zero, or exit 0 yet leave no pip.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.

  4. Rehash itself dropped pip even when it was installed. Invoke-Rehash scanned the version root and the Scripts/bin subfolders with inconsistent branches — non-.exe entries under Scripts/bin only got a .lnk shortcut, 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-ShortcutShim is removed entirely — the COM WScript.Shell call throws on non-Windows and in Constrained Language Mode, and .lnk files aren't runnable by name via PATHEXT anyway.

Related: uninstall couldn't accept what install produced

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.

Tests

Regression tests for all five fixes: version-table exe/msi-over-zip precedence (both orderings, plus msi-vs-zip), Install-PythonZip invoking pip installation, the pip shim family end-to-end from the Scripts scan, non-.exe Scripts entries, rehash resilience when a single file fails, and uninstall resolving a bare version to its arch-suffixed folder.

Closes #5

Branch Tree

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
nikolasd force-pushed the fix/pip-shims-not-created branch 2 times, most recently from 1d97454 to 5b55961 Compare August 14, 2026 20:58
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.
@nikolasd
nikolasd marked this pull request as ready for review August 18, 2026 14:29
@nikolasd
nikolasd merged commit bfb001f into master Aug 18, 2026
1 check passed
@nikolasd
nikolasd deleted the fix/pip-shims-not-created branch August 18, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Pip shims are not being created

1 participant