endurium
endurium

Reputation: 1011

MSBuild 17: do properties and items have scope within a project file?

I'm a bit confused about property scope within an MSBuild project. My understanding was that a property declared outside of a target would be global within the project file. For example:

  <PropertyGroup>
    <TestProp>Unassigned</TestProp>
  </PropertyGroup>

  <ItemGroup>
    <TestEnvironments Include="Development;UAT;Production" />
  </ItemGroup>
  
  <Target Name="TestScope">
    <PropertyGroup>
      <TestProp>Some test data</TestProp>
    </PropertyGroup>

    <Message Text="Test property scope $(TestProp)" />
    
    <CallTarget Targets="ForEachTestScope" />
  </Target>

  <Target Name="ForEachTestScope" Inputs="@(TestEnvironments)" Outputs="%(Identity).done">

    <Message Text="Test property scope $(TestProp)" />
    <Message Text="Selected environment: @(TestEnvironments)" />

  </Target>

When executing the TestScope target the output is:

TestScope:
  Test property scope Some test data
ForEachTestScope:
  Test property scope Unassigned
  Selected environment: Development
ForEachTestScope:
  Test property scope Unassigned
  Selected environment: UAT
ForEachTestScope:
  Test property scope Unassigned
  Selected environment: Production

I would have expected the value of $(TestProp) in the called target ForEachTestScope to be that which was set in the calling target, i.e. Some TestData

Is the scope of a "locally" declared PropertyGroup (or ItemGroup for that matter) always the scope of the target the declaration is contained in?

Upvotes: 0

Views: 189

Answers (1)

Jonathan Dodds
Jonathan Dodds

Reputation: 5008

First, MSBuild is a declarative language. Don't use CallTarget to try to write procedural style code in MSBuild. Targets are not subroutines and CallTarget is not a subroutine call. There is no stack frame providing a scope to the called target.

Given the following test.proj file, which only ever defines TestProp within a target:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="Test0">
        <PropertyGroup>
            <TestProp Condition="'$(TestProp)' != ''">$(TestProp);Some test data</TestProp>
            <TestProp Condition="'$(TestProp)' == ''">Some test data</TestProp>
        </PropertyGroup>
        <Message Text="$(TestProp)" />
    </Target>

    <Target Name="Test1">
        <PropertyGroup>
            <TestProp Condition="'$(TestProp)' != ''">$(TestProp);Some other test data</TestProp>
            <TestProp Condition="'$(TestProp)' == ''">Some other test data</TestProp>
        </PropertyGroup>
        <Message Text="$(TestProp)" />
    </Target>
</Project>

The commands msbuild test.proj /t:"test0;test1" and msbuild test.proj /t:"test1;test0" will produce different outputs.

The outputs will be

Test0:
  Some test data
Test1:
  Some test data;Some other test data

and

Test1:
  Some other test data
Test0:
  Some other test data;Some test data

respectively.

Coming back to CallTarget, the task documentation explains that

When using CallTarget, MSBuild evaluates the called target in a new scope, as opposed to the same scope it's called from. This means that any item and property changes in the called target are not visible to the calling target. To pass information to the calling target, use the TargetOutputs output parameter.

Your testing with ForEachTestScope2 demonstrates this.

Rewriting your code to not use CallTarget might look like the following (but I'm guessing at your intents).

    <ItemDefinitionGroup>
        <TestEnvironments>
            <TestProp>Unassigned</TestProp>
        </TestEnvironments>
    </ItemDefinitionGroup>

    <ItemGroup>
        <TestEnvironments Include="Development;UAT;Production" />
    </ItemGroup>

    <Target Name="TestEnvironment" DependsOnTargets="SetTestData" Inputs="@(TestEnvironments)" Outputs="%(Identity).done">
        <Message Text="Selected environment: @(TestEnvironments->'Test %(identity) with %(TestProp)')" />
    </Target>

    <Target Name="SetTestData">
        <Message Text="in SetTestData"/>
        <ItemGroup>
            <TestEnvironments Condition="'%(identity)' == 'Development'">
                <TestProp>Some development test data</TestProp>
            </TestEnvironments>
            <TestEnvironments Condition="'%(identity)' != 'Development'">
                <TestProp>Some test data</TestProp>
            </TestEnvironments>
        </ItemGroup>
    </Target>

Running the TestEnvironment target produces:

SetTestData:
  in SetTestData
TestEnvironment:
  Selected environment: Test Development with Some development test data
TestEnvironment:
  Selected environment: Test UAT with Some test data
TestEnvironment:
  Selected environment: Test Production with Some test data

Note that a default of Unassigned is created for TestProp. The SetTestData target runs only once and is changing the value of TestProp. SetTestData may set different values for different environments.

Upvotes: 1

Related Questions