Introduction à zc.buildout

Author:Vincent Fretin
Contributors:

Copyright (C) 2010 Vincent Fretin <vincentfretin AT gmail.com>.

Chacun est autorisé à copier, distribuer et/ou modifier ce document suivant les termes de la licence Paternité-Pas d’Utilisation Commerciale-Partage des Conditions Initiales à l’Identique 2.0 France accessible à http://creativecommons.org/licenses/by-nc-sa/2.0/fr

Le code source présent dans ce document est soumis aux conditions de la « Zope Public License », Version 2.1 (ZPL).

THE SOURCE CODE IN THIS DOCUMENT AND THE DOCUMENT ITSELF IS PROVIDED “AS IS” AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE.

Définition

Un buildout est un dossier contenant plusieurs fichiers de configuration permettant de reproduire à l’identique un environnement de développement ou de production. Paster est l’outil qui permet de générer le code squelette pour démarrer.

Introduction

Buildout est un système d’installation permettant d’obtenir le même résultat d’une machine à l’autre. L’installation est reproductible.

zc.buildout permet d’avoir un environnement (pas forcément isolé), mais d’une autre manière que virtualenv.

Une configuration buildout spécifie un ensemble de parts qui se composent :

  • d’une recette (recipe)
  • et/ou de données de configuration

Organisation d’un buildout

Un buildout est organisé de la manière suivante :

  • buildout.cfg : contient la configuration du buildout
  • bootstrap.py : script d’amorçage de buildout utilisé la première fois
  • bin/ : dossier contenant les exécutables éventuellement générés par les parts
  • src/ : par convention, dossier où sont placés les eggs en développement
  • parts/ : dossier où sont stockées les données générées des parts si besoin
  • .installed.cfg : fichier caché mémorisant l’état du buildout
  • .mr.developer.cfg : fichier caché mémorisant l’état des packages en développement si vous utilisez l’extension mr.developer.
  • eggs/ : dossier contenant les eggs activés ou non. Peut ne pas exister si on utilise un dossier partagé pour plusieurs buildouts.
  • develop-eggs/ : dossier contenant les liens vers les eggs en développement
  • extends-cache/ si vous mettez en cache les fichiers de versions téléchargés.

Seuls les fichiers buildout.cfg, bootstrap.py et extends-cache/ ont besoin d’être mis dans une gestionnaire de versions.

Nous allons prendre comme exemple le buildout.cfg pour installer archgenxml.

Suivez la page d’installation du manuel d’archgenxml2

Créez un dossier archgenxml_buildout/, placez y une copie du fichier bootstrap.py et créez un fichier buildout.cfg avec le contenu suivant :

[buildout]
parts =
    archgenxml

[archgenxml]
recipe = zc.recipe.egg:scripts
eggs = archgenxml

Note

Les versions de zc.buildout >= 1.5.2 permettent d’avoir un environnement isolé du système, cela est désactivé par défaut pour garder la compatibilité avec les versions antérieures. Pour avoir pleinement cette fonctionnalité, il faut que vous activiez l’option include-site-packages = false et que vous remplaciez zc.recipe.egg par z3c.recipe.scripts. Si vous utilisez d’autres recipes, il faut qu’elles utilisent aussi z3c.recipe.scripts en interne. Regardez dans le changelog de ces recipes, s’il est indiqué que la recipe dépend maintenant d’une version >= 1.5.0, c’est qu’elle a dû être adapté pour utiliser z3c.recipe.scripts.

Section principale [buildout]

La variable parts contient la liste des parts que buildout doit installer. La convention est de mettre une part par ligne, indenté de 4 espaces. Il peut y avoir des lignes vides et des commentaires commencant au début de la ligne.

Chaque part fournit une variable recipe pour indiquer comment ce part doit être installé, mis à jour et supprimé.

Il se peut qu’une section ne fournisse pas de recipe. Elle sert généralement pour des données de configuration que d’autres parts utiliseront.

Amorcez le buildout (choisissez bien la version de l’exécutable python) :

$ python bootstrap.py -d
Downloading http://pypi.python.org/packages/source/d/distribute/distribute-0.6.14.tar.gz
...
Creating directory '/home/vincentfretin/archgenxml_buildout/bin'.
Creating directory '/home/vincentfretin/archgenxml_buildout/parts'.
Creating directory '/home/vincentfretin/archgenxml_buildout/eggs'.
Creating directory '/home/vincentfretin/archgenxml_buildout/develop-eggs'.
Generated script '/home/vincentfretin/archgenxml_buildout/bin/buildout'.
$ tree
.
|-- bin
|   `-- buildout
|-- bootstrap.py
|-- buildout.cfg
|-- develop-eggs
|-- eggs
|   |-- distribute-0.6.28-py2.7.egg
|   `-- zc.buildout-1.5.2-py2.7.egg
`-- parts
    `-- buildout

L’amorçage (ou bootstrapping) installe distribute et zc.buildout. L’amorçage génère le script bin/buildout.

Exécutez maintenant :

$ bin/buildout
Getting distribution for 'zc.recipe.egg'.
Got zc.recipe.egg 1.3.2.
Installing archgenxml.
Getting distribution for 'archgenxml'.
Got archgenxml 2.6.
...
Generated script '/home/vincentfretin/archgenxml_buildout/bin/archgenxml'.
Generated script '/home/vincentfretin/archgenxml_buildout/bin/agx_argouml_profile'.
Generated script '/home/vincentfretin/archgenxml_buildout/bin/agx_stereotypes'.
Generated script '/home/vincentfretin/archgenxml_buildout/bin/agx_taggedvalues'.

Le egg archgenxml et ses dépendances sont téléchargés et installés et divers scripts sont générés à la fin. C’est la recipe qui dit tout ça.

À première vue, l’exécution de buildout pourrait se résumer à cet algorithme : pour chaque part de l’option parts de la section [buildout], appeler la recipe associée au part.

Revenons à la ligne recipe = zc.recipe.egg:scripts. zc.recipe.egg est un egg à deux espaces de nom et le scripts après les deux points est un entry point du groupe zc.buildout.

Si vous ne spécifiez pas de point d’entrée, un point d’entrée nommé default sera alors recherché dans l’egg.

Jetez un œil au setup.py de zc.recipe.egg :

entry_points = {'zc.buildout': ['default = %s:Scripts' % name,
                                'script = %s:Scripts' % name,
                                'scripts = %s:Scripts' % name,
                                'eggs = %s:Eggs' % name,
                                'custom = %s:Custom' % name,
                                'develop = %s:Develop' % name,
                                ]
                },

Dans ce cas, nous serions arrivé au même résultat si nous avions mis l’une de ces lignes :

recipe = zc.recipe.egg
recipe = zc.recipe.egg:default
recipe = zc.recipe.egg:scripts
recipe = zc.recipe.egg:script

L’entry point scripts pointe vers la classe Scripts du package zc.recipe.egg.

Cette classe, comme toute recipe, contient des méthodes install et update. Voir l’API des recettes

Le fichier caché .installed.cfg garde la configuration de la dernière exécution de buildout.

Lorsqu’une section a été supprimée de la configuration, cette partie sera désinstallée lors de la relance de bin/buildout. Si une section a été mise à jour, cette partie sera réinstallée. Pour les nouvelles sections avec recipe, les parties seront installées.

Notre algorithme de tout à l’heure est maintenant :

  • pour chaque part de l’option parts de la section [buildout] :
    • installer la recipe du part ;
    • récupérer l’entry point spécifié (default si non spécifié) depuis la recipe ;
    • si le part est actuellement installé mais sa configuration a changé, appeler la méthode update() de l’entry point ;
    • si le part n’a pas encore été installé, appeler la méthode install() de l’entry point ;
    • si un part existe dans .installed.cfg et n’est plus dans la liste des parts de [buildout], alors le part est désintallé.

Jetez un œil sur les scripts générés. Rappel : les scripts bin/archgenxml et bin/agx_* sont générés car le egg archgenxml a des entry points dans le groupe console_scripts. :

bin/archgenxml

#!/usr/bin/python

import sys
sys.path[0:0] = [
  '/home/vincentfretin/archgenxml_buildout/eggs/archgenxml-2.6-py2.7.egg',
  ...
  ]

import archgenxml.ArchGenXML

if __name__ == '__main__':
    archgenxml.ArchGenXML.main()

La première ligne est le shebang, la version du python avec laquelle vous avez fait command:python bootstrap.py -d au début. archgenxml et toutes ses dépendances sont ajoutés au début du sys.path.

Partage du dossier eggs et downloads

Lors de l’exécution de buildout, le fichier ~/.buildout/default.cfg est lu, c’est dans ce fichier que nous pouvons mettre des options qui seront partagées par tous nos buildouts.

Si le dossier ~/.buildout/ n’existe pas, créez le ainsi que les sous-dossiers :

$ mkdir ~/.buildout ~/.buildout/eggs ~/.buildout/downloads \
  ~/.buildout/configs

Créez le fichier ~/.buildout/default.cfg avec ce contenu :

[buildout]
eggs-directory = /home/vincentfretin/.buildout/eggs
download-cache = /home/vincentfretin/.buildout/downloads
#extends-cache = /home/vincentfretin/.buildout/configs

Remplacez ici bien sûr vincentfretin par votre compte. Vous ne pouvez pas utiliser le caractère ~ ici.

À savoir que les valeurs par défaut de ces variables sont :

eggs-directory = ${buildout:directory}/eggs
download-cache = pas précisé, on ne garde pas les archives par défaut
extends-cache = pas de cache par défaut

${buildout:directory} étant le dossier du buildout. C’est pour cela que le dossier eggs était créé dans le buildout.

Vous pouvez supprimer le dossier eggs de votre buildout, le réamorcer et reexécuter bin/buildout

Pinning des versions

À chaque fois que vous exécutez bin/buildout, une requête est faite au serveur central pour savoir si nous avons la dernière version du egg. C’est le comportement par défaut. En effet nous avons l’option newest = true dans la section [buildout] par défaut.

Vous pouvez faire bin/buildout -N pour ne pas vérifier les mises à jour.

Si vous aviez newest = false dans votre buildout, la commande bin/buildout -n la remettrait à true pour l’exécution.

Le problème est que votre buildout n’est pas reproductible à l’identique. Pour qu’il soit reproductible à l’identique il faudrait que votre buildout précise quelles versions des eggs doivent être installées.

C’est là qu’intervient l’option versions de la section [buildout], vous spécifiez dans quelle section vous avez les informations sur les versions. La convention est de l’appeler également versions. Pour geler (verbe “to pin” en anglais, “pinned” au participe passé) archgenxml, modifiez votre buildout de cette manière :

[buildout]
versions = versions
parts = archgenxml

[versions]
archgenxml = 2.6
# ...

[archgenxml]
recipe = zc.recipe.egg:scripts
eggs = archgenxml

Mais cela ne suffit pas pour que le buildout soit reproductible car nous n’avons pas gelé les dépendances d’archgenxml.

Extension buildout.dumppickedversions

zc.buildout peut être étendu avec des extensions. Il y en a une particulièrement intéressante qui va vous sortir la liste des eggs qui ne sont pas gelés avec leur version.

Une extension s’ajoute avec l’option extensions de la section [buildout] Ajoutez donc l’extension buildout.dumppickedversions à votre fichier buildout.cfg :

[buildout]
extensions = buildout.dumppickedversions
# ...

Relancez bin/buildout et à la fin de l’exécution vous verrez apparaitre la liste des versions à geler. Ajoutez-les tous à votre section versions, et reexécutez bin/buildout, il ne devrait plus avoir de versions.

Il existe aussi une option allow-picked-versions = false disponible dans le cœur de zc.buildout qui permet de stopper le buildout si un egg n’est pas gelé. Cette option et l’extension buildout.dumppickedversions sont mutuellement exclusives.

Attention

Cette extension n’est plus compatible avec buildout 2. La fonctionnalité a été intégré dans le cœur. Il suffit de préciser l’option show-picked-version = true à la place de extensions = buildout.dumppickedversions.

Option extends

Il est possible d’étendre le fichier buildout.cfg. Vous verrez souvent un fichier deployment.cfg ou development.cfg qui étend le fichier buildout.cfg de base.

Créez un fichier development.cfg :

[buildout]
extends = buildout.cfg
parts = omelette

[omelette]
recipe = collective.recipe.omelette
eggs = archgenxml

L’option extends dit de lire le fichier buildout.cfg, et les options que l’on spécifiera par la suite seront écrasées.

Par défaut, buildout cherche un fichier buildout.cfg, l’option -c permet d’indiquer un fichier alternatif.

Relancez la machinerie buildout avec ce fichier :

$ bin/buildout -c development.cfg
Uninstalling archgenxml.
Installing omelette.

Oh que s’est-il passé ? Vous avez écrasé l’option parts, il n’y a donc plus que le part omelette à installer.

Vous vouliez garder aussi le part archgenxml, rectifiez ça en transformant le = par +=, ce qui donne parts += omelette.

Toutes les options supportant une liste fonctionnent ainsi (parts, eggs, develop).

Relancez buildout. Là les deux parts sont bien installées.

Remarque

imaginez toujours que vous avez implicitement extends = ~/.buildout/default.cfg dans votre buildout.cfg de base, à partir duquel vous étendez d’autres configurations, ici c’est le fichier buildout.cfg.

Vous pouvez récupérer une version mis à plat de la configuration avec la commande suivante :

$ bin/buildout -c development.cfg annotate
...
[buildout]
parts=
archgenxml
omelette
    /home/vincentfretin/archgenxml_buildout/buildout.cfg
+=  /home/vincentfretin/archgenxml_buildout/development.cfg
...

Vous voyez bien ici que l’option parts a été mis à plat, c’est la concaténation de l’option trouvée dans buildout.cfg et de development.cfg.

Option extends-cache

L’option extends-cache disponible depuis la version 1.4.0 de zc.buildout permet de mettre en cache les fichiers référencés dans l’option extends.

Prenons l’exemple suivant :

extends = http://dist.plone.org/release/4.2/versions.cfg

Le fichier versions.cfg sera mis en cache dans le dossier ~/.buildout/configs/, le fichier créé étant la somme md5 de l’url.

Une fois mis en cache, il est possible de relancer le buildout en mode hors ligne avec la commande bin/buildout -o.

Recipe collective.recipe.omelette

On ne fait pas d’omelette sans casser des eggs. Cette recipe permet de mettre à plat les eggs :

[omelette]
recipe = collective.recipe.omelette
eggs = archgenxml

Cette recipe génère une arborescence dans parts/omelette/ contenant archgenxml et toutes ses dépendances directes ou indirectes :

$ tree parts/omelette/
parts/omelette
|-- archgenxml -> /home/vincentfretin/.buildout/eggs/archgenxml-2.5-py2.6.egg/archgenxml
|-- easy_install.py -> /usr/local/lib/python2.6/dist-packages/distribute-0.6.14-py2.6.egg/easy_install.py
|-- i18ndude -> /home/vincentfretin/.buildout/eggs/i18ndude-3.2-py2.6.egg/i18ndude
|-- pkg_resources.py -> /usr/local/lib/python2.6/dist-packages/distribute-0.6.14-py2.6.egg/pkg_resources.py
|-- plone
|   |-- __init__.py
|   `-- i18n -> /home/vincentfretin/.buildout/eggs/plone.i18n-2.0-py2.6.egg/plone/i18n
|-- setuptools -> /usr/local/lib/python2.6/dist-packages/distribute-0.6.14-py2.6.egg/setuptools
|-- site.py -> /usr/local/lib/python2.6/dist-packages/distribute-0.6.14-py2.6.egg/site.py
|-- stripogram -> /home/vincentfretin/.buildout/eggs/stripogram-1.5-py2.6.egg/stripogram
|-- xmiparser -> /home/vincentfretin/.buildout/eggs/xmiparser-1.5-py2.6.egg/xmiparser
`-- zope
    |-- __init__.py
    |-- browser -> /home/vincentfretin/.buildout/eggs/zope.browser-1.3-py2.6.egg/zope/browser
    |-- component -> /home/vincentfretin/.buildout/eggs/zope.component-3.10.0-py2.6.egg/zope/component
    |-- configuration -> /home/vincentfretin/.buildout/eggs/zope.configuration-3.7.2-py2.6.egg/zope/configuration
    |-- contenttype -> /home/vincentfretin/.buildout/eggs/zope.contenttype-3.5.1-py2.6.egg/zope/contenttype
    |-- documenttemplate -> /home/vincentfretin/.buildout/eggs/zope.documenttemplate-3.4.2-py2.6.egg/zope/documenttemplate
    |-- event -> /home/vincentfretin/.buildout/eggs/zope.event-3.5.0_1-py2.6.egg/zope/event
    |-- exceptions -> /home/vincentfretin/.buildout/eggs/zope.exceptions-3.6.1-py2.6.egg/zope/exceptions
    |-- i18n -> /home/vincentfretin/.buildout/eggs/zope.i18n-3.7.4-py2.6.egg/zope/i18n
    |-- i18nmessageid -> /home/vincentfretin/.buildout/eggs/zope.i18nmessageid-3.5.3-py2.6-linux-x86_64.egg/zope/i18nmessageid
    |-- interface -> /home/vincentfretin/.buildout/eggs/zope.interface-3.6.1-py2.6-linux-x86_64.egg/zope/interface
    |-- location -> /home/vincentfretin/.buildout/eggs/zope.location-3.9.0-py2.6.egg/zope/location
    |-- proxy -> /home/vincentfretin/.buildout/eggs/zope.proxy-3.6.1-py2.6-linux-x86_64.egg/zope/proxy
    |-- publisher -> /home/vincentfretin/.buildout/eggs/zope.publisher-3.12.4-py2.6.egg/zope/publisher
    |-- schema -> /home/vincentfretin/.buildout/eggs/zope.schema-3.7.0-py2.6.egg/zope/schema
    |-- security -> /home/vincentfretin/.buildout/eggs/zope.security-3.7.4-py2.6-linux-x86_64.egg/zope/security
    |-- structuredtext -> /home/vincentfretin/.buildout/eggs/zope.structuredtext-3.5.0-py2.6.egg/zope/structuredtext
    `-- tal -> /home/vincentfretin/.buildout/eggs/zope.tal-3.5.2-py2.6.egg/zope/tal

25 directories, 5 files

Cela permet d’avoir les packages à plat, ce qui facilite la recherche, exemple :

$ find -L parts/omelette -name "interfaces.py"
$ grep -r "register" parts/omelette

Si vous développez avec Eclipse/Pydev, vous pouvez ajouter le répertoire parts/omelette/ au PYTHONPATH du projet, ceci permet l’exploration du code en mode “hypertexte” (ctrl+clic sur un symbole) et l’auto completion intelligente de fonctionner correctement.

Utilisation de variable

Vous avez écrit “archgenxml” à deux endroits, une fois dans la recipe zc.recipe.egg et une autre fois dans la recipe collective.recipe.omelette. Il est plus intéressant de l’indiquer une seule fois dans une variable et d’utiliser cette variable ensuite dans les recipes.

Une convention est de définir une variable eggs dans la section [buildout] et de récupérer cette variable avec ${buildout:eggs}.

Voici ce que donnent les deux fichiers une fois reécrits.

buildout.cfg

[buildout]
versions = versions
parts = archgenxml
eggs = archgenxml

[versions]
archgenxml = 2.5
distribute = 0.6.14
xmiparser = 1.5
zc.buildout = 1.5.2
zc.recipe.egg = 1.3.2
zope.contenttype = 3.5.1
zope.i18nmessageid = 3.5.3
zope.tal = 3.5.2
Unidecode = 0.04.1
i18ndude = 3.2
ordereddict = 1.1
plone.i18n = 2.0
pytz = 2010l
stripogram = 1.5
zope.browser = 1.3
zope.component = 3.10.0
zope.configuration = 3.7.2
zope.documenttemplate = 3.4.2
zope.event = 3.5.0-1
zope.exceptions = 3.6.1
zope.i18n = 3.7.4
zope.interface = 3.6.1
zope.location = 3.9.0
zope.proxy = 3.6.1
zope.publisher = 3.12.4
zope.schema = 3.7.0
zope.security = 3.7.4
zope.structuredtext = 3.5.0

[archgenxml]
recipe = zc.recipe.egg:scripts
eggs = ${buildout:eggs}

development.cfg

[buildout]
extends = buildout.cfg
parts += omelette

[omelette]
recipe = collective.recipe.omelette
eggs = ${buildout:eggs}

Vous pouvez référencer n’importe quelle variable de la section que vous souhaitez avec ${nomdesection:variable}. Vous pouvez référencer une variable de la même section avec juste ${:variable}.

Repinning et fusion de section de données

Vous voyez que la recipe collective.recipe.omelette n’est pas gelé. Faites donc les modifications suivantes dans development.cfg :

[buildout]
# ...
versions = vers

[vers]
collective.recipe.omelette = 0.9

Relancez buildout. Vous voyez que seul collective.recipe.omelette est gelé maintenant car l’option versions a été écrasée.

Renommez vers en versions et relancez le buildout.

Tous les eggs sont gélés. En effet la section versions de buildout.cfg et la section versions de development.cfg ont fusionné.

En conclusion, nommez toujours versions la section contenant les versions.

Mais maintenir deux listes est source d’erreur. Créez un fichier versions.cfg qui contient une section [versions] (pas de section [buildout]), et dites à buildout.cfg d’étendre versions.cfg. Relancez buildout pour voir que tout fonctionne.

Ordre d’installation des parts

L’ordre d’installation des parts n’est pas forcément l’ordre donné dans l’option parts de [buildout]. Et une part peut très bien être installée si elle n’est pas listée dans parts.

Modifiez votre buildout.cfg comme ceci :

[buildout]
# ...
parts = zopeskel archgenxml

[archgenxml]
# ...
packages = ZopeSkel

[zopeskel]
recipe = zc.recipe.egg
eggs = ${archgenxml:packages}

Dans cet exemple, il n’y aucune raison particulière pour installer une part avant l’autre. C’est juste pour l’exercice.

L’ordre d’installation sera [archgenxml], puis [zopeskel]. Si maintenant vous supprimez la part [archgenxml] de parts, elle sera toujours installée car la part [zopeskel] en dépend, faisant référence à une de ses variables.

L’option develop

Note

Cette partie dépend du chapitre Concept de Python eggs.

L’option develop permet d’indiquer quels eggs sont en mode développement.

Créez un dossier src/ et déplacez y vos deux eggs foo.bar et foo.rab.

Ajoutez foo.bar à l’omelette et relancez bin/buildout -c development.cfg. Buildout tente d’aller chercher foo.bar sur Pypi et échoue.

Nous allons donc lui dire où le trouver.

Ajoutez à la section [buildout] de development.cfg :

[buildout]
# ...
find-links +=
    http://devagile:8080/site/products/simple

develop = src/foo.bar

Relancez bin/buildout -c development.cfg.

Le egg foo.bar est en mode développement :

$ ls -l develop-eggs/
total 12
-rw-r--r-- 1 vincentfretin vincentfretin 57 2009-05-26 18:20 foo.bar.egg-link
-rw-r--r-- 1 vincentfretin vincentfretin 32 2009-05-26 15:38 setuptools.egg-link

Nous pouvons également utiliser un wildcard dans l’option develop. Mettez develop = src/*, du coup les deux eggs foo.bar et foo.rab sont en développement.


blog comments powered by Disqus