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