From f0416ebddd39b100046d4b259c8eeb8193bd8512 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 5 Dec 2009 00:23:35 -0500 Subject: [PATCH] use shutil.move for rollback-save (fixes #27) If TMPDIR is on a different filesystem, this has the potential to lose file ownership info. In general that shouldn't be an issue; seems to be the best we can do. --- pip/req.py | 20 ++++++++++++++++---- pip/util.py | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/pip/req.py b/pip/req.py index 949190326..3f01069dc 100644 --- a/pip/req.py +++ b/pip/req.py @@ -19,7 +19,7 @@ from pip.log import logger from pip.util import display_path, rmtree, format_size from pip.util import splitext, ask, backup_dir from pip.util import url_to_filename, filename_to_url -from pip.util import is_url, is_filename, is_framework_layout +from pip.util import is_url, is_filename, renames, is_framework_layout from pip.util import make_path_relative, is_svn_page, file_contents from pip.util import has_leading_dir, split_leading_dir from pip.util import get_file_content @@ -455,6 +455,13 @@ execfile(__file__) logger.error("Can't rollback %s, nothing uninstalled." % (self.project_name,)) + def commit_uninstall(self): + if self.uninstalled: + self.uninstalled.commit() + else: + logger.error("Can't commit %s, nothing uninstalled." + % (self.project_name,)) + def archive(self, build_dir): assert self.source_dir create_archive = True @@ -748,6 +755,7 @@ class RequirementSet(object): def uninstall(self, auto_confirm=False): for req in self.requirements.values(): req.uninstall(auto_confirm=auto_confirm) + req.commit_uninstall() def install_files(self, finder, force_root_egg_info=False): unnamed = list(self.unnamed_requirements) @@ -1135,6 +1143,9 @@ class RequirementSet(object): if requirement.conflicts_with and not requirement.install_succeeded: requirement.rollback_uninstall() raise + else: + if requirement.conflicts_with and requirement.install_succeeded: + requirement.commit_uninstall() requirement.remove_temporary_source() finally: logger.indent -= 2 @@ -1418,13 +1429,14 @@ class UninstallPathSet(object): for path in self.compact(self._refuse): logger.notify(path) if response == 'y': - self.save_dir = tempfile.mkdtemp('-uninstall', 'pip-') + self.save_dir = tempfile.mkdtemp(suffix='-uninstall', + prefix='pip-') for path in paths: new_path = os.path.join(self.save_dir, path.lstrip(os.path.sep)) logger.info('Removing file or directory %s' % path) self._moved_paths.append(path) - os.renames(path, new_path) + renames(path, new_path) for pth in self.pth.values(): pth.remove() logger.notify('Successfully uninstalled %s' % self.dist.project_name) @@ -1441,7 +1453,7 @@ class UninstallPathSet(object): for path in self._moved_paths: tmp_path = os.path.join(self.save_dir, path.lstrip(os.path.sep)) logger.info('Replacing %s' % path) - os.renames(tmp_path, path) + renames(tmp_path, path) for pth in self.pth: pth.rollback() diff --git a/pip/util.py b/pip/util.py index bb23f774d..4968c5cdc 100644 --- a/pip/util.py +++ b/pip/util.py @@ -17,7 +17,7 @@ __all__ = ['rmtree', 'display_path', 'backup_dir', 'strip_prefix', 'is_svn_page', 'file_contents', 'split_leading_dir', 'has_leading_dir', 'make_path_relative', 'is_framework_layout', - 'get_file_content'] + 'get_file_content', 'renames'] def rmtree(dir): shutil.rmtree(dir, ignore_errors=True, @@ -294,3 +294,19 @@ def get_file_content(url, comes_from=None): content = f.read() f.close() return url, content + +def renames(old, new): + """Like os.renames(), but handles renaming across devices.""" + # Implementation borrowed from os.renames(). + head, tail = os.path.split(new) + if head and tail and not os.path.exists(head): + os.makedirs(head) + + shutil.move(old, new) + + head, tail = os.path.split(old) + if head and tail: + try: + os.removedirs(head) + except OSError: + pass