Commit 65b4b94c authored by Xavier Thompson's avatar Xavier Thompson

[fix] Fix working set sorting

If a dist in the computed working set is at a location shared with
other dists - such as site-packages - then when generating scripts
these other packages may overshadow the next items on the sys.path
and result in importing a different version than the one installed
and intended by buildout.

To avert this, a sort of the working set was introduced at various
points just before generating a script.

However, that sort put the paths referenced from an `.egg-link` in
./develop-eggs first. This is truly problematic because dists from
site-packages which are not eggs - e.g. dists installed with pip -
can become referenced as `.egg-link` during buildout bootstrap and
the sort then causes site-packages to be one of the first items in
sys.path.

In particular when running buildout bootstrap from a venv in which
zc.buildout was installed by pip, if any one of zc.buildout or its
dependencies from the venv meets the version requirements, then it
can cause the generated bin/buildout to import the dists only from
the venv's site-packages, even when some do not meet requirements.

To fix this, the sort now puts the dists from `./eggs` first as we
know their locations contain only a single dist, and then puts the
dists from ./develop-eggs which have locations inside the buildout
directory before the others.

The previous sort was also activating all the dists from the paths
of the already activated dists.

Note that this also means that any working set must be manipulated
with care in general to avoid activating unintended dists from the
locations of the already activated dists.
parent 8a649631
...@@ -658,10 +658,12 @@ class Buildout(DictMixin): ...@@ -658,10 +658,12 @@ class Buildout(DictMixin):
ws = pkg_resources.WorkingSet(entries) ws = pkg_resources.WorkingSet(entries)
ws.require('zc.buildout') ws.require('zc.buildout')
options = self['buildout'] options = self['buildout']
buildout_dir = options['directory']
eggs_dir = options['eggs-directory'] eggs_dir = options['eggs-directory']
develop_eggs_dir = options['develop-eggs-directory'] develop_eggs_dir = options['develop-eggs-directory']
ws = zc.buildout.easy_install.sort_working_set( ws = zc.buildout.easy_install.sort_working_set(
ws, ws,
buildout_dir=buildout_dir,
eggs_dir=eggs_dir, eggs_dir=eggs_dir,
develop_eggs_dir=develop_eggs_dir develop_eggs_dir=develop_eggs_dir
) )
...@@ -1204,10 +1206,12 @@ class Buildout(DictMixin): ...@@ -1204,10 +1206,12 @@ class Buildout(DictMixin):
# the new dist is different, so we've upgraded. # the new dist is different, so we've upgraded.
# Update the scripts and return True # Update the scripts and return True
options = self['buildout'] options = self['buildout']
buildout_dir = options['directory']
eggs_dir = options['eggs-directory'] eggs_dir = options['eggs-directory']
develop_eggs_dir = options['develop-eggs-directory'] develop_eggs_dir = options['develop-eggs-directory']
ws = zc.buildout.easy_install.sort_working_set( ws = zc.buildout.easy_install.sort_working_set(
ws, ws,
buildout_dir=buildout_dir,
eggs_dir=eggs_dir, eggs_dir=eggs_dir,
develop_eggs_dir=develop_eggs_dir develop_eggs_dir=develop_eggs_dir
) )
......
...@@ -1958,7 +1958,7 @@ def _move_to_eggs_dir_and_compile(dist, dest): ...@@ -1958,7 +1958,7 @@ def _move_to_eggs_dir_and_compile(dist, dest):
return newdist return newdist
def sort_working_set(ws, eggs_dir, develop_eggs_dir): def get_develop_paths(develop_eggs_dir):
develop_paths = set() develop_paths = set()
pattern = os.path.join(develop_eggs_dir, '*.egg-link') pattern = os.path.join(develop_eggs_dir, '*.egg-link')
for egg_link in glob.glob(pattern): for egg_link in glob.glob(pattern):
...@@ -1966,21 +1966,30 @@ def sort_working_set(ws, eggs_dir, develop_eggs_dir): ...@@ -1966,21 +1966,30 @@ def sort_working_set(ws, eggs_dir, develop_eggs_dir):
path = f.readline().strip() path = f.readline().strip()
if path: if path:
develop_paths.add(path) develop_paths.add(path)
return develop_paths
sorted_paths = []
egg_paths = [] def sort_working_set(ws, buildout_dir, eggs_dir, develop_eggs_dir):
other_paths = [] develop_paths = get_develop_paths(develop_eggs_dir)
dists_priorities = tuple([] for i in range(5))
for dist in ws: for dist in ws:
path = dist.location path = dist.location
if path in develop_paths: if os.path.commonprefix([path, eggs_dir]) == eggs_dir:
sorted_paths.append(path) # Dists from eggs first because we know they contain a single dist.
elif os.path.commonprefix([path, eggs_dir]) == eggs_dir: priority = 0
egg_paths.append(path) if os.path.commonprefix([path, buildout_dir]) == buildout_dir:
# We assume internal locations contain a single dist too.
priority = 1 + 2 * (path not in develop_paths) # 1 or 3
else: else:
other_paths.append(path) priority = 2 + 2 * (path not in develop_paths) # 2 or 4
sorted_paths.extend(egg_paths) dists_priorities[priority].append(dist)
sorted_paths.extend(other_paths) # We add dists to an empty working set manually instead of adding the paths
return pkg_resources.WorkingSet(sorted_paths) # to avoid activating other dists at the same locations.
ws = pkg_resources.WorkingSet([])
for dists in dists_priorities:
for dist in dists:
ws.add(dist)
return ws
NOT_PICKED_AND_NOT_ALLOWED = """\ NOT_PICKED_AND_NOT_ALLOWED = """\
......
...@@ -51,6 +51,7 @@ class Eggs(object): ...@@ -51,6 +51,7 @@ class Eggs(object):
if host.strip() != '']) if host.strip() != ''])
self.allow_hosts = allow_hosts self.allow_hosts = allow_hosts
self.buildout_dir = b_options['directory']
options['eggs-directory'] = b_options['eggs-directory'] options['eggs-directory'] = b_options['eggs-directory']
options['_e'] = options['eggs-directory'] # backward compat. options['_e'] = options['eggs-directory'] # backward compat.
options['develop-eggs-directory'] = b_options['develop-eggs-directory'] options['develop-eggs-directory'] = b_options['develop-eggs-directory']
...@@ -77,6 +78,7 @@ class Eggs(object): ...@@ -77,6 +78,7 @@ class Eggs(object):
distributions=orig_distributions + list(extra), distributions=orig_distributions + list(extra),
develop_eggs_dir=options['develop-eggs-directory'], develop_eggs_dir=options['develop-eggs-directory'],
eggs_dir=options['eggs-directory'], eggs_dir=options['eggs-directory'],
buildout_dir=self.buildout_dir,
offline=(buildout_section.get('offline') == 'true'), offline=(buildout_section.get('offline') == 'true'),
newest=(buildout_section.get('newest') == 'true'), newest=(buildout_section.get('newest') == 'true'),
links=self.links, links=self.links,
...@@ -98,6 +100,7 @@ class Eggs(object): ...@@ -98,6 +100,7 @@ class Eggs(object):
distributions, distributions,
eggs_dir, eggs_dir,
develop_eggs_dir, develop_eggs_dir,
buildout_dir,
offline=False, offline=False,
newest=True, newest=True,
links=(), links=(),
...@@ -140,7 +143,7 @@ class Eggs(object): ...@@ -140,7 +143,7 @@ class Eggs(object):
allow_hosts=allow_hosts, allow_hosts=allow_hosts,
allow_unknown_extras=allow_unknown_extras) allow_unknown_extras=allow_unknown_extras)
ws = zc.buildout.easy_install.sort_working_set( ws = zc.buildout.easy_install.sort_working_set(
ws, eggs_dir, develop_eggs_dir ws, buildout_dir, eggs_dir, develop_eggs_dir
) )
cache_storage[cache_key] = ws cache_storage[cache_key] = ws
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment