Automatyzacja projektu z MSBuild-em - 3. Początki

Jul 15, 2008 - 6 minute read -

W ramach dalszych odcinków będę budował od podstaw pliki do MSBuild na przykładzie prostego projektu w .NET 3.5 tak aby każdy mógł “poczuć” działające rozwiązanie. Poszczególne etapy rozwiązania znajdują się w repozytorium SVN w podkatalogach o numeracji odpowiadającej poszczególnym odcinkom.

Zanim przystąpimy do pracy warto zapatrzeć się w dwa proste narzędzia:

  • MSBuild Community Tasks Project - Biblioteka rozszerzająca funkcjonalność MSBuild.
  • Visual Debugger for MSBuild Projects - Jak sama nazwa wskazuje debugger do plików MSBuild, co prawda średnio stabilny, ale potrafi czasem pomóc.
  • PowerCommands for Visual Studio 2008 - Dodatek do Visual Studio, który m.in. ułatwia edycję plików csproj z poziomu naszego IDE dodając opcję “Edit Project File” spod prawego klawisza w “Solution Explorer”. Bez tego też nam się to uda, ale należy wówczas  zrobić “Unload Project” a potem “Edit Prtoject File”.

Pliki podstawowe

Będziemy tworzyć automatyzację do bardzo prostej aplikacji, która ma strukturę katalogów taką jak opisywałem w pierwszej części. Plik “MySolution.sln” zawiera odniesienie do dwóch plików projektowych “MyProject.csproj” oraz “MyProject.Tests.csproj”.

Solution

Z linii poleceń możemy skompilować nasze rozwiązanie w całości lub poszczególne projekty wykonując następujące polecenia:

C:\...\trunk> msbuild MySolution.sln
C:\...\trunk> msbuild src\app\MyProject\MyProject.csproj
C:\...\trunk> msbuild src\test\MyProject.Tests\MyProject.Tests.csproj

W tej chwili nie mamy  żadnego punktu centralnego gdzie moglibyśmy zarządzać wspólnymi parametrami. Pliki .csproj nie mają żadnego wspólnego punktu stycznego z którego mogłyby czerpać informację. Dodatkowo plik MySolution.sln nie jest plikiem, który możemy edytować. Aktualnie jest następujący workflow pomiędzy plikami w trakcie budowy.

Typical dependencies

Aby zapewnić lepszą elastyczność i centralizacje parametrów doprowadźmy do takiego powiązania plików …

Custom dependencies

To powiązanie jest łudząco podobne to układu w projekcie NHibernate.

Plik Settings.proj będzie plikiem zawierającym wspólne parametry, które będą wykorzystywane zarówno przez Default.proj jak i Common.Targets.

<?xml version="1.0" encoding="utf-8"?>
<!--
    ==================================================
        Settings file for VS and external build files
    ==================================================
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
 
    <!-- Root Path definition relative for actual build file -->
    <PropertyGroup>
        <RootPath Condition=" '$(RootPath)' == '' ">$(MSBuildProjectDirectory)</RootPath>
    </PropertyGroup>
    
    <!-- Project folders -->
    <PropertyGroup>
        <ToolsPath>$(RootPath)\tools</ToolsPath>
    </PropertyGroup>
    
    <!-- Project files -->
    <PropertyGroup>
        <SolutionPath>$(RootPath)\MySolution.sln</SolutionPath>
    </PropertyGroup>
 
</Project>

Property “RootPath” ma zawsze wskazywać na główny katalog naszego projektu i poprzez niego będziemy się zawsze odwoływać do wszystkich ścieżek. W ten sam sposób tworzymy “zmienne” ze ścieżkami pomocniczymi do katalogów np. “ToolsPath” lub plików jak “SolutionPath”.

Plik Default.proj będzie zawierał parametry oraz akcje wykonywane przez inne procesy niż Visual Studio np. przez Continuous Integration lub poprzez ręczne uruchomienie. Na początku będzie wyglądał bardzo prosto …

<?xml version="1.0" encoding="utf-8" ?>
<!--
==================================================
    Default file for external builds
==================================================
-->
<Project DefaultTargets="BuildAll" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file must be imported first -->
<Import Project="Settings.proj" />

<Target Name="BuildAll">
    <MSBuild Projects="$(SolutionPath)" Targets="Build"/>
</Target>
</Project>

Należy mieć na uwadze, że plik “Settings.proj” ma być importowany jak najwcześniej. Domyślnym celem jest “BuildAll”, który nie robi nic innego tylko w prosty sposób buduje solution. Teraz juz wystarczy …

C:\...\trunk> msbuild Default.proj

Każdy plik .csproj powinien być powiązany z plikiem Common.Targets, który na razie będzie wyglądał tak …

<?xml version="1.0" encoding="utf-8"?>
<!--
    ==================================================
        Common file for VS and external build files
    ==================================================
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
    <!-- This file must be imported first -->
    <Import Project="Settings.proj" />
    
    <Import Condition="Exists('$(RootPath)\Common.User.Targets')" Project="$(RootPath)\Common.User.Targets" />
</Project>

Plik Common.User.Targets to plik opcjonalny, który może zawierać parametry lub akcje specyficzne dla użytkownika. Dlatego daje ten plik do ignorowania przez svn, gdyż każdy programista w zespole może chcieć umieścić w nim różne zadania, niezależne od reszty zespołu.

Teraz można przystąpić do edycji plików .csproj. Na początku pliku należy poprawnie zdeklarować property RootPath …

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <!-- Root Path definition relative for actual build file -->
    <PropertyGroup>
        <RootPath Condition=" '$(RootPath)' == '' ">$(MSBuildProjectDirectory)\..\..\..</RootPath>
    </PropertyGroup>
    <PropertyGroup>
        <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
        <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
        <ProductVersion>9.0.21022</ProductVersion>
...

$(MSBuildProjectDirectory) wskazuje na katalog w którym aktualnie znajduje sie uruchamiany skrypt MSBuild. Następnie na końcu pliku projektu dodajemy nasz Common.Targets do importu. Czyli …

...
    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
    <Import Project="$(RootPath)\Common.Targets" />
    <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
        <Target Name="BeforeBuild">
    </Target>
    <Target Name="AfterBuild">
    </Target>
    -->
</Project>

Uwaga: Wartości odnoszące się do ścieżek nie zawsze na końcu zwracają “\“.  Warto o tym pamiętać.

Jeżeli  nie popełniliśmy żadnego błędu to po załadowaniu naszego solution do Visual Studio powinniśmy zobaczyć tylko komunikaty ostrzegawcze o modyfikacjach plików .csproj.

Warning

Należy wybrać “Load project normally”. Jeżeli będzie jakiś błąd w ścieżkach wówczas Visual Studio poinformuje nas o błędzie i pozostawi błędny projekt w stanie “Unload”. Należy poprawić błąd i zrobić “Reload Project”. Uczulam przed modyfikacja plików, które dodaliśmy w momencie, kiedy mamy otwarty projekt w Visual Studio. Czasem te pliki są cacheowane i wywołanie “Build” spod VS może nie uwzględnić zmian. W trakcie tworzenia własnych ulepszeń proponuje testować po prostu z linii poleceń.

Współdzielone pliki Targets

Przydałoby się zaimplementować jakieś proste zadanie wykorzystujące MSBuild Community Tasks. Zapewne już zainstalowaliśmy te rozszerzenia do MSbuild-a, ale czy pozostali programiści w zespole też to zrobili ? Aby uniknąć przykrych niespodzianek warto skopiować zawartosc katalogu “C:\Program Files\MSbuild” do naszego “tools\msbuild”. Należy również zmienić zawartość property “MSBuildExtensionsPath” oraz zaimportować rozszerzenia. Robimy to dodając do pliku “Settings.proj” wpis …

...
<!-- 3rd Party Program Paths -->
<PropertyGroup>
    <MSBuildExtensionsPath Condition="Exists('$(ToolsPath)\msbuild')" >$(ToolsPath)\msbuild</MSBuildExtensionsPath>
</PropertyGroup>
...

A do Common.Targets dodajemy linie importu…

...
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
...

Pierwsze wspólne zadanie do kompilacji

Teraz mamy już poprawnie powiązane nasze pliki. Spróbujmy zatem stworzyć jakieś wspólne zadanie np. dźwięk i tekst na ekranie po kompilacji. Zacznijmy od utworzenia następującego zadania w pliku Common.Targets.

...
<Target Name="SayHi">
    <Message Text="Project $(ProjectName) says 'HI' to everyone." Importance="High" />
    <Beep/>
</Target>
...

Teraz należy doprowadzić do wywołania zadania przed kompilacją każdego z projektów. Za kompilacje projektów C# odpowiada “$(MSBuildToolsPath)\Microsoft.CSharp.targets” i tam można znaleźć odpowiedź w jaki sposób oraz w jakiej kolejności są wykonywane czynności podczas budowy projektu. Dzięki temu możemy bardzo łatwo wcisnąć nasze zadanie pomiędzy zadaniami kompilacji …

...
<!-- Add additional depends to Build target -->
<PropertyGroup>
    <CoreCompileDependsOn>
        SayHi;
        $(CoreCompileDependsOn)
    </CoreCompileDependsOn>
</PropertyGroup>
...

Oczywiście wpis ten dokonujemy w pliku Common.Targets. Dla tych, którym nie chce się przeglądać pliku “Microsoft.CSharp.targets” polecam stronę How To: Add Custom Process at Specific Points During Build (Method #2). Teraz  kompilując nasz projekt z lini poleceń powinniśmy doświadczyć czegoś takiego …

C:\...\trunk> msbuild Default.proj
...
  Finished processing 0 edmx files
SayHi:
  Project MyProject says 'HI' to everyone.
CopyFilesToOutputDirectory:
  Copying file from "obj\Debug\MyProject.dll" to "bin\Debug\MyProject.dll".
...

  Finished processing 0 edmx files
SayHi:
  Project MyProject.Tests says 'HI' to everyone.
CopyFilesToOutputDirectory:
  Copying file from "obj\Debug\MyProject.Tests.dll" to "bin\Debug\MyProject.Tests.dll".
...

… oraz usłyszeć dwa razy “beep”. Nawet jak zrobimy Build  z Visual Studio wówczas również usłyszymy “beep”. :)

Updated: Okazuje się że teraz “beep” nawet usłyszymy kiedy będziemy chcieli dodać nową  lub usunąć istniejącą referencje do projektu :). Polecam przetestować.

Pełny projekt do tej części dostępny jest przez Svn-a -> Part003

Można powiedzieć, że tekst dzisiejszy zamyka w sumie częśc wprowadzającą. Od następnego odcinka zajmiemy się bardziej przydatnymi rzeczami w codzinnej pracy programisty. Wiec mam nadzieje że zaawansowani czytelnicy również znajdą pożyteczne dla nich informacje :). Zapraszam niebawem …