Easily creating repeatable buildouts

published Jan 19, 2008, last modified Aug 04, 2016

When you create a buildout for a production website, you want to be able to recreate that exact buildout one year from now on a different server. How do you do that? This is a more simple and better working version of an earlier post.

Background

I first wrote a rather difficult guide to getting a stable, repeatable buildout. After some helpful comments from Martijn Faassen I could make this much simpler. I decided to keep the old weblog entry around: it could be helpful for people googling for strange errors in their buildout.

I assume you know what buildout is, otherwise you would probably not be reading this. Martin Aspeli has written a tutorial on Managing projects with zc.buildout. This should be your first stop for learning more about buildout, at least if you are a Plone developer. [Edit: I cannot find this tutorial anymore. Instead see for example buildout.org or an old manual for Plone.]

My brother and colleague Reinout has written a weblog entry about the Buildout development/production strategy that we are starting to use at Zest Software. The goal is to use the same buildout for both development and production deployment.

As the buildout should be repeatable, you may want to read the repeatable test file from the zc.buildout package.

Definitions

Now I am going to write about creating a really stable and repeatable buildout. In Reinout's story that would fit in with creating a stable.cfg. If you are not concerning yourself with development or if you have a different buildout for production deployment, this would just be the buildout.cfg file.

Let's begin with some perhaps arbitrary definitions.

With stable I mean: you run the buildout script in a directory; one year later you run buildout for the second time; you get the same result as one year earlier. Use case: by accident you removed the entire parts directory and you need to rebuild it.

With repeatable I mean: you run the buildout script in a directory; one year later you run the buildout on a completely different server; both directories are exactly the same. Use case: you want to move the zope instance of a customer to a new server.

Creating the buildout

What I write in this entry should be applicable to any buildout with or without Plone. My main focus and example is a Plone buildout though. You can fix up a current buildout of course, but here let's start with a clean default buildout for Plone 3, which you can create with paster:

paster create -t plone3_buildout test
cd test
python2.4 bootstrap.py

No newer packages

When running bin/buildout by default it picks the newest package it can find for any dependency. We can tell buildout to first look if some version of that package is already available:

[buildout]
newest = false

This way, a second call of bin/buildout will not try to get any new packages. So it will finish quickly and just keep the current packages. This already makes the buildout stable in the sense that a new run of bin/buildout in that same directory will give you no new packages.

But if you want to recreate that buildout in a different directory or even a different server, you can get other versions. For starters, currently this buildout config will give you Plone 3.0.5, but a few months from now you will probably get Plone 3.1. So this is not yet a repeatable buildout.

Simple index

As an aside, while we are in the buildout section, let's quickly specify a simpler (read: faster) index than the default CheeseShop:

[buildout]
newest = false
index = http://download.zope.org/ppix

See my brother's weblog entry on ppix instead of pypi from now on.

By the way, this has nothing to do with making your buildout more stable or repeatable.

Pinning versions

Let's go get some more stability in our buildout. For some recipes and packages there are more ways to pin versions. But none of them work in all cases, except one. So we will just use that one way:

[buildout]
...
versions = versions

[versions]
...

So in your [buildout] section you add an option versions in which you point to another section that contains the versions that packages should be pinned to.

Pinning the plone version

First things first: we pick a Plone version. We simply add a line to the [versions] section that we added above:

[versions]
plone.recipe.plone = 3.0.5

This recipe has strict versions for the Plone products and packages that make up a Plone release, so for example Products.CMFPlone, Products.CMFCore, the various plone.* packages, etcetera.

And this has a zope2-url property that tells the plone.recipe.zope2install recipe that it should use Zope 2.10.5.

So the most important versions have already been pinned now.

Knowing which versions are not pinned

Wichert Akkerman came up with the following one-liner to get a list of versions that are not pinned by you, but picked by buildout:

bin/buildout -Novvvvv |sed -ne 's/^Picked: //p' | sort | uniq

Currently this returns this list:

elementtree = 1.2.7-20070827-preview
plone.recipe.distros = 1.3
plone.recipe.zope2install = 1.2
plone.recipe.zope2instance = 1.3
python-openid = 2.0.1
setuptools = 0.6c7
zc.recipe.egg = 1.0.0

This basically means that the mentioned packages are not pinned by our buildout config, but chosen (picked) by buildout. This means that they are not stable yet: rerunning this script in a few months time will likely give you different versions.

So what should you do? Very simple: you literally add those lines to the [versions] section. Now all your packages are pinned.

Pinning extra products

You can of course also pin any extra products that you want to use in your buildout. Best is to use an official release, such as a tar ball:

[productdistros]
recipe = plone.recipe.distros
urls =
    http://plone.org/products/poi/releases/1.1/poi_1.1.tgz

or a checkout of a subversion tag:

[productcheckouts]
recipe = infrae.subversion
urls =
  http://svn.plone.org/svn/collective/eXtremeManagement/tags/1.5.2/ eXtremeManagement

If for some reason there is no tag you can use, you can still specify a revision in the url with the @ sign. Thanks to Guido Wesdorp for pointing this out:

[productcheckouts]
recipe = infrae.subversion
urls =
    http://getpaid.googlecode.com/svn/trunk/products/PloneGetPaid@1132 PloneGetPaid

For now let's keep the example simple here and remove these products again from our buildout.cfg.

Conclusions

A buildout can be made very stable: just put newest = false in your buildout section. If one year later you accidentally remove the parts directory, you can rerun buildout and get your original directory back. If you remove your eggs though, you are in trouble.

Making a buildout repeatable is not that difficult either after all. Put this in your buildout.cfg:

[buildout]
...
versions = versions

[versions]

Right below [versions] add the result of this one-liner:

bin/buildout -Novvvvv |sed -ne 's/^Picked: //p' | sort | uniq

Contrary to some earlier conclusions that I drew, this is everything that is needed to pin all packages.

For pinning old-style Products, see the section on Pinning extra products.

Bonus

plone.recipe.plone rigourously depends on specific packages and products. So what do you do if you want to use a newer version of just one or two packages? For instance, I did some fixes for plone.locking which are still not in Plone 3.0.5. And for multilingual sites you really want to use a newer version with an important bug fix. You might at first think that this would be enough:

[versions]
plone.locking = 1.0.5
plone.app.i18n = 1.0.2

But running bin/buildout then gives an error:

The version, 1.0.2, is not consistent with the requirement,
'plone.app.i18n==1.0.1'.
While:
  Installing plone.
Error: Bad version 1.0.2

So you need to add two lines to the [plone] section:

[plone]
recipe = plone.recipe.plone
eggs =
    plone.locking
    plone.app.i18n

The versions for these two packages are already specified in the recipe, but with different versions than we want. By listing those two eggs here, we tell the recipe to forget its version pinnings and be happy with any version of those two packages. After this, the pinnings for these two eggs in the [versions] section can finally take effect.

The final version

Let's end with showing what our buildout.cfg now looks like. Or actually, let's let's show a buildout that uses some earlier versions: Plone 3.0.2, older recipes, and older setuptools and elementtree. And instead of plone.app.i18n 1.0 that is required by Plone 3.0.2 we pin this package to a slightly newer version 1.0.1, which is not the latest version in the CheeseShop. This older buildout should show nicely that you can have a stable, repeatable buildout months later. Try it! So here is the file itself:

[buildout]
newest = false
index = http://download.zope.org/ppix
parts =
    instance
    zope2
    plone
    zopepy
    productdistros

find-links =
    http://dist.plone.org
    http://download.zope.org/ppix/
    http://download.zope.org/distribution/
    http://effbot.org/downloads

# Add additional eggs here
# elementtree is required by Plone
eggs =
    elementtree

versions = versions

[versions]
setuptools = 0.6c6
zc.recipe.egg = 1.0.0b6
plone.recipe.plone = 3.0.2
elementtree = 1.2.6-20050316
plone.recipe.distros = 0.3
plone.recipe.zope2install = 1.0
plone.recipe.zope2instance = 1.0
python-openid = 2.0.1
plone.app.i18n = 1.0.1

[plone]
recipe = plone.recipe.plone
eggs =
    plone.app.i18n

[zope2]
recipe = plone.recipe.zope2install
url = ${plone:zope2-url}

[productdistros]
recipe = plone.recipe.distros
urls =
nested-packages =
version-suffix-packages = 

[instance]
recipe = plone.recipe.zope2instance
zope2-location = ${zope2:location}
user = admin:
http-address = 8080
#debug-mode = on
#verbose-security = on

# If you want Zope to know about any additional eggs, list them here.
# This should include any development eggs you listed in develop-eggs above,
# e.g. eggs = ${buildout:eggs} ${plone:eggs} my.package
eggs =
    ${buildout:eggs}
    ${plone:eggs}

# If you want to register ZCML slugs for any packages, list them here.
# e.g. zcml = my.package my.other.package
zcml = 

products =
    ${productdistros:location}
    ${plone:products}

[zopepy]
recipe = zc.recipe.egg
eggs = ${instance:eggs}
interpreter = zopepy
extra-paths = ${zope2:location}/lib/python
scripts = zopepy