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

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?

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.

Related

Target runs even if dependency target condition false

I was surprised that a target runs even if the target it's after (via AfterTargets) does not:
<Target Name="StepOne" AfterTargets="PostBuildEvent" Condition="false">
<Message Text="StepOne" Importance="high"/>
</Target>
<Target Name="StepTwo" AfterTargets="StepOne">
<Message Text="StepTwo" Importance="high"/>
</Target>
Output:
1>StepTwo
Any way to make a chain of targets that stops when one of them has a false condition? Adding DependsOnTargets="StepOne" didn't help. CallTarget works but then properties aren't shared with subsequent targets, which I want.
MSBuild creates a dependency graph of all of the targets. The targets will then be invoked in order. Conditions don't change the dependency graph and conditions are not checked until the target is invoked.
The chain of targets doesn't stop because one of the targets has a false condition.
But a target can set properties that are used in the conditions of other targets. For example:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" TreatAsLocalProperty="IsStepOneEnabled">
<PropertyGroup>
<!-- IsStepOneEnabled: default to true if not set; normalize to true|false -->
<IsStepOneEnabled Condition="'$(IsStepOneEnabled)' == ''">true</IsStepOneEnabled>
<IsStepOneEnabled Condition="'$(IsStepOneEnabled)' != 'true'">false</IsStepOneEnabled>
<!-- IsStepOne: initilize to false -->
<IsStepOne>false</IsStepOne>
</PropertyGroup>
<Target Name="Test">
<Message Text="Test" />
<Message Text="Step One will be run." Condition="$(IsStepOneEnabled)"/>
</Target>
<Target Name="StepOne" AfterTargets="Test" Condition="$(IsStepOneEnabled)">
<PropertyGroup>
<IsStepOne>true</IsStepOne>
</PropertyGroup>
<Message Text="StepOne" />
</Target>
<Target Name="StepTwo" AfterTargets="StepOne" Condition="$(IsStepOne)">
<Message Text="StepTwo" />
</Target>
</Project>
Save this in a file named test.proj and run it as with the command:
msbuild test2.proj
and the output will be:
Test:
Test
Step One will be run.
StepOne:
StepOne
StepTwo:
StepTwo
Run it with the command:
msbuild test2.proj /p:IsStepOneEnabled=false
and the output will be:
Test:
Test

Issue with using MSBuild to build and copy all outputs to a common folder

We are trying to write a msbuild script that will build the solution and copy over all the compiled binaries and dependencies over to a specific output folder. While the build script that we have does build and copy over the binaries to a common folder, but we are not getting the dependencies copied.
This probably has to do with the way we have used the msbuild task to build the solution and we are accepting the targetoutputs of the task into an itemgroup and iterating over the item group to copy all the compiled dlls and exes over to a common folder. But this is not including the dependency dlls which gets placed into the individual bin folder of each project.
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ParentSolutionFile />
</PropertyGroup>
<ItemGroup>
<Assemblies Include="*.dll, *.exe" />
</ItemGroup>
<Target Name="BuildAll">
<CombinePath BasePath="$(MSBuildProjectDirectory)" Paths="Source\Solutions\xxx.sln">
<Output TaskParameter="CombinedPaths" PropertyName="ParentSolutionFile" />
</CombinePath>
<Message Text="$(ParentSolutionFile)" />
<MSBuild Projects="$(ParentSolutionFile)">
<Output TaskParameter="TargetOutputs" ItemName="Assemblies" />
</MSBuild>
<Message Text="%(Assemblies.Identity)" />
<Copy SourceFiles="%(Assemblies.Identity)" DestinationFolder="$(MSBuildProjectDirectory)\Binary" OverwriteReadOnlyFiles="True" SkipUnchangedFiles="True" />
</Target>
What will be the preferred way to copy over all the binaries along with the necessary dependencies to a common output folder?
Does not overriding OutputPath do the trick alone?
<MSBuild Projects="$(ParentSolutionFile)" Properties="OutputPath=$(MSBuildProjectDirectory)\Binary">
<Output TaskParameter="TargetOutputs" ItemName="Assemblies" />
</MSBuild>
And leave out the copy task alltogether?
The build process will place the final result in the directory represented by OutputPath - at least if you are building c# projects. For C/C++ the internal structure and variable names are completely different.
Thus, in theory, you could pass the OutputPath in the MsBuild-task that builds the solution.
<MsBuild Projects="$(ParentSolutionFile)"
Properties="OutputPath=$(MSBuildProjectDirectory)\Binary"/>
However, the csproj-files will overwrite that value unconditionally with the following code:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug\</OutputPath>
I have solved this by injecting my own build system in each and every csproj-file.
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\..\build\buildsystem.targets" />
The path is relative to the csproj-file. An absolute path is fine too, or a variable. The trick is to make it work on all dev machines as well as the build agents.
Now, in buildsystem.targets, simply redefine OutputPath as much as you like. Again, the trick is to ensure you get the same - or at least a well defined - location regardless of who builds it (dev, build agent) and regardless how the build was initiated (VS, command line).
A simple way of handling the differences is to import conditionally.
<Import Project="..\..\..\build\buildsystem.targets"
Condition="'$(BuildingInsideVisualStudio)'!='true'"/>
That will give you no changes if initiating the build from VS and whatever changes you code for if you build from command line.
--Jesper

Check a property on startup, depending on selected targets

I'm having some trouble with a msbuild file.
I'd like the build to have three targets, executed in this order:
Cleanup: Cleans the output of a previous build
Build: The actual build
CopyFiles: A task that packages the output of the build for an easier deployment
Now the problem is: I want a property (ReleasePath) for the CopyFiles target to be explicitly set by the caller. Also, the caller shouldn't have to set the output path if only calling the Cleanup and Build tasks.
My first try has been:
<PropertyGroup>
<ReleasePath></ReleasePath>
</PropertyGroup>
<Target Name="Initialize">
<Error Text="The ReleasePath property isn't defined" Condition="'$(ReleasePath)' == ''"/>
</Target>
<Target Name="CopyFiles" DependsOnTargets="Initialize">
</Target>
It works fine, but the Initialize target is executed right before CopyFiles, that is after Build. And the Buildtarget can take quite a long time to execute, so if something's wrong I'd like the user to be notified right away.
So basically, is there a way to run the Initialize target at the beginning, but only if CopyFiles is included in the targets?
I also thought of putting Initialize in the InitialTargets attribute of the project, then put a condition to execute this target only if CopyFiles is selected, but to my surprise I couldn't find a property containing the list of targets selected by the caller.
I think the easiest way to accomplish your goal would be to use a Property to toggle CopyFiles rather than the list of targets passed in. If you always add CopyFiles to the AfterBuild target but condition it on a property like "ShouldCopyFiles" then you could also conditionalize the DependsOnTargets of Build:
<PropertyGroup>
<ShouldCopyFiles Condition="'$(ShouldCopyFiles)'==''">false</ShouldCopyFiles>
<ReleasePath></ReleasePath>
</PropertyGroup>
<PropertyGroup>
<BuildDependsOn>
BeforeBuild;
CoreBuild;
AfterBuild;
CopyFiles;
</BuildDependsOn>
</PropertyGroup>
<PropertyGroup Condition="'$(ShouldCopyFiles)'=='true'">
<BeforeBuildDependsOn>
Initialize;
</BeforeBuildDependsOn>
</PropertyGroup>
<Target Name="Initialize" Condition="'$(ShouldCopyFiles)'=='true'">CheckStuff
</Target>
<Target Name="Build" DependsOnTargets="$(BuildDependsOn)"/>
<Target Name="CoreBuild">YourBuildStuff</Target>
<Target Name="BeforeBuild" DependsOnTargets="$(BeforeBuildDependsOn)"/>
<Target Name="AfterBuild" />
<Target Name="CopyFiles" Condition="'$(ShouldCopyFiles)'=='true'">
CopyStuff
</Target>
At this point you could call MSBuild like this:
msbuild my.proj /targets:Clean;Build /p:ShouldCopyFiles=true
and that would toggle it on.

TeamCity MSBuild refer to build counter

I have a property group which includes a property for the build_number which is being passed in from TeamCity as solely the Build Counter. The build number format being set in TeamCity as simply {0} for the counter.
<PropertyGroup>
<Major>10</Major>
<Minor>1</Minor>
<Build>$(BUILD_NUMBER)</Build>
<Release>0</Release>
...
</PropertyGroup>
The Major, Minor and Release properties are then updated from values in a file in source control.
So that TeamCity logs the build as the full 4 part build reference (not just the counter), I set it thus:
<TeamCitySetBuildNumber BuildNumber="$(Major).$(Minor).$(Build).$(Release)" />
However, now when I reference my $(Build) property, it's now set to the 4 part build reference, and any property I have made which makes reference to $(BUILD_NUMBER) prior to setting using TeamCitySetBuildNumber also gets overwritten with the 4 part reference.
NB I've also changed it with a system message:
<Message Text="##teamcity[buildNumber '$(Major).$(Minor).$(Build).$(Release)']" />
but the overall effect is the same.
How Can I refer to the build counter (only) AFTER I have set the BuildNumber above?
If you're using a project file, you could try calling the TeamCitySetBuildNumber command in the AfterBuild section of the *.vbproj or *.csproj file:
<Target Name="AfterBuild">
<TeamCitySetBuildNumber BuildNumber="$(Major).$(Minor).$(Build).$(Release)" />
</Target>
If you're using a solution file, I'd create a *.proj file that calls your solution file and then after that call the TeamCitySetBuildNumber command (not sure if you can call the TeamCitySetBuildNumber command within the target like this though...):
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="SetBuildNumber">
<PropertyGroup>
<Major>10</Major>
<Minor>1</Minor>
<Build>$(BUILD_NUMBER)</Build>
<Release>0</Release>
</PropertyGroup>
<Target Name="Build">
<Message Text="Build task called... " Importance="high"/>
<MSBuild Projects="$(teamcity_build_checkoutDir)\your_solution.sln" Properties="Configuration=Release"/>
</Target>
<Target Name="SetBuildNumber" DependsOnTargets="Build">
<Message Text="Setting build number back to TeamCity... " Importance="high"/>
<TeamCitySetBuildNumber BuildNumber="$(Major).$(Minor).$(Build).$(Release)" />
</Target>
</Project>

try...finally equivalent in MsBuild

How can I run a certain cleanup task after my "Test" target runs, regardless of whether the Test target succeeded or failed (like the try...finally construct in C#/Java).
The Target element has an OnError attribute you could set to a target to execute on error, but as it only executes if the target is in error, it only solves half your scenario.
Have you considered chaining together targets to represent the test 'steps' you'd like to execute?
<PropertyGroup>
<TestSteps>TestInitialization;Test;TestCleanup</TestSteps>
</PropertyGroup>
The 'TestInitialization' target is where you can perform any test initialization, the 'Test' target executes the test, the 'TestCleanup' target does any sort of post test clean up.
Then, execute these targets using the CallTarget task, using the RunEachTargetSeparately attribute set to True. This will execute all the targets, regardless of success or failure.
The complete sample is below:
<Project DefaultTargets = "TestRun"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
<!-- Insert additional tests between TestInitialization and TestCleanup as necessary -->
<PropertyGroup>
<TestSteps>TestInitialization;Test;TestCleanup</TestSteps>
</PropertyGroup>
<Target Name = "TestRun">
<CallTarget Targets="$(TestSteps)" RunEachTargetSeparately="True" />
</Target>
<Target Name = "TestInitialization">
<Message Text="Executing Setup..."/>
</Target>
<Target Name = "Test">
<Message Text="Executing Test..."/>
<!-- this will fail (or should unless you meet the conditions below on your machine) -->
<Copy
SourceFiles="test.xml"
DestinationFolder="c:\output"/>
</Target>
<Target Name = "TestCleanup">
<Message Text="Executing Cleanup..."/>
</Target>
</Project>
Or use <OnError> to call your target in the error case, and DependsOnTargets or CallTarget to call your same target in the normal case.