News

Update der .NET Build Projekt Vorlagen für Visual Studio 2008

16.05. 2008

Die Projektvorlagen für die .NET Buildumgebung wurden für Visual Studio 2008 ergänzt. Ausserdem wurde ein Checkout Problem in der Vorlage für Visual Studio 2005 mit SourceSafe behoben.

Weiterlesen …

Neuer Artikel über das Debuggen des .NET Frameworks

04.05. 2008

Ein neuer Artikel über das Debuggen der .NET Framework Basisklassen ist verfügbar.

Weiterlesen …

Newsfeed

Die News sind auch als RSS Feed verfügbar und können mit einem entsprechenden Programm abonniert werden. Wie geht das? Lesen sie mehr dazu auf Wikipedia.

Wissen > Build Umgebung für .NET > Der ContinuousBuild

Der ContinuousBuild

Der ContinuousBuild wird abhängig von den eigenen Präferenzen in einem bestimmten Intervall ausgeführt. Er sollte mindestens einmal pro Tag/Nacht laufen, kann aber auch nach jedem Commit (Checkin) ins Repository durchgeführt werden.
Der ContinuousBuild versioniert die Assemblies, kompiliert den Source Code, führt die Tests durch und publiziert diese Resultate auf dem Portal.

Dieser Build wird in der CruiseControl.Net Projekt Konfiguration so definiert, dass die Targets BuildAll und RunTests ausgeführt werden. Das MSBuild Skript wird wie folgt definiert:


<Project 
         DefaultTargets="BuildAll" 
         InitialTargets="GetFrameworkPaths;GetProjectsFromSolution" 
         xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
>
Wenn also das Build Skript ohne Angabe von Targets aufgerufen wird, so wird das BuildAll Target ausgeführt. In jedem Fall werden die in InitialTargets definierten Targets GetFrameworkPaths und GetProjectsFromSolution vorgängig ausgeführt.


<!-- Get the framework and SDK paths -->
<Target Name="GetFrameworkPaths">
    <GetFrameworkSdkPath>
        <Output TaskParameter="Path"
                PropertyName="FrameworkSdkPath" />
    </GetFrameworkSdkPath>

    <Message Text="FrameworkSdkPath: $(FrameworkSdkPath)" Importance="high" />

    <GetFrameworkPath>
        <Output TaskParameter="Path"
                PropertyName="FrameworkPath" />
    </GetFrameworkPath>
    
    <Message Text="FrameworkPath: $(FrameworkPath)$(NEW_LINE)" Importance="high" />
</Target>

<!-- Getting all projects from the specified solution to build -->
<Target Name="GetProjectsFromSolution">
    <Message Text="Getting solution projects for $(DOUBLE_QUOTES)$(SolutionFile)$(DOUBLE_QUOTES)" Importance="high" />
    
    <!-- Get all the projects associated with the solution -->
    <GetSolutionProjects Solution="$(SolutionFile)">
        <Output TaskParameter="Output" ItemName="SolutionProjects" />
    </GetSolutionProjects>

    <!-- Filter out solution folders and non .csproj items -->
    <RegexMatch Input="@(SolutionProjects)" Expression=".[\.]csproj$">
        <Output TaskParameter="Output" ItemName="CSProjects"/>
    </RegexMatch>

    <!-- Resolve the test projects -->
    <RegexMatch Input="@(CSProjects)" Expression=".*(UnitTest|IntegrationTest).*.csproj">
        <Output TaskParameter="Output" ItemName="TestProjects"/>
    </RegexMatch>

    <!-- Resolve the code projects -->
    <CreateItem Include="@(CSProjects)"
                Exclude="@(TestProjects)">
        <Output TaskParameter="Include" ItemName="CodeProjects"/>
    </CreateItem>

    <Message Text="$(NEW_LINE)CodeProjects:$(NEW_LINE)$(TAB)@(CodeProjects->'%(RelativeDir)%(FileName)%(Extension)', '$(NEW_LINE)$(TAB)')" Importance="high"/>
    <Message Text="$(NEW_LINE)TestProjects:$(NEW_LINE)$(TAB)@(TestProjects->'%(RelativeDir)%(FileName)%(Extension)', '$(NEW_LINE)$(TAB)')" Importance="high"/>
    
    <!-- Get all WiX projects from the setup solution -->
    <GetSolutionProjects Solution="$(WixSolutionFileName)">
        <Output TaskParameter="Output" ItemName="WixProjects" />
    </GetSolutionProjects>
    
    <Message Text="$(NEW_LINE)WixProjects:$(NEW_LINE)$(TAB)@(WixProjects->'%(RelativeDir)%(FileName)%(Extension)', '$(NEW_LINE)$(TAB)')" Importance="high"/>
</Target>
Das Target GetFrameworkPaths verwendet zwei Standard MSBuild Tasks um zwei Properties zu definieren, welche die Pfade zum .NET SDK und zum .NET 2.0 Framwork Verzeichnis beinhalten. Über diese Pfade können dann diverse .NET Tools (z.B. um Assemblies zu signieren) angesprochen werden.

Das Target GetProjectsFromSolution verwendet die Community Tasks GetSolutionProjects und RegexMatch um alle Projekte der Visual Studio Solution in den Items CodeProjects und TestProjects zu speichern. Über diese Items können dann die Projekte kompiliert werden.
Es ist deshalb wichtig, dass alle Projekte in der Solution, die im Property $(SolutionFile) eingetragen ist, erfasst sind. Diese Solution ist nicht notwendigerweise auch diejenige, mit welcher die Entwickler arbeiten, oft ist es gerade bei umfangreichen Projekten sinnvoll, mehrere Solutions anzulegen, um nur gewisse Projektbestandteile zu bearbeiten, oder um beispielsweise die Test Projekte vom Produktionscode zu trennen.
Damit ein Test Projekt als solches erkannt wird und von NUnit ausgeführt werden kann, muss das Testprojekt entweder "UnitTest" oder "IntegrationTest" als Teil des Namens haben.
Am Schluss dieses Targets werden alle WiX-Setup Projekte der Solution im Install Verzeichnis im Item WixProjects gespeichert. Sie werden später für das Erstellen der MSI Setups benötigt.

Die nächsten Targets widmen sich der Versionierung der Assemblies:


<!-- Get the current build number from the file -->
<Target Name="GetBuildNumber">
    <ReadLinesFromFile File="$(BuildNumberFile)">
        <Output TaskParameter="Lines" ItemName="BuildNumberFileContents"/>
    </ReadLinesFromFile>

    <CreateItem Include="@(BuildNumberFileContents)">
        <Output TaskParameter="Include" PropertyName="BuildNumber"/>
    </CreateItem>
    
    <Message Text="Running build $(BuildNumber)" Importance="high" />
</Target>

<!-- Increment the build number in the file -->
<Target Name="IncrementBuildNumber"	DependsOnTargets="GetBuildNumber">

    <!-- Increment the iteration number before the commit -->
    <Add Numbers="$(BuildNumber);1">
        <Output TaskParameter="Result" PropertyName="NextBuildNumber"/>
    </Add>

    <!-- Write the new build number into the file -->
    <WriteLinesToFile File="$(BuildNumberFile)"
                      Lines="$(NextBuildNumber)"
                      Overwrite="true"/>
</Target>

<!-- Get the revision number of the local working copy -->
<Target Name="GetRevisionNumber">
    <!--
    <SvnInfo LocalPath="$(MSBuildProjectDirectory)">
        <Output TaskParameter="Revision" PropertyName="RevisionNumber"/>
    </SvnInfo>
    -->
    <!-- TODO -->
    <CreateProperty	Value="123">
        <Output TaskParameter="Value" PropertyName="RevisionNumber" />
    </CreateProperty>

    <Message Text="Running build for revision $(Revision)" Importance="high" />
</Target>

<!-- Create the AssemblyInfoCommon.cs file which is linked from all projects -->
<Target Name="CreateAssemblyInfoCommon" DependsOnTargets="GetBuildNumber;GetRevisionNumber">
    
    <!-- Create the version -->
    <CreateProperty Value="$(Major).$(Minor).$(BuildNumber).$(RevisionNumber)">
        <Output TaskParameter="Value" PropertyName="AssemblyVersion"/>
    </CreateProperty>

    <Message Text="AssemblyVersion number generated: $(AssemblyVersion)" Importance="high"/>
    
    <!-- Create all assembly info file tokens -->
    <CreateItem Include="AssemblyVersion" AdditionalMetadata="ReplacementValue=$(AssemblyVersion)">
        <Output TaskParameter="Include" ItemName="AssemblyInfoTokens" />
    </CreateItem>
    <CreateItem Include="AssemblyConfiguration" AdditionalMetadata="ReplacementValue=$(Configuration)">
        <Output TaskParameter="Include" ItemName="AssemblyInfoTokens" />
    </CreateItem>
    <CreateItem Include="Company" AdditionalMetadata="ReplacementValue=$(Company)">
        <Output TaskParameter="Include" ItemName="AssemblyInfoTokens" />
    </CreateItem>
    <CreateItem Include="ProductName" AdditionalMetadata="ReplacementValue=$(ProductName)">
        <Output TaskParameter="Include" ItemName="AssemblyInfoTokens" />
    </CreateItem>

    <!-- Create or overwrite the AssemblyInfoCommon.cs file -->
    <TemplateFile Template="$(AssemblyInfoTemplate)"
                  OutputFilename="$(AssemblyInfoFile)"
                  Tokens="@(AssemblyInfoTokens)"/>
</Target>
Bevor der Source Code kompiliert wird, sollen die Versions, Firmen und Copyright Attribute in für jedes Projekt der Solution gesetzt werden. Damit nun nicht in jedem Projekt die AssemblyInfo.cs Datei angepasst werden muss, verweist jedes Projekt der Solution auf eine zentrale Datei AssemblyInfoCommon.cs. Diese beinhaltet diejenigen Attribute, welche für alle Assemblies gleich sind. Diese Attribute müssen in der standardmässig angelegten Datei AssemblyInfo.cs entfernt werden, weil ein Attribut nicht zwei Mal definiert werden darf.
Wenn ein neues Projekt in der Solution angelegt wird, kann ein Link auf die Solution Datei AssemblyInfoCommon.cs gesetzt werden wie dies im einfachen Beispielprojekt im src Verzeichnis gezeigt wird.

Das Target GetBuildNumber verwendet den Task ReadLinesFromFile, um aus der Datei BuildNumber.txt die aktuelle Build Nummer zu lesen. Grundsätzlich wäre diese Nummer auch vom Build Server (CruiseControl.Net) abrufbar, das Speichern der Nummer erlaubt jedoch auch Mal ein Zurücksetzen oder neu definieren der Numer und macht den Build etwas weniger abhängig von einem bestimmten System.
Die Build Nummer bildet die dritte Stelle der Assembly Versionsinformation (Major.Minor.Build.Revision).

Im Target IncrementBuildNumber wird diese Nummer um 1 erhöht, damit der nächste Build eine aktualisierte Nummer vorfindet. Die aktualisierte Datei BuildNumber.txt wird später ins Repository kopiert.

Das Target GetRevisionNumber benutzt den Community Task SvnInfo, um die Subversion Revision der aktuellen Arbeitskopie abzufragen. Diese Nummer bildet die vierte Stelle der Assembly Versionsinformation (Major.Minor.Build.Revision).

Im Target CreateAssemblyInfoCommon wird schliesslich die zentrale Datei AssemblyInfoCommon.cs überschrieben, welche in alle C# Projekte gelinkt ist. Dazu wird der sehr nützliche Community Task TemplateFile verwendet.

Visual SourceSafe
Die Projektvorlage für Visual SourceSafe enthält gegenüber der Version für Subversion 2 zusätzliche Targets VssCheckout und VssUndoCheckout.
Um in einer SourceSafe Umgebung mit Dateien arbeiten zu können, müssen diese vorgängig ausgecheckt werden. Da im ContinuousBuild aber die Änderungen nicht im Repository gespeichert werden, wird nach dem Kompilieren und Ausführen der Tests ein Undo Checkout durchgeführt.


Die nun folgenden Targets bauen auf diesem Gerüst auf und implementieren das eigentliche Kompilieren und Testen des Codes.


<!-- Clean the solution output -->
<Target Name="CleanSolution">

    <Message Text="$(CleanDependsOn)" />
    
    <!-- Create item collection of assemblies and other artifacts produced by the build -->
    <CreateItem Include="@(CodeProjects->'%(RelativeDir)bin\**\*.*');
                         @(CodeProjects->'%(RelativeDir)obj\**\*.*');
                         @(TestProjects->'%(RelativeDir)bin\**\*.*');
                         @(CodeProjects->'%(RelativeDir)obj\**\*.*');">
        <Output TaskParameter="Include" ItemName="SolutionOutput" />
    </CreateItem>

    <Message Text="Deleting previous build output files..." Importance="high" />
    <Message Text="@(SolutionOutput->'%(FullPath)', '$(NEW_LINE)')" />

    <!-- Delete all the solution created artifacts -->
    <Delete Files="@(SolutionOutput)"/>

    <!-- Change the default BuildTarget to include a Clean before a Build -->
    <CreateProperty Value="Clean;$(BuildTargets)">
        <Output TaskParameter="Value" PropertyName="BuildTargets"/>
    </CreateProperty>

    <Message Text="BuildTargets: $(BuildTargets)" />
</Target>

<!-- Compile all projects -->
<Target Name="BuildAll" DependsOnTargets="BuildCode;BuildTests" />

<!-- Compile the code projects -->
<Target Name="BuildCode" DependsOnTargets="CreateAssemblyInfoCommon">
    <Message Text="Building the code projects..." Importance="high" />
    
    <!-- Build the assemblies -->
    <MSBuild Projects="@(CodeProjects)"
             Targets="$(BuildTargets)"
             Properties="Configuration=$(Configuration);Platform=$(Platform);RunCodeAnalysis=$(RunCodeAnalysis)">
        <Output TaskParameter="TargetOutputs"
                ItemName="CodeAssemblies"/>
    </MSBuild>

    <!-- Add the compiled code assemblies to the master list of all compiled assemblies for the build -->
    <CreateItem Include="@(CodeAssemblies)">
        <Output TaskParameter="Include" ItemName="CompiledAssemblies"/>
    </CreateItem>
</Target>

<!-- Compile the test projects -->
<Target Name="BuildTests">
    <Message Text="Building the test projects..." Importance="high" />
    
    <!-- Build the assemblies -->
    <MSBuild Projects="@(TestProjects)"
             Targets="$(BuildTargets)"
             Properties="Configuration=$(Configuration);Platform=$(Platform);RunCodeAnalysis=$(RunCodeAnalysis)">
        <Output TaskParameter="TargetOutputs"
                ItemName="TestAssemblies"/>
    </MSBuild>

    <!-- Add the compiled test assemblies to the master list of all compiled assemblies for the build -->
    <CreateItem Include="@(CodeAssemblies)">
        <Output TaskParameter="Include" ItemName="TestAssemblies"/>
    </CreateItem>
</Target>

<!-- Run NUnit tests -->
<Target Name="RunTests" DependsOnTargets="BuildTests">
    <Message Text="Running the test projects..." Importance="high" />
    
    <NUnit Assemblies="@(TestAssemblies)"
           ToolPath="$(NUnitPath)"
           WorkingDirectory="%(TestAssemblies.RootDir)%(TestAssemblies.Directory)"
           OutputXmlFile="@(TestAssemblies->'%(FullPath)$(TestResultFileAppendix)')"
           ContinueOnError="true">
        <Output TaskParameter="ExitCode" ItemName="TestExitCodes"/>
    </NUnit>

    <!-- Copy the test results for the CCNet build before the build is possibly declared as failed -->
    <CallTarget Targets="CopyTestResults" />

    <!-- Fail the build if any test failed -->
    <Error Text="Test error(s) occured" Condition=" '%(TestExitCodes.Identity)' != '0'"/>

</Target>

<!-- Copy test results for reporting when running @ CCNet server -->
<Target Name="CopyTestResults"
        Condition=" '$(CCNetProject)' != '' ">

    <Message Text="Copying test results..." Importance="high" />
    
    <Message Text="%(TestAssemblies.FullPath)" />
    
    <!-- Create item collection of test results -->
    <CreateItem Include="%(TestAssemblies.FullPath)$(TestResultFileAppendix)">
        <Output TaskParameter="Include" ItemName="TestResults"/>
    </CreateItem>
    
    <!-- Create item collection of existing test results -->
    <CreateItem Include="$(CCNetArtifactDirectory)\*$(TestResultFileAppendix)">
        <Output TaskParameter="Include" ItemName="ExistingTestResults"/>
    </CreateItem>

    <Message Text="TestResults:$(NEW_LINE)$(TAB)@(TestResults->'%(FullPath)', '$(NEW_LINE)$(TAB)')$(NEW_LINE)" Importance="low"/>
    
    <Delete Files="@(ExistingTestResults)"/>

    <Copy SourceFiles="@(TestResults)"
          DestinationFolder="$(CCNetArtifactDirectory)"
          ContinueOnError="true"/>

</Target>
Das CleanSolution Target löscht alle von vorherigen Builds erstellten Dateien in allen obj und bin Verzeichnissen.

Das BuildAll Target fasst lediglich die Targets BuildCode und BuildTests zusammen. Diese verwenden für jedes Projekt jeweils den eingebauten MSBuild Task und übergeben die zu verwendende Konfiguration (Debug/Release und Plattform). Die Pfade der kompilierten Assemblies werden in den Items CodeAssemblies und TestAssemblies gespeichert.

Die Unit Tests werden im Target RunTests ausgeführt. Die zuvor gespeicherten Test Assemblies werden an den Community Task NUnit übergeben. Falls einer der Unit Tests nicht erfolgreich durchläuft, wird der gesamte Build durch den Aufruf des Error Tasks als fehlerhaft deklariert. Zuvor werden die von NUnit erzeugten Test Resultate durch den Aufruf des Targets CopyTestResults jedoch noch kopiert, damit sie im Portal zur Verfügung stehen.
CopyTestResults wird durch das Setzen einer Bedingung nur auf dem Build Server ausgeführt. Das Property $(CCNetProject) wird durch CruiseControl.Net abgefüllt, und ist dadurch auf dem Entwickler PC nicht gesetzt.

Dies sind bereits alle Targets, welche vom ContinuousBuild aufgerufen werden. Diese Targets sind für die Projektvolage mit Subversion und Visual SourceSafe dieselben.

Die nun folgenden Targets des ReleaseBuilds befassen sich mit dem Update des Repositories und können für diese beiden Projektvorlagen geringfügig voneinander abweichen.