Automatyzacja projektu z MSBuild-em - 6. Numer wersji z SVN revision
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.