Exemple d'automatisation de livraison d'une application WPF avec MSBuild
Je travaille depuis quelque temps sur un projet WPF. Mon client nous met à disposition un automate d’intégration continu sur lequel nous n’avons pas la main en terme de customisation.
Ce dernier permet d’effectuer des étapes importantes telle que l’exécution de tests unitaires, couverture de code, vérification de la qualité de code, … cependant certaines étapes à mon sens sont manquantes.
Dans cet article je vais vous présenter un exemple de processus automatisé que j’utilise en complément qui me permet de sécuriser la livraison de l’application en environnement de test.
Je pars du postulat que sur mon poste de développement, seulement le framework.net et Visual Studio 2010 sont installés.
L’idée est de créer ce script le plus vite possible avec les outils en ma possession
=>Je vais donc créer un script MSBuild
Définition des étapes de processus de livraison
Le script est structuré en sept étapes:
- GetLatestVersion;
- IncrementVersion;
- Clean;
- Compil;
- Deploy;
- Check;
- CheckinCode;
GetLatestVersion / CheckInCode
Ces targets permettent de réccupérer la dernière version du code et de faire un check-in du code modifié par ce processus. A ce niveau là tout dépend de votre gestionnaire de configuration.
Si vous utilisez Microsoft Team Foundation Server les Tasks sont disponibleq dans Microsoft.TeamFoundation.Build.targets (C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\TeamBuild pour une machine x64).
Sinon, pour les autres gestionnaires de configuration, il suffit d’obtenir des tasks tier:
- MSBuild Community Tasks : https://msbuildtasks.tigris.org/
- MSBuid Extension Pack : https://msbuildextensionpack.codeplex.com/
IncrementVersion
Incrémente automatiquement le numéro de version des composants de la solution;
Soit une Custom Task peut être créée, soit les MSBuild Community Tasks offrent une tâche qui permet de modifier le fichier Assemblyinfo.
Clean
Nettoyage du bin des projets de la solutions. Tous les répertoires bin/debug et bin/release sont nettoyés avec la task suivante:
Code Snippet
- <Target Name="Clean">
- <MSBuild Projects="$(SolutionName)" Targets="Clean" />
- </Target>
Compil
Cette simple target compile la solution dans la configuration définie:
Code Snippet
- <Target Name="Compil">
- <MSBuild Projects="$(SolutionName)" Targets="Rebuild" Properties="Configuration=$(Configuration)" />
- </Target>
Deploy
La target Deploy copie les fichiers, présents dans le projet de démarrage de la solution, dans le répertoire de destination. Si le répertoire n’existe pas il est créé.
Code Snippet
- <Target Name="Deploy">
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(ExeFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(DllFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(PdbFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(ConfigFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(XmlFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(IconesFiles)" />
- </Target>
Check
A ce niveau là, j’ai créé une custom task qui, à partir d’un fichier Xml, vérifie les fichiers déposés dans le répertoire de livraison.
Cette custom task vérifie:
- le nombre de binaires présents;
- la version de ces binaires;
Dans la artie suivante de ce post, je vais rentrer en détail sur la façon de faire une Custom Task MSBuild.
Voici les grandes lignes du script MSBuild =>MyBuild.proj
Code Snippet
- <Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="START">
- <ItemGroup>
- <SubSteps Include="GetLatestVersionCode" />
- <SubSteps Include="IncrementVersion" />
- <SubSteps Include="Clean" />
- <SubSteps Include="Compil" />
- <SubSteps Include="Deploy" />
- <SubSteps Include="Check" />
- <SubSteps Include="CheckInCode" />
- </ItemGroup>
- <ItemGroup>
- <SubCheckSteps Include="CheckDllVersion" />
- </ItemGroup>
- <PropertyGroup>
- <Version Condition="'$(Version)' == ''" >1.0.0.23</Version>
- <SolutionName Condition="'$(SolutionName)' == ''" >WPFApplication.sln</SolutionName>
- <Configuration Condition="'$(Configuration)' == ''" >Release</Configuration>
- <OutputFolder Condition="'$(OutputFolder)' == ''" >$(MSBuildProjectDirectory)\WPFApplication\bin\$(Configuration)</OutputFolder>
- <CurrentVersion Condition="'$(CurrentVersion)' == ''" >$(ApplicationPrefix)_V$(Version)_$([System.DateTime]::Now.ToString('yy-MM-dd'))</CurrentVersion>
- <VersionFile Condition="'$(VersionFile)' == ''" >$(MSBuildProjectDirectory)\iCRM.Framework\Application\ProjectInfo.cs</VersionFile>
- <DestinationRootFolder Condition="'$(DestinationRootFolder)' == ''" >c:\Livraison</DestinationRootFolder>
- <DestinationFolder Condition="'$(DestinationFolder)' == ''" >$(DestinationRootFolder)\$(CurrentVersion)</DestinationFolder>
- <TestFile Condition="'$(TestFile)' == ''">$(MSBuildProjectDirectory)\CheckVersion.xml</TestFile>
- </PropertyGroup>
- <ItemGroup>
- <ExeFiles Include="$(iCRMOutputFolder)\*.exe" />
- <DllFiles Include="$(iCRMOutputFolder)\*.dll" />
- <PdbFiles Include="$(iCRMOutputFolder)\*.pdb" />
- <ConfigFiles Include="$(iCRMOutputFolder)\*.config" />
- <XmlFiles Include="$(iCRMOutputFolder)\*.xml" />
- <IconesFiles Include="$(iCRMOutputFolder)\*.ico" />
- </ItemGroup>
- <Target Name="START" DependsOnTargets="@(SubSteps)">
- </Target>
- <Target Name="GetLatestVersionCode">
- <!-- Task to get latestet version of code -->
- </Target>
- <Target Name="IncrementVersion">
- <!-- Incrementation of assembly version -->
- </Target>
- <Target Name="Clean">
- <MSBuild Projects="$(SolutionName)" Targets="Clean" />
- </Target>
- <Target Name="Compil">
- <MSBuild Projects="$(SolutionName)" Targets="Rebuild" Properties="Configuration=$(Configuration)" />
- </Target>
- <Target Name="Deploy">
- <Message Text="Config" />
- <Message Text="$(OutputFolder)\*.exe" />
- <Message Text="@(ExeFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(ExeFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(DllFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(PdbFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(ConfigFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(XmlFiles)" />
- <Copy DestinationFolder="$(DestinationFolder)" SourceFiles="@(IconesFiles)" />
- </Target>
- <Target Name="Check">
- <!-- Task to check deployment -->
- </Target>
- <Target Name="CheckInCode">
- <!-- Task to check-in code -->
- </Target>
- </Project>
Dans ce script, deux patterns MSBuild sont vraiment intéressants:
- l’utilisation de DependsOnTarget pour permettre une meilleure granularité de target. Comme pour du code .net, je met le minimum de logique dans une target => 1 target = 1 rôle;
- la pré-initialisation des properties au début du script afin d’avoir lors de l’exécution du script MSBuild un minimum de paramettres à saisir.
Execution du script
Pour démarrer ce script, ouvrez le Visual Studio Command Prompt (2010) pour avoir MSBuild dans le path.
Naviguez jusqu’à votre fichier proj MyBuild.proj, lancez la ligne de commande suivante:
msbuild MyBuild.proj /p:Version=1.0.0.24
Cette ligne de commande lance le processus pour la version 1.0.0.24, la compilation est faite en release.
Exemple de Custom Task MSBuild
Créez un projet de classe de librairie nommé par exemple MyCustomTaskMSBuild.
Ajouter les références aux dlls Microsoft.Build.Framework.dll:
Renommez la classe Class1.cs en MyCustomTask.cs et faites la hériter de l’interface ITask.
Ensuite, à vous de jouer, après avoir surchargé les méthodes/propriétés de ITask, vous pouvez créer un code celui-ci:
Code Snippet
- using Microsoft.Build.Framework;
- namespace MyCustomTaskMSBuild
- {
- public class MyCustomTask : ITask
- {
- public IBuildEngine BuildEngine
- {
- get; set;
- }
- public ITaskHost HostObject
- {
- get; set;
- }
- [Required]
- public string MyInputParam { get; set; }
- public bool Execute()
- {
- // Add task logic here
- Trace(string.Format("My First Task - {0}",MyInputParam), MessageImportance.Normal);
- return true;
- }
- protected void Trace(string message, MessageImportance importance)
- {
- BuildMessageEventArgs args = new BuildMessageEventArgs(message, string.Empty, TaskName, importance);
- this.BuildEngine.LogMessageEvent(args);
- }
- public string TaskName { get { return "My Custom Task"; } }
- }
- }
Pour tout ce qui est logging de messages, utilisez la méthode Trace.
J’ai rajouté un MyInputParam, avec l’attribut Required il devient obligatoire lors de l’exécution du script MSBuild.
Comment insérer cette nouvelle Task dans mon fichier proj
En entête du fichier proj, rajoutez la ligne suivante:
Code Snippet
- <UsingTask AssemblyFile="MyCustomTaskMSBuild.dll" TaskName="MyCustomTaskMSBuild.MyCustomTask" />
Cette ligne défini le lien avec la Custom Task, il suffit ensuite d’appeller cette Task dans une target comme suit:
Code Snippet
- <Target Name="test">
- <MyCustomTask MyInputParam="!!!" />
- </Target>
Et maintenant, testez:
Ensuite, en vie réelle, vérifiez que le ficher proj est dans le même répertoire que la dll de la Custom Task.
Comment debugger cette Custom Task
Pour debugger cetteTask, forcez le projet MyCustomTaskMSBuild à Startup Project.
Etre sûr que le fichier proj est bien dans le répertoire bin\debug du projet MyCustomTaskMSBuild.
Dans l’onglet Debug des propriétés de ce projet. Sélectionner Start exsternal program et pointez vers l’exécutable MSBuild, dans mon cas C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe
Dans les Command Line Arguments, il suffit de mettre le nom du fichier MSBuild avec les bons paramètres.
F5 et le debug peut commencer.
Quelques liens
Documentation MSBuild sur MSDN :
- https://msdn.microsoft.com/en-us/library/dd393574(VS.110).aspx
- https://msdn.microsoft.com/en-us/library/dd393574.aspx
Blog de l’équipe MSBuild
Benoît