Commit a3acb0da authored by jim's avatar jim

Added a recipe update method.


git-svn-id: http://svn.zope.org/repos/main/zc.buildout/trunk@70561 62d5b8a3-27da-0310-9561-8e5933582275
parent 66e1e9ce
...@@ -28,7 +28,12 @@ Next Release ...@@ -28,7 +28,12 @@ Next Release
Feature Changes Feature Changes
--------------- ---------------
Renamed the runsetup command to setup. (The old name still works.) - Renamed the runsetup command to setup. (The old name still works.)
- Added a recipe update method. Now install is only called when a part
is installed for the first time, or after an uninstall. Otherwise,
update is called. For backward compatibility, recipes that don't
define update methiods are still supported.
1.0.0b9 (2006-10-02) 1.0.0b9 (2006-10-02)
==================== ====================
......
...@@ -288,10 +288,21 @@ class Buildout(dict): ...@@ -288,10 +288,21 @@ class Buildout(dict):
for part in reversed(installed_parts): for part in reversed(installed_parts):
if part in install_parts: if part in install_parts:
old_options = installed_part_options[part].copy() old_options = installed_part_options[part].copy()
old_options.pop('__buildout_installed__') installed_files = old_options.pop('__buildout_installed__')
new_options = self.get(part) new_options = self.get(part)
if old_options == new_options: if old_options == new_options:
# The options are the same, but are all of the
# installed files still there? If not, we should
# reinstall.
if not installed_files:
continue continue
for f in installed_files.split('\n'):
if not os.path.exists(self._buildout_path(f)):
break
else:
continue
# output debugging info
for k in old_options: for k in old_options:
if k not in new_options: if k not in new_options:
self._logger.debug("Part: %s, dropped option %s", self._logger.debug("Part: %s, dropped option %s",
...@@ -305,6 +316,7 @@ class Buildout(dict): ...@@ -305,6 +316,7 @@ class Buildout(dict):
if k not in old_options: if k not in old_options:
self._logger.debug("Part: %s, new option %s", self._logger.debug("Part: %s, new option %s",
part, k) part, k)
elif not uninstall_missing: elif not uninstall_missing:
continue continue
...@@ -316,17 +328,52 @@ class Buildout(dict): ...@@ -316,17 +328,52 @@ class Buildout(dict):
# install new parts # install new parts
for part in install_parts: for part in install_parts:
signature = self[part].pop('__buildout_signature__')
saved_options = self[part].copy()
if part in installed_parts:
self._logger.info('Updating %s', part)
old_options = installed_part_options[part]
old_installed_files = old_options['__buildout_installed__']
try:
update = recipes[part].update
except AttributeError:
update = recipes[part].install
self._logger.warning(
"The recipe for %s doesn't define an update "
"method. Using it's install method",
part)
try:
installed_files = update()
except:
installed_parts.remove(part)
self._uninstall(old_installed_files)
raise
if installed_files is None:
installed_files = old_installed_files.split('\n')
else:
self._logger.info('Installing %s', part) self._logger.info('Installing %s', part)
installed_part_options[part] = self[part].copy() installed_files = recipes[part].install()
del self[part]['__buildout_signature__'] if installed_files is None:
installed_files = recipes[part].install() or () self._logger.warning(
"The %s install returned None. A path or "
"iterable os paths should be returned.",
part)
installed_files = ()
if isinstance(installed_files, str): if isinstance(installed_files, str):
installed_files = [installed_files] installed_files = [installed_files]
installed_part_options[part]['__buildout_installed__'] = (
'\n'.join(installed_files) installed_part_options[part] = saved_options
) saved_options['__buildout_installed__'
] = '\n'.join(installed_files)
saved_options['__buildout_signature__'] = signature
if part not in installed_parts: if part not in installed_parts:
installed_parts.append(part) installed_parts.append(part)
finally: finally:
installed_part_options['buildout']['parts'] = ' '.join( installed_part_options['buildout']['parts'] = ' '.join(
[p for p in conf_parts if p in installed_parts] [p for p in conf_parts if p in installed_parts]
...@@ -475,7 +522,9 @@ class Buildout(dict): ...@@ -475,7 +522,9 @@ class Buildout(dict):
return self._buildout_path(self['buildout']['installed']) return self._buildout_path(self['buildout']['installed'])
def _uninstall(self, installed): def _uninstall(self, installed):
for f in installed.split(): for f in installed.split('\n'):
if not f:
continue
f = self._buildout_path(f) f = self._buildout_path(f)
if os.path.isdir(f): if os.path.isdir(f):
shutil.rmtree(f) shutil.rmtree(f)
......
...@@ -14,9 +14,8 @@ This document describes how to define buildouts using buildout ...@@ -14,9 +14,8 @@ This document describes how to define buildouts using buildout
configuration files and recipes. There are three ways to set up the configuration files and recipes. There are three ways to set up the
buildout software and create a buildout instance: buildout software and create a buildout instance:
1. Install the zc.buildout egg with easy_install `easy_install 1. Install the zc.buildout egg with easy_install and use the buildout
<http://peak.telecommunity.com/DevCenter/EasyInstall>`_ and use the script installed in a Python scripts area.
buildout script installed in a Python scripts area.
2. Use the buildout bootstrap script to create a buildout that 2. Use the buildout bootstrap script to create a buildout that
includes both the setuptools and zc.buildout eggs. This allows you includes both the setuptools and zc.buildout eggs. This allows you
...@@ -31,7 +30,7 @@ buildout software and create a buildout instance: ...@@ -31,7 +30,7 @@ buildout software and create a buildout instance:
Often, a software project will be managed in a software repository, Often, a software project will be managed in a software repository,
such as a subversion repository, that includes some software source such as a subversion repository, that includes some software source
directories, buildout configuration files, and a copy of the buildout directories, buildout configuration files, and a copy of the buildout
bootstrap script, To work on the project, one would check out the bootstrap script. To work on the project, one would check out the
project from the repository and run the bootstrap script which project from the repository and run the bootstrap script which
installs setuptools and zc.buildout into the checkout as well as any installs setuptools and zc.buildout into the checkout as well as any
parts defined. parts defined.
...@@ -92,14 +91,23 @@ A part is simply something to be created by a buildout. It can be ...@@ -92,14 +91,23 @@ A part is simply something to be created by a buildout. It can be
almost anything, such as a Python package, a program, a directory, or almost anything, such as a Python package, a program, a directory, or
even a configuration file. even a configuration file.
Recipes
-------
A part is created by a recipe. Recipes are always installed as Python A part is created by a recipe. Recipes are always installed as Python
eggs. They can be downloaded from a package server, such as the eggs. They can be downloaded from a package server, such as the
Python Package Index, or they can be developed as part of a project. Python Package Index, or they can be developed as part of a project
Let's create a recipe as part of the sample project. We'll create a using a "develop" egg.
recipe for creating directories.
A develop egg is a special kind of egg that gets installed as an "egg
link" that contains the name of a source directory. Develop eggs
don't have to be packaged for distribution to be used and can be
modified in place, which is especially useful while they are being
developed.
First, we'll create a recipes directory for Let's create a recipe as part of the sample project. We'll create a
our local recipes: recipe for creating directories. First, we'll create a recipes source
directory for our local recipes:
>>> mkdir(sample_buildout, 'recipes') >>> mkdir(sample_buildout, 'recipes')
...@@ -128,56 +136,81 @@ and then we'll create a source file for our mkdir recipe: ...@@ -128,56 +136,81 @@ and then we'll create a source file for our mkdir recipe:
... ...
... def install(self): ... def install(self):
... path = self.options['path'] ... path = self.options['path']
... if not os.path.isdir(path):
... logging.getLogger(self.name).info( ... logging.getLogger(self.name).info(
... 'Creating directory %s', os.path.basename(path)) ... 'Creating directory %s', os.path.basename(path))
... os.mkdir(path) ... os.mkdir(path)
... return path ... return path
...
... def update(self):
... pass
... """) ... """)
The recipe defines a constructor that takes a buildout object, a part Currently, recipes must define 3 methods [#future_recipe_methods]_:
name, and an options dictionary. It saves them in instance attributes.
If the path is relative, we'll interpret it as relative to the - a constructor,
buildout directory. The buildout object passed in is a mapping from
section name to a mapping of options for that section. The buildout - an install method, and
directory is available as the directory option of the buildout
section. We normalize the path and save it back into the options
directory.
Any time we use data from another section, it is important to reflect - an update method.
that data in the recipe's options when the recipe is constructed.
The constructor is responsible for updating a parts options to reflect
data read from other sections. The buildout system keeps track of
whether a part specification has changed. A part specification has
changed if it's options, after ajusting for data read from other
sections, has changed, or if the recipe has changed. Only the options
for the part are considered. If data are read from other sections,
then that information has to be reflected in the parts options. In
the Mkdir example, the given path is interpreted relative to the
buildout directory, and data from the buildout directory is read. The
path option is updated to reflect this. If the directory option was
changed in the buildout sections, we would know to update parts
created using the mkdir recipe using relative path names.
When buildout is run, it saves configuration data for installed parts When buildout is run, it saves configuration data for installed parts
in a file named installed.cfg. In subsequent runs, it compares in a file named ".installed.cfg". In subsequent runs, it compares
part-configuration data stored in the installed.cfg file and the part-configuration data stored in the .installed.cfg file and the
part-configuration data loaded from the configuration files as part-configuration data loaded from the configuration files as
modified by recipe constructors to decide if the configuration of a modified by recipe constructors to decide if the configuration of a
part has changed. If the configuration has changed, or if the recipe part has changed. If the configuration has changed, or if the recipe
has changed, then the part is uninstalled before reinstalling it. The has changed, then the part is uninstalled and reinstalled. The
buildout only looks at the part's options, so any data used to buildout only looks at the part's options, so any data used to
configure the part needs to be reflected in the part's options. It is configure the part needs to be reflected in the part's options. It is
the job of a recipe constructor to make sure that the options include the job of a recipe constructor to make sure that the options include
all rel event data. all relevent data.
Of course, parts are also uninstalled if they are no-longer used. Of course, parts are also uninstalled if they are no-longer used.
The install method is responsible for creating the part. In this The recipe defines a constructor that takes a buildout object, a part
case, we need the path of the directory to create. We'll use a name, and an options dictionary. It saves them in instance attributes.
path option from our options dictionary. If the path is relative, we'll interpret it as relative to the
buildout directory. The buildout object passed in is a mapping from
The install method logs what it's doing using the Python logging call. section name to a mapping of options for that section. The buildout
directory is available as the directory option of the buildout
section. We normalize the path and save it back into the options
directory.
We return the path that we installed. If the part is uninstalled or The install method is responsible for creating the part. In this
reinstalled, then the path returned will be removed by the buildout case, we need the path of the directory to create. We'll use a path
machinery. A recipe install method is expected to return None, a option from our options dictionary. The install method logs what it's
string, or an iterable of strings containing paths to be removed if a doing using the Python logging call. We return the path that we
part is uninstalled. For most recipes, this is all of the uninstall installed. If the part is uninstalled or reinstalled, then the path
support needed. A recipe can provide custom uninstall support as will returned will be removed by the buildout machinery. A recipe install
be described later. method is expected to return a string, or an iterable of strings
containing paths to be removed if a part is uninstalled. For most
recipes, this is all of the uninstall support needed.
The update method is responsible for updating an already installed
part. An empty method is often provided, as in this example, if parts
can't be updated. An update method can return None, a string, or an
iterable of strings. If a string or iterable of strings is returned,
then the saved list of paths to be uninstalled is updated with the new
information.
We need to provide packaging information so that our recipe can be We need to provide packaging information so that our recipe can be
installed as an egg. We need to define a setup script for this: installed as a develop egg. The minimum information we need to specify
[#packaging_info]_ is a name. For recipes, we also need to define the
names of the recipe classes as entry points. Packaging information is
provided via a setup.py script:
>>> write(sample_buildout, 'recipes', 'setup.py', >>> write(sample_buildout, 'recipes', 'setup.py',
... """ ... """
...@@ -189,21 +222,11 @@ installed as an egg. We need to define a setup script for this: ...@@ -189,21 +222,11 @@ installed as an egg. We need to define a setup script for this:
... ) ... )
... """) ... """)
This setup script is incomplete. It doesn't describe what is to be
included in a distribution. This is fine if we never actually create
a distribution. If recipes are going to be used only internally in a
buildout, then we needn't include distribution information. If we
wanted to use the same recipes in multiple buildouts, then we'd need
to include proper distribution data. To find out more about creating
distributions, see the setuptools documentation.
Our setup script defines an entry point. Entry points provide Our setup script defines an entry point. Entry points provide
a way for an egg to define the services it provides. Here we've said a way for an egg to define the services it provides. Here we've said
that we define a zc.buildout entry point named default. Recipe that we define a zc.buildout entry point named mkdir. Recipe
classes must be exposed as entry points in the zc.buildout group. we classes must be exposed as entry points in the zc.buildout group. we
give entry points names within the group. The name "default" is give entry points names within the group.
somewhat special because it allows a recipe to be referenced using a
package name without naming an entry point.
We also need a README.txt for our recipes to avoid an annoying warning We also need a README.txt for our recipes to avoid an annoying warning
from distutils, on which setuptools and zc.buildout are based: from distutils, on which setuptools and zc.buildout are based:
...@@ -326,6 +349,15 @@ we'll see that the directory gets removed and recreated: ...@@ -326,6 +349,15 @@ we'll see that the directory gets removed and recreated:
d parts d parts
d recipes d recipes
If any of the files or directories created by a recipe are removed,
the part will be reinstalled:
>>> rmdir(sample_buildout, 'mydata')
>>> print system(buildout),
buildout: Develop: /sample-buildout/recipes/setup.py
buildout: Uninstalling data-dir
buildout: Installing data-dir
data-dir: Creating directory mydata
Error reporting Error reporting
--------------- ---------------
...@@ -404,6 +436,9 @@ allow us to see interactions with the buildout: ...@@ -404,6 +436,9 @@ allow us to see interactions with the buildout:
... items.sort() ... items.sort()
... for option, value in items: ... for option, value in items:
... print option, value ... print option, value
... return ()
...
... update = install
... """) ... """)
In this example, we've used a simple base class that provides a In this example, we've used a simple base class that provides a
...@@ -488,8 +523,8 @@ the buildout: ...@@ -488,8 +523,8 @@ the buildout:
>>> print system(buildout), >>> print system(buildout),
buildout: Develop: /sample-buildout/recipes/setup.py buildout: Develop: /sample-buildout/recipes/setup.py
buildout: Installing data-dir buildout: Updating data-dir
buildout: Installing debug buildout: Updating debug
File 1 mydata/file File 1 mydata/file
File 2 mydata/file.out File 2 mydata/file.out
File 3 var/file3 File 3 var/file3
...@@ -799,7 +834,7 @@ Options can also be combined in the usual Unix way, as in: ...@@ -799,7 +834,7 @@ Options can also be combined in the usual Unix way, as in:
>>> print system(buildout+' -vcother.cfg debug:op1=foo'), >>> print system(buildout+' -vcother.cfg debug:op1=foo'),
buildout: Develop: /sample-buildout/recipes/setup.py buildout: Develop: /sample-buildout/recipes/setup.py
buildout: Installing debug buildout: Updating debug
name other name other
op1 foo op1 foo
recipe recipes:debug recipe recipes:debug
...@@ -1000,8 +1035,8 @@ Now, if we run the buildout without the install command: ...@@ -1000,8 +1035,8 @@ Now, if we run the buildout without the install command:
x 1 x 1
buildout: Installing d2 buildout: Installing d2
d2: Creating directory data2 d2: Creating directory data2
buildout: Installing d3 buildout: Updating d3
buildout: Installing d4 buildout: Updating d4
We see the output of the debug recipe and that data2 was created. We We see the output of the debug recipe and that data2 was created. We
also see that d1 and d2 have gone away: also see that d1 and d2 have gone away:
...@@ -1346,3 +1381,13 @@ We see that out extension is loaded and executed: ...@@ -1346,3 +1381,13 @@ We see that out extension is loaded and executed:
ext ['buildout'] ext ['buildout']
buildout: Develop: /sample-bootstrapped/demo/setup.py buildout: Develop: /sample-bootstrapped/demo/setup.py
.. [#future_recipe_methods] In the future, additional mathods may be
added. Older recipes with fewer methods will still be
supported.
.. [#packaging_info] If we wanted to create a distribution from this
package, we would need specify much more information. See the
`setuptools documentation
<http://peak.telecommunity.com/DevCenter/setuptools>`_.
...@@ -178,6 +178,8 @@ def test_comparing_saved_options_with_funny_characters(): ...@@ -178,6 +178,8 @@ def test_comparing_saved_options_with_funny_characters():
... def install(self): ... def install(self):
... open('t', 'w').write('t') ... open('t', 'w').write('t')
... return 't' ... return 't'
...
... update = install
... ''') ... ''')
...@@ -214,7 +216,7 @@ uninstalling anything because the configuration hasn't changed. ...@@ -214,7 +216,7 @@ uninstalling anything because the configuration hasn't changed.
>>> print system(buildout), # doctest: +ELLIPSIS >>> print system(buildout), # doctest: +ELLIPSIS
buildout: Develop: ...setup.py buildout: Develop: ...setup.py
buildout: Installing debug buildout: Updating debug
""" """
...@@ -277,22 +279,22 @@ Options: ...@@ -277,22 +279,22 @@ Options:
<BLANKLINE> <BLANKLINE>
-q -q
<BLANKLINE> <BLANKLINE>
Deccreaae the level of verbosity. This option can be used multiple times. Decrease the level of verbosity. This option can be used multiple times.
<BLANKLINE> <BLANKLINE>
-c config_file -c config_file
<BLANKLINE> <BLANKLINE>
Specify the path to the buildout configuration file to be used. Specify the path to the buildout configuration file to be used.
This defaults to the file named"buildout.cfg" in the current This defaults to the file named "buildout.cfg" in the current
working directory. working directory.
<BLANKLINE> <BLANKLINE>
Assignments are of the form: section:option=value and are used to Assignments are of the form: section:option=value and are used to
provide configuration options that override those givem in the provide configuration options that override those given in the
configuration file. For example, to run the buildout in offline mode, configuration file. For example, to run the buildout in offline mode,
use buildout:offline=true. use buildout:offline=true.
<BLANKLINE> <BLANKLINE>
Options and assignments can be interspersed. Options and assignments can be interspersed.
<BLANKLINE> <BLANKLINE>
Commmonds: Commands:
<BLANKLINE> <BLANKLINE>
install [parts] install [parts]
<BLANKLINE> <BLANKLINE>
...@@ -324,22 +326,22 @@ Options: ...@@ -324,22 +326,22 @@ Options:
<BLANKLINE> <BLANKLINE>
-q -q
<BLANKLINE> <BLANKLINE>
Deccreaae the level of verbosity. This option can be used multiple times. Decrease the level of verbosity. This option can be used multiple times.
<BLANKLINE> <BLANKLINE>
-c config_file -c config_file
<BLANKLINE> <BLANKLINE>
Specify the path to the buildout configuration file to be used. Specify the path to the buildout configuration file to be used.
This defaults to the file named"buildout.cfg" in the current This defaults to the file named "buildout.cfg" in the current
working directory. working directory.
<BLANKLINE> <BLANKLINE>
Assignments are of the form: section:option=value and are used to Assignments are of the form: section:option=value and are used to
provide configuration options that override those givem in the provide configuration options that override those given in the
configuration file. For example, to run the buildout in offline mode, configuration file. For example, to run the buildout in offline mode,
use buildout:offline=true. use buildout:offline=true.
<BLANKLINE> <BLANKLINE>
Options and assignments can be interspersed. Options and assignments can be interspersed.
<BLANKLINE> <BLANKLINE>
Commmonds: Commands:
<BLANKLINE> <BLANKLINE>
install [parts] install [parts]
<BLANKLINE> <BLANKLINE>
......
...@@ -44,6 +44,7 @@ zc.buildout used: ...@@ -44,6 +44,7 @@ zc.buildout used:
... for project in 'zc.buildout', 'setuptools': ... for project in 'zc.buildout', 'setuptools':
... req = pkg_resources.Requirement.parse(project) ... req = pkg_resources.Requirement.parse(project)
... print project, pkg_resources.working_set.find(req).version ... print project, pkg_resources.working_set.find(req).version
... return ()
... """) ... """)
......
...@@ -8,6 +8,8 @@ To do ...@@ -8,6 +8,8 @@ To do
Change History Change History
************** **************
Updated to work with zc.buildout 1.0.0b10.
1.0.0b1 1.0.0b1
======= =======
......
...@@ -38,6 +38,9 @@ around the egg recipe: ...@@ -38,6 +38,9 @@ around the egg recipe:
... for d in ws: ... for d in ws:
... print d ... print d
... print 'extra paths:', self.egg.extra_paths ... print 'extra paths:', self.egg.extra_paths
... return ()
...
... update = install
... """) ... """)
Here we instantiated the egg recipe in the constructor, saving it in Here we instantiated the egg recipe in the constructor, saving it in
......
...@@ -67,7 +67,7 @@ class Custom: ...@@ -67,7 +67,7 @@ class Custom:
def install(self): def install(self):
if self.buildout['buildout'].get('offline') == 'true': if self.buildout['buildout'].get('offline') == 'true':
return return ()
options = self.options options = self.options
distribution = options.get('eggs', self.name).strip() distribution = options.get('eggs', self.name).strip()
build_ext = dict([ build_ext = dict([
...@@ -80,3 +80,6 @@ class Custom: ...@@ -80,3 +80,6 @@ class Custom:
self.links, self.index, options['executable'], [options['_e']], self.links, self.index, options['executable'], [options['_e']],
) )
return ()
update = install
...@@ -119,3 +119,6 @@ class Egg: ...@@ -119,3 +119,6 @@ class Egg:
interpreter=options.get('interpreter'), interpreter=options.get('interpreter'),
) )
return ()
update = install
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
Change History Change History
************** **************
Updated to work with zc.buildout 1.0.0b10.
1.0.0b2 1.0.0b2
======= =======
......
...@@ -54,6 +54,8 @@ class TestRunner: ...@@ -54,6 +54,8 @@ class TestRunner:
)), )),
) )
update = install
arg_template = """[ arg_template = """[
'--test-path', %(TESTPATH)s, '--test-path', %(TESTPATH)s,
]""" ]"""
......
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