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

Jul 30, 2008 - 4 minute read -

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

```xml
<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