mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
Don't delete or modify files outside sys.prefix
This commit is contained in:
parent
46c1d4aa79
commit
83f9d9c18b
197
pip.py
197
pip.py
|
@ -33,7 +33,8 @@ import httplib
|
|||
import time
|
||||
import logging
|
||||
import ConfigParser
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
class InstallationError(Exception):
|
||||
"""General exception during installation"""
|
||||
|
||||
|
@ -49,6 +50,7 @@ else:
|
|||
base_prefix = os.path.join(os.getcwd(), 'build')
|
||||
base_src_prefix = os.path.join(os.getcwd(), 'src')
|
||||
|
||||
# FIXME doesn't account for venv linked to global site-packages
|
||||
if sys.platform == 'win32':
|
||||
lib_py = os.path.join(sys.prefix, 'Lib')
|
||||
else:
|
||||
|
@ -1554,96 +1556,96 @@ execfile(__file__)
|
|||
% (self.url, vc_type))
|
||||
|
||||
def uninstall(self, auto_confirm=False):
|
||||
"""
|
||||
Uninstall the distribution currently satisfying this requirement.
|
||||
|
||||
Prompts before removing or modifying files unless
|
||||
``auto_confirm`` is True.
|
||||
|
||||
Refuses to delete or modify files outside of ``sys.prefix`` -
|
||||
thus uninstallation within a virtual environment can only
|
||||
modify that virtual environment, even if the virtualenv is
|
||||
linked to global site-packages.
|
||||
|
||||
"""
|
||||
assert self.check_if_exists(), "Cannot uninstall requirement %s, not installed" % (self.name,)
|
||||
dist = self.satisfied_by
|
||||
remove_paths = set()
|
||||
remove_from_easy_install_pth = set()
|
||||
easy_install_pth = os.path.join(lib_py, os.path.join('site-packages', 'easy-install.pth'))
|
||||
paths_to_remove = set()
|
||||
entries_to_remove = defaultdict(set)
|
||||
|
||||
logger.notify('Uninstalling %s' % self.name)
|
||||
logger.indent += 2
|
||||
try:
|
||||
pip_egg_info_path = os.path.join(dist.location, dist.egg_name()) + '.egg-info'
|
||||
pip_egg_info_path = os.path.join(dist.location,
|
||||
dist.egg_name()) + '.egg-info'
|
||||
easy_install_egg = dist.egg_name() + '.egg'
|
||||
develop_egg_link = os.path.join(lib_py, 'site-packages', dist.project_name) + '.egg-link'
|
||||
# FIXME this won't find a globally-installed develop egg
|
||||
# if we're in a virtualenv (lib_py is based on
|
||||
# sys.prefix). (There doesn't seem to be any metadata in
|
||||
# the Distribution object for a develop egg that points
|
||||
# back to its .egg-link and easy-install.pth files).
|
||||
# That's OK, because we restrict ourselves to making
|
||||
# changes within sys.prefix anyway.
|
||||
develop_egg_link = os.path.join(lib_py, 'site-packages',
|
||||
dist.project_name) + '.egg-link'
|
||||
|
||||
if os.path.exists(pip_egg_info_path):
|
||||
# package installed by pip
|
||||
remove_paths.add(pip_egg_info_path)
|
||||
paths_to_remove.add(pip_egg_info_path)
|
||||
if dist.has_metadata('installed-files.txt'):
|
||||
for installed_file in dist.get_metadata('installed-files.txt').splitlines():
|
||||
path = os.path.normpath(os.path.join(pip_egg_info_path, installed_file))
|
||||
if os.path.exists(path):
|
||||
remove_paths.add(path)
|
||||
paths_to_remove.add(path)
|
||||
if dist.has_metadata('top_level.txt'):
|
||||
for top_level_pkg in [p for p
|
||||
in dist.get_metadata('top_level.txt').splitlines()
|
||||
if p]:
|
||||
path = os.path.join(dist.location, top_level_pkg)
|
||||
if os.path.exists(path):
|
||||
remove_paths.add(path)
|
||||
paths_to_remove.add(path)
|
||||
elif os.path.exists(path + '.py'):
|
||||
remove_paths.add(path + '.py')
|
||||
paths_to_remove.add(path + '.py')
|
||||
if os.path.exists(path + '.pyc'):
|
||||
remove_paths.add(path + '.pyc')
|
||||
paths_to_remove.add(path + '.pyc')
|
||||
|
||||
elif dist.location.endswith(easy_install_egg):
|
||||
# package installed by easy_install
|
||||
remove_paths.add(dist.location)
|
||||
remove_from_easy_install_pth.add('./' + easy_install_egg)
|
||||
paths_to_remove.add(dist.location)
|
||||
easy_install_pth = os.path.join(os.path.dirname(dist.location),
|
||||
'easy-install.pth')
|
||||
entries_to_remove[easy_install_pth].add('./' + easy_install_egg)
|
||||
|
||||
elif os.path.isfile(develop_egg_link):
|
||||
# develop egg
|
||||
fh = open(develop_egg_link, 'r')
|
||||
link_pointer = fh.readline().strip()
|
||||
fh.close()
|
||||
assert (link_pointer == dist.location), 'Egg-link %s does not match installed location of %s (at %s)' % (link_pointer, self.name, dist.location)
|
||||
remove_paths.add(dist.location)
|
||||
remove_paths.add(develop_egg_link)
|
||||
remove_from_easy_install_pth.add(dist.location)
|
||||
|
||||
def _strip_prefix(path):
|
||||
if path.startswith(sys.prefix):
|
||||
return path.replace(sys.prefix, '')
|
||||
paths_to_remove.add(dist.location)
|
||||
paths_to_remove.add(develop_egg_link)
|
||||
easy_install_pth = os.path.join(os.path.dirname(develop_egg_link),
|
||||
'easy-install.pth')
|
||||
entries_to_remove[easy_install_pth].add(dist.location)
|
||||
|
||||
if remove_from_easy_install_pth:
|
||||
logger.notify('Removing entries from %s:' % _strip_prefix(easy_install_pth))
|
||||
logger.indent += 2
|
||||
try:
|
||||
if auto_confirm:
|
||||
response = 'y'
|
||||
else:
|
||||
for path in remove_from_easy_install_pth:
|
||||
logger.notify(path)
|
||||
response = ask('Proceed with removal (y/n)? ', ('y', 'n'))
|
||||
if response == 'y':
|
||||
remove_entries_from_file(easy_install_pth, remove_from_easy_install_pth)
|
||||
finally:
|
||||
logger.indent -= 2
|
||||
for filename, entries in entries_to_remove.items():
|
||||
if strip_sys_prefix(filename) is None:
|
||||
logger.notify('Will not modify %s, outside local environment.' % filename)
|
||||
continue
|
||||
remove_entries_from_file(filename, entries, auto_confirm)
|
||||
|
||||
to_remove = set()
|
||||
for path in paths_to_remove:
|
||||
if strip_sys_prefix(path) is None:
|
||||
logger.notify('Will not remove %s, outside of local environment.' % path)
|
||||
else:
|
||||
to_remove.add(path)
|
||||
if to_remove:
|
||||
remove_paths(to_remove, auto_confirm)
|
||||
|
||||
if remove_paths:
|
||||
logger.notify('Within environment %s, removing:' % sys.prefix)
|
||||
logger.indent += 2
|
||||
try:
|
||||
if auto_confirm:
|
||||
response = 'y'
|
||||
else:
|
||||
for path in remove_paths:
|
||||
logger.notify(_strip_prefix(path))
|
||||
response = ask('Proceed with removal (y/n)? ', ('y', 'n'))
|
||||
if response == 'y':
|
||||
for path in remove_paths:
|
||||
if os.path.isdir(path):
|
||||
logger.notify('Removing directory %s' % _strip_prefix(path))
|
||||
shutil.rmtree(path)
|
||||
elif os.path.isfile(path):
|
||||
logger.notify('Removing file %s' % _strip_prefix(path))
|
||||
os.remove(path)
|
||||
finally:
|
||||
logger.indent -= 2
|
||||
finally:
|
||||
logger.indent -= 2
|
||||
|
||||
|
||||
|
||||
def install(self, install_options):
|
||||
if self.editable:
|
||||
self.install_editable()
|
||||
|
@ -3894,25 +3896,6 @@ def open_logfile_append(filename):
|
|||
print >> log_fp, '%s run on %s' % (sys.argv[0], time.strftime('%c'))
|
||||
return log_fp
|
||||
|
||||
def remove_entries_from_file(filename, entries):
|
||||
assert os.path.isfile(filename), "Trying to remove entries from nonexistent file %s" % filename
|
||||
fh = open(filename, 'r')
|
||||
lines = fh.readlines()
|
||||
fh.close()
|
||||
logger.indent += 2
|
||||
try:
|
||||
for entry in entries:
|
||||
logger.notify('Removing entry: %s' % entry)
|
||||
try:
|
||||
lines.remove(entry + '\n')
|
||||
except ValueError:
|
||||
pass
|
||||
finally:
|
||||
logger.indent -= 2
|
||||
fh = open(filename, 'w')
|
||||
fh.writelines(lines)
|
||||
fh.close()
|
||||
|
||||
def is_url(name):
|
||||
"""Returns true if the name looks like a URL"""
|
||||
if ':' not in name:
|
||||
|
@ -3981,6 +3964,70 @@ def package_to_requirement(package_name):
|
|||
else:
|
||||
return name
|
||||
|
||||
def strip_sys_prefix(path):
|
||||
""" If ``path`` begins with sys.prefix, return ``path`` with
|
||||
sys.prefix stripped off. Otherwise return None."""
|
||||
if path.startswith(sys.prefix):
|
||||
return path.replace(sys.prefix, '')
|
||||
return None
|
||||
|
||||
def remove_entries_from_file(filename, entries, auto_confirm=True):
|
||||
"""Remove ``entries`` from text file ``filename``, with
|
||||
confirmation (unless ``auto_confirm`` is True)."""
|
||||
assert os.path.isfile(filename), "Cannot remove entries from nonexistent file %s" % filename
|
||||
logger.notify('Removing entries from %s:' % filename)
|
||||
logger.indent += 2
|
||||
try:
|
||||
if auto_confirm:
|
||||
response = 'y'
|
||||
else:
|
||||
for entry in entries:
|
||||
logger.notify(entry)
|
||||
response = ask('Proceed with removal (y/n)? ', ('y', 'n'))
|
||||
if response == 'y':
|
||||
fh = open(filename, 'r')
|
||||
lines = fh.readlines()
|
||||
fh.close()
|
||||
logger.indent += 2
|
||||
try:
|
||||
for entry in entries:
|
||||
logger.notify('Removing entry: %s' % entry)
|
||||
try:
|
||||
lines.remove(entry + '\n')
|
||||
except ValueError:
|
||||
pass
|
||||
finally:
|
||||
logger.indent -= 2
|
||||
fh = open(filename, 'w')
|
||||
fh.writelines(lines)
|
||||
fh.close()
|
||||
finally:
|
||||
logger.indent -= 2
|
||||
|
||||
def remove_paths(paths, auto_confirm=False):
|
||||
"""Remove paths in iterable ``paths`` with confirmation
|
||||
(unless ``auto_confirm`` is True)."""
|
||||
|
||||
logger.notify('Within environment %s, removing:' % sys.prefix)
|
||||
logger.indent += 2
|
||||
try:
|
||||
if auto_confirm:
|
||||
response = 'y'
|
||||
else:
|
||||
for path in paths:
|
||||
logger.notify(strip_sys_prefix(path))
|
||||
response = ask('Proceed with removal (y/n)? ', ('y', 'n'))
|
||||
if response == 'y':
|
||||
for path in paths:
|
||||
if os.path.isdir(path):
|
||||
logger.notify('Removing directory %s' % strip_sys_prefix(path))
|
||||
shutil.rmtree(path)
|
||||
elif os.path.isfile(path):
|
||||
logger.notify('Removing file %s' % strip_sys_prefix(path))
|
||||
os.remove(path)
|
||||
finally:
|
||||
logger.indent -= 2
|
||||
|
||||
def splitext(path):
|
||||
"""Like os.path.splitext, but take off .tar too"""
|
||||
base, ext = posixpath.splitext(path)
|
||||
|
|
Loading…
Reference in a new issue