Target runs even if dependency target condition false - msbuild

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

Related

Target with AfterTargets="Publish" executes in unpublishable project

I have a project in my solution that I wanna publish separately from the rest of the solution. So the way to skip it is by setting the IsPublishable property to false, which works like a charm. It seems though that no matter the publishable status of the project, targets set to run after the publish target (AfterTargets="Publish") are still executed when I try to publish the entire solution.
Is this intended? Is there any way to prevent this? I am using VS 2022 preview.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<LangVersion>9.0</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<BaseOutputPath>..\Build</BaseOutputPath>
<IsPublishable>false</IsPublishable>
</PropertyGroup>
<ItemGroup>
<SomeFiles Include="$(SolutionDir)SomeFiles\**\*.txt" />
</ItemGroup>
<Target Name="CopyCustomContentBuild" AfterTargets="AfterBuild">
<Copy SourceFiles="#(SomeFiles)" DestinationFolder="$(TargetDir)SomeFiles" />
<Message Text="Files copied successfully." Importance="high" />
</Target>
<Target Name="CopyCustomContentPublish" AfterTargets="Publish">
<Copy SourceFiles="#(SomeFiles)" DestinationFolder="$(PublishDir)SomeFiles" />
<Message Text="Files copied successfully to publish dir." Importance="high" />
</Target>
</Project>
That is the intended behaviour. When you set IsPublishable to false MsBuild still logs when a Publish target is supposed to run and continues onto your AfterTargets="Publish" target.
You'll have to set a condition on your actions inside the target to make sure they do not get executed when IsPublishable is false.
<Target Name="CopyCustomContentPublish" AfterTargets="Publish">
<Copy SourceFiles="#(SomeFiles)" DestinationFolder="$(PublishDir)SomeFiles" Condition=" '$(IsPublishable)' == 'true' " />
<Message Text="Files copied successfully to publish dir." Importance="high" Condition=" '$(IsPublishable)' == 'true' " />
</Target>

MSBuild - can I compile all solutions in child directories?

Is there a way in MSBuild to compile all solutions in folders, and sub-folders, and sub... under a specified parent?
We have a bunch of sample programs we ship with our library. I want to add to the build process that we know they all compile.
You can create own targets for restore and build operations. For example:
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" InitialTargets="EntryRestore" DefaultTargets="EntryMSBuild">
<ItemGroup>
<SolutionFile Include="./**/*.sln"/>
</ItemGroup>
<Target Name="EntryRestore">
<Message Text="-----Entry-----" Importance="high"/>
<Message Text=" Restore " Importance="high"/>
<Message Text="-----Entry-----" Importance="high"/>
<MSBuild Projects="%(SolutionFile.Identity)" Targets="restore"/>
</Target>
<Target Name="EntryMSBuild">
<Message Text="-----Entry-----" Importance="high" />
<Message Text=" Build " Importance="high" />
<Message Text="-----Entry-----" Importance="high" />
<MSBuild Projects="%(SolutionFile.Identity)" Targets="build" />
</Target>
</Project>
Item SolutionFile will contains paths for all .sln files that located in current directory and its subdirectories. You also may define path in CLI and perform searching relative it.
<ItemGroup>
<SolutionFile Include="$(MyDir)/**/*.sln"/>
</ItemGroup>
MSBuild task launches MSBuild for performing specified targets. In our cas it are restore and build. It task used 'batching' that allow iterate over items by metadata.
&"D:\Visual Studio\MSBuild\15.0\Bin\msbuild.exe" .\Make.targets
MSBuild targets | Item element | Item metadata in task batching | MSBuild task

MSBuild batch task output

I have several solutions (plugins) for a project. For each solution there is a defined range of metadata:
<ItemGroup>
<Plugins Include="Plugin1\Plugin1.sln">
<Disabled>false</Disabled>
<ProjectDirectory>plugin1\</ProjectDirectory>
<ProjectName>Plugin1</ProjectName>
</Plugins>
<Plugins Include="Plugin2\Plugin2.sln">
<Disabled>true</Disabled>
<ProjectDirectory>plugin2\</ProjectDirectory>
<ProjectName>Plugin2</ProjectName>
</Plugins>
<Plugins Include="Plugin3\Plugin3.sln">
<Disabled>false</Disabled>
<ProjectDirectory>plugin3\</ProjectDirectory>
<ProjectName>Plugin3</ProjectName>
</Plugins>
</ItemGroup>
I need to build not Disabled plugins by running its own build script and add the result directory to Plugins metadata for subsequent processing (for example: Copy each plugin build output to its own folder).
But I can't find a way to concatenate it.
Below is my target:
<Target Name="BuildPlugin" Inputs="%(Plugins.Identity)" Outputs="%(Plugins.Identity -> %(PluginOutput.Identity))" Returns="%(PluginOutput.Identity)">
<MSBuild
Condition="!%(Disabled)"
Projects='%(ProjectDirectory)BuildProject.target'
Targets="Clean;Build;" >
<Output ItemName="PluginOutput" TaskParameter="TargetOutputs"/>
</MSBuild>
<ItemGroup>
<Plugins Condition="%(ProjectName)=%(Plugins.ProjectName)">
<PluginOutput>%(PluginOutput.Identity)</PluginOutput>
</Plugins>
</ItemGroup>
<Message Text="%(Plugins.ProjectName) %(PluginOutput.Identity)" Condition="%(Plugins.Disabled)" />
</Target>
BuildProject.target returns output directories (Ex:Plugin1\Plugin1\bin\Release\)
In this case buuilding fails with next errors:
error MSB4096: item list "PluginOutput" does not define a value for
metadata "ProjectName". In order to use this metadata, either qualify
it by specifying %(PluginOutput.ProjectName), or ensure that all items
in this list define a value for this metadata.
error MSB4113: Specified condition "%(Plugins.Disabled)" evaluates to
"" instead of a boolean.
But if remove ItemGroup and condition for Message task
<Target Name="BuildPlugin" Inputs="%(Plugins.Identity)" Outputs="%(Plugins.Identity -> %(PluginOutput.Identity))" Returns="%(PluginOutput.Identity)">
<MSBuild
Condition="!%(Disabled)"
Projects='%(ProjectDirectory)BuildProject.target'
Targets="Clean;Build;" >
<Output ItemName="PluginOutput" TaskParameter="TargetOutputs"/>
</MSBuild>
<Message Text="%(Plugins.ProjectName) %(PluginOutput.Identity)" />
</Target>
seems msbuild correctly batches plugins. BuildPlugin target output produced by Message task is:
BuildPlugin:
Plugin1
Plugin1\Plugin1\bin\Release\
BuildPlugin:
Plugin2
BuildPlugin:
Plugin3
Plugin3\Plugin3\bin\Release
But in this case I don't have any ability to filter disabled plugins and add plugins output folder to metadata.
Any ideas?
The following should work
<Target Name="BuildPlugin" Outputs="%(Plugins.Identity -> %(PluginOutput.Identity))" Returns="%(PluginOutput.Identity)">
<MSBuild
Condition="!%(Plugins.Disabled)"
Projects='%(Plugins.ProjectDirectory)BuildProject.target'
Targets="Clean;Build;" >
<Output ItemName="PluginOutput" TaskParameter="TargetOutputs"/>
</MSBuild>
<Message Text="%(Plugins.ProjectName) %(PluginOutput.Identity)" Condition="!%(Plugins.Disabled)"/>
</Target>
I couldn't test all steps, because of missing BuildProject.target, but this should work:
<Target Name="BuildPlugin"
Inputs="%(Plugins.Identity)"
Outputs="%(Plugins.Identity)\dummy.txt"
Returns="%(PluginOutput.Identity)">
<PropertyGroup> <!-- transform Metadata to Properties -->
<ProjectName>%(Plugins.ProjectName)</ProjectName>
<PluginDisabled>%(Plugins.Disabled)</PluginDisabled>
</PropertyGroup>
<MSBuild
Condition="'$(PluginDisabled)' != 'true'"
Projects='%(Plugins.ProjectDirectory)BuildProject.target'
Targets="Clean;Build;" >
<Output ItemName="PluginOutput" TaskParameter="TargetOutputs"/>
</MSBuild>
<ItemGroup>
<!-- batching 2 ItemGroups in one task is usually not working or creates side effects-->
<Plugins Condition="'%(PluginOutput.ProjectName)' == '$(ProjectName)'">
<PluginOutput>%(PluginOutput.Identity)</PluginOutput>
</Plugins>
</ItemGroup>
<!-- batching 2 ItemGroups in one task is usually not working or creates side effects-->
<Message Text="$(ProjectName) %(PluginOutput.Identity)" Condition="$(PluginDisabled)" />
</Target>

MSBuild Copy task not copying files the first time round

I created a build.proj file which consists of a task to copy files that will be generated after the build is complete. The problem is that these files are not copied the first time round and I have to run msbuild again on the build.proj so that the files can be copied. Please can anyone tell me whats wrong with the following build.proj file:
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
<SourcePath Condition="'$(SourcePath)' == ''">$(MSBuildProjectDirectory)</SourcePath>
<BuildDir>$(SourcePath)\build</BuildDir>
</PropertyGroup>
<ItemGroup>
<Projects
Include="$(SourcePath)\src\myApp\application.csproj">
</Projects>
</ItemGroup>
<Target Name="Build">
<Message text = "Building project" />
<MSBuild
Projects="#(Projects)"
Properties="Configuration=$(Configuration)" />
</Target>
<ItemGroup>
<OutputFiles Include ="$(MSBuildProjectDirectory)\**\**\bin\Debug\*.*"/>
</ItemGroup>
<Target Name="CopyToBuildFolder">
<Message text = "Copying build items" />
<Copy SourceFiles="#(OutputFiles)" DestinationFolder="$(BuildDir)"/>
</Target>
<Target Name="All"
DependsOnTargets="Build; CopyToBuildFolder"/>
</Project>
The itemgroups are evaluated when the script is parsed. At that time your files aren't there yet. To be able to find the files you'll have to fill the itemgroup from within a target.
<!-- SQL Scripts which are needed for deployment -->
<Target Name="BeforeCopySqlScripts">
<CreateItem Include="$(SolutionRoot)\04\**\Databases\**\*.sql">
<Output ItemName="CopySqlScript" TaskParameter="Include"/>
</CreateItem>
</Target>
This example creates the ItemGroup named "CopySqlScript" using the expression in the Include attribute.
Edit:
Now I can read your script: add the CreateItem tag within your CopyToBuildFolder target

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.