How the Use of CallTarget can be a Code Smell in MSBuild
CallTarget, because it vaguely resembles a sub-routine call, is sometimes used to approximate a procedural approach in MSBuild — but this is a problem because MSBuild is a declarative language and Targets are not sub-routines.
Example
To elaborate, imagine there are two processes to be implemented in MSBuild. Process A has steps A-1, A-2, and A-3. Process B has steps B-1, B-2, and B-3. However, steps A-2 and B-2 are actually identical. The two processes have a common step. It makes sense to factor the common step into its own target that both processes can use.
From a procedural frame of mind, an implementation might look like the following. The output shows that steps A-1, Common (aka A-2), and A-3 are run for Process A. Likewise when running Process B.
<!-- calltarget-example-01.targets -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="PerformProcessA">
<Message Text="perform Step A-1"/>
<CallTarget Targets="Common"/>
<Message Text="perform Step A-3"/>
</Target>
<!--
Output:
PerformProcessA:
perform Step A-1
Common:
perform Common Step
PerformProcessA:
perform Step A-3
-->
<Target Name="PerformProcessB">
<Message Text="perform Step B-1"/>
<CallTarget Targets="Common"/>
<Message Text="perform Step B-3"/>
</Target>
<!--
Output:
PerformProcessB:
perform Step B-1
Common:
perform Common Step
PerformProcessB:
perform Step B-3
-->
<Target Name="Common">
<Message Text="perform Common Step"/>
</Target>
</Project>However an implementation that is MSBuild native would eschew the CallTarget task and might look like the following.
<!-- calltarget-example-02.targets -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="PerformProcessA" DependsOnTargets="StepA1;Common">
<Message Text="perform Step A-3"/>
</Target>
<!--
Output:
StepA1:
perform Step A-1
Common:
perform Common Step
PerformProcessA:
perform Step A-3
-->
<Target Name="PerformProcessB" DependsOnTargets="StepB1;Common">
<CallTarget Targets="Common"/>
<Message Text="perform Step B-3"/>
</Target>
<!--
Output:
StepB1:
perform Step B-1
Common:
perform Common Step
PerformProcessB:
perform Step B-3
-->
<Target Name="Common">
<Message Text="perform Common Step"/>
</Target>
<Target Name="StepA1">
<Message Text="perform Step A-1"/>
</Target>
<Target Name="StepB1">
<Message Text="perform Step B-1"/>
</Target>
</Project>Both implementations perform the steps; so what's the difference and why does the difference matter?
Scope
The difference is that CallTarget creates a new scope. And the new scope is initialized with the state of Properties and Items at the point where the Target containing the CallTarget task was started; Changes made to Properties and Items within the containing Target will not be seen.
Following is a revision of the example code that uses CallTarget. A Property has been added that is modified at different points.
<!-- calltarget-example-01a.targets -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="PerformProcessA">
<Message Text="perform Step A-1"/>
<PropertyGroup>
<ExampleValue>$(ExampleValue);Modified in PerformProcessA</ExampleValue>
</PropertyGroup>
<Message Text="PerformProcessA: ExampleValue = $(ExampleValue)"/>
<CallTarget Targets="Common"/>
<Message Text="perform Step A-3"/>
<Message Text="PerformProcessA: ExampleValue = $(ExampleValue)"/>
</Target>
<!--
Output:
PerformProcessA:
perform Step A-1
PerformProcessA: ExampleValue = Initialized in Project;Modified in PerformProcessA
Common:
perform Common Step
Common: ExampleValue = Initialized in Project;Modified in Common
PerformProcessA:
perform Step A-3
PerformProcessA: ExampleValue = Initialized in Project;Modified in PerformProcessA
-->
<Target Name="PerformProcessB">
<Message Text="perform Step B-1"/>
<CallTarget Targets="Common"/>
<Message Text="perform Step B-3"/>
</Target>
<!--
Output:
PerformProcessB:
perform Step B-1
Common:
perform Common Step
PerformProcessB:
perform Step B-3
-->
<Target Name="Common">
<Message Text="perform Common Step"/>
<PropertyGroup>
<ExampleValue>$(ExampleValue);Modified in Common</ExampleValue>
</PropertyGroup>
<Message Text="Common: ExampleValue = $(ExampleValue)"/>
</Target>
<PropertyGroup>
<ExampleValue>Initialized in Project</ExampleValue>
</PropertyGroup>
</Project>Note that the change to append ";Modified in PerformProcessA" to the Property is not visible in the CallTarget task's scope. Also note that the change to append ";Modified in Common" which is done within the CallTarget task's scope is not seen by the Target that contains the CallTarget task.
Next is a similar revision of the example code that doesn't use CallTarget.
<!-- calltarget-example-02a.targets -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="PerformProcessA" DependsOnTargets="StepA1;Common">
<Message Text="perform Step A-3"/>
<Message Text="PerformProcessA: ExampleValue = $(ExampleValue)"/>
</Target>
<!--
Output:
StepA1:
perform Step A-1
StepA1: ExampleValue = Initialized in Project;Modified in StepA1
Common:
perform Common Step
Common: ExampleValue = Initialized in Project;Modified in StepA1;Modified in Common
PerformProcessA:
perform Step A-3
PerformProcessA: ExampleValue = Initialized in Project;Modified in StepA1;Modified in Common
-->
<Target Name="PerformProcessB" DependsOnTargets="StepB1;Common">
<Message Text="perform Step B-3"/>
</Target>
<!--
Output:
StepB1:
perform Step B-1
Common:
perform Common Step
PerformProcessB:
perform Step B-3
-->
<Target Name="Common">
<Message Text="perform Common Step"/>
<PropertyGroup>
<ExampleValue>$(ExampleValue);Modified in Common</ExampleValue>
</PropertyGroup>
<Message Text="Common: ExampleValue = $(ExampleValue)"/>
</Target>
<Target Name="StepA1">
<Message Text="perform Step A-1"/>
<PropertyGroup>
<ExampleValue>$(ExampleValue);Modified in StepA1</ExampleValue>
</PropertyGroup>
<Message Text="StepA1: ExampleValue = $(ExampleValue)"/>
</Target>
<Target Name="StepB1">
<Message Text="perform Step B-1"/>
</Target>
<PropertyGroup>
<ExampleValue>Initialized in Project</ExampleValue>
</PropertyGroup>
</Project>Note that, with this code, the final value of the Property is the result of all of the append operations: "Initialized in Project;Modified in StepA1;Modified in Common".
Project
MSBuild files are XML and the root element of an MSBuild file is the Project element. The Project will contain everything defined in the MSBuild file and everything from all imported files.
A Target is not a function. Targets have no arguments and no return values. A Target is a unit of work performed on or with the data in the Project.
Summary
Defining the target build order doesn't have the overhead of creating a new scope. CallTarget is not a substitute for an ordering attribute.
There are special cases where CallTarget is uniquely useful but those cases are uncommon.
Rampant use of CallTarget is an indication that the author of the code was working in the wrong paradigm.
