Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Remote backup dla SVN na GoogleCode

Backupy dla SVN można wykonać za pomocą komendy "svnadmin dump". Możliwe jest to jedynie wyłącznie kiedy mamy fizyczny dostęp do repozytorium. Co zrobić kiedy nasz projekt jest hostowany ?

  • Należy zrobić lokalny mirror. W tym celu napisałem mały skrypt WSH, który automatyzuje poszczególne kroki. Można go pobrać stąd -> svnmirror.zip oraz uruchomić podając gdzie ma się znajdować lokalne repozytorium a gdzie zdalne. Przykład
C:\SVN>svnmirror C:\SVN\RubyView.Mirror https://dabrowski.daniel@rubyview.googlecode.com/svn
  • Teraz już wystarczy wykonać dump naszego mirror-a czyli:
C:\SVN>svnadmin dump C:\SVN\RubyView.Mirror > RubyView.Mirror.20080821
opublikowano przez rod | 2 komentarzy
Filed under:

Dynamiczne referencje do bibliotek w Visual Studio

Często, w trakcie korzystania z zewnętrznych bibliotek w naszym  projekcie, pojawia się pewien dylemat. Czy dołączone biblioteki, powinny być skompilowane jako "debug" czy jako "release" ?  Zamiast podejmować trudną decyzję zróbmy sobie proste rozwiązanie.

W Automatyzacja projektu z MSBuild-em - 1. Struktura proponowałem przechowywanie "third-party libraries" w katalogu "lib". Teraz dodatkowo zróbmy tam podział na "debug" i "release". Przykład z bibliotekami z IronRuby, które wykorzystuje w aktualnym projekcie:

  • lib
    • net
      • 2.0
        • IronRuby
          • debug
          • release

Struktura jest na tyle oczywista że chyba nie trzeba opisywać.

Teraz w naszym pliku projektu ".csproj" wystarczy tak zmodyfikować referencje do bibiliotek:

<ItemGroup>
  <Reference Include="IronRuby">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>..\..\..\lib\net\2.0\IronRuby\$(Configuration)\IronRuby.dll</HintPath>
  </Reference>
  <Reference Include="IronRuby.Libraries">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>..\..\..\lib\net\2.0\IronRuby\$(Configuration)\IronRuby.Libraries.dll</HintPath>
  </Reference>
  <Reference Include="Microsoft.Scripting">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>..\..\..\lib\net\2.0\IronRuby\$(Configuration)\Microsoft.Scripting.dll</HintPath>
  </Reference>
  <Reference Include="Microsoft.Scripting.Core">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>..\..\..\lib\net\2.0\IronRuby\$(Configuration)\Microsoft.Scripting.Core.dll</HintPath>
  </Reference>
  <Reference Include="System" />
  <Reference Include="System.Data" />
  <Reference Include="System.Xml" />
</ItemGroup>

Jezeli nasz projekt jest kompilowany w trybie "Debug", wówczas kompilacja odbędzie się z wykorzystaniem innych bibliotek z podkatalogu "debug". Analogicznie z "Release".

opublikowano przez rod | 0 komentarzy
Filed under:

Testowanie kompatybilnosci przegladarek dla Helpers w MonoRail

Helpers w MonoRail są często wykorzystywane do generowania kodu Html. Bywają sytuacje kiedy tworzony Html powinien wyglądać inaczej dla różnych przeglądarek. Czasem do wykrycia rodzaju oraz wersji przeglądarki stosuje się System.Web.HttpRequest.Browser, który zwraca klasę System.Web.HttpBrowserCapabilities. Niestety to rozwiązanie ma trzy zasadnicze wady:

  • Trudno jest mockować klasę HttpBrowserCapabilities
  • Rozwiązanie to opiera się na pliku browscap.ini. W momencie uruchamiania aplikacji  z serwera firmy hostingowej, nie mamy żadnej kontroli nad aktualizacja pliku browscap.ini.
  • System.Web.HttpRequest różni się od  Castle.MonoRail.Framework.IRequest przede wszystkim tym, że IRequest nie ma property Browser :) i nie korzysta z HttpBrowserCapabilities :). Całe szczęście.

Pozostaje nam stare dobre rozwiązanie a mianowicie własnoręczne rozpoznawanie "HTTP_USER_AGENT" w naszym Helperze.

public virtual bool IsCompatibleBrowser()
{
    string agent = Context.Request.Params["HTTP_USER_AGENT"];
 
    // IE
    if (agent.IndexOf("MSIE") >= 0 && agent.IndexOf("Windows") >= 0 && agent.IndexOf("Opera") < 0)
    {
        var match = Regex.Match(agent, @"(?<=MSIE )[\d\.]+");
        return (match.Success && float.Parse(match.Value, CultureInfo.InvariantCulture) >= 5.5);
    }
...

Lecz w jaki sposób to testować ? Najprostszym sposobem jest wykorzystanie istniejących we frameworku MonoRail klas typu stub.

using Castle.MonoRail.Framework.Test;
...
// SUT = System Under Test czyli mój helper
private MyHelper _sut;
 
[SetUp]
public void SetUp()
{
    _sut = new MyHelper();
 
    _sut.SetController(new HomeController(), new ControllerContext());
    _sut.SetContext(new StubEngineContext(new StubRequest(), new StubResponse(), new UrlInfo("area", "home", "index", "/app", "sdm")));
    _sut.ServerUtility = new StubServerUtility();
}

Przydadzą nam się linki do dwóch stron, które zawierają zbiór wszelakiej maści "HTTP_USER_AGENT".

Dzięki nim łatwo możemy stworzyć następujące testy

// IE 5.5+ on Windows
[TestCase(@"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; WOW64; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506; Media Center PC 5.0; .NET CLR 1.1.4322)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; InfoPath.1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; Dealio Deskball 3.0)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; NeosBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)")]
// FireFox 1.5+
[TestCase(@"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1")]
[TestCase(@"Mozilla/5.0 (X11; U; Linux x86_64; sv-SE; rv:1.8.1.12) Gecko/20080207 Ubuntu/7.10 (gutsy) Firefox/2.0.0.12")]
[TestCase(@"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060614 Fedora/1.5.0.4-1.2.fc5 Firefox/1.5.0.4 pango-text")]
[TestCase(@"Mozilla/5.0 (X11; U; Darwin Power Macintosh; en-US; rv:1.8.0.12) Gecko/20070803 Firefox/1.5.0.12 Fink Community Edition")]
[TestCase(@"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8) Gecko/20051201 Firefox/1.5")]
public void IsCompatibleBrowser_ThisBrowserShouldBeCompatible(string agent)
{
    _sut.Context.Request.Params["HTTP_USER_AGENT"] = agent;
    Assert.That(_sut.IsCompatibleBrowser());
}
 
// IE < 5.5 on Windows
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 4.01; Windows NT 5.0)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; .NET CLR 1.0.3705)")]
// IE on Mac
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC)")]
[TestCase(@"Mozilla/4.0 (compatible; MSIE 5.5b1; Mac_PowerPC)")]
[TestCase(@"Mozilla/5.0 (MSIE 7.0; Macintosh; U; SunOS; X11; gu; SV1; InfoPath.2; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648)")]
// FireFox < 1.5
[TestCase(@"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8b4) Gecko/20050908 Firefox/1.4")]
[TestCase(@"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.13) Gecko/20060410 Firefox/1.0.8")]
[TestCase(@"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.2b) Gecko/20020923 Phoenix/0.1")]
public void IsCompatibleBrowser_ThisBrowserShouldNOTBeCompatible(string agent)
{
    _sut.Context.Request.Params["HTTP_USER_AGENT"] = agent;
    Assert.That(!_sut.IsCompatibleBrowser());
}

Nadmienię tylko, że korzystam z wersji NUnit 2.5 Alpha-3 Release, stąd atrybut "TestCase", który jest odpowiednikiem np. "RowTest" w MbUnit.

opublikowano przez rod | 1 komentarzy
Filed under: ,

Automatyzacja projektu z MSBuild-em - 7. Inputs i Outputs, czyli fast & furious

Podczas kompilacji często możemy dostrzec następujący komunikat.

CoreCompile:
Skipping target "CoreCompile" because all output files are up-to-date with respect to the input files.

Jest to efekt funkcjonalnosci budowy przyrostowej - "incremental build". Dzieki niej nasze skrypty mogą być o wiele wydajniejsze. Każdy target może mieć parametry Inputs i Outputs. Przed wykonaniem targetu MSBuild sprawdza timestamp plików w Inputs plikami w Outputs.  I jeżeli Inputs > Outputs, wówczas przystępuje do wykonania zadania a jeżeli Inputs =< Outputs wówczas "pomija" zadanie z uwzględnieniem "output inferral" ... o tym pózniej.

<Target Name="Build" 
    Inputs="@(CSFile)" 
    Outputs="hello.exe">
 
    <Csc
        Sources="@(CSFile)" 
        OutputAssembly="hello.exe"/>
</Target>

Powyższy przykład pokazuje, że kompilacja zostanie wykonana w momencie, kiedy data któregoś z plików "@(CSFile)" będzie większa niż docelowy plik, czyli "hello.exe".

Spróbujmy zmienić nasz testowy target SayHi tak, aby się wykonywał przyrostowo porównująć pliki do skompilowania z plikami juz skompilowanymi.

<Target Name="SayHi" Inputs="@(Compile)" Outputs="$(OutputPath)\$(TargetName).dll">
    <Message Text="Project $(ProjectName) says 'HI' to everyone." Importance="High" />
    <Beep/>
</Target>

Nie jest to do końca idealne rozwiązanie. Czasem np. zmiana zawartosci pliku Resource powinna również wymusić przyrostowe wykonanie. W powyższym przykładzie to się nie stanie gdyż sprawdzamy wyłącznie "@(Compile)". Co zatem powinniśmy brać pod uwagę ? Wystarczy przyjżeć się taskowi "CoreCompile" z "Microsoft.CSharp.Targets".

<Target
    Name="CoreCompile"
    Inputs="$(MSBuildAllProjects);
            @(Compile);                               
            @(_CoreCompileResourceInputs);
            $(ApplicationIcon);
            $(AssemblyOriginatorKeyFile);
            @(ReferencePath);
            @(CompiledLicenseFile);
            @(EmbeddedDocumentation); 
            $(Win32Resource);
            $(Win32Manifest);
            @(CustomAdditionalCompileInputs)"
    Outputs="@(DocFileItem);
             @(IntermediateAssembly);
             @(_DebugSymbolsIntermediatePath);                 
             $(NonExistentFile);
             @(CustomAdditionalCompileOutputs)"
    DependsOnTargets="$(CoreCompileDependsOn)"
>
...

Zatem moglibyśmy skopiować parametry "Inputs" i "Outputs" z "CoreCompile" do "SayHi". Jednak to nie wystarczy. W momencie gdy będziemy chcieli uruchomić "SayHi" po wykonaniu "CoreCompile", wówczas nasz target nigdy nie zostanie wykonany gdyż "Outputs" będa już "up-to-date". Zróbmy sobie zatem naszą własną zmienną - "IsCompileUpToDate".

Output inferral

Mało kto o tym wie, ale istnieje pewna ukryta cecha w "incremental build".

Uwaga: Bez względu na "Inputs" i "Outputs", MSBuild zawsze skanuje target i zawsze wykonuje elementy odpowiedzialene za tworzenie lub zmianę Property i Item.

Cecha ta nazywa się "output inferral" i ma ona niwelować negatywny wplyw modyfikowanych zmiennych w zadaniach pominiętych, na realizację zadań jeszcze nie wykonanych. Zobaczymy to na przykładzie tworzenia naszej pomocniczej zmiennej.

<Target Name="SetIsCompileUpToDate" DependsOnTargets="_InitializeIsCompileUpToDate;_CheckIsCompileUpToDate" />
        
<Target Name="_CheckIsCompileUpToDate"
        Inputs="$(MSBuildAllProjects);
                @(Compile);
                @(_CoreCompileResourceInputs);
                $(ApplicationIcon);
                $(AssemblyOriginatorKeyFile);
                @(ReferencePath);
                @(CompiledLicenseFile);
                @(EmbeddedDocumentation);
                $(Win32Resource);
                $(Win32Manifest);
                @(CustomAdditionalCompileInputs)"
        Outputs="@(DocFileItem);
                @(IntermediateAssembly);
                @(_DebugSymbolsIntermediatePath);
                $(NonExistentFile);
                @(CustomAdditionalCompileOutputs)">
    <CreateProperty Value="false">
        <Output PropertyName="IsCompileUpToDate" TaskParameter="ValueSetByTask"/>
    </CreateProperty>
    <Message Text="_CheckIsCompileUpToDate $(IsCompileUpToDate)" />
</Target>
 
<Target Name="_InitializeIsCompileUpToDate">
    <CreateProperty Value="true" >
        <Output PropertyName="IsCompileUpToDate" TaskParameter="ValueSetByTask" />
    </CreateProperty>
    <Message Text="_InitializeIsCompileUpToDate $(IsCompileUpToDate)" />
</Target>

Początkowo inicjalizujemy naszą zmienną wartością "true", a następnie w zależności od Inputs i Outputs zmieniamy jej wartość na "false". Zgodnie z "output inferral", pomimo iż target "_CheckIsCompileUpToDate" byłby teoretycznie pomijany to zmiennej IsCompileUpToDate i tak nadana by była wartość "false". Od wersji MSBuild 3.5 mamy nowy typ "TaskParameter" a mianowicie "ValueSetByTask", który zastosowałem powyżej. Dzieki niemu omijamy "output inferral" i wszsytko działa tak jak zamierzaliśmy.

Teraz pytanie, w którym momencie powinniśmy uruchomić "SetIsCompileUpToDate" ? Jak zauważylismy w jednym z poprzednich odcinków, dodanie targetu do "CoreCompileDependsOn" nie jest najlepszym rozwiązaniem gdyż np. dodawanie referencji do projektu spod Visual Studio uruchamia target "CoreCompile". Z drugiej strony musimy być pewni że nasze zmienne w Inputs i Outpus są wypełnione przez proces budowy. Na przykład  "_CoreCompileResourceInputs" dopiero powstaje w "_GenerateCompileInputs" w Microsoft.Common.targets. Przyjżyjmy się jak wygląda wogóle target "Compile".

<PropertyGroup>
    <CompileDependsOn>
      ResolveReferences;
      ResolveKeySource;
      SetWin32ManifestProperties;
      _GenerateCompileInputs;
      BeforeCompile;
      _TimeStampBeforeCompile;
      CoreCompile;
      _TimeStampAfterCompile;
      AfterCompile
    </CompileDependsOn>
</PropertyGroup>
<Target
    Name="Compile"
    DependsOnTargets="$(CompileDependsOn)"/>

Teraz już widzimy ... najlepiej w "BeforeCompile". Ten target możemy poprostu nadpisać w naszym Commons.Targets

<Target Name="BeforeCompile" DependsOnTargets="SetIsCompileUpToDate" />

Zatem zróbmy już docelowy refaktoring zarówno dla "SayHi" jak i "IncludeGeneratedAssemblyInfo".

Pamiętacie nasz trick z "Touch" przy generowaniu AssemblyInfo.cs ? Teraz możemy go pominąć, ale aby zachować kompatybilność, być może ktoś nie bedzie chciał korzystać z metody "SetIsCompileUpToDate", zmodyfikujmy go tak ...

<Touch Files="$(AssemblyInfoFile)" Time="2000-01-01" Condition="$(IsCompileUpToDate) == ''" />

... natomiast Commons.Target będzie wyglądał tak ...

<!-- Add additional depends to Build target -->
<PropertyGroup>
    <BuildDependsOn>
        $(BuildDependsOn);
        SayHi;
    </BuildDependsOn>
</PropertyGroup>
...
<Target Name="BeforeCompile" DependsOnTargets="SetIsCompileUpToDate">
    <CallTarget Targets="IncludeGeneratedAssemblyInfo" Condition="$(IsCompileUpToDate) == 'false'" />
</Target>
 
<Target Name="SayHi" Condition="$(IsCompileUpToDate) == 'false'" >
    <Message Text="Project $(ProjectName) says 'HI' to everyone. " Importance="High" />
    <Beep/>
</Target>

Kod do dzisiejszego odcinka dostępny tutaj -> part007.

Na konieć nadmienię że istnieje bardzo ciekawe narzędzie, które nazywa sie MSBuild Profiller. Sposób działania jest bardzo prosty. Opiera się on na własnym Loggerze do MSBuild i po zainstalowaniu uruchamia się go w następujący sposób

MSBuild.exe mybuildfile.proj /t:mytarget /l:MSBuildProfileLogger,MSBuildProfiler,Version=1.0.1.0,Culture=neutral,PublicKeyToken=09544254e89d148c

.. i mamy wówczas taki efekt

opublikowano przez rod | 0 komentarzy
Filed under: ,

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.

opublikowano przez rod | 0 komentarzy

Automatyzacja projektu z MSBuild-em - 5. Makefile z Mono

W tym odcinku chciałbym przedstawić jak można dynamicznie budować kompilację, nie używając do tego w ogóle Visual Studio. W następnym odcinku temat będzie podobny, ale już z uwzględnieniem Visual Studio.

Za przykład posłuży nam projekt, który oryginalne był tworzony pod mono z wykorzystaniem plików Makefile. Mowa tu o FaRetSys aka eithne. Do budowy wykorzystamy źródła wersji 0.4.2. Aby być niezależnym oraz nie modyfikować plików źródłowych w żaden sposób, przyjąłem następującą strukturę:

  • ..
    • eithne - katalog, do którego wrzucamy źródła eithne. Na ten katalog będzie wskazywać property $(SourcePath)
    • eithne.msbuild - katalog z plikami, które my będziemy tworzyć oraz dostarczać - $(RootPath)
      • build - zostanie utworzony dynamicznie i tu znajdzie się skompilowana aplikacja
      • lib - katalog, do którego wrzucamy potrzebne biblioteki - $(LibraryPath)
      • tools - raczej oczywiste

Projekt będziemy budować za pomocą "csc.exe" czyli .NET Framework a nie Mono. Do tego celu wykorzystamy oczywiście Microsoft.CSharp.targets. Jako pierwsze skompilujemy źródła IPlugin. Makefile do tych źródeł wygląda następująco.

MCS = mcs
 
TARGET = ../IPlugin.dll
 
IPLUGIN = \
    BPP.cs \
    CommSocket.cs \
    Config.cs \
    DialogMessage.cs \
    DialogQuestion.cs \
    GConfConfig.cs \
    IBlock.cs \
    ICommImage.cs \
    ICommResult.cs \
    IConfig.cs \
    IFactory.cs \
    IImage.cs \
    IInfo.cs \
    IInPlugin.cs \
    IOutPlugin.cs \
    IPlugin.cs \
    IResult.cs \
    IType.cs \
    PluginException.cs \
    Program.cs \
    RegistryConfig.cs \
    ResultSorter.cs \
    Utility.cs
 
RESOURCES = \
    DialogMessage.glade \
    DialogQuestion.glade
 
RESFILES = $(addprefix resources/,$(RESOURCES))
RESCMD = $(addprefix -resource:,$(RESFILES))
 
all: $(TARGET)
 
$(TARGET): $(IPLUGIN) $(RESFILES)
    $(MCS) $(IPLUGIN) $(RESCMD) -out:$(TARGET) -target:library -r:Mono.Posix -unsafe -debug -pkg:gconf-sharp-2.0 -pkg:gtk-sharp-2.0 -pkg:glade-sharp-2.0
 
clean:
    rm -f $(TARGET) $(TARGET).mdb

Jak widać z tego pliku, musimy skompilować "library" z wyszczególnionych plików .cs, załączyć pliki resources oraz stworzyć referencje do m.in. gtk-sharp itp. Za naszą budowę tej biblioteki będzie odpowiadał plik "IPlugin.proj", który umieścimy w $(RootPath).

Źródła do kompilacji

Visual Studio zazwyczaj dodaje pliki do kompilacji pojedynczo. W naszym przypadku wszystkie pliki znajdują się w jednym miejscu więc bez obaw robimy następujący ItemGroup.

<ItemGroup>
    <Compile Include="$(SourcePath)\IPlugin\*.cs" />
</ItemGroup>

EmbeddedResource

Biblioteka ma zawierać dwa pliki resource "DialogMessage.glade" oraz "DialogQuestion.glade". Cóż prostszego ...

<ItemGroup>
    <EmbeddedResource Include="$(SourcePath)\IPlugin\resources\*.glade" />
</ItemGroup>

... i tutaj mamy pierwszą pułapkę. Kod programu odwołuje się do naszych resources w następujący sposób.

Glade.XML gxml = new Glade.XML(Assembly.GetExecutingAssembly()
                            , "DialogMessage.glade"
                            , "DialogMessageWindow"
                            , null);

Aby taki kod mógł zadziałać musimy dodać metatag LogicalName, czyli nasz kawałek definiujący resource w projekcie powinien wyglądać tak ...

<ItemGroup>
    <EmbeddedResource Include="$(SourcePath)\IPlugin\resources\DialogMessage.glade">
        <LogicalName>DialogMessage.glade</LogicalName>
    </EmbeddedResource>
</ItemGroup>

Projekt IPlugin wymaga dwóch plików jako resources ale w trakcie budowy Eithne.exe będziemy potrzebować ich prawie 50. Dodamy je zatem dynamicznie. Do tego potrzebujemy dodatkowy ItemGroup

<ItemGroup>
    <ResourcesToEmbed Include="$(SourcePath)\IPlugin\resources\*.glade" />
</ItemGroup>

Teraz wygenerujemy EmbeddedResource z metatagiem LogicalName za pomocą następującego zadania, które jako ogólnodostępne umieścimy je w Common.Targets

<Target Name="GenerateEmbeddedResources">
    <CreateItem Include="@(ResourcesToEmbed)" AdditionalMetadata="LogicalName=%(ResourcesToEmbed.FileName)%(ResourcesToEmbed.Extension)">
        <Output ItemName="EmbeddedResource" TaskParameter="Include"/>
    </CreateItem>
</Target>

Referencje

Biblioteki potraktujmy jeszcze bardziej brutalnie :). Dodamy je wszystkie i zamiast tworzyć czegoś na styl ...

<Reference Include="Mono.Posix">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>..\..\lib\Mono.Posix.dll</HintPath>
</Reference>

... skorzystamy z zadania, które umieszczamy w Common.Targets ...

<Target Name="GenerateReferencesFromLibrary">
    <ItemGroup>
        <Libraries Include="$(LibraryPath)\*.dll"/>
    </ItemGroup>
    
    <CreateItem Include="@(Libraries.FileName)" AdditionalMetadata="HintPath=%(Libraries.Identity)">
        <Output ItemName="Reference" TaskParameter="Include"/>
    </CreateItem>
</Target>

Zadania "GenerateEmbeddedResources" oraz "GenerateReferencesFromLibrary" uruchamiany tuż przed budową czyli w pliku Common.Targets modyfikujemy property "BuildDependsOn" w taki sposób

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

Pozostałe parametry kompilacji

Ostateczny wygląd IPlugin.proj jest następujący ...

<?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>
    <Import Project="$(RootPath)\Settings.proj" />
    <PropertyGroup>
        <OutputType>Library</OutputType>
        <AssemblyName>IPlugin</AssemblyName>
    </PropertyGroup>
    <ItemGroup>
        <Reference Include="System" />
        <Reference Include="System.Data" />
        <Reference Include="System.Xml" />
    </ItemGroup>
    <ItemGroup>
        <Compile Include="$(SourcePath)\IPlugin\*.cs" />
    </ItemGroup>
    <ItemGroup>
        <ResourcesToEmbed Include="$(SourcePath)\IPlugin\resources\*.glade" />
    </ItemGroup>
    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
    <Import Project="$(RootPath)\Common.Targets" />
</Project>

OutputType definiuje nam czym mają być skompilowane assembly, w tym wypadku jako biblioteka dll, a AssemblyName jaka ma być jego nazwa. Pozostałe parametry kompilacji znajdują się w pliku Settings.proj, gdyż można powiedzieć, że są wspólne dla pozostałych projektów również.

<PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <SchemaVersion>2.0</SchemaVersion>
    <TargetFrameworkVersion>v2.0</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>$(BuildPath)\Debug\bin\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <BaseIntermediateOutputPath>$(BuildPath)\temp\Debug\obj\</BaseIntermediateOutputPath>
    <IntermediateOutputPath>$(BuildPath)\temp\Debug\obj\</IntermediateOutputPath>
    <UseHostCompilerIfAvailable>true</UseHostCompilerIfAvailable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>$(BuildPath)\Release\bin\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <BaseIntermediateOutputPath>$(BuildPath)\temp\Release\obj\</BaseIntermediateOutputPath>
    <IntermediateOutputPath>$(BuildPath)\temp\Release\obj\</IntermediateOutputPath>
    <UseHostCompilerIfAvailable>true</UseHostCompilerIfAvailable>
</PropertyGroup>

Zwróćcie uwagę na ustawienie true dla AllowUnsafeBlocks, które jest wymagane w tym wypadku do kompilacji oraz na to że pliki automatycznie są wysyłane do odpowiedniego podkatalogu w katalogu "build" czyli $(BuildPath).

Circular reference

Jednak istnieje jedno zagrożenie. Plik Settings.proj jest zarówno importowany na początku pliku IPlugin.proj jak i na początku Commons.Target. Oznacza to, że przypisywanie zmiennych w projektach odbywa się w następującej kolejności:

  1. Settings.proj
  2. IPlugin.Proj
  3. Settings.proj
  4. Common.Targets

Jeżeli Setting.proj ustawia jakąś wartość zmiennej ABC, a potem IPlugin.proj zmienia tę wartość, to zanim dotrze ona do Common.Targets, z powrotem zostanie zamieniona na wartość z Settings.proj. Jest to w większości wypadków efekt niepożądany i nawet msbuild nas o tym informuje stosownym komunikatem

C:\Projects\eithne.msbuild>msbuild IPlugin.proj
...
C:\Projects\eithne.msbuild\Common.Targets(9,10): warning MSB4011: There is a circular
reference involving the import of file "C:\Projects\eithne.msbuild\Settings.proj". 
This file may have been imported more than once, or you may have attempted to import
the main project file. All except the first instance of this file will be ignored.
...

Aby się przed tym uchronić należy nadać warunek przed kolejnym importem najlepiej oparty o jakąś zmienną, która jest zdefiniowana tylko w pliku Settings.proj i która nie będziemy zazwyczaj modyfikować przy użyciu linii poleceń. Zatem import w pliku Common.Target powinien wyglądać tak...

<Import Project="Settings.proj" Condition="$(ToolsPath) == ''"/>

Budowa pliku Exe

Budowa gdk-cairo.dll wygląda tak samo jak IPlugin.proj. Służy od tego plik Gdk-Cairo.proj.

Za plik exe odpowiadać będzie Eithne.proj. W stosunku do pozostałych plików proj różni się typem budowanego assembly ...

<PropertyGroup>
    <OutputType>WinExe</OutputType>
    <AssemblyName>Eithne</AssemblyName>
</PropertyGroup>

... większej ilości resources ...

<ItemGroup>
    <ResourcesToEmbed Include="$(SourcePath)\resources\*.glade"/>
    <ResourcesToEmbed Include="$(SourcePath)\resources\pixmaps\*.png"/>
</ItemGroup>

... wykluczenia pliku gdk-cairo.cs z kompilacji ...

<ItemGroup>
    <Compile Include="$(SourcePath)\*.cs" Exclude="$(SourcePath)\gdk-cairo.cs" />
</ItemGroup>

... oraz ustawienia zależności pomiędzy pozostałymi projektami czyli ...

<ItemGroup>
    <ProjectReference Include="$(RootPath)\IPlugin.proj">
    </ProjectReference>
    <ProjectReference Include="$(RootPath)\Gdk-Cairo.proj">
    </ProjectReference>
</ItemGroup>

Teraz kompilując projekt Eithne.proj, pozostałe dwa projekty również zostaną zbudowane.

Kompilacja hurtowa pluginów

Źródła pluginów znajdują się w katalogu Plugins, który jest wskazywany przez zmienną $(PluginsSourcePath). Jest ich 28 i oznacza to 28 plików Makefile. Czy również oznacza to że musimy robić 28 plików .proj ? Niekoniecznie. Wszystkie te pluginy tak naprawdę różnią się nazwą zatem możemy zrobić projekt w stylu szablonu. Nazwijmy go PluginTemplate.proj. Oto on ...

<?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>
    <Import Project="$(RootPath)\Settings.proj" />
    <PropertyGroup>
        <ProjectName>$(PluginName)</ProjectName>
        <OutputType>Library</OutputType>
        <AssemblyName>$(PluginName)</AssemblyName>
    </PropertyGroup>
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
        <OutputPath>$(BuildPath)\temp\Debug\bin\Plugins\</OutputPath>
        <DeployPath>$(BuildPath)\Debug\bin\Plugins</DeployPath>
    </PropertyGroup>
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
        <OutputPath>$(BuildPath)\temp\Release\bin\Plugins\</OutputPath>
        <DeployPath>$(BuildPath)\Release\bin\Plugins</DeployPath>
    </PropertyGroup>
    <ItemGroup>
        <Reference Include="System" />
        <Reference Include="System.Data" />
        <Reference Include="System.Xml" />
    </ItemGroup>
    <ItemGroup>
        <ProjectReference Include="$(RootPath)\IPlugin.proj"/>
    </ItemGroup>
    <ItemGroup>
        <Compile Include="$(PluginsSourcePath)\$(PluginName)\*.cs" />
    </ItemGroup>
    <ItemGroup>
        <ResourcesToEmbed Include="$(PluginsSourcePath)\$(PluginName)\resources\*.*" />
    </ItemGroup>
    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
    <Import Project="$(RootPath)\Common.Targets" />
    <Target Name="AfterBuild">
        <ItemGroup>
            <BuildFiles Include="$(TargetDir)\$(TargetName).*" ></BuildFiles>
        </ItemGroup>
        <Copy SourceFiles="@(BuildFiles)" DestinationFolder="$(DeployPath)" ContinueOnError="true"/>
    </Target>    
    
</Project>

Jak widać, najistotniejszy jest parametr $(PluginName). Dzięki niemu możemy budować plugin w następujący sposób.

C:\Projects\eithne.msbuild> msbuild PluginTemplate.proj /p:PluginName=Best

Jedyną różnicą w porównaniu od poprzednich projektów jest prymitywny deployment.  OutputPath musi wskazywać na jakąś tymczasową lokalizację a potem gotowe assemblies muszą być kopiowane do nowej lokalizacji. Jeżeli tego nie zrobimy zadziała wówczas target "IncrementalClean", który nam wyczyści z "OutputPath" pliki z poprzednio budowanego plugina. Dzieję się tak gdyż tak naprawdę projekt o jednej nazwie PluginTemplate.proj buduje assemblies za każdym razem o innych nazwach więc traktuje poprzednio budowane pliki jako obce. Ale ten prosty deployment umieszczony w "AfterBuild" chroni nas przed tym.

Dzieki takiemu rozwiazaniu, dołożenie przez programistę nowego pluginu nie powoduje zmiany plików proj. Wystarczy, że umieści go w nowym podkatalogu. Mamy coś w stylu Convention over Configuration.

Teraz już pozostaje zbudować jednym poleceniem wszystkie pluginy. Zrobimy to przy wykorzystaniu nowego pliku Plugins.proj, który wygląda tak...

<?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>
    <Import Project="$(RootPath)\Settings.proj" />
    
    <ItemGroup>
        <PluginsToBuild Include="$(PluginsSourcePath)\**" Exclude="$(PluginsSourcePath)\**\resources\*;$(PluginsSourcePath)\*" ></PluginsToBuild>
    </ItemGroup>
 
    <!-- Import 3rd party targets -->
    <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
 
    <Target Name="Build"  DependsOnTargets="GetPluginNames">
        <MSBuild Projects="PluginTemplate.proj" Targets="Build" Properties="PluginName=%(PluginNames.Identity)"></MSBuild>
    </Target>
    
    <Target Name="GetPluginNames">
        <RegexReplace Input="%(PluginsToBuild.RecursiveDir)" Expression="\\" Replacement="" Count="1">
            <Output ItemName ="PluginNames" TaskParameter="Output" />
        </RegexReplace>
    </Target>
</Project>

W zadaniu "GetPluginNames" tworzymy listę nazw podkatalogów, którą później wykorzystujemy do uruchomienia tasku MSBuild  budującego projekt PluginTemplate.proj tyle razy ile jest podkatalogów.

Podsumowanie

Gotowe rozwiązanie można pobrać stąd -> eithne.msbuild.zip. Nie zawiera ono bibliotek wymaganych do kompilacji i uruchomienia, typu mono, gdk itp. Dla chętnych biblioteki są w oddzielnym pliku eithne.msbuild.lib.zip lub można dostarczyć je samemu kopiując do katalogu "lib".

Jak widać do naszej budowy potrzebujemy mniej własnych plików .proj aniżeli plików Makefile w oryginale. Aby rozkoszować się aplikacją zbudowaną na Windowsach bez użycia Mono wystarczy ...

C:\Projects\eithne.msbuild> msbuild Eithne.proj
C:\Projects\eithne.msbuild> msbuild Plugins.proj
C:\Projects\eithne.msbuild> build\Debug\bin\Eithne.exe
opublikowano przez rod | 0 komentarzy

Resize Form czyli WinForms-y okiem laika

W WinFormsach programuje zupełnie sporadycznie. Wole warstwy domenowe, serwisowe, bazodanowe i prezentacyjne (w modelach MVP). Widoki toleruje tylko pod MonoRail i ASP.NET. Ale WinForms ? .... e to nie dla mnie. Niestety życie bywa brutalne.  W moim aktualnym projekcie integruje system finansowo księgowy z danymi dostarczanymi z działu aktuarialnego. Procesem przetwarzania danych kieruje aplikacja WinForms-owa. Prosty wygląd w postaci zakładek, gdzie dwie zakładki posiadają kontrolki na wprowadzenie parametrów. Natomiast jedna posiada textbox z zawartością logu, a jeszcze inna ReportViewer z Microsoft Reporting. <