Commit c13d5c58 authored by Jim Fulton's avatar Jim Fulton

Added newest keyword parameter to the install and build functions to

allow for getting less than the newest but still getting what's
necessary.
parent f752e932
############################################################################## #############################################################################
# #
# Copyright (c) 2005 Zope Corporation and Contributors. # Copyright (c) 2005 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
...@@ -85,8 +85,47 @@ def _get_index(executable, index_url, find_links): ...@@ -85,8 +85,47 @@ def _get_index(executable, index_url, find_links):
_indexes[key] = index _indexes[key] = index
return index return index
def _satisfied(req, env, dest, executable, index, links): clear_index_cache = _indexes.clear
dists = [dist for dist in env[req.project_name] if dist in req]
if sys.platform == 'win32':
# work around spawn lamosity on windows
# XXX need safe quoting (see the subproces.list2cmdline) and test
def _safe_arg(arg):
return '"%s"' % arg
else:
_safe_arg = str
_easy_install_cmd = _safe_arg(
'from setuptools.command.easy_install import main; main()'
)
class Installer:
def __init__(self,
dest=None,
links=(),
index=None,
executable=sys.executable,
always_unzip=False,
path=None,
newest=True,
):
self._dest = dest
self._links = list(links)
self._index_url = index
self._executable = executable
self._always_unzip = always_unzip
path = (path and path[:] or []) + buildout_and_setuptools_path
if dest is not None and dest not in path:
path.insert(0, dest)
self._path = path
self._newest = newest
self._env = pkg_resources.Environment(path,
python=_get_version(executable))
self._index = _get_index(executable, index, links)
def _satisfied(self, req):
dists = [dist for dist in self._env[req.project_name] if dist in req]
if not dists: if not dists:
logger.debug('We have no distributions for %s', req.project_name) logger.debug('We have no distributions for %s', req.project_name)
return None return None
...@@ -99,6 +138,12 @@ def _satisfied(req, env, dest, executable, index, links): ...@@ -99,6 +138,12 @@ def _satisfied(req, env, dest, executable, index, links):
logger.debug('We have a develop egg for %s', req) logger.debug('We have a develop egg for %s', req)
return dist return dist
if not self._newest:
# We don't need the newest, so we'll use the newest one we
# find, which is the first returned by
# Environment.__getitem__.
return dists[0]
# Find an upprt limit in the specs, if there is one: # Find an upprt limit in the specs, if there is one:
specs = [(pkg_resources.parse_version(v), op) for (op, v) in req.specs] specs = [(pkg_resources.parse_version(v), op) for (op, v) in req.specs]
specs.sort() specs.sort()
...@@ -139,8 +184,9 @@ def _satisfied(req, env, dest, executable, index, links): ...@@ -139,8 +184,9 @@ def _satisfied(req, env, dest, executable, index, links):
# any are newer. We only do this if we're willing to install # any are newer. We only do this if we're willing to install
# something, which is only true if dest is not None: # something, which is only true if dest is not None:
if dest is not None:
best_available = _get_index(executable, index, links).obtain(req) if self._dest is not None:
best_available = self._index.obtain(req)
else: else:
best_available = None best_available = None
...@@ -148,38 +194,27 @@ def _satisfied(req, env, dest, executable, index, links): ...@@ -148,38 +194,27 @@ def _satisfied(req, env, dest, executable, index, links):
# That's a bit odd. There aren't any distros available. # That's a bit odd. There aren't any distros available.
# We should use the best one we have that meets the requirement. # We should use the best one we have that meets the requirement.
logger.debug( logger.debug(
'There are no distros available that meet %s. Using our best.', req) 'There are no distros available that meet %s. Using our best.',
req)
return best_we_have return best_we_have
else: else:
# Let's find out if we already have the best available: # Let's find out if we already have the best available:
if best_we_have.parsed_version >= best_available.parsed_version: if best_we_have.parsed_version >= best_available.parsed_version:
# Yup. Use it. # Yup. Use it.
logger.debug('We have the best distribution that satisfies\n%s', req) logger.debug(
'We have the best distribution that satisfies\n%s',
req)
return best_we_have return best_we_have
return None return None
def _call_easy_install(self, spec, ws, dest):
if sys.platform == 'win32': path = self._get_dist(pkg_resources.Requirement.parse('setuptools'),
# work around spawn lamosity on windows ws, False).location
# XXX need safe quoting (see the subproces.list2cmdline) and test
def _safe_arg(arg):
return '"%s"' % arg
else:
_safe_arg = str
_easy_install_cmd = _safe_arg(
'from setuptools.command.easy_install import main; main()'
)
def _call_easy_install(spec, env, ws, dest, links, index,
executable, always_unzip):
path = _get_dist(pkg_resources.Requirement.parse('setuptools'),
env, ws, dest, links, index, executable, False).location
args = ('-c', _easy_install_cmd, '-mUNxd', _safe_arg(dest)) args = ('-c', _easy_install_cmd, '-mUNxd', _safe_arg(dest))
if always_unzip: if self._always_unzip:
args += ('-Z', ) args += ('-Z', )
level = logger.getEffectiveLevel() level = logger.getEffectiveLevel()
if level > logging.DEBUG: if level > logging.DEBUG:
...@@ -195,23 +230,23 @@ def _call_easy_install(spec, env, ws, dest, links, index, ...@@ -195,23 +230,23 @@ def _call_easy_install(spec, env, ws, dest, links, index,
args += (dict(os.environ, PYTHONPATH=path), ) args += (dict(os.environ, PYTHONPATH=path), )
sys.stdout.flush() # We want any pending output first sys.stdout.flush() # We want any pending output first
exit_code = os.spawnle(os.P_WAIT, executable, executable, *args) exit_code = os.spawnle(os.P_WAIT, self._executable, self._executable,
*args)
assert exit_code == 0 assert exit_code == 0
def _get_dist(requirement, env, ws, def _get_dist(self, requirement, ws, always_unzip):
dest, links, index_url, executable, always_unzip):
# Maybe an existing dist is already the best dist that satisfies the # Maybe an existing dist is already the best dist that satisfies the
# requirement # requirement
dist = _satisfied(requirement, env, dest, executable, index_url, links) dist = self._satisfied(requirement)
if dist is None: if dist is None:
if dest is not None: if self._dest is not None:
logger.info("Getting new distribution for %s", requirement) logger.info("Getting new distribution for %s", requirement)
# Retrieve the dist: # Retrieve the dist:
index = _get_index(executable, index_url, links) index = self._index
dist = index.obtain(requirement) dist = index.obtain(requirement)
if dist is None: if dist is None:
raise zc.buildout.UserError( raise zc.buildout.UserError(
...@@ -233,7 +268,7 @@ def _get_dist(requirement, env, ws, ...@@ -233,7 +268,7 @@ def _get_dist(requirement, env, ws,
% requirement) % requirement)
newloc = os.path.join( newloc = os.path.join(
dest, os.path.basename(dist.location)) self._dest, os.path.basename(dist.location))
if os.path.isdir(dist.location): if os.path.isdir(dist.location):
# we got a directory. It must have been # we got a directory. It must have been
...@@ -241,7 +276,7 @@ def _get_dist(requirement, env, ws, ...@@ -241,7 +276,7 @@ def _get_dist(requirement, env, ws,
shutil.copytree(dist.location, newloc) shutil.copytree(dist.location, newloc)
else: else:
if always_unzip: if self._always_unzip:
should_unzip = True should_unzip = True
else: else:
metadata = pkg_resources.EggMetadata( metadata = pkg_resources.EggMetadata(
...@@ -270,9 +305,7 @@ def _get_dist(requirement, env, ws, ...@@ -270,9 +305,7 @@ def _get_dist(requirement, env, ws,
dist = index.fetch_distribution(requirement, tmp) dist = index.fetch_distribution(requirement, tmp)
# May need a new one. Call easy_install # May need a new one. Call easy_install
_call_easy_install( self._call_easy_install(dist.location, ws, self._dest)
dist.location, env, ws, dest, links, index_url,
executable, always_unzip)
finally: finally:
shutil.rmtree(tmp) shutil.rmtree(tmp)
...@@ -284,11 +317,11 @@ def _get_dist(requirement, env, ws, ...@@ -284,11 +317,11 @@ def _get_dist(requirement, env, ws,
# cache. # cache.
sys.path_importer_cache.clear() sys.path_importer_cache.clear()
env.scan([dest]) self._env.scan([self._dest])
dist = env.best_match(requirement, ws) dist = self._env.best_match(requirement, ws)
logger.info("Got %s", dist) logger.info("Got %s", dist)
else: else:
dist = env.best_match(requirement, ws) dist = self._env.best_match(requirement, ws)
if dist is None: if dist is None:
raise ValueError("Couldn't find", requirement) raise ValueError("Couldn't find", requirement)
...@@ -297,12 +330,14 @@ def _get_dist(requirement, env, ws, ...@@ -297,12 +330,14 @@ def _get_dist(requirement, env, ws,
if dist.has_metadata('dependency_links.txt'): if dist.has_metadata('dependency_links.txt'):
for link in dist.get_metadata_lines('dependency_links.txt'): for link in dist.get_metadata_lines('dependency_links.txt'):
link = link.strip() link = link.strip()
if link not in links: if link not in self._links:
links.append(link) self._links.append(link)
self._index = _get_index(self._executable,
self._index_url, self._links)
return dist return dist
def _maybe_add_setuptools(ws, dist, env, dest, links, index, executable): def _maybe_add_setuptools(self, ws, dist):
if dist.has_metadata('namespace_packages.txt'): if dist.has_metadata('namespace_packages.txt'):
for r in dist.requires(): for r in dist.requires():
if r.project_name == 'setuptools': if r.project_name == 'setuptools':
...@@ -317,33 +352,21 @@ def _maybe_add_setuptools(ws, dist, env, dest, links, index, executable): ...@@ -317,33 +352,21 @@ def _maybe_add_setuptools(ws, dist, env, dest, links, index, executable):
dist) dist)
requirement = pkg_resources.Requirement.parse('setuptools') requirement = pkg_resources.Requirement.parse('setuptools')
if ws.find(requirement) is None: if ws.find(requirement) is None:
dist = _get_dist(requirement, env, ws, dist = self._get_dist(requirement, ws, False)
dest, links, index, executable,
False)
ws.add(dist) ws.add(dist)
def install(specs, dest, def install(self, specs, working_set=None):
links=(), index=None,
executable=sys.executable, always_unzip=False,
path=None, working_set=None):
logger.debug('Installing %r', specs) logger.debug('Installing %r', specs)
path = path and path[:] or [] path = self._path
dest = self._dest
if dest is not None and dest not in path: if dest is not None and dest not in path:
path.insert(0, dest) path.insert(0, dest)
path += buildout_and_setuptools_path requirements = [pkg_resources.Requirement.parse(spec)
for spec in specs]
links = list(links) # make copy, because we may need to mutate
# For each spec, see if it is already installed. We create a working
# set to keep track of what we've collected and to make sue than the
# distributions assembled are consistent.
env = pkg_resources.Environment(path, python=_get_version(executable))
requirements = [pkg_resources.Requirement.parse(spec) for spec in specs]
if working_set is None: if working_set is None:
ws = pkg_resources.WorkingSet([]) ws = pkg_resources.WorkingSet([])
...@@ -351,11 +374,9 @@ def install(specs, dest, ...@@ -351,11 +374,9 @@ def install(specs, dest,
ws = working_set ws = working_set
for requirement in requirements: for requirement in requirements:
dist = _get_dist(requirement, env, ws, dist = self._get_dist(requirement, ws, self._always_unzip)
dest, links, index, executable, always_unzip)
ws.add(dist) ws.add(dist)
_maybe_add_setuptools(ws, dist, self._maybe_add_setuptools(ws, dist)
env, dest, links, index, executable)
# OK, we have the requested distributions and they're in the working # OK, we have the requested distributions and they're in the working
# set, but they may have unmet requirements. We'll simply keep # set, but they may have unmet requirements. We'll simply keep
...@@ -372,42 +393,22 @@ def install(specs, dest, ...@@ -372,42 +393,22 @@ def install(specs, dest,
[requirement] = err [requirement] = err
if dest: if dest:
logger.debug('Getting required %s', requirement) logger.debug('Getting required %s', requirement)
dist = _get_dist(requirement, env, ws, dist = self._get_dist(requirement, ws, self._always_unzip)
dest, links, index, executable, always_unzip)
ws.add(dist) ws.add(dist)
_maybe_add_setuptools(ws, dist, self._maybe_add_setuptools(ws, dist)
env, dest, links, index, executable)
else: else:
break break
return ws return ws
def build(spec, dest, build_ext, def build(self, spec, build_ext):
links=(), index=None,
executable=sys.executable,
path=None):
index_url = index
logger.debug('Building %r', spec) logger.debug('Building %r', spec)
path = path and path[:] or []
if dest is not None:
path.insert(0, dest)
path += buildout_and_setuptools_path
links = list(links) # make copy, because we may need to mutate
# For each spec, see if it is already installed. We create a working
# set to keep track of what we've collected and to make sue than the
# distributions assembled are consistent.
env = pkg_resources.Environment(path, python=_get_version(executable))
requirement = pkg_resources.Requirement.parse(spec) requirement = pkg_resources.Requirement.parse(spec)
dist = _satisfied(requirement, env, dest, executable, index_url, links) dist = self._satisfied(requirement)
if dist is not None: if dist is not None:
return [dist.location] return dist.location
undo = [] undo = []
try: try:
...@@ -416,8 +417,8 @@ def build(spec, dest, build_ext, ...@@ -416,8 +417,8 @@ def build(spec, dest, build_ext,
tmp2 = tempfile.mkdtemp('build') tmp2 = tempfile.mkdtemp('build')
undo.append(lambda : shutil.rmtree(tmp2)) undo.append(lambda : shutil.rmtree(tmp2))
index = _get_index(executable, index_url, links) dist = self._index.fetch_distribution(
dist = index.fetch_distribution(requirement, tmp2, False, True) requirement, tmp2, False, True)
if dist is None: if dist is None:
raise zc.buildout.UserError( raise zc.buildout.UserError(
"Couldn't find a source distribution for %s." "Couldn't find a source distribution for %s."
...@@ -448,19 +449,36 @@ def build(spec, dest, build_ext, ...@@ -448,19 +449,36 @@ def build(spec, dest, build_ext,
setuptools.command.setopt.edit_config( setuptools.command.setopt.edit_config(
setup_cfg, dict(build_ext=build_ext)) setup_cfg, dict(build_ext=build_ext))
tmp3 = tempfile.mkdtemp('build', dir=dest) tmp3 = tempfile.mkdtemp('build', dir=self._dest)
undo.append(lambda : shutil.rmtree(tmp3)) undo.append(lambda : shutil.rmtree(tmp3))
_call_easy_install(base, env, pkg_resources.WorkingSet(), self._call_easy_install(base, pkg_resources.WorkingSet(), tmp3)
tmp3, links, index_url, executable, True)
return _copyeggs(tmp3, dest, '.egg', undo) return _copyeggs(tmp3, self._dest, '.egg', undo)
finally: finally:
undo.reverse() undo.reverse()
[f() for f in undo] [f() for f in undo]
def install(specs, dest,
links=(), index=None,
executable=sys.executable, always_unzip=False,
path=None, working_set=None, newest=True):
installer = Installer(dest, links, index, executable, always_unzip, path,
newest)
return installer.install(specs, working_set)
def build(spec, dest, build_ext,
links=(), index=None,
executable=sys.executable,
path=None, newest=True):
installer = Installer(dest, links, index, executable, True, path, newest)
return installer.build(spec, build_ext)
def _rm(*paths): def _rm(*paths):
for path in paths: for path in paths:
if os.path.isdir(path): if os.path.isdir(path):
...@@ -478,7 +496,7 @@ def _copyeggs(src, dest, suffix, undo): ...@@ -478,7 +496,7 @@ def _copyeggs(src, dest, suffix, undo):
os.rename(os.path.join(src, name), new) os.rename(os.path.join(src, name), new)
result.append(new) result.append(new)
assert len(result) == 1 assert len(result) == 1, str(result)
undo.pop() undo.pop()
return result[0] return result[0]
......
...@@ -21,7 +21,10 @@ level that is similar to easy_install, with a few exceptions: ...@@ -21,7 +21,10 @@ level that is similar to easy_install, with a few exceptions:
- Distutils options for building extensions can be passed. - Distutils options for building extensions can be passed.
The easy_install module provides a method, install, for installing one Distribution installation
-------------------------
The easy_install module provides a function, install, for installing one
or more packages and their dependencies. The install function takes 2 or more packages and their dependencies. The install function takes 2
positional arguments: positional arguments:
...@@ -68,6 +71,13 @@ working_set ...@@ -68,6 +71,13 @@ working_set
you to call install multiple times, if necessary, to gather you to call install multiple times, if necessary, to gather
multiple sets of requirements. multiple sets of requirements.
newest
A boolian value indicating whether to search for new distributions
when already-installed distributions meet the requirement. When
this is true, the default, and when the destination directory is
not None, then the install function will search for the newest
distributions that satisfy the requirements.
The install method returns a working set containing the distributions The install method returns a working set containing the distributions
needed to meet the given requirements. needed to meet the given requirements.
...@@ -111,8 +121,17 @@ And the actual eggs were added to the eggs directory. ...@@ -111,8 +121,17 @@ And the actual eggs were added to the eggs directory.
- demo-0.2-py2.4.egg - demo-0.2-py2.4.egg
- demoneeded-1.1-py2.4.egg - demoneeded-1.1-py2.4.egg
If we ask for the demo distribution without a version restriction, If we remove the version restriction on demo, but specify a false
we'll get the newer version: value for newest, no new didstributions will be installed:
>>> ws = zc.buildout.easy_install.install(
... ['demo'], dest, links=[link_server], index=link_server+'index/',
... newest=False)
>>> ls(dest)
- demo-0.2-py2.4.egg
- demoneeded-1.1-py2.4.egg
If we leave off the newst option, we'll get an update for demo:
>>> ws = zc.buildout.easy_install.install( >>> ws = zc.buildout.easy_install.install(
... ['demo'], dest, links=[link_server], index=link_server+'index/') ... ['demo'], dest, links=[link_server], index=link_server+'index/')
...@@ -480,9 +499,13 @@ path ...@@ -480,9 +499,13 @@ path
A list of additional directories to search for locally-installed A list of additional directories to search for locally-installed
distributions. distributions.
always_unzip newest
A flag indicating that newly-downloaded distributions should be A boolian value indicating whether to search for new distributions
directories even if they could be installed as zip files. when already-installed distributions meet the requirement. When
this is true, the default, and when the destination directory is
not None, then the install function will search for the newest
distributions that satisfy the requirements.
Our link server included a source distribution that includes a simple Our link server included a source distribution that includes a simple
extension, extdemo.c:: extension, extdemo.c::
...@@ -534,6 +557,57 @@ Now if we look in our destination directory, we see we have an extdemo egg: ...@@ -534,6 +557,57 @@ Now if we look in our destination directory, we see we have an extdemo egg:
d demoneeded-1.1-py2.4.egg d demoneeded-1.1-py2.4.egg
d extdemo-1.4-py2.4-unix-i686.egg d extdemo-1.4-py2.4-unix-i686.egg
Let's update our link server with a new version of extdemo:
>>> update_extdemo()
>>> print get(link_server),
<html><body>
<a href="demo-0.1-py2.4.egg">demo-0.1-py2.4.egg</a><br>
<a href="demo-0.2-py2.4.egg">demo-0.2-py2.4.egg</a><br>
<a href="demo-0.3-py2.4.egg">demo-0.3-py2.4.egg</a><br>
<a href="demoneeded-1.0.zip">demoneeded-1.0.zip</a><br>
<a href="demoneeded-1.1.zip">demoneeded-1.1.zip</a><br>
<a href="extdemo-1.4.zip">extdemo-1.4.zip</a><br>
<a href="extdemo-1.5.zip">extdemo-1.5.zip</a><br>
<a href="index/">index/</a><br>
<a href="other-1.0-py2.4.egg">other-1.0-py2.4.egg</a><br>
</body></html>
The easy_install caches information about servers to reduce network
access. To see the update, we have to call the clear_index_cache
function to clear the index cache:
>>> zc.buildout.easy_install.clear_index_cache()
If we run build with newest set to False, we won't get an update:
>>> zc.buildout.easy_install.build(
... 'extdemo', dest,
... {'include-dirs': os.path.join(sample_buildout, 'include')},
... links=[link_server], index=link_server+'index/',
... newest=False)
'/sample-install/extdemo-1.4-py2.4-linux-i686.egg'
>>> ls(dest)
d demo-0.3-py2.4.egg
d demoneeded-1.1-py2.4.egg
d extdemo-1.4-py2.4-unix-i686.egg
But if we run it with the default True setting for newest, then we'll
get an updated egg:
>>> zc.buildout.easy_install.build(
... 'extdemo', dest,
... {'include-dirs': os.path.join(sample_buildout, 'include')},
... links=[link_server], index=link_server+'index/')
'/sample-install/extdemo-1.5-py2.4-unix-i686.egg'
d demo-0.3-py2.4.egg
d demoneeded-1.1-py2.4.egg
d extdemo-1.4-py2.4-unix-i686.egg
d extdemo-1.5-py2.4-unix-i686.egg
Handling custom build options for extensions in develop eggs Handling custom build options for extensions in develop eggs
------------------------------------------------------------ ------------------------------------------------------------
...@@ -586,6 +660,7 @@ egg link: ...@@ -586,6 +660,7 @@ egg link:
d demo-0.3-py2.4.egg d demo-0.3-py2.4.egg
d demoneeded-1.1-py2.4.egg d demoneeded-1.1-py2.4.egg
d extdemo-1.4-py2.4-linux-i686.egg d extdemo-1.4-py2.4-linux-i686.egg
d extdemo-1.5-py2.4-linux-i686.egg
- extdemo.egg-link - extdemo.egg-link
And that the source directory contains the compiled extension: And that the source directory contains the compiled extension:
......
...@@ -1047,19 +1047,22 @@ initextdemo(void) ...@@ -1047,19 +1047,22 @@ initextdemo(void)
extdemo_setup_py = """ extdemo_setup_py = """
from distutils.core import setup, Extension from distutils.core import setup, Extension
setup(name = "extdemo", version = "1.4", url="http://www.zope.org", setup(name = "extdemo", version = "%s", url="http://www.zope.org",
author="Demo", author_email="demo@demo.com", author="Demo", author_email="demo@demo.com",
ext_modules = [Extension('extdemo', ['extdemo.c'])], ext_modules = [Extension('extdemo', ['extdemo.c'])],
) )
""" """
def add_source_dist(test): def add_source_dist(test, version=1.4):
tmp = test.globs['extdemo'] = test.globs['tmpdir']('extdemo') if 'extdemo' not in test.globs:
test.globs['extdemo'] = test.globs['tmpdir']('extdemo')
tmp = test.globs['extdemo']
write = test.globs['write'] write = test.globs['write']
try: try:
write(tmp, 'extdemo.c', extdemo_c); write(tmp, 'extdemo.c', extdemo_c);
write(tmp, 'setup.py', extdemo_setup_py); write(tmp, 'setup.py', extdemo_setup_py % version);
write(tmp, 'README', ""); write(tmp, 'README', "");
write(tmp, 'MANIFEST.in', "include *.c\n"); write(tmp, 'MANIFEST.in', "include *.c\n");
test.globs['sdist'](tmp, test.globs['sample_eggs']) test.globs['sdist'](tmp, test.globs['sample_eggs'])
...@@ -1075,6 +1078,8 @@ def easy_install_SetUp(test): ...@@ -1075,6 +1078,8 @@ def easy_install_SetUp(test):
add_source_dist(test) add_source_dist(test)
test.globs['link_server'] = test.globs['start_server']( test.globs['link_server'] = test.globs['start_server'](
test.globs['sample_eggs']) test.globs['sample_eggs'])
test.globs['update_extdemo'] = lambda : add_source_dist(test, 1.5)
egg_parse = re.compile('([0-9a-zA-Z_.]+)-([0-9a-zA-Z_.]+)-py(\d[.]\d).egg$' egg_parse = re.compile('([0-9a-zA-Z_.]+)-([0-9a-zA-Z_.]+)-py(\d[.]\d).egg$'
).match ).match
......
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