Configuration d'un serveur d'intégration continue - [Partie 1] - Les bases
By Michael DELVA on Thursday 14 January 2010, 11:20 - Miscellaneous - Permalink
TweetOn trouve sur internet un paquet de tutoriaux sur la configuration de divers serveurs d'intégration continue, basés sur des solutions différentes (TeamCity, Hudson, Cruise Control.NET...), avec des options différentes (FxCop, avec ou sans tests unitaires...). Le problème est que ces tutoriaux ciblent tout le temps .NET 3.5 ou 2.0 (normal me direz vous) et que les "options" proposées ne me conviennent pas, notamment concernant les tests unitaires (Personne n'utilise donc xUnit?)
Je me suis donc efforcé de faire fonctionner un serveur d'intégration continue qui remplirait les objectifs suivants:
- Framework ciblé : .NET 4.0
- Librairie de tests unitaires : xUnit
- Utilisation d'outils d'analyse de code : FxCop
- Couverture de code : nCover
Et je me propose donc de vous en relater les étapes lors de cette série d'articles.
Première étape : Le choix du serveur d'intégration
Les principaux serveurs d'intégration continue en .NET ne sont pas légion. On peut même grossièrement restreindre la liste à 3 principaux concurrents:
- TeamCity, de JetBrains, la boite qui développe Resharper
- CruiseControl.NET
- Hudson
TeamCity, essai raté
Etant un grand fan de Resharper, je me suis tourné en premier lieu vers TeamCity, qui a la réputation d'être très simple à prendre en main, et très rapide à configurer. Sauf que... j'ai eu en fait beaucoup de soucis. Le premier d'entre eux n'étant d'ailleurs pas de la faute de TeamCity, puisque c'était l'antivirus qui bloquait le transfert des fichiers entre le serveur et le client (oui, TeamCity est une application distribuée). Après quelques jours passés à trouver le souci (résolu grâce à leur forum, très efficace soit dit en passant), j'ai rencontré d'autres soucis liés au .NET 4.0, et NAnt (dont je parlerai plus tard). Après beaucoup de temps passé sans avancer, j'ai finalement laissé tomber, pour tenter le coup avec un autre serveur.
Hudson, la libération
Comme deuxième essai, j'ai essayé Hudson, surtout parce qu'un ami l'avait déjà utilisé dans sa boîte, et que au boulot, c'est finalement ce qu'on a choisi également. Et grand bien m'en a pris, car tout ce que je n'avais pas réussi à faire avec TeamCity après beaucoup de temps, j'ai réussi avec Hudson, et très rapidement. Très bon point donc. Et je l'ai finalement gardé, vu que j'ai réussi à implémenter une procédure de build complète qui satisfait tous mes besoins.
Installation de Hudson : C'est parti!!!
Je ne vais pas écrire un pavé là dessus, tout est très bien expliqué sur leur site. Mais en gros, vous installez le JRE de Java, vous téléchargez le fichier .war de Hudson, vous créez un fichier batch au même endroit que là où vous avez enregistré le .war, dedans vous mettez :
@echo off
cls
java -jar hudson.war
et vous lancez ce .bat.
Voilà, Hudson est lancé. Pour l'administrer, vous ouvrez votre navigateur préféré, et vous tapez : http://localhost:8080/
Création du projet
C'est très simple: dans hudson, vous cliquez sur Nouveau Job, puis vous nommez votre projet, et vous choisissez Construire un projet free-style:
Pour automatiser toutes les étapes du build, et ne pas être dépendant de Hudson pour ça, nous allons utiliser NAnt, dont je vous ai déjà parlé précédemment, et qui est un outil de build gratuit. Concrètement, vous définissez dans un fichier XML une série de tâches à accomplir, et NAnt va exécuter celles de votre choix, en prenant en compte les dépendances entre tâches. Je ne m'apesentirai pas outre mesure sur le sujet, il est déjà copieusement abordé sur le net, et de plus la documentation sur leur site est bien faite. Je vais donc entrer directement dans le vif du sujet.
Avant toute chose, pour pouvoir utiliser NAnt dans Hudson, vous devez installer le plugin idoine, via la page d'administration, puis Gestion des plugins. Ensuite vous devez le configurer, en cliquant sur Configurer le Système, toujours depuis la page d'administration. Une nouvelle ligne NAnt est apparue sous Shell, et vous permet de spécifier des installations de NAnt différentes:
Si Hudson vous met un message d'erreur vous disant D:\blabla\NAnt\ is not a directory, il vous faut savoir que le plugin NAnt vous impose d'installer NAnt dans un répertoire bin, situé dans le dossier que vous avez spécifié dans la configuration. Par exemple, sur la capture d'écran précédente j'ai spécifié comme répertoire : D:\Integration\NAnt, mais celui-ci est en fait installé dans un sous-répertoire Bin:
Maintenant que le plugin a été installé et configuré, nous allons revenir à la configuration de notre projet.
Gestion de code source
J'utilise SVN comme Content Versionning System. Pour l'utiliser avec Hudson, rendez-vous sur la page d'administration de Hudson pour télécharger et installer le plugin. De retour sur la page de configuration du projet, une nouvelle ligne est apparue dans la catégorie Gestion de code source, intitulée avec beaucoup d'a-propos Subversion :) Pour configurer, c'est très simple. J'ai pour ma part employé la même syntaxe qu'avec TortoiseSVN, et ça prend même en charge les clés SSH. Rien de difficile donc.
Arrivé à ce stade, si vous sauvegardez votre projet et lancez un nouveau build, vous verrez que Hudson va créer un dossier Test (ou le nom de votre projet) dans le répertoire jobs, qui est lui situé dans le répertoire d'installation de Hudson. Puis il va créer dans ce dossier Test un dossier builds et un dossier workspace. Il va ensuite faire un checkout SVN de votre projet dans ce dossier workspace. De cette manière, vous avez les dernières sources de votre projet, et qui seront mises à jour à chaque fois que vous lancerez un nouveau build avec Hudson.
Organisation du projet
Dans cette partie je vais parler de la manière dont j'ai organisé les sources de mon projet. L'intérêt de cette organisation étant que lorsqu'un checkout du projet est effectué, la personne profite dès le départ d'un environnement de travail complet qui lui permette de travailler tout de suite, sans avoir à installer les librairies annexes, ou à modifier les chemins des fichiers pour que ça compile. Une copie d'écran valant mieux qu'un long discours, voilà ce que j'ai choisi:
Dans le répertoire de base de la solution se trouve le fichier .sln de Visual Studio. Les autres répertoires ne posent pas de souci de compréhension. A noter que je vais placer dans Tools les différents fichiers dont je vais me servir lors du build. Notamment le fichier fxcop et le fichier .build que va utiliser NAnt.
Ajouter NAnt au projet Hudson
Puisque je parle de NAnt, voyons comment configurer Hudson pour utiliser ce fameux fichier .build contenant les actions que NAnt devra accomplir: retournez dans la configuration de votre job, puis dans la catégorie Build cliquez sur Ajouter une étape au build et choisissez Invoke top-level Nant targets. Choisissez dans le combobox NAnt Version la version que vous avez configuré dans Hudson précédemment. Pour le chemin vers le fichier de build, le répertoire par défaut est celui où se trouve votre fichier sln. Comme nous avons placé le fichier build dans le répertoire Tools de notre solution, nous allons donc remplir le chemin comme suit: Tools/test.build. Targets va rester vide, afin d'exécuter la tâche définie par défaut dans le fichier de build de NAnt.
Notre premier fichier NAnt
Avant de rentrer dans les méandres de la configuration du fichier de build de NAnt (bien que ça ne soit pas des plus compliqué), il nous faut d'abord modifier le fichier de configuration de NAnt, car celui-ci (en version 0.86 beta) n'est pas prévu pour cibler du .Net 4.0. J'ai trouvé la marche à suivre sur ce site. Il faut ouvrir le fichier nant.exe.config qui se trouve au même niveau que l'exécutable de NAnt. Pour commencer, allez tout en bas, au niveau du noeud <startup>, et ajoutez la ligne suivante:
<supportedRuntime version="v4.0.21006" />
Ensuite modifiez le noeud <runtime> pour qu'il ressemble à ceci:
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="lib" />
<loadFromRemoteSources enabled="true" />
</assemblyBinding>
</runtime>
Enfin, il nous reste à ajouter un dernier noeud au niveau de configuration\\nant\\frameworks\\platform name="win32". Vous voyez qu'il existe à ce niveau plusieurs noeuds framework, ciblant chacun un framework différent. Nous allons ajouter un noeud qui va cibler la dernière version du framework .Net 4.0:
<framework
name="net-4.0"
family="net"
version="4.0"
description="Microsoft .NET Framework 4.0"
sdkdirectory="${path::combine(sdkInstallRoot, 'bin')}"
frameworkdirectory="${path::combine(installRoot, 'v4.0.21006')}"
frameworkassemblydirectory="${path::combine(installRoot, 'v4.0.21006')}"
clrversion="4.0.21006"
>
<runtime>
<probing-paths>
<directory name="lib/net/2.0" />
<directory name="lib/net/neutral" />
<directory name="lib/common/2.0" />
<directory name="lib/common/neutral" />
</probing-paths>
<modes>
<strict>
<environment>
<variable name="COMPLUS_VERSION" value="v4.0.21006" />
</environment>
</strict>
</modes>
</runtime>
<reference-assemblies basedir="${path::combine(installRoot, 'v4.0.21006')}">
<include name="Accessibility.dll" />
<include name="mscorlib.dll" />
<include name="Microsoft.Build.Engine.dll" />
<include name="Microsoft.Build.Framework.dll" />
<include name="Microsoft.Build.Utilities.dll" />
<include name="Microsoft.Vsa.dll" />
<include name="Microsoft.VisualBasic.dll" />
<include name="Microsoft.VisualBasic.Compatibility.dll" />
<include name="Microsoft.VisualBasic.Compatibility.Data.dll" />
<include name="System.Configuration.dll" />
<include name="System.Configuration.Install.dll" />
<include name="System.Data.dll" />
<include name="System.Data.OracleClient.dll" />
<include name="System.Data.SqlXml.dll" />
<include name="System.Deployment.dll" />
<include name="System.Design.dll" />
<include name="System.DirectoryServices.dll" />
<include name="System.dll" />
<include name="System.Drawing.Design.dll" />
<include name="System.Drawing.dll" />
<include name="System.EnterpriseServices.dll" />
<include name="System.Management.dll" />
<include name="System.Messaging.dll" />
<include name="System.Runtime.Remoting.dll" />
<include name="System.Runtime.Serialization.Formatters.Soap.dll" />
<include name="System.Security.dll" />
<include name="System.ServiceProcess.dll" />
<include name="System.Transactions.dll" />
<include name="System.Web.dll" />
<include name="System.Web.Mobile.dll" />
<include name="System.Web.RegularExpressions.dll" />
<include name="System.Web.Services.dll" />
<include name="System.Windows.Forms.dll" />
<include name="System.Xml.dll" />
</reference-assemblies>
<task-assemblies>
<include name="extensions/net/neutral/**/*.dll" />
<include name="extensions/net/2.0/**/*.dll" />
<include name="NAnt.MSNetTasks.dll" />
<include name="NAnt.MSNet.Tests.dll" />
<include name="extensions/common/2.0/**/*.dll" />
</task-assemblies>
<tool-paths>
<directory name="${path::combine(sdkInstallRoot, 'bin')}"
if="${property::exists('sdkInstallRoot')}" />
<directory name="${path::combine(installRoot, 'v4.0.21006')}" />
<directory name="${path::combine(installRoot, 'v3.0')}" />
<directory name="${path::combine(installRoot, 'v3.5')}" />
</tool-paths>
<project>
<readregistry
property="installRoot"
key="SOFTWARE\Microsoft\.NETFramework\InstallRoot"
hive="LocalMachine" />
<readregistry
property="sdkInstallRoot"
key="SOFTWARE\Microsoft\Microsoft SDKs\Windows\v7.0A\WinSDK-NetFx40Tools\InstallationFolder"
hive="LocalMachine"
failonerror="false" />
</project>
<tasks>
<task name="csc">
<attribute name="supportsnowarnlist">true</attribute>
<attribute name="supportswarnaserrorlist">true</attribute>
<attribute name="supportskeycontainer">true</attribute>
<attribute name="supportskeyfile">true</attribute>
<attribute name="supportsdelaysign">true</attribute>
<attribute name="supportsplatform">true</attribute>
<attribute name="supportslangversion">true</attribute>
</task>
<task name="vbc">
<attribute name="supportsdocgeneration">true</attribute>
<attribute name="supportsnostdlib">true</attribute>
<attribute name="supportsnowarnlist">true</attribute>
<attribute name="supportskeycontainer">true</attribute>
<attribute name="supportskeyfile">true</attribute>
<attribute name="supportsdelaysign">true</attribute>
<attribute name="supportsplatform">true</attribute>
<attribute name="supportswarnaserrorlist">true</attribute>
</task>
<task name="jsc">
<attribute name="supportsplatform">true</attribute>
</task>
<task name="vjc">
<attribute name="supportsnowarnlist">true</attribute>
<attribute name="supportskeycontainer">true</attribute>
<attribute name="supportskeyfile">true</attribute>
<attribute name="supportsdelaysign">true</attribute>
</task>
<task name="resgen">
<attribute name="supportsassemblyreferences">true</attribute>
<attribute name="supportsexternalfilereferences">true</attribute>
</task>
<task name="delay-sign">
<attribute name="exename">sn</attribute>
</task>
<task name="license">
<attribute name="exename">lc</attribute>
<attribute name="supportsassemblyreferences">true</attribute>
</task>
</tasks>
</framework>
Pour tester que tout cela fonctionne correctement, nous allons créer le fichier Test.build dans le répertoire Tools de la solution, et nous allons y mettre le XML suivant:
<?xml version="1.0"?>
<project name="Emidee" default="full.build">
<property name="solution.dir" value=".." />
<property name="solution.file.name" value="test.sln" />
<property name="solution.file.path" value="${solution.dir}/${solution.file.name}" />
<property name="project.config" value="debug" />
<target name="full.build" depends="compile.sources" />
<target name="clean.sources">
<exec program="${framework::get-framework-directory(framework::get-target-framework())}\msbuild.exe"
commandline="${solution.file.path} /t:Clean /p:Configuration=${project.config} /v:q" />
<echo message="Clean Finished" />
</target>
<target name="compile.sources" depends="clean.sources">
<exec program="${framework::get-framework-directory(framework::get-target-framework())}\msbuild.exe"
commandline="${solution.file.path} /t:Rebuild /p:Configuration=${project.config} /v:q" />
<echo message="Compile Finished" />
</target>
</project>
On commence par définir quelques variables, on définit la tâche full.build comme étant celle à exécuter par défaut. Cette tâche va dépendre de la tâche compile.sources, qui va elle-même dépendre de clean.sources. C'est à dire que NAnt va s'assurer de lancer les tâches dont dépendes d'autres tâches avant d'exécuter ces dernières. Bien entendu, il faudra que les tâches dépendantes aient réussi pour que la procédure continue. A noter que si plusieurs tâches dépendent de la même tâche, cette dernière ne sera pas exécutée plusieurs fois, mais bel et bien une seule et unique fois.
Dans le XML, la tâche clean.sources va aller appeler directement msbuild.exe, en lui passant en paramètre le chemin vers la solution, en spécifiant qu'on souhaite un cleanup de la solution, le tout en debug, et de manière silencieuse. La tâche compile.sources va elle demander à msbuild.exe de faire un rebuild des sources.
Avant de continuer, une petite précision sur la valeur de la propriété solution.dir, ici à "..". Il vous faut juste savoir que le répertoire courant pour NAnt est le répertoire où se trouve le fichier build. Comme il se trouve dans Tools, nous devons donc remonter la hiérarchie d'un niveau pour nous trouver au niveau du répertoire courant de la solution.
Première compilation du projet sur le build server
Quand le fichier build est enregistré, il suffit de faire un commit de la solution sur le serveur SVN, puis dans Hudson de lancer un nouveau build. Celui-ci va alors mettre à jour sa copie de la solution depuis le serveur SVN, puis va lancer NAnt en lui spécifiant le fichier build que l'on vient de créer. Vous devriez normalement voir dans la console de sortie de Hudson que msbuild a été appelé 2 fois: la première fois pour nettoyer la solution, et la deuxième fois pour faire un rebuild. Il vous suffit d'aller dans le répertoire de Hudson là où se trouve la copie courante (dans jobs\Test\workspace), puis dans le répertoire bin de votre projet pour voir qu'il a bien été compilé.
C'est tout (pour le moment)
Voilà la fin de la première partie de ce tutorial. Nous savons maintenant récupérer les sources de la solution sur le serveur SVN, et les compiler. Nous verrons dans les parties suivantes comment modifier le fichier NAnt pour ajouter de nouvelles étapes au build process (lancer les tests unitaires, une analyse FxCop, du code coverage) et comment utiliser Hudson et ses plugins pour afficher les rapports de ces analyses directement dans l'interface du serveur.
Et comme on le disent si bien les commentateurs de catch: La suite... Au prochain numéro !!! ;)