Skip to content

Commit 419d853

Browse files
Virtual environment: handle pip updates (#129)
Instead of putting our payload into pip and making pip3 and pip3.12 symlinks to pip, put our payload into pip_patched and symlink all pip variants to that. Then we can check for a problematic update by testing whether readlink() of the current executing file still points to pip_patched. If it doesn't, put it back. Resolves #128 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent d6ea94d commit 419d853

File tree

2 files changed

+82
-17
lines changed

2 files changed

+82
-17
lines changed

pyodide_build/out_of_tree/venv.py

+47-17
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,45 @@ def platform_tags():
174174
sysconfig._init_config_vars()
175175
del os.environ["_PYTHON_SYSCONFIGDATA_NAME"]
176176
"""
177+
# Handle pip updates.
178+
#
179+
# The pip executable should be a symlink to pip_patched. If it is not a
180+
# link, or it is a symlink to something else, pip has been updated. We
181+
# have to restore the correct value of pip. Iterate through all of the
182+
# pip variants in the folder and remove them and replace with a symlink
183+
# to pip_patched.
184+
"""
185+
from pathlib import Path
186+
187+
file_path = Path(__file__)
188+
189+
190+
def pip_is_okay():
191+
try:
192+
return file_path.readlink() == file_path.with_name("pip_patched")
193+
except OSError as e:
194+
if e.strerror != "Invalid argument":
195+
raise
196+
return False
197+
198+
199+
def maybe_repair_after_pip_update():
200+
if pip_is_okay():
201+
return
202+
203+
venv_bin = file_path.parent
204+
pip_patched = venv_bin / "pip_patched"
205+
for pip in venv_bin.glob("pip*"):
206+
if pip == pip_patched:
207+
continue
208+
pip.unlink(missing_ok=True)
209+
pip.symlink_to(venv_bin / "pip_patched")
210+
211+
212+
import atexit
213+
214+
atexit.register(maybe_repair_after_pip_update)
215+
"""
177216
)
178217

179218

@@ -183,26 +222,17 @@ def create_pip_script(venv_bin):
183222
# Python in the shebang. Use whichever Python was used to invoke
184223
# pyodide venv.
185224
host_python_path = venv_bin / f"python{get_pyversion()}-host"
186-
pip_path = venv_bin / "pip"
187-
pyversion = get_pyversion()
188-
other_pips = [
189-
venv_bin / "pip3",
190-
venv_bin / f"pip{pyversion}",
191-
venv_bin / f"pip-{pyversion}",
192-
]
225+
pip_path = venv_bin / "pip_patched"
193226

194227
# To support the "--clear" and "--no-clear" args, we need to remove
195228
# the existing symlinks before creating new ones.
196-
if host_python_path.exists():
197-
host_python_path.unlink()
198-
if (venv_bin / "python-host").exists():
199-
(venv_bin / "python-host").unlink()
200-
if pip_path.exists():
201-
pip_path.unlink()
202-
for pip in other_pips:
203-
if pip.exists():
204-
pip.unlink()
205-
pip.symlink_to(pip_path)
229+
host_python_path.unlink(missing_ok=True)
230+
(venv_bin / "python-host").unlink(missing_ok=True)
231+
for pip in venv_bin.glob("pip*"):
232+
if pip == pip_path:
233+
continue
234+
pip.unlink(missing_ok=True)
235+
pip.symlink_to(pip_path)
206236

207237
host_python_path.symlink_to(sys.executable)
208238
# in case someone needs a Python-version-agnostic way to refer to python-host

pyodide_build/tests/test_venv.py

+35
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,38 @@ def test_pip_install(base_test_dir, packages):
221221
venv_path.glob(f"**/{package.replace('-', '_')}-*.dist-info")
222222
)
223223
assert len(dist_info_dirs) > 0, f"{package} not found in the venv"
224+
225+
226+
@pytest.mark.integration
227+
def test_pip_downgrade(base_test_dir):
228+
"""Test that our monkeypatched pip can upgrade/downgrade itself"""
229+
venv_path = base_test_dir / "test_venv"
230+
231+
venv.create_pyodide_venv(venv_path, [])
232+
venv_pip_path = venv_path / "bin" / "pip"
233+
assert venv_pip_path.exists(), "pip wasn't found in the virtual environment"
234+
235+
result = subprocess.run(
236+
[
237+
str(venv_pip_path),
238+
"install",
239+
"--upgrade",
240+
"pip==24.0",
241+
"-v",
242+
"--disable-pip-version-check",
243+
],
244+
capture_output=True,
245+
text=True,
246+
check=False,
247+
)
248+
assert result.returncode == 0, f"Failed to downgrade pip: {result.stderr}"
249+
250+
result = subprocess.run(
251+
[str(venv_pip_path), "--version"],
252+
capture_output=True,
253+
text=True,
254+
check=False,
255+
)
256+
assert result.returncode == 0, result.stderr
257+
assert result.stdout.startswith("pip 24.0")
258+
assert venv_pip_path.readlink() == venv_pip_path.with_name("pip_patched")

0 commit comments

Comments
 (0)