From 83f9d9c18b33d7eaefd25541ebfcc8eb4c825233 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 1 Apr 2009 16:51:35 -0400 Subject: [PATCH] Don't delete or modify files outside sys.prefix --- pip.py | 197 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 122 insertions(+), 75 deletions(-) diff --git a/pip.py b/pip.py index ad6b0d688..0db23dfbc 100755 --- a/pip.py +++ b/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)