Již od počátku technologie .NET, byl zaveden nový způsob číslování verzí Assembly při jejím buildu. Mám na mysli způsob například s číslem verze 1.0.4208.16952, kdy poslední dvě čísla jsou získána jako Build – počet dnů od 1.1.2000 do dneška a Revision – počet sekund od půlnoci děleno dvěma. S tím také funguje, že uvedením např.: [assembly: AssemblyVersion("1.0.*")] do souboru AssemblyInfo.cs, docílíme automatického verzování při každé kompilaci.
Toto nastavení bylo dříve jako výchozí při založení projektu, což přispělo k tomu, že jsme si na tento způsob číslování už zvykli a začali ho používat. Postupem času ale nastal problém, zjistilo se, že měnit číslo verze u AssemblyVersion při každém buildu není dobré, uvedu několik důvodů:
- Pokud máme podepsanou assembly, kterou referencujeme, je AssemblyVersion součástí reference. Pokud tedy při buildu změníme její AssemblyVersion, musíme minimálně přebuildovat i projekt, který tuto změněnou assembly referencuje, jinak jí .NET runtime nenačte (nebo najde verzi původní při používání GAC).
- Pokud serializujeme nějaký typ, může být AssemblyVersion součástí serializovaných dat. Pokud pak objekt obnovujeme z perzistentního uložiště, musíme řešit rozdílnost verzí.
- Podobně je problém při používání WPF controlů, atd..
Řešením je mít AssemblyVersion stále stejné a měnit pouze verzi vlastního DLL souboru (File version), tj. do assembly infa kromě atributu AssemblyVersion přidat i AssemblyFileVersion, tedy např.:
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.4208.16952")]
Zde je ale právě problém, protože AssemblyFileVersion neumožňuje ono automatické číslování pomoci “*”, takže nezbývá než při release buildech čísla měnit ručně. Abychom měli čísla verzí tvořena stejným způsobem jako .NET automatické číslování, můžeme si pro jeho generování napsat následující metodu (např. jako extenzi k Version objektu):
internal static class VersionExtensions
{
public static Version GetCurrentBuildVersion(this Version version)
{
DateTime d = DateTime.Now;
return new Version(version.Major, version.Minor,
(DateTime.Today - new DateTime(2000, 1, 1)).Days,
((int)new TimeSpan(d.Hour, d.Minute, d.Second).TotalSeconds) / 2);
}
}
Parametrem metody je číslo verze, ze které se berou první dvě složky - Major, Minor.
Protože se ale situace s nepodporující “*” po sedmi letech a třech verzích Visual Studia nijak nezměnila, ruční přepisování verzí mě už po těch letech tak ňák přestávalo bavit a jiné řešení (které by dodrželo tuto .NET konvenci) jsem nikde také nenašel, vytvořil jsem vlastní MSBuild Task, který vyhledá soubor AssemblyInfo.cs a AssemblyFileVersion v něm změní. Popíšeme si, jak se takový MSBuild task vytvoří, a jak se v projektu nastaví jeho používání.
Vytvoření tasku UpdateAssemblyFileVersion
Pro MSBuild tasky založíme ve VS projekt typu Class Library a přidáme do něj reference na tyto assembly:
Microsoft.Build.Framework.dll
Microsoft.Build.Utilities.v4.0.dll
Jednotlivé tasky (projekt jich může obsahovat více) jsou třídy, odvozené od abstraktní třídy Microsoft.Build.Utilities.Task. Třída musí implementovat metodu Execute, ve které je výkonný kód prováděné akce. Jako parametry akcí jsou používány vlastností třídy, ty se dají podle potřeby opatřit atributem Required pro povinný parametr.
Task UpdateAssemblyFileVersion pro změnu verze v AssembyInfo vypadá následovně:
namespace IMP.CustomBuildTasks
{
/// <summary>
/// MSBuild task to update File Version in the AssemblyInfo.cs file.
/// </summary>
public class UpdateAssemblyFileVersion : Task
{
#region member varible and default property initialization
private string m_FileName;
private string m_Version;
#endregion
#region action methods
public override bool Execute()
{
try
{
if (string.IsNullOrEmpty(m_Version))
{
m_Version = Environment.GetEnvironmentVariable("MSBuild_Version");
if (string.IsNullOrEmpty(m_Version))
{
m_Version = new Version(1, 0, 0, 0).GetCurrentBuildVersion().ToString();
Environment.SetEnvironmentVariable("MSBuild_Version", m_Version);
Log.LogMessage("Version set to {0}.", m_Version);
}
}
if (string.IsNullOrEmpty(m_FileName))
{
string projfile = this.BuildEngine.ProjectFileOfTaskNode;
if (!string.IsNullOrEmpty(projfile))
{
//Get AssemblyInfo.cs file name from project file
m_FileName = AssemblyInfoHelper.GetAssemblyInfoFileName(projfile);
}
}
if (string.IsNullOrEmpty(m_FileName) || !System.IO.File.Exists(m_FileName))
{
this.Log.LogError("The AssemblyInfo file not found.");
return false;
}
Version version = AssemblyInfoHelper.UpdateFileVersion(m_FileName, System.Version.Parse(m_Version));
if (version == null)
{
Log.LogError("Unable to replace assembly file version in file {0}.", m_FileName);
return false;
}
Log.LogMessage("Assembly file version in file {0} replaced to {1}.", m_FileName, version.ToString());
}
catch (Exception ex)
{
Log.LogErrorFromException(ex);
return false;
}
return true;
}
#endregion
#region property getters/setters
/// <summary>
/// Optional full path and filename to the AssemblyInfo.cs file to update File Version.
/// </summary>
public string FileName
{
get { return m_FileName; }
set { m_FileName = value; }
}
/// <summary>
/// Optional version to get build and revision numbers from.
/// </summary>
public string Version
{
get { return m_Version; }
set { m_Version = value; }
}
#endregion
}
}
Třída je ke stažení zde.
Task má dva nepovinné parametry:
FileName – jméno AssemblyInfo.cs souboru, pokud není parametr uveden je AssemblyInfo hledáno ve výchozích umístění projektu.
Version – verze ze které se vezmou Build a Revision čísla pro AssemblyFileVersion v souboru AssemblyInfo.cs, pokud není uvedena, jsou Build a Revision vygenerovány (čísla Major a Minor se nemění a zůstávají tak jak jsou zapsána v atributu AssemblyFileVersion).
Popíšeme si jednotlivé části kódu v metodě Execute:
if (string.IsNullOrEmpty(m_Version))
{
m_Version = Environment.GetEnvironmentVariable("MSBuild_Version");
if (string.IsNullOrEmpty(m_Version))
{
m_Version = new Version(1, 0, 0, 0).GetCurrentBuildVersion().ToString();
Environment.SetEnvironmentVariable("MSBuild_Version", m_Version);
Log.LogMessage("Version set to {0}.", m_Version);
}
}
Tato část kódu v případě že není verze specifikovaná, číslo verze vygeneruje. Aby bylo při buildování celé solution ve všech projektech číslo verze stejné, pracuje task tak, že poprvé číslo verze vygeneruje a uloží do Environment Variable, když je pak task volán podruhé např. při kompilaci druhého projektu v solution, je již verze načtena z této Environment Variable. Vlastní generování čísla verze se provádí pomoci výše popsané extension metody GetCurrentBuildVersion. (Pozn.: v m_Version jsou důležitá pouze čísla Build a Revision.)
if (string.IsNullOrEmpty(m_FileName))
{
string projfile = this.BuildEngine.ProjectFileOfTaskNode;
if (!string.IsNullOrEmpty(projfile))
{
//Get AssemblyInfo.cs file name from project file
m_FileName = AssemblyInfoHelper.GetAssemblyInfoFileName(projfile);
}
}
Tato další část kódu nastaví FileName (pokud není nastaven z parametru) na soubor AssemblyInfo.cs. V objektu BuildEngine tasku je k dispozici vlastnost ProjectFileOfTaskNode, která vrací cestu a jméno souboru kompilovaného projektu. Z této cesty se pak v metodě GetAssemblyInfoFileName v pomocné třídě AssemblyInfoHelper (kód této třídy je k dispozici zde) soubor AssemblyInfo.cs hledá v umístění <projektDir>\Properties\AssemblyInfo.cs a <projektDir>\AssemblyInfo.cs.
Version version = AssemblyInfoHelper.UpdateFileVersion(m_FileName, System.Version.Parse(m_Version));
if (version == null)
{
Log.LogError("Unable to replace assembly file version in file {0}.", m_FileName);
return false;
}
Log.LogMessage("Assembly file version in file {0} replaced to {1}.", m_FileName, version.ToString());
Poslední část metody Execute provede pomoci volání UpdateFileVersion (kód opět zde) vlastní záměnu čísel Build a Revision v atributu AssemblyFileVersion v souboru AssemblyInfo.cs. Dále se pak provádí ještě nezbytné výpisy zda se akce zdařila či ne.
Nastavení spouštění tasku UpdateAssemblyFileVersion
Task máme hotový a nyní je nutné do souboru projektu začlenit jeho volání. Každý soubor projektu Visual Studia (např. .csproj) nejen že je XML soubor, ale zároveň je to přímo build script pro MSBuild. Přidávání volání nejrůznějších custom akcí to těchto souborů proto není nic neobvyklého, jediný nedostatkem je pouze to, že k tomu nelze použít žádný designer / UI nástroj z Visual Studia.
Na projektu ve Visual Studiu zvolíme volbu Unload Project a následně volbu Edit <jméno souboru .csproj>, tím soubor projektu otevřeme v XML editoru.
Do souboru projektu přidáme hned na začátek do elementu Project následující řádky:
<Project ...
<PropertyGroup>
<TasksPath Condition="'$(TasksPath)'==''">c:\BuildBinaries</TasksPath>
</PropertyGroup>
<UsingTask TaskName="IMP.CustomBuildTasks.UpdateAssemblyFileVersion" AssemblyFile="$(TasksPath)\VersionBuildTask.dll"/>
<PropertyGroup ...
</Project>
Nejdůležitější je zde řádek UsingTask , který nám zavádí náš task UpdateAssemblyFileVersion. Atribut TaskName obsahuje odkaz na třídu tasku (pozor musí být uveden včetně jeho namespace) a atribut AssemblyFile určuje cestu a jméno assembly, ve které je task zkompilován. Samozřejmě by zde šlo uvést cestu a jméno assembly natvrdo, ale aby se cesta dala konfigurovat je zde zavedena proměnná TasksPath, která pokud není nastavena je přenastavena na hodnotu c:\BuildBinaries.
Změnu verze assembly infa potřebujeme provést před buildem projektu, proto task budeme volat v akci BeforeBuild. Na konec souboru projektu (před ukončovacím elementem Project) přidáme následující řádky, pokud by projekt již používal Target BeforeBuild, rozšíříme jen obsah této sekce:
<Target Name="BeforeBuild">
<IMP.CustomBuildTasks.UpdateAssemblyFileVersion Version="$(BuildVersion)" />
</Target>
</Project>
V targetu uvedeme elementem jméno našeho tasku a můžeme atributy nastavit jeho parametry. Zde nastavujeme parametr Version na hodnotu BuildVersion. Při normálním buildu nebude proměnná nastavená a task nám číslo buildu vždy vygeneruje, ale máme umožněno verzi nastavit např. command line parametrem při volání MSBuild (to můžeme využít např. ve spojení s automatizovanými buildy v TFS).
Nyní můžeme projekt reloadnout (volbou Reload project) do Visual Studia.
Nyní bude při provedení buildu, verze automaticky aktualizovaná. Při tomto nastavení u všech projektů v solution, bude dále zajištěno, že všechny najednou buildované projekty, budou mít číslo verze nastavené stejně.
Pokud používáme pro VS projekt nějaký Source Control, může být při aktualizaci verze problém, že je soubor AssemblyInfo.cs měněn pouze lokálně, bez vazby na verzi v Source Control. Proto si příště ukážeme rozšíření tohoto řešení o podporu pro TFS Source Control.
A ještě některé jiné zdroje příkladů na práci s MSBuild tasky:
http://community.bartdesmet.net/blogs/bart/archive/2006/04/13/3896.aspx
http://derekpinkerton.com/post/Continuous-Integration-(Part-1-MSBuild).aspx
a něktedé knihovny s MSBuild tasky:
http://msbuildtasks.tigris.org
http://sdctasks.codeplex.com