diff --git a/news/12666.bugfix.rst b/news/12666.bugfix.rst new file mode 100644 index 00000000000..72a4a65155e --- /dev/null +++ b/news/12666.bugfix.rst @@ -0,0 +1 @@ +Fix intermittent "cannot locate t64.exe" errors when upgrading pip. diff --git a/src/pip/_vendor/distlib/scripts.py b/src/pip/_vendor/distlib/scripts.py index cfa45d2af18..e16292b8330 100644 --- a/src/pip/_vendor/distlib/scripts.py +++ b/src/pip/_vendor/distlib/scripts.py @@ -49,6 +49,24 @@ sys.exit(%(func)s()) ''' +# Pre-fetch the contents of all executable wrapper stubs. +# This is to address https://github.com/pypa/pip/issues/12666. +# When updating pip, we rename the old pip in place before installing the +# new version. If we try to fetch a wrapper *after* that rename, the finder +# machinery will be confused as the package is no longer available at the +# location where it was imported from. So we load everything into memory in +# advance. + +# Issue 31: don't hardcode an absolute package name, but +# determine it relative to the current package +distlib_package = __name__.rsplit('.', 1)[0] + +WRAPPERS = { + r.name: r.bytes + for r in finder(distlib_package).iterator("") + if r.name.endswith(".exe") +} + def enquote_executable(executable): if ' ' in executable: @@ -409,15 +427,11 @@ def _get_launcher(self, kind): bits = '32' platform_suffix = '-arm' if get_platform() == 'win-arm64' else '' name = '%s%s%s.exe' % (kind, bits, platform_suffix) - # Issue 31: don't hardcode an absolute package name, but - # determine it relative to the current package - distlib_package = __name__.rsplit('.', 1)[0] - resource = finder(distlib_package).find(name) - if not resource: + if name not in WRAPPERS: msg = ('Unable to find resource %s in package %s' % (name, distlib_package)) raise ValueError(msg) - return resource.bytes + return WRAPPERS[name] # Public API follows diff --git a/tests/functional/test_self_update.py b/tests/functional/test_self_update.py new file mode 100644 index 00000000000..c507552208a --- /dev/null +++ b/tests/functional/test_self_update.py @@ -0,0 +1,22 @@ +# Check that pip can update itself correctly + +from typing import Any + + +def test_self_update_editable(script: Any, pip_src: Any) -> None: + # Test that if we have an environment with pip installed in non-editable + # mode, that pip can safely update itself to an editable install. + # See https://github.com/pypa/pip/issues/12666 for details. + + # Step 1. Install pip as non-editable. This is expected to succeed as + # the existing pip in the environment is installed in editable mode, so + # it only places a .pth file in the environment. + proc = script.pip("install", pip_src) + assert proc.returncode == 0 + # Step 2. Using the pip we just installed, install pip *again*, but + # in editable mode. This could fail, as we'll need to uninstall the running + # pip in order to install the new copy, and uninstalling pip while it's + # running could fail. This test is specifically to ensure that doesn't + # happen... + proc = script.pip("install", "-e", pip_src) + assert proc.returncode == 0 diff --git a/tools/vendoring/patches/distlib.patch b/tools/vendoring/patches/distlib.patch new file mode 100644 index 00000000000..de2834710a3 --- /dev/null +++ b/tools/vendoring/patches/distlib.patch @@ -0,0 +1,47 @@ +diff --git a/src/pip/_vendor/distlib/scripts.py b/src/pip/_vendor/distlib/scripts.py +index cfa45d2af..e16292b83 100644 +--- a/src/pip/_vendor/distlib/scripts.py ++++ b/src/pip/_vendor/distlib/scripts.py +@@ -49,6 +49,24 @@ if __name__ == '__main__': + sys.exit(%(func)s()) + ''' + ++# Pre-fetch the contents of all executable wrapper stubs. ++# This is to address https://github.com/pypa/pip/issues/12666. ++# When updating pip, we rename the old pip in place before installing the ++# new version. If we try to fetch a wrapper *after* that rename, the finder ++# machinery will be confused as the package is no longer available at the ++# location where it was imported from. So we load everything into memory in ++# advance. ++ ++# Issue 31: don't hardcode an absolute package name, but ++# determine it relative to the current package ++distlib_package = __name__.rsplit('.', 1)[0] ++ ++WRAPPERS = { ++ r.name: r.bytes ++ for r in finder(distlib_package).iterator("") ++ if r.name.endswith(".exe") ++} ++ + + def enquote_executable(executable): + if ' ' in executable: +@@ -409,15 +427,11 @@ class ScriptMaker(object): + bits = '32' + platform_suffix = '-arm' if get_platform() == 'win-arm64' else '' + name = '%s%s%s.exe' % (kind, bits, platform_suffix) +- # Issue 31: don't hardcode an absolute package name, but +- # determine it relative to the current package +- distlib_package = __name__.rsplit('.', 1)[0] +- resource = finder(distlib_package).find(name) +- if not resource: ++ if name not in WRAPPERS: + msg = ('Unable to find resource %s in package %s' % + (name, distlib_package)) + raise ValueError(msg) +- return resource.bytes ++ return WRAPPERS[name] + + # Public API follows +