To my English followers: I will try to translate this post as I find the time to do it.

Temo di averlo ripetuto alla nausea: la build incrementale è una faccenda molto delicata ed è facile combinare pasticci se non si controlla costantemente. Perciò ne sconsiglio, in generale l’impiego. Recentemente ho messo però insieme una ricetta che limita alcuni degli inconvenienti associati ad essa ed è questo l’argomento del post.

In questo post mostrerò come preparare un template di build personalizzato che incrementi indipendentemente i numeri di versione dei singoli progetti Visual Studio e ci permetta però di rintracciare con facilità la team build che ha prodotto gli eseguibili, come illustrato in figura.

2012-12-10_133228

Estendere la build dei progetti

Il primo tassello della soluzione è estendere la normale compilazione dei progetti Visual Studio (C#, VB o altro). Lavorare a livello MSBuild permette di conoscere se un determinato progetto è soggetto o meno a ricompilazione.

L’insieme dei file TARGETS che compone la compilazione .NET e Visual Studio prevede alcune utili proprietà, parzialmente documentate su Common MSBuild Project Properties. Parzialmente perché quella che preferisco, e ci servirà, è CustomAfterMicrosoftCommonTargets. Impostando tale proprietà MSBuild al percorso di un file Targets, questo viene aggiunto dinamicamente e possiamo cambiare la definizione di target già definiti o di estenderne il comportamento. Bello e pericoloso!

Il secondo tassello è scegliere uno strumento per modificare i contenuti dei file AssemblyInfo. Poiché l’attività va condotta a livello MSBuild la scelta naturale è di utilizzare il task AssemblyInfo da MSBuild Extension Pack (http://msbuildextensionpack.codeplex.com/); il task è contenuto in MSBuild.ExtensionPack.dll che, a sua volta, richiede Ionic.Zip.dll. Questo task ha moltissimi argomenti, tanto che gli sviluppatori hanno fornito il file MSBuild.ExtensionPack.VersionNumber.targets pronto per l’impiego.

Se mettiamo in una stessa cartella questi tre file possiamo già provare il funzionamento di base digitando, da un Developer Command Prompt, qualcosa del tipo:

MSBuild <path ad un progetto>.csproj /p:CustomAfterMicrosoftCommonTargets=<path_di>MSBuild.ExtensionPack.VersionNumber.targets

e vedere gli effetti di impostare valori diversi in MSBuild.ExtensionPack.VersionNumber.targets.

MSBuild.ExtensionPack.VersionNumber.targets

Originale

Modificato

<!-- Properties for controlling the Assembly Version -->
<PropertyGroup>
    <AssemblyMajorVersion>1</AssemblyMajorVersion>
    <AssemblyMinorVersion>0</AssemblyMinorVersion>
    <AssemblyBuildNumber></AssemblyBuildNumber>
    <AssemblyRevision></AssemblyRevision>
    <AssemblyBuildNumberType>DateString</AssemblyBuildNumberType>
    <AssemblyBuildNumberFormat>MMdd</AssemblyBuildNumberFormat>
    <AssemblyRevisionType>AutoIncrement</AssemblyRevisionType>
    <AssemblyRevisionFormat>00</AssemblyRevisionFormat>
</PropertyGroup>
<!-- Properties for controlling the Assembly Version -->
<PropertyGroup>
    <AssemblyMajorVersion>1</AssemblyMajorVersion>
    <AssemblyMinorVersion>0</AssemblyMinorVersion>
    <AssemblyBuildNumber>0</AssemblyBuildNumber>
    <AssemblyRevision>0</AssemblyRevision>
</PropertyGroup>
<!-- Properties for controlling the Assembly File Version -->
<PropertyGroup>
    <AssemblyFileMajorVersion>1</AssemblyFileMajorVersion>
    <AssemblyFileMinorVersion>0</AssemblyFileMinorVersion>
    <AssemblyFileBuildNumber></AssemblyFileBuildNumber>
    <AssemblyFileRevision></AssemblyFileRevision>
    <AssemblyFileBuildNumberType>DateString</AssemblyFileBuildNumberType>
    <AssemblyFileBuildNumberFormat>MMdd</AssemblyFileBuildNumberFormat>
    <AssemblyFileRevisionType>AutoIncrement</AssemblyFileRevisionType>
    <AssemblyFileRevisionFormat>00</AssemblyFileRevisionFormat>
</PropertyGroup>
<!-- Properties for controlling the Assembly File Version -->
<PropertyGroup>
    <AssemblyFileMajorVersion>1</AssemblyFileMajorVersion>
    <AssemblyFileMinorVersion>0</AssemblyFileMinorVersion>
    <AssemblyFileBuildNumber></AssemblyFileBuildNumber>
    <AssemblyFileRevision>0</AssemblyFileRevision>
    <AssemblyFileBuildNumberType>AutoIncrement</AssemblyFileBuildNumberType>
    <AssemblyFileBuildNumberFormat>000</AssemblyFileBuildNumberFormat>
</PropertyGroup>
<!-- Propeties for controlling extended assembly attributes -->
<PropertyGroup>
    <AssemblyCompany></AssemblyCompany>
    <AssemblyConfiguration></AssemblyConfiguration>
    <AssemblyCopyright></AssemblyCopyright>
    <AssemblyCulture></AssemblyCulture>
    <AssemblyDescription></AssemblyDescription>
    <AssemblyProduct></AssemblyProduct>
    <AssemblyTitle></AssemblyTitle>
</PropertyGroup>
<!-- Propeties for controlling extended assembly attributes -->
<PropertyGroup>
    <AssemblyCompany>Fictious company</AssemblyCompany>
    <AssemblyConfiguration>$(ConfigurationName)</AssemblyConfiguration>
    <AssemblyCopyright>(c) Fictious company 2012</AssemblyCopyright>
    <AssemblyCulture></AssemblyCulture>
    <AssemblyDescription></AssemblyDescription>
    <AssemblyProduct>My product</AssemblyProduct>
    <AssemblyTitle></AssemblyTitle>
    <AssemblyInformationalVersion>$(TeamBuildNumber)</AssemblyInformationalVersion>
</PropertyGroup>

In breve la regola è di:

  • Non alterare l’AssemblyVersion
  • Aggiornare l’AssemblyFileVersion con un numero progressivo rappresentante la build della singola componente
  • Riportare nell’AssemblyInformationalVersion la proprietà $(TeamBuildNumber)

In $(TeamBuildNumber) passeremo l’identificatore della team build che ha ricompilato il progetto. Per provare il meccanismo possiamo usare un comando come:

MSBuild <path ad un progetto>.csproj /p:CustomAfterMicrosoftCommonTargets=<path_di>MSBuild.ExtensionPack.VersionNumber.targets /p:TeamBuildNumber=pseudo-identificativo di build

Un template di build

Abbiamo visto a livello MSBuild come aggiornare automaticamente i file AssemblyInfo.cs (o .vb) dei progetti che vengono ricompilati senza modificare i file di progetto stessi (.csproj o .vbproj o simili); vediamo ora come inglobare queste modifiche in un template per la build di Team Foundation Server in modo che sia facilmente riutilizzabile.

L’intervento sul Default Template è minimo e non richiede custom activity ma è tutto a livello XAML, con il beneficio collaterale di non dover gestire il path degli assembly per le custom activities.

Anzitutto creiamo una cartella sotto BuildProcessTemplate del TeamProject e vi copiamo i tre file precedenti.

GVTFS12_2012-12-11_00002 - NO TEMPLATE

Ricordiamo infatti che la cartella (e le sottocartelle) contenenti il template vengono scaricate automaticamente dall’agent di build.

Creiamo un clone del default template.

GVTFS12_2012-12-11_00003

GVTFS12_2012-12-11_00005

A questo punto abbiamo predisposto tutti i file che servono.

Aggiungiamo un parametro che l’utente inserirà nella build definition, come da figura.

GVTFS12_2012-12-10_00001GVTFS12_2012-12-10_00002

Il parametro zzzVersionNumberTargetsFile indica il file Targets da usare: in questo modo possiamo usare diverse regole in build definition diverse.

La seguente figura illustra il punto del workflow da alterare.

2012-05-17_162944 GVTFS12_2012-12-10_00003

A livello di RunOnAgent aggiungiamo una Sequence che aggiunge dati alla variabile MSBuildArguments; il valore di questa è l’insieme di argomenti passati ad MSBuild al momento di compilare una solution od un project Visual Studio. Inoltre il file MSBuild.ExtensionPack.VersionNumber.targets viene cercato nella stessa cartella che contiene il template; la posizione della cartella viene determinata dalla activity ConvertWorkspaceItem.

Il codice XAML è abbastanza semplice da interpretare.

<Sequence DisplayName="*** Hook CustomAfterMicrosoftCommonTargets">
  <Sequence.Variables>
    <Variable x:TypeArguments="x:String" Name="customAfterMicrosoftCommonTargets" />
    <Variable x:TypeArguments="x:String" Name="localBuildTemplatePath" />
  </Sequence.Variables>
  <mtbwa:ConvertWorkspaceItem DisplayName="Convert BuildTemplate path to local" Input="[BuildDetail.BuildDefinition.Process.ServerPath]" Result="[localBuildTemplatePath]" Workspace="[Workspace]" />
  <Assign DisplayName="Assign customAfterMicrosoftCommonTargets">
    <Assign.To>
      <OutArgument x:TypeArguments="x:String">[customAfterMicrosoftCommonTargets]</OutArgument>
    </Assign.To>
    <Assign.Value>
      <InArgument x:TypeArguments="x:String" xml:space="preserve">[System.IO.Path.Combine(
System.IO.Path.GetDirectoryName(localBuildTemplatePath),
zzzVersionNumberTargetsFile)]</InArgument>
    </Assign.Value>
  </Assign>
  <Assign DisplayName="Append CustomAfterMicrosoftCommonTargets to MSBuildArguments">
    <Assign.To>
      <OutArgument x:TypeArguments="x:String">[MSBuildArguments]</OutArgument>
    </Assign.To>
    <Assign.Value>
      <InArgument x:TypeArguments="x:String">[String.Format("{0} /p:CustomAfterMicrosoftCommonTargets={1} ", MSBuildArguments, customAfterMicrosoftCommonTargets)]</InArgument>
    </Assign.Value>
  </Assign>
  <Assign DisplayName="Append TeamBuildNumber to MSBuildArguments">
    <Assign.To>
      <OutArgument x:TypeArguments="x:String">[MSBuildArguments]</OutArgument>
    </Assign.To>
    <Assign.Value>
      <InArgument x:TypeArguments="x:String">[String.Format("{0} /p:TeamBuildNumber={1} ", MSBuildArguments, BuildDetail.BuildNumber)]</InArgument>
    </Assign.Value>
  </Assign>
</Sequence> 
        

La proprietà MSBuild TeamBuildNumber viene valorizzata con BuildDetail.BuildNumber, ossia l’identificativo della build in corso, come abbiamo visto in precedenza.

Salvando tutto nel version control siamo pronti a definire dei profili di build basati su questo template personalizzato.

Definiamo le build

A questo punto creare una nuova build definition richiede solo un paio di accortezze.

GVTFS12_2012-12-11_00001

Come spiegato in Define a Build Process that is Based on the Default Template, le build incrementali si basano sul valore del parametro Clean Workspace. Nel nostro esempio lo poniamo a None, cosicché solamente i file aggiornati nel version control sono portati nell’area di build e quindi solo i progetti strettamente necessari vengono ricompilati. La ricompilazione aggancia automaticamente la modifica del file AssemblyInfo.CS (o .VB). È possibile che la build non trovi necessario ricompilare alcun progetto e di conseguenza i binari prodotti siano identici a quelli della precedente build.

Dobbiamo inoltre valorizzare il parametro Targets File for Version Numbering con il nome del file MSBuild.ExtensionPack.VersionNumber.targets da usare. La scelta di rendere parametrico (e obbligatorio) il nome del file con le regole di incremento della versione ci permette di usare regole differenti per build definition diverse. Ad esempio un build definition che rilascia hotfix incrementerà il numero revision invece del build.

Finale

Un comodo ritocco al nostro template custom è di porre a None il valore di default per Clean Workspace, così da non doverlo cambiare ad ogni nuova definizione.

GVTFS12_2012-12-11_00006

Inoltre possiamo accorgerci se è stato modificato rispetto il nuovo default.

 

Se dobbiamo verificare rapidamente i numeri di versione di molti file si può usare il seguente comando PowerShell

Get-ChildItem \\path_to_build_outputs\*.dll | foreach { $_.VersionInfo }

 

Infine il

     contiene tutto il sorgente dell’esempio descritto nel post.

Salute a tutti e alla prossima,

Giulio