During seeding properly uninstall present versions of the wheels (#2186)

* During seeding remove dist-info for present versions of the wheels

An existing dist-info may contain entrypoints that may interfere with
normal functioning of the redeployed seeded wheel if there is a version
mismatch

fixes #2185

* Remove package directories from dist-info top_level packages
Remove other recorded files from RECORD
Remove dist-info itself

* Do not resolve paths prior to removal for symlink mode
In the test ensure the directories are compared as sets and not lists
Add setuptools downgrade to ensure proper cleanup of the existing version

* PR Feedback

Signed-off-by: Bernát Gábor <gaborjbernat@gmail.com>

Co-authored-by: Bernát Gábor <gaborjbernat@gmail.com>
This commit is contained in:
Arcadiy Ivanov 2021-09-24 05:56:28 -04:00 committed by GitHub
parent 3b9d95481a
commit e13ea2a7c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 48 additions and 8 deletions

View file

@ -0,0 +1,3 @@
Fixed a bug where while creating a venv on top of an existing one, without cleaning, when seeded
wheel version mismatch occurred, multiple ``.dist-info`` directories may be present, confounding entrypoint
discovery - by :user:`arcivanov`

View file

@ -5,6 +5,7 @@ import os
import re
import zipfile
from abc import ABCMeta, abstractmethod
from itertools import chain
from tempfile import mkdtemp
from distlib.scripts import ScriptMaker, enquote_executable
@ -31,14 +32,10 @@ class PipInstall(object):
def install(self, version_info):
self._extracted = True
self._uninstall_previous_version()
# sync image
for filename in self._image_dir.iterdir():
into = self._creator.purelib / filename.name
if into.exists():
if into.is_dir() and not into.is_symlink():
safe_delete(into)
else:
into.unlink()
self._sync(filename, into)
# generate console executables
consoles = set()
@ -150,6 +147,36 @@ class PipInstall(object):
result.extend(Path(i) for i in new_files)
return result
def _uninstall_previous_version(self):
dist_name = self._dist_info.stem.split("-")[0]
in_folders = chain.from_iterable([i.iterdir() for i in {self._creator.purelib, self._creator.platlib}])
paths = (p for p in in_folders if p.stem.split("-")[0] == dist_name and p.suffix == ".dist-info" and p.is_dir())
existing_dist = next(paths, None)
if existing_dist is not None:
self._uninstall_dist(existing_dist)
@staticmethod
def _uninstall_dist(dist):
dist_base = dist.parent
logging.debug("uninstall existing distribution %s from %s", dist.stem, dist_base)
top_txt = dist / "top_level.txt" # add top level packages at folder level
paths = {dist.parent / i.strip() for i in top_txt.read_text().splitlines()} if top_txt.exists() else set()
paths.add(dist) # add the dist-info folder itself
base_dirs, record = paths.copy(), dist / "RECORD" # collect entries in record that we did not register yet
for name in (i.split(",")[0] for i in record.read_text().splitlines()) if record.exists() else ():
path = dist_base / name
if not any(p in base_dirs for p in path.parents): # only add if not already added as a base dir
paths.add(path)
for path in sorted(paths): # actually remove stuff in a stable order
if path.exists():
if path.is_dir() and not path.is_symlink():
safe_delete(path)
else:
path.unlink()
def clear(self):
if self._image_dir.exists():
safe_delete(self._image_dir)

View file

@ -52,7 +52,7 @@ def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies)
pip = site_package / "pip"
setuptools = site_package / "setuptools"
files_post_first_create = list(site_package.iterdir())
files_post_first_create = set(site_package.iterdir())
assert pip in files_post_first_create
assert setuptools in files_post_first_create
for pip_exe in [
@ -82,15 +82,25 @@ def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies)
assert not process.returncode
assert site_package.exists()
files_post_first_uninstall = list(site_package.iterdir())
files_post_first_uninstall = set(site_package.iterdir())
assert pip in files_post_first_uninstall
assert setuptools not in files_post_first_uninstall
# install a different setuptools to test that virtualenv removes this before installing new
version = "setuptools<{}".format(bundle_ver["setuptools"].split("-")[1])
install_cmd = [str(result.creator.script("pip")), "--verbose", "--disable-pip-version-check", "install", version]
process = Popen(install_cmd)
process.communicate()
assert not process.returncode
assert site_package.exists()
files_post_downgrade = set(site_package.iterdir())
assert setuptools in files_post_downgrade
# check we can run it again and will work - checks both overwrite and reuse cache
result = cli_run(create_cmd)
coverage_env()
assert result
files_post_second_create = list(site_package.iterdir())
files_post_second_create = set(site_package.iterdir())
assert files_post_first_create == files_post_second_create
# Windows does not allow removing a executable while running it, so when uninstalling pip we need to do it via