diff options
| -rwxr-xr-x | setuptools/command/easy_install.py | 139 | 
1 files changed, 109 insertions, 30 deletions
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index ad7f4725..0b282d47 100755 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -840,23 +840,30 @@ Please make the appropriate changes for your system and try again.                  dir_util.remove_tree(destination, dry_run=self.dry_run)              elif os.path.exists(destination):                  self.execute(os.unlink,(destination,),"Removing "+destination) -            uncache_zipdir(destination) -            if os.path.isdir(egg_path): -                if egg_path.startswith(tmpdir): -                    f,m = shutil.move, "Moving" +            try: +                new_dist_is_zipped = False +                if os.path.isdir(egg_path): +                    if egg_path.startswith(tmpdir): +                        f,m = shutil.move, "Moving" +                    else: +                        f,m = shutil.copytree, "Copying" +                elif self.should_unzip(dist): +                    self.mkpath(destination) +                    f,m = self.unpack_and_compile, "Extracting"                  else: -                    f,m = shutil.copytree, "Copying" -            elif self.should_unzip(dist): -                self.mkpath(destination) -                f,m = self.unpack_and_compile, "Extracting" -            elif egg_path.startswith(tmpdir): -                f,m = shutil.move, "Moving" -            else: -                f,m = shutil.copy2, "Copying" - -            self.execute(f, (egg_path, destination), -                (m+" %s to %s") % -                (os.path.basename(egg_path),os.path.dirname(destination))) +                    new_dist_is_zipped = True +                    if egg_path.startswith(tmpdir): +                        f,m = shutil.move, "Moving" +                    else: +                        f,m = shutil.copy2, "Copying" +                self.execute(f, (egg_path, destination), +                    (m+" %s to %s") % +                    (os.path.basename(egg_path),os.path.dirname(destination))) +                update_dist_caches(destination, +                    fix_zipimporter_caches=new_dist_is_zipped) +            except: +                update_dist_caches(destination, fix_zipimporter_caches=False) +                raise          self.add_output(destination)          return self.egg_distribution(destination) @@ -1582,24 +1589,74 @@ def auto_chmod(func, arg, exc):      et, ev, _ = sys.exc_info()      reraise(et, (ev[0], ev[1] + (" %s %s" % (func,arg)))) -def uncache_zipdir(path): +def update_dist_caches(dist_path, fix_zipimporter_caches):      """ -    Remove any globally cached zip file related data for `path` - -    Stale zipimport.zipimporter objects need to be removed when a zip file is -    replaced as they contain cached zip file directory information. If they are -    asked to get data from their zip file, they will use that cached -    information to calculate the data location in the zip file. This calculated -    location may be incorrect for the replaced zip file, which may in turn -    cause the read operation to either fail or return incorrect data. - -    Note we have no way to clear any local caches from here. That is left up to -    whomever is in charge of maintaining that cache. +    Fix any globally cached `dist_path` related data + +    `dist_path` should be a path of a newly installed egg distribution (zipped +    or unzipped). + +    sys.path_importer_cache contains finder objects that have been cached when +    importing data from the original distribution. Any such finders need to be +    cleared since the replacement distribution might be packaged differently, +    e.g. a zipped egg distribution might get replaced with an unzipped egg +    folder or vice versa. Having the old finders cached may then cause Python +    to attempt loading modules from the replacement distribution using an +    incorrect loader. + +    zipimport.zipimporter objects are Python loaders charged with importing +    data packaged inside zip archives. If stale loaders referencing the +    original distribution, are left behind, they can fail to load modules from +    the replacement distribution. E.g. if an old zipimport.zipimporter instance +    is used to load data from a new zipped egg archive, it may cause the +    operation to attempt to locate the requested data in the wrong location - +    one indicated by the original distribution's zip archive directory +    information. Such an operation may then fail outright, e.g. report having +    read a 'bad local file header', or even worse, it may fail silently & +    return invalid data. + +    If asked, we can fix all existing zipimport.zipimporter instances instead +    of having to track them down and remove them one by one, by updating their +    shared cached zip archive directory information. This, of course, assumes +    that the replacement distribution is packaged as a zipped egg. + +    If not asked to fix existing zipimport.zipimporter instances, we do our +    best to clear any remaining zipimport.zipimporter related cached data that +    might somehow later get used when attempting to load data from the new +    distribution and thus cause such load operations to fail. Note that when +    tracking down such remaining stale data, we can not catch every possible +    usage from here, and we clear only those that we know of and have found to +    cause problems if left alive. Any remaining caches should be updated by +    whomever is in charge of maintaining them, i.e. they should be ready to +    handle us replacing their zip archives with new distributions at runtime.      """ -    normalized_path = normalize_path(path) -    _uncache(normalized_path, zipimport._zip_directory_cache) +    normalized_path = normalize_path(dist_path)      _uncache(normalized_path, sys.path_importer_cache) +    if fix_zipimporter_caches: +        _replace_zip_directory_cache_data(normalized_path) +    else: +        # Clear the relevant zipimport._zip_directory_cache data. This will not +        # remove related zipimport.zipimporter instances but should at least +        # not leave the old zip archive directory data behind to be reused by +        # some newly created zipimport.zipimporter loaders. Not strictly +        # necessary, but left in because this cache clearing was done before +        # we started replacing the zipimport._zip_directory_cache if possible, +        # and there are no relevent unit tests that we can depend on to tell us +        # if this is really needed. +        _uncache(normalized_path, zipimport._zip_directory_cache) +        # N.B. Other known sources of stale zipimport.zipimporter instances +        # that we do not clear here, but might if ever given a reason to do so. +        # * Global setuptools pkg_resources.working_set (a.k.a. 'master working +        #   set') may contain distributions which may in turn contain their +        #   zipimport.zipimporter loaders. +        # * Several zipimport.zipimporter loaders held by local variables +        #   further up the function call stack when running the setuptools +        #   installation. +        # * Already loaded modules may have their __loader__ attribute set to +        #   the exact loader instance used when importing them. Python 3.4 docs +        #   state that this information is intended mostly for introspection +        #   and so is not expected to cause us problems.  def _uncache(normalized_path, cache):      to_remove = [] @@ -1612,6 +1669,28 @@ def _uncache(normalized_path, cache):      for p in to_remove:          del cache[p] +def _replace_zip_directory_cache_data(normalized_path): +    cache = zipimport._zip_directory_cache +    to_update = [] +    prefix_len = len(normalized_path) +    for p in cache: +        np = normalize_path(p) +        if (np.startswith(normalized_path) and +                np[prefix_len:prefix_len + 1] in (os.sep, '')): +            to_update.append(p) +    # N.B. In theory, we could load the zip directory information just once for +    # all updated path spellings, and then copy it locally and update its +    # contained path strings to contain the correct spelling, but that seems +    # like a way too invasive move (this cache structure is not officially +    # documented anywhere and could in theory change with new Python releases) +    # for no significant benefit. +    for p in to_update: +        old_entry = cache.pop(p) +        zipimport.zipimporter(p) +        old_entry.clear() +        old_entry.update(cache[p]) +        cache[p] = old_entry +  def is_python(text, filename='<string>'):      "Is this string a valid Python script?"      try:  | 
