Lors de la phase d’industrialisation du processus de build d’un projet, on se retrouve souvent confronté à la problématique suivante : comment gérer ses différents environnements (développement, qualification, pré production et production par exemple) ? En effet, chaque environnement va faire appel à des bases de données différentes, des noms de serveurs web distincts ou bien encore des emplacements de publication différenciés si l’on utilise ClickOnce. C’est ce dernier point que je vais prendre pour exemple ici. Comment gérer les différentes urls de déploiement en fonction d’un environnement donné. Bien sûr, ce qui s’applique pour ClickOnce est tout à fait utilisable pour d’autres besoins.

Première étape : Ajouter MsBuildTasks avec NuGet

Lorsque l’on déploie un projet avec ClickOnce en utilisant Visual Studio, ce dernier va générer au cours du processus un fichier nommé publish.htm qui comportera les différentes caractéristiques du projet comme son nom et sa version. Mais ce fichier n’est pas généré si l’on effectue la publication avec msbuild, il faut donc le construire. C’est pour cette raison que l’on utilise les MsBuildsTasks : pouvoir modifier un template en lui donnant les informations nécessaires.

msbuildtasks nuget

Deuxième étape : Création du fichier paramétrage

Pour cela ajoutez un nouveau fichier dans votre projet nommé par exemple Customized.targets. Ce fichier contiendra les éléments suivants :

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
	<PropertyGroup>
		<BuildEnvironment>Production</BuildEnvironment>
		<BuildEnvironment>PreProduction</BuildEnvironment>
		<BuildEnvironment>Qualification</BuildEnvironment>
		<BuildEnvironment>Developpement</BuildEnvironment>
	</PropertyGroup>

	<Choose>
		<When Condition=" '$(BuildEnvironment)' == 'Production' ">
		  <PropertyGroup>
			<PublishDir>C:\ClickOnce\Production\</PublishDir>
			<PublishUrl>$(PublishDir)</PublishUrl>
			<InstallUrl>http://deployserver/Production/</InstallUrl>
		  </PropertyGroup>
		</When>
		<When Condition=" '$(BuildEnvironment)' == 'PreProduction' ">
		  <PropertyGroup>
			<PublishDir>C:\ClickOnce\PreProduction\</PublishDir>
			<PublishUrl>$(PublishDir)</PublishUrl>
			<InstallUrl>http://deployserver/PreProduction/</InstallUrl>
		  </PropertyGroup>
		</When>
		<When Condition=" '$(BuildEnvironment)' == 'Qualification' ">
		  <PropertyGroup>
			<PublishDir>C:\ClickOnce\Qualification\</PublishDir>
			<PublishUrl>$(PublishDir)</PublishUrl>
			<InstallUrl>http://deployserver/Qualification/</InstallUrl>
		  </PropertyGroup>
		</When>
		<When Condition=" '$(BuildEnvironment)' == 'Developpement' ">
		  <PropertyGroup>
			<PublishDir>C:\ClickOnce\Developpement\</PublishDir>
			<PublishUrl>$(PublishDir)</PublishUrl>
			<InstallUrl>http://deployserver/Developpement/</InstallUrl>
		  </PropertyGroup>
		</When>	
    </Choose>	
	
	<PropertyGroup>
		<!-- Note this must be done AFTER the above Choose (so PublishDir is set)-->
		<PublishFilePath>$(PublishDir)Publish.htm</PublishFilePath>
	</PropertyGroup>

	<ItemGroup>
		<Tokens Include="PublisherName">
		  <ReplacementValue>$(PublisherName)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="ProductName">
		  <ReplacementValue>$(ProductName)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="ApplicationVersion">
		  <ReplacementValue>$(ApplicationVersion)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="Prerequsites">
		  <ReplacementValue>@(BootstrapperPackage->'&lt;li&gt;%(ProductName)&lt;/li&gt;','%0D%0A')</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="Username">
		  <ReplacementValue>$(Username)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
	</ItemGroup> 

	<Target Name="AfterPublish">
		<Time Format="dd/MM/yyyy HH:mm">
		  <Output TaskParameter="FormattedTime" PropertyName="PublishTime" />
		</Time>
		<TemplateFile Template="Publish\Publish.htm" Tokens="@(Tokens)" OutputFilename="$(PublishFilePath)" />
		<FileUpdate Files="$(PublishFilePath)" Regex="\${PublishTime}" ReplacementText="$(PublishTime)" />
	</Target>

</Project>
 

Dans ce fichier on retrouve plusieurs sections. La première permet de définir l’ensemble des environnements. La deuxième permet de surcharger les valeurs contenues dans le fichier csproj. C’est cette partie qui permet de déployer dans différents répertoires en définissant pour le paramètre PublishDir des valeurs différentes. Quant à la troisième section, elle permet de définir les valeurs de remplacement pour le fichier publish.htm. Enfin, la dernière permet d’appliquer au fichier publish.htm la transformation décrite précédemment.

Troisième étape : création du template publish.htm

Cette étape consiste à créer le template qui générera le page publish.htm, pour cela il faut créer un nouveau fichier Publish.htm dans le répertoire Publish à la racine du projet. Voici un exemple de contenu :

<HTML>
  <HEAD>
    <TITLE>${ProductName}</TITLE>
    <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8" />
    <STYLE TYPE="text/css">
<!--
BODY{margin-top:20px; margin-left:20px; margin-right:20px; color:#000000; font-family:Tahoma; background-color:white}
A:link {font-weight:normal; color:#000066; text-decoration:none}
A:visited {font-weight:normal; color:#000066; text-decoration:none}
A:active {font-weight:normal; text-decoration:none}
A:hover {font-weight:normal; color:#FF6600; text-decoration:none}
P {margin-top:0px; margin-bottom:12px; color:#000000; font-family:Tahoma}
PRE {border-right:#f0f0e0 1px solid; padding-right:5px; border-top:#f0f0e0 1px solid; margin-top:-5px; padding-left:5px; font-size:x-small; padding-bottom:5px; border-left:#f0f0e0 1px solid; padding-top:5px; border-bottom:#f0f0e0 1px solid; font-family:Courier New; background-color:#e5e5cc}
TD {font-size:12px; color:#000000; font-family:Tahoma}
H2 {border-top: #003366 1px solid; margin-top:25px; font-weight:bold; font-size:1.5em; margin-bottom:10px; margin-left:-15px; color:#003366}
H3 {margin-top:10px; font-size: 1.1em; margin-bottom: 10px; margin-left: -15px; color: #000000}
UL {margin-top:10px; margin-left:20px}
OL {margin-top:10px; margin-left:20px}
LI {margin-top:10px; color: #000000}
FONT.value {font-weight:bold; color:darkblue}
FONT.key {font-weight: bold; color: darkgreen}
.divTag {border:1px; border-style:solid; background-color:#FFFFFF; text-decoration:none; height:auto; width:auto; background-color:#cecece}
.BannerColumn {background-color:#000000}
.Banner {border:0; padding:0; height:8px; margin-top:0px; color:#ffffff; filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=1,StartColorStr='#1c5280',EndColorStr='#FFFFFF');}
.BannerTextCompany {font:bold; font-size:18pt; color:#cecece; font-family:Tahoma; height:0px; margin-top:0; margin-left:8px; margin-bottom:0; padding:0px; white-space:nowrap; filter:progid:DXImageTransform.Microsoft.dropshadow(OffX=2,OffY=2,Color='black',Positive='true');}
.BannerTextApplication {font:bold; font-size:18pt; font-family:Tahoma; height:0px; margin-top:0; margin-left:8px; margin-bottom:0; padding:0px; white-space:nowrap; filter:progid:DXImageTransform.Microsoft.dropshadow(OffX=2,OffY=2,Color='black',Positive='true');}
.BannerText {font:bold; font-size:18pt; font-family:Tahoma; height:0px; margin-top:0; margin-left:8px; margin-bottom:0; padding:0px; filter:progid:DXImageTransform.Microsoft.dropshadow(OffX=2,OffY=2,Color='black',Positive='true');}
.BannerSubhead {border:0; padding:0; height:16px; margin-top:0px; margin-left:10px; color:#ffffff; filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=1,StartColorStr='#4B3E1A',EndColorStr='#FFFFFF');}
.BannerSubheadText {font:bold; height:11px; font-size:11px; font-family:Tahoma; margin-top:1; margin-left:10; filter:progid:DXImageTransform.Microsoft.dropshadow(OffX=2,OffY=2,Color='black',Positive='true');}
.FooterRule {border:0; padding:0; height:1px; margin:0px; color:#ffffff; filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=1,StartColorStr='#4B3E1A',EndColorStr='#FFFFFF');}
.FooterText {font-size:11px; font-weight:normal; text-decoration:none; font-family:Tahoma; margin-top:10; margin-left:0px; margin-bottom:2; padding:0px; color:#999999; white-space:nowrap}
.FooterText A:link {font-weight:normal; color:#999999; text-decoration:underline}
.FooterText A:visited {font-weight:normal; color:#999999; text-decoration:underline}
.FooterText A:active {font-weight:normal; color:#999999; text-decoration:underline}
.FooterText A:hover {font-weight:normal; color:#FF6600; text-decoration:underline}
.ClickOnceInfoText {font-size:11px; font-weight:normal; text-decoration:none; font-family:Tahoma; margin-top:0; margin-right:2px; margin-bottom:0; padding:0px; color:#000000}
.InstallTextStyle {font:bold; font-size:14pt; font-family:Tahoma; a:#FF0000; text-decoration:None}
.DetailsStyle {margin-left:30px}
.ItemStyle {margin-left:-15px; font-weight:bold}
.StartColorStr {background-color:#4B3E1A}
.JustThisApp A:link {font-weight:normal; color:#000066; text-decoration:underline}
.JustThisApp A:visited {font-weight:normal; color:#000066; text-decoration:underline}
.JustThisApp A:active {font-weight:normal; text-decoration:underline}
.JustThisApp A:hover {font-weight:normal; color:#FF6600; text-decoration:underline}
-->

</STYLE>

</HEAD>
  <BODY>
    <TABLE WIDTH="100%" CELLPADDING="0" CELLSPACING="2" BORDER="0">

<!-- Begin Banner -->
<TR><TD><TABLE CELLPADDING="2" CELLSPACING="0" BORDER="0" BGCOLOR="#cecece" WIDTH="100%"><TR><TD><TABLE BGCOLOR="#1c5280" WIDTH="100%" CELLPADDING="0" CELLSPACING="0" BORDER="0"><TR><TD CLASS="Banner" /></TR><TR><TD CLASS="Banner"><SPAN CLASS="BannerTextCompany">${PublisherName}</SPAN></TD></TR><TR><TD CLASS="Banner"><SPAN CLASS="BannerTextApplication">${ProductName}</SPAN></TD></TR><TR><TD CLASS="Banner" ALIGN="RIGHT" /></TR></TABLE></TD></TR></TABLE></TD></TR>
<!-- End Banner -->


<!-- Begin Dialog -->
<TR><TD ALIGN="LEFT"><TABLE CELLPADDING="2" CELLSPACING="0" BORDER="0" WIDTH="540"><TR><TD WIDTH="496">

<!-- Begin AppInfo -->
<TABLE><TR><TD COLSPAN="3">&nbsp;</TD></TR><TR><TD><B>Name:</B></TD><TD WIDTH="5"><SPACER TYPE="block" WIDTH="10" /></TD><TD>${ProductName}</TD></TR><TR><TD COLSPAN="3">&nbsp;</TD></TR><TR><TD><B>Version:</B></TD><TD WIDTH="5"><SPACER TYPE="block" WIDTH="10" /></TD><TD>${ApplicationVersion}</TD></TR><TR><TD COLSPAN="3">&nbsp;</TD></TR><TR><TD><B>Publisher:</B></TD><TD WIDTH="5"><SPACER TYPE="block" WIDTH="10" /></TD><TD>${PublisherName}</TD></TR><tr><td colspan="3">&nbsp;</td></tr></TABLE>
<!-- End AppInfo -->


<!-- Begin Prerequisites -->
<TABLE ID="BootstrapperSection" BORDER="0"><TR><TD COLSPAN="2">The following prerequisites are required:</TD></TR><TR><TD WIDTH="10">&nbsp;</TD><TD><UL>
<LI>Windows Installer 4.5</LI>
<LI>Microsoft .NET Framework 4 (x86 and x64)</LI>
<LI>Microsoft .NET Framework 4 (x86 and x64) and Update for .NET Framework 4 (KB2468871)</LI>
</UL></TD></TR><TR><TD COLSPAN="2">
If these components are already installed, you can <SPAN CLASS="JustThisApp"><A HREF="Unibail.Fid.Launcher.application">launch</A></SPAN> the application now. Otherwise, click the button below to install the prerequisites and run the application.
</TD></TR><TR><TD COLSPAN="2">&nbsp;</TD></TR></TABLE>
<!-- End Prerequisites -->


</TD></TR></TABLE>
<!-- Begin Buttons -->
<TR><TD ALIGN="LEFT"><TABLE CELLPADDING="2" CELLSPACING="0" BORDER="0" WIDTH="540" STYLE="cursor:hand" ONCLICK="window.navigate(InstallButton.href)"><TR><TD ALIGN="LEFT"><TABLE CELLPADDING="1" BGCOLOR="#333333" CELLSPACING="0" BORDER="0"><TR><TD><TABLE CELLPADDING="1" BGCOLOR="#cecece" CELLSPACING="0" BORDER="0"><TR><TD><TABLE CELLPADDING="1" BGCOLOR="#efefef" CELLSPACING="0" BORDER="0"><TR><TD WIDTH="20"><SPACER TYPE="block" WIDTH="20" HEIGHT="1" /></TD><TD><A ID="InstallButton" HREF="setup.exe">Install</A></TD><TD width="20"><SPACER TYPE="block" WIDTH="20" HEIGHT="1" /></TD></TR></TABLE></TD></TR></TABLE></TD></TR></TABLE></TD><TD WIDTH="15%" ALIGN="right" /></TR></TABLE></TD></TR>
<!-- End Buttons -->
</TD></TR>
<!-- End Dialog -->



<!-- Spacer Row -->
<TR><TD>&nbsp;</TD></TR>

<TR><TD>
<!-- Begin Footer -->
<TABLE WIDTH="100%" CELLPADDING="0" CELLSPACING="0" BORDER="0" BGCOLOR="#ffffff"><TR><TD HEIGHT="5"><SPACER TYPE="block" HEIGHT="5" /></TD></TR><TR><TD CLASS="FooterText" ALIGN="center"><A HREF="http://go.microsoft.com/fwlink/?LinkId=154571">ClickOnce and .NET Framework Resources</A>
</TD></TR><TR><TD HEIGHT="5"><SPACER TYPE="block" HEIGHT="5" /></TD></TR><TR><TD HEIGHT="1" bgcolor="#cecece"><SPACER TYPE="block" HEIGHT="1" /></TD></TR></TABLE>
<!-- End Footer -->
</TD></TR>

</TABLE>
  </BODY>
</HTML>

Ce fichier comporte un certain nombre de balises comme par exemple celles servant à générer le titre : ${ProductName}. Ce sont ces balises qui seront remplacées par les valeurs prédéfinies précédemment. Il est ainsi possible de générer pour chaque publication un fichier différent qui comportera les bonnes informations.

Dernière étape : modification du csproj et lancement du build

Maintenant que tout est prêt, il reste uniquement à modifier le csproj du projet principal en ajoutant les deux lignes suivantes :

  <Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets" />
  <Import Project="Customized.targets" />

Ces deux lignes permettent d’ajouter la prise en charge des MsBuildTasks et de notre fichier. Maintenant avec la commande ci-dessous, on peut lancer une publication avec Msbuild en spécifiant l’environnement souhaité. Il ne reste plus qu’à intégrer cette commande dans votre server de build.

Msbuild Projet.sln /target:Publish /p:BuildEnvironment=Production /p:ApplicationVersion=1.1.0.0