Automatyzacja projektu z MSBuild-em - 6. Numer wersji z SVN revision

Jul 25, 2008 - 4 minute read -

Tym razem zajmiemy się dynamiczną kompilacją, która wykona się również podczas budowy w Visual Studio. Naszym celem będzie stworzenie pliku “AssemblyInfo.cs” oraz dynamiczne włączenie go do kompilacji. Efektem tego, będzie brak AssemblyInfo w naszej strukturze plików widocznej w “Solution Explorer”. Nie będzie to kusiło żadnego z członków zespołu aby go modyfikować. Parametry do jego zawartości będą w centralnym miejscu.

W tym odcinku pojawi się nowy plik - “tools\msbuild\rod.Commons\rod.Commons.Targets”. W nim znajdują się taski, które będą opisane poniżej. Używam je we wszystkich swoich projektach, dlatego są wyodrębnione do oddzielnego pliku. W naszym wypadku, równie dobrze jego zawartość można by umieścić w pliku Common.Targets. Ale zamiast tego umieścimy tam tylko Import.

<Import Project="$(MSBuildExtensionsPath)\rod.Commons\rod.Commons.Targets" Condition="$(RodCommonsTargetsIsLoaded) == ''" />

Generowanie AssemblyInfo.cs

Następnym krokiem jest oczywiście “Exclude From Project” dla istniejących AssemblyInfo.cs. Ja dodatkowo oznaczam je jako ignore w SVN property. Pliki AssemblyInfo.cs będziemy generować za pomocą tasku “AssemblyInfo”. Parametry zapiszemy w Settings.proj.

<!-- AssemblyInfo Properties -->
<PropertyGroup>
    <AssemblyInfoFile>Properties\AssemblyInfo.cs</AssemblyInfoFile>
    <AssemblyTitle>MySolution - $(AssemblyTitle)</AssemblyTitle>
    <AssemblyDescription>Sample application.</AssemblyDescription>
    <AssemblyCompany>rod</AssemblyCompany>
    <AssemblyCopyright>Copyright 2008 rod</AssemblyCopyright>
    <AssemblyKeyFile>$(RootPath)\MySolution.snk</AssemblyKeyFile>
    <AssemblyProduct>MySolution</AssemblyProduct>
    <AssemblyVersion>1.0.0.0</AssemblyVersion>
</PropertyGroup>

Dodatkowo możemy w każdym z projektów zmodyfikować poszczególne properties. Nagłówki naszych plików projektowych możemy zmienić w następujący sposób:

MyProject.csproj

<!-- Root Path definition relative for actual build file -->
<PropertyGroup>
    <RootPath Condition=" '$(RootPath)' == '' ">$(MSBuildProjectDirectory)\..\..\..\</RootPath>
    <AssemblyTitle>Sample library</AssemblyTitle>
    <AssemblyGuid>F5830C28-699B-4789-AEA4-95AAB38A73CF</AssemblyGuid>
</PropertyGroup>

MyProject.Tests.csproj

<!-- Root Path definition relative for actual build file -->
<PropertyGroup>
    <RootPath Condition=" '$(RootPath)' == '' ">$(MSBuildProjectDirectory)\..\..\..\</RootPath>
    <AssemblyTitle>$(AssemblyTitle) - Unit Tests for Sample library</AssemblyTitle>
</PropertyGroup>

Za wygenerowanie AssemblyInfo.cs odpowiedzialny jest następujący target:

<Target Name="GenerateAssemblyInfo" DependsOnTargets="CalculateAssemblyVersion" >
    <AssemblyInfo CodeLanguage="CS"
        OutputFile="$(AssemblyInfoFile)"
        AssemblyTitle="$(AssemblyTitle)"
        AssemblyDescription="$(AssemblyDescription)"
        AssemblyCompany="$(AssemblyCompany)"
        AssemblyCopyright="$(AssemblyCopyright)"
        AssemblyProduct="$(AssemblyProduct)"
        AssemblyVersion="$(AssemblyVersion)"
        AssemblyFileVersion="$(AssemblyVersion)"
        AssemblyKeyFile="$(AssemblyKeyFile)"
        Guid="$(AssemblyGuid)" />
</Target>

Target “CalculateAssemblyVersion”, od którego jest uzależniony “GenerateAssemblyInfo”, będzie omówiony później. W poprzednim odcinku dowiedzieliśmy się, że za zbiór plików do kompilacji odpowiada ItemGroup “Compile”. Tym razem też go użyjemy:

<Target Name="IncludeGeneratedAssemblyInfo" DependsOnTargets="GenerateAssemblyInfo" Condition="Exists('$(AssemblyInfoFile)')">
    <CreateItem Include="$(AssemblyInfoFile)">
        <Output ItemName="Compile" TaskParameter="Include"/>
    </CreateItem>
    <Touch Files="$(AssemblyInfoFile)" Time="2000-01-01" />
</Target>

Wywołanie tasku Touch jest swego rodzaju trickiem. AssemblyInfo.cs bedzie generowany przy każdym wywołaniu Build-a. Jak wiemy, jeżeli pliki źródłowe nie zostały zmodyfikowane, wówczas kompilacja podczas budowy jest pomijana. Gdybyśmy nie zastosowali powyższego tasku, wówczas kompilacja odbywałaby się za każdym razem i podczas budowy solution w Visual Studio kompilowały by się wszystkie projekty, nawet te, które nie były zmodyfikowane. Wywołamy powyższy target przed samą budową, wpisując w Common.Targets…

<!-- Add additional depends to Build target -->
<PropertyGroup>
    <BuildDependsOn>
        IncludeGeneratedAssemblyInfo;
        $(BuildDependsOn)
    </BuildDependsOn>
</PropertyGroup>

Dzięki temu plik AssemblyInfo.cs będzie się generował również podczas budowy Visual Studio … i za pomocą NAnta nie dalibyśmy rady tego uzyskać lub byłoby to dosyć skomplikowane.

Tworzenie numeru wersji na podstawie wartości w pliku tekstowym oraz SVN

revision.

W swoich projektach zazwyczaj stosuję następującą strategię wersjonowania 1.0.NumerIteracji.SVNRevision. Numer iteracji zapisuję w pliku w roocie projektu …

<!-- Helper Files -->
<PropertyGroup>
    <IterationNumberFile Condition=" '$(IterationNumberFile)' == '' ">$(RootPath)\IterationNumber.txt</IterationNumberFile>
</PropertyGroup>

Ten plik może być generowany przez zewnętrzne narzędzie. Dobrym przykładem jest np. numer poprawnie przetestowanego builda przez narzędzie do Continuous Integration. Do pobrania numeru z pliku zastosujemy…

<!-- Gets the iteration number from file -->
<Target Name="GetIterationNumber">
    <!-- Read the the iteration number file contents -->
    <ReadLinesFromFile File="$(IterationNumberFile)">
        <Output TaskParameter="Lines" ItemName="IterationNumberFileContents"/>
    </ReadLinesFromFile>
 
    <!-- Assign file contents to IterationNumber property -->
    <CreateProperty Value="@(IterationNumberFileContents->'%(Identity)')">
        <Output TaskParameter="Value" PropertyName="IterationNumber"/>
    </CreateProperty>
 
    <!-- If tehere is no IterationNumber, set zero -->
    <CreateProperty Value="0" Condition="$(IterationNumber) == ''">
        <Output TaskParameter="Value" PropertyName="IterationNumber"/>
    </CreateProperty>
</Target>

Z SVN revision jest trochę inaczej. Jeżeli dany projekt nie został zmodyfikowany będę nadal chciał kompilować go z SVN revision jego ostatniego commit-u. Ta informacja jest zapisana w “LastChangedRevision”. Jeżeli projekt został zmodyfikowany, zastosuje “Revision”, które równa się najbliższemu numerowi, który zostanie nadany podczas następnego Commit. Jeżeli ktoś w międzyczasie zrobi własny revision, wówczas ten numer nam wskaże na jego commit. Dobrą praktyką jest zatem zrobienie Commit potem Update a potem Build końcowy.

<!-- Get the revision number of the local working copy -->
<Target Name="GetSvnRevision">
    <SvnVersion LocalPath="$(MSBuildProjectDirectory)" ContinueOnError="true">
        <Output TaskParameter="Modifications" PropertyName="SvnModified" />
    </SvnVersion>
 
    <SvnVersion
        LocalPath="$(MSBuildProjectDirectory)"
        UseLastCommittedRevision="!$(SvnModified)"
        ContinueOnError="true">
        <Output TaskParameter="Revision" PropertyName="SvnRevision"/>
    </SvnVersion>
 
    <PropertyGroup>
        <SvnRevision Condition="$(SvnRevision) == ''">0</SvnRevision>
    </PropertyGroup>
</Target>

Na końcu tasku zabezpieczamy property na wypadek, kiedy jeszcze nie mamy projektu pod kontrolą SVN-u. Ostatnim krokiem jest już złożenie wersji w całość.

<Target Name="CalculateAssemblyVersion" DependsOnTargets="GetIterationNumber;GetSvnRevision">
    <CreateProperty Value="$(AssemblyVersion).$(IterationNumber).$(SvnRevision)">
        <Output TaskParameter="Value" PropertyName="AssemblyVersion"/>
    </CreateProperty>
    <Message Text="Calculated Assembly Version: $(AssemblyVersion)" Importance="normal"/>
</Target>

Należy pamiętać jednak aby zmienić w Settings.proj nasz AssemblyVersion na postać dwucyfrową np.

<AssemblyVersion>1.0</AssemblyVersion>

Kod do dzisiejszego odcinka znajdziecie tutaj -> part006.

Update 2008-07-30

Mała aktualizacja związana z podpisywaniem Assemblies. Już od wersji NET 2.0 assemblies powinno się podpisywać przy wykorzystaniu parametru do kompilatora a nie poprzez wpis w AssemblyInfo.cs. Kwestie bezpieczeństwa. Jeżeli zrobimy to wg starego sposobu, wówczas podczas budowy pojawi się następujące ostrzeżenie.

warning CS1699: Use command line option '/keyfile' or appropriate project settings instead of 'AssemblyKeyFile'

Zatem w Settings.proj dodajemy następujące linie:

<!-- Signing Properties-->
<PropertyGroup>
    <SignAssembly>true</SignAssembly>
    <AssemblyOriginatorKeyFile>$(RootPath)\MySolution.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

… i oczywiście usuwamy property “AssemblyKeyFile”, które zadeklarowaliśmy wcześniej w tym odcinku.