Automatyzacja projektu z MSBuild-em - 2. Dlaczego MSBuild ?

Jul 14, 2008 - 4 minute read -

Aktualnie do dyspozycji mamy kilka możliwych narzędzi wspomagających budowę aplikacji w NET:

Mój ostateczny wybór, trochę metodą eliminacji padł na MSBuild.

A dlaczego nie Bake ?

Bake, poprzednio Boobs, jest to narzędzie wzorowane na Rake, systemie do automatyzacji zadań w środowisku Ruby. Oto przykład Bake:

Task "remove build dir":
    RmDir("build", true) if Exist("build")
 
Task "init build dir":
    MkDir("build")
    Cp(["lib/*.dll"], "build", true)
 
Task "build Bake", ["build engine", "build extensions", "build win32 helper"]:
    Booc(
        SourcesSet   : ["tools/Bake/**/*.boo"],
        OutputFile   : "build/Bake.exe",
        ReferencesSet: ["build/Bake.engine.dll", "build/boo.lang.useful.dll"]
        ).Execute()

Jego główną zaletą jest to, że skrypty są tworzone za pomocą języka Boo wzbogaconego o dodatkowa semantykę Domain Specific Language. Hmm co w tym nowego ? Stare, poczciwe skrypty dla “make” również zawierały swój własny DSL, który był zdefiniowany w zewnętrznych bibliotekach. NAnt i MSBuild to też DSL tylko że XML-owy. Definicja znaczników XML-owych też znajduje się w zewnętrznych bibliotekach. Skrypt dla Bake natomiast jest czymś w rodzaju “aplikacji” w momencie uruchamiania. Skrypt w naturalny sposób staje się po prostu czytelnym programem do, którego można wstawiać kod w naturalny sposób.  Można by się pokusić o stwierdzenie że NAnt też to potrafi chociażby dzięki taskowi “script”:

<script language="C#" >
    <references>
        <include name="System.Data.dll" />
    </references>
    <imports>
        <import namespace="System.Data.SqlClient" />
    </imports>
    <code>
      <![CDATA[
        public static void ScriptMain(Project project) {
            string dbUserName = "nant";
            string dbPassword = "nant";
            string dbServer = "(local)";
            string dbDatabaseName = "NAntSample";
            string connectionString = String.Format("Server={0};uid={1};pwd={2};", dbServer, dbUserName, dbPassword);
            
            SqlConnection connection = new SqlConnection(connectionString);
            string createDbQuery = "CREATE DATABASE " + dbDatabaseName;
            SqlCommand createDatabaseCommand = new SqlCommand(createDbQuery);
            createDatabaseCommand.Connection = connection;
            
            connection.Open();
            
            try {
                createDatabaseCommand.ExecuteNonQuery();
                project.Log(Level.Info, "Database added successfully: " + dbDatabaseName);
            } catch (Exception e) {
                project.Log(Level.Error, e.ToString());
            } finally {
                connection.Close();
            }
        }
      ]]>
    </code>
</script>

Jednak można stwierdzić zgodnie, że XML nie powstał z myślą o tworzeniu za jego pomocą czytelnych algorytmów. Niestety projekt Bake jest dopiero we wczesnej fazie rozwojowej i na dodatek od jakiegoś czasu nie jest uaktualniany. Aczkolwiek stanowi interesujący przykład w jakim kierunku powinny pójść narzędzia do zarządzania kompilacją kodu.

Warto również zwrócić uwagę na projekt Mite.Net. Jest on wzorowany na “Rails migrations”, czyli zarządzaniu zmianami struktury bazy danych pomiędzy różnymi wersjami aplikacji. Projekt ten również wykorzystuje Boo i posiada własny DSL.

company = "Company"
employee = "Employee"
 
up:
    add_table company:
         string "Name", { max_length = 65, unique = true }
    
    add_table employee:
         string "Name", { max_length = 65 }
         int32 "CompanyId"
    
    add_relation "employment", Cardinality.OneToMany, company, employee + ".CompanyId"
 
down:
    drop_table employee
    drop_table company

Narzędzie to na pewno będzie mogło się przydać nie tyle do kompilacji ale np. do aktualizacji bazy danych przed uruchomieniem testów. Projekt Mite.Net również jest w początkowej fazie rozwoju.

A dlaczego nie NAnt ?

Ależ oczywiście że jestem na “TAK”, a raczej byłem. Pierwsze kroki stawiałem właśnie z NAnt. Podczas pisania plików do NAnt-a wzorowałem się rozwiązaniach programistów w innych projektach OpenSource. Świetnym przykładem organizacji plików do NAnta są projekty NHibernate i Castle. W NHibernate każdy projekt Visual Studio ma swój odpowiednik NAnt z rozszerzeniem.build

<?xml version="1.0" ?>
 
<project 
    name="NHibernate" 
    default="build" 
    xmlns="http://nant.sf.net/release/0.85-rc3/nant.xsd"
>
 
    <property name="root.dir" value="../.." />
    <include buildfile="${root.dir}/build-common/common-project.xml" />
 
    <target name="init" depends="common.init">
        <property name="assembly.description" value="An object persistence library for relational databases." />
        <property name="assembly.allow-partially-trusted-callers" value="true" />
        <property name="clover.instrument" value="true" />
 
        <assemblyfileset id="project.references" basedir="${bin.dir}">
            <include name="System.dll" />
            <include name="System.Transactions.dll" />
            <include name="System.Configuration.dll" />
            <include name="System.XML.dll" />
            <include name="System.Data.dll" />
            <include name="System.Data.OracleClient" />
            <include name="System.Web.dll" />
            <include name="Iesi.Collections.dll" />
            <include name="log4net.dll" />
            <include name="Castle.Core.dll" />
            <include name="Castle.DynamicProxy2.dll" />
        </assemblyfileset>
 
        <resourcefileset id="project.resources" prefix="NHibernate" dynamicprefix="true">
            <include name="*.xsd" />
            <include name="**/*.xml" />
            <exclude name="bin/**/*.xml" />
        </resourcefileset>
        
        <fileset id="project.sources">
            <include name="**/*.cs" />
        </fileset>
    </target>
 
    <target name="generate-assemblyinfo" depends="init common.generate-assemblyinfo" />
 
    <target name="build" description="Build NHibernate"
        depends="generate-assemblyinfo common.compile-dll">
        <copy file="${bin.dir}/NHibernate.dll" tofile="${root.dir}/${lib.framework.dir}/NHibernate.dll"/>
    </target>
 
</project>

Następnie w głównym katalogu projektu znajduje się plik default.build konsolidujący ze sobą wszystkie pliki .build w poszczególnych projektach. Dodatkowo jest jeszcze katalog “build-common”, który zawiera pliki z parametrami do budowy oraz ze wspólnymi taskami. Dzięki takiemu rozwiązaniu uzyskujemy następujące zalety:

  • możemy niezależnie budować oraz testować aplikacje z parametrami przeznaczonymi dla różnych wersji frameworków np. mono
  • wspólne parametry jak wersjonowanie lub parametry do wspólnych zadań są zarządzane w jednym miejscu
  • każdy projekt może być skompilowany pojedynczo lub wszystkie razem

Czego chcieć więcej ? Zazwyczaj “potrzeba” jest matką wynalazków w moim wypadku to było “lenistwo”. W momencie kiedy dodawałem nową bibliotekę do referencji projektu, musiałem pamiętać aby ją uwzględnić również w pliku .build. Podobnie z plikami, które chciałem traktować jako resources w assembly również musiałem dodawać do pliku .build. Jeżeli dodawałem nowy projekt do solution, musiałem pamiętać by dodać ją również do pliku .build w głównym katalogu. Po co robić to dwa razy skoro i tak robimy to używając Visual Studio ? Przecież pliki .csproj są niczym innym jak plikami do MSBuilda a plik .sln służy za zbiór plików z projektami i może być bez problemu parametrem dla MSBuild-a.

msbuild MySolution.sln /t:build

Dodatkowo nie zamierzam kompilować projektów pod mono więc … wybór padł na MSBuild. Teraz czas udowdnić, że MSBuild z powodzeniem może zastąpić NAnt-a oraz przedstawić techniki z których korzystam w codziennej pracy. A to już w następnych odcinkach.