MSBuild batch task output - msbuild

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>

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

Add output or transfer data from child to parent project

I use msbuild in main.proj to build a project like this:
<MSBuild Projects="outs.proj" Targets="Build">
<Output ItemName="CustomOutputs" TaskParameter="TargetOutputs"/>
</MSBuild>
Inside outs.proj I have a custom Target, I need to add an output from this target to get .dll,.pdb,..., and .mycustomfiles
How can I send data from child project to parent project ?
Thanks in advance for your help.
I'd recommend you simply Import the dependant project, however the basic scenario you described can be achieved with Target's Outputs or Returns and corresponding Output's TargetOutputs although there are few caveats as it's designed for incremental builds and not as a data transfer object.
foo.build
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Foo1">
<MSBuild Projects="bar.build">
<Output TaskParameter="TargetOutputs" ItemName="Bar" />
</MSBuild>
<Message Text="%(Bar.Identity)" />
</Target>
<Import Project="bar.build" />
<Target Name="Foo2" DependsOnTargets="Bar">
<Message Text="%(Bar.Identity)" />
</Target>
</Project>
bar.build
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Bar" Outputs="#(Bar)">
<ItemGroup>
<Bar Include="**\*.dll" />
</ItemGroup>
</Target>
</Project>

In MSBuild, can I use the String.Replace function on a MetaData item?

In MSBuild v4 one can use functions (like string.replace) on Properties. But how can I use functions on Metadata?
I'd like to use the string.replace function as below:
<Target Name="Build">
<Message Text="#(Files->'%(Filename).Replace(".config","")')" />
</Target>
Unfortunately this outputs as (not quite what I was going for):
log4net.Replace(".config","");ajaxPro.Replace(".config","");appSettings.Replace(".config","");cachingConfiguration20.Replace(".config","");cmsSiteConfiguration.Replace(".config","");dataProductsGraphConfiguration.Replace(".config","");ajaxPro.Replace(".config","");appSettings.Replace(".config","");cachingConfiguration20.Replace(".config","");cmsSiteConfiguration
Any thoughts?
You can do this with a little bit of trickery:
$([System.String]::Copy('%(Filename)').Replace('config',''))
Basically, we call the static method 'Copy' to create a new string (for some reason it doesn't like it if you just try $('%(Filename)'.Replace('.config',''))), then call the replace function on the string.
The full text should look like this:
<Target Name="Build">
<Message Text="#(Files->'$([System.String]::Copy("%(Filename)").Replace(".config",""))')" />
</Target>
Edit: MSBuild 12.0 seems to have broken the above method. As an alternative, we can add a new metadata entry to all existing Files items. We perform the replace while defining the metadata item, then we can access the modified value like any other metadata item.
e.g.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Files Include="Alice.jpg"/>
<Files Include="Bob.not-config.gif"/>
<Files Include="Charlie.config.txt"/>
</ItemGroup>
<Target Name="Build">
<ItemGroup>
<!--
Modify all existing 'Files' items so that they contain an entry where we have done our replace.
Note: This needs to be done WITHIN the '<Target>' (it's a requirment for modifying existing items like this
-->
<Files>
<FilenameWithoutConfig>$([System.String]::Copy('%(Filename)').Replace('.config', ''))</FilenameWithoutConfig>
</Files>
</ItemGroup>
<Message Text="#(Files->'%(FilenameWithoutConfig)')" Importance="high" />
</Target>
</Project>
Result:
D:\temp>"c:\Program Files (x86)\MSBuild\12.0\Bin\MSBuild.exe" /nologo test.xml
Build started 2015/02/11 11:19:10 AM.
Project "D:\temp\test.xml" on node 1 (default targets).
Build:
Alice;Bob.not-config;Charlie
Done Building Project "D:\temp\test.xml" (default targets).
I needed to do something similar, the following worked for me.
<Target Name="Build">
<Message Text="#(Files->'%(Filename)'->Replace('.config', ''))" />
</Target>
Those functions works in properties only (as I know). So create target which will perform operation throw batching:
<Target Name="Build"
DependsOnTargets="ProcessFile" />
<Target Name="ProcessFile"
Outputs="%(Files.Identity)">
<PropertyGroup>
<OriginalFileName>%(Files.Filename)</OriginalFileName>
<ModifiedFileName>$(OriginalFileName.Replace(".config",""))</ModifiedFileName>
</PropertyGroup>
<Message Text="$(ModifiedFileName)" Importance="High"/>
</Target>
Do you really need in your example such kind of task? I mean there exists MSBuild Well-known Item Metadata
EDIT: I should specify that this task processes all items in #(Files).
i dont think you can use functions directly with itemgroups and metadata (that would be easy)
However you can use batching:
Taking the ideas from this post:
array-iteration
I was trying to trim an itemgroup to send to a commandline tool (i needed to lose .server off the filename)
<Target Name="ProcessFile" DependsOnTargets="FullPaths">
<ItemGroup>
<Environments Include="$(TemplateFolder)\$(Branch)\*.server.xml"/>
</ItemGroup>
<MSBuild Projects=".\Configure.msbuild"
Properties="CurrentXmlFile=%(Environments.Filename)"
Targets="Configure"/>
</Target>
<Target Name="Configure" DependsOnTargets="FullPaths">
<PropertyGroup>
<Trimmed>$(CurrentXmlFile.Replace('.server',''))</Trimmed>
</PropertyGroup>
<Message Text="Trimmed: $(Trimmed)"/>
<Exec Command="ConfigCmd $(Trimmed)"/>
</Target>
For MSBuild 12.0, here's an alternative.
<Target Name="Build">
<Message Text="$([System.String]::Copy("%(Files.Filename)").Replace(".config",""))" />
</Target>
Got the same problem (except with MakeRelative), so I passed with another solution : Using good old CreateItem that take a string and transform to Item :)
<ItemGroup>
<_ToUploadFTP Include="$(PublishDir)**\*.*"></_ToUploadFTP>
</ItemGroup>
<CreateItem Include="$([MSBuild]::MakeRelative('c:\$(PublishDir)','c:\%(relativedir)%(filename)%(_ToUploadFTP.extension)'))">
<Output ItemName="_ToUploadFTPRelative" TaskParameter="Include"/>
</CreateItem>
<FtpUpload Username="$(UserName)"
Password="$(UserPassword)"
RemoteUri="$(FtpHost)"
LocalFiles="#(_ToUploadFTP)"
RemoteFiles="#(_ToUploadFTPRelative->'$(FtpSitePath)/%(relativedir)%(filename)%(extension)')"
UsePassive="$(FtpPassiveMode)" ></FtpUpload>

MsBuild Copy output and remove part of path

I have an MsBuild project which builds various solutions and then copies the output of Web Deployment Projects into a destination folder with two sub folder as follows:
The WDP output folders are copied over from the BuildFolder "Release".
DestFolder/PresentationTier/MyProject.xxx0Services_deploy/**Release**/Files...
DestFolder/MidTier/MyProject.xx1UI_deploy/**Release**/Files...
This works but I want to remove the $(Configuration) value from the output.
So the desired output folder layout is to be:
DestFolder/PresentationTier/MyProject.xxx0Services_deploy/Files...
DestFolder/MidTier/MyProject.xx1UI_deploy/Files...
Note the removal of "Release" folder
My code is below.
How can I change this to give the desired out please:
Code extract is as follows
<Target Name="CopyMidTierBuildOutput" DependsOnTargets="CopyPresentationTierBuildOutput" >
<Message Text="Copying midTier Build Output=================" />
<CreateItem Include="$(DeploymentRoot)**/MyProject.xxx0Services_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/MyProject.xxx1Services.Host_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/MyProject.xxx2.Host.IIS.csproj_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/MyProject.xxx3Services_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/Nad.xxx4_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/Nad.xxx5Services.Host_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/Nad.xxx6Services.Host_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/Nad.xxx7Service.Host.IIS_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/Nad.xxx8Services.Host_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/Nad.xxx9Service.Host.IIS.csproj_deploy/$(Configuration)/**/*.*;
$(DeploymentRoot)**/Nad.xxx10Services.Host_deploy/$(Configuration)/**/*.*">
<Output TaskParameter="Include" ItemName="MidTierDeploys"/>
</CreateItem>
<Copy
SourceFiles="#(MidTierDeploys)"
DestinationFolder="$(DestFolder)/MidTier/%(RecursiveDir)" ContinueOnError="false" />
You can implement expected behaviour with biltin features of MSBuild 4:
<ItemGroup>
<DeploymentProjects Include="1_deploy" />
<DeploymentProjects Include="2_deploy" />
</ItemGroup>
<Target Name="CopyMidTierBuildOutput" >
<Message Text="Copying midTier Build Output" Importance="High"/>
<ItemGroup>
<MidTierDeploys Include="$(DeploymentRoot)**\%(DeploymentProjects.Identity)\$(Configuration)\**\*.*">
<DeploymentProject>%(DeploymentProjects.Identity)</DeploymentProject>
</MidTierDeploys>
</ItemGroup>
<Msbuild Targets="CopyDeploymentItem"
Projects="$(MSBuildProjectFile)"
Properties="ItemFullPath=%(MidTierDeploys.FullPath);ItemRecursiveDir=%(MidTierDeploys.RecursiveDir);ItemDeploymentProject=%(MidTierDeploys.DeploymentProject);Configuration=$(Configuration);DestFolder=$(DestFolder)" />
</Target>
<Target Name="CopyDeploymentItem" >
<PropertyGroup>
<ItemExcludePath>$(ItemDeploymentProject)\$(Configuration)</ItemExcludePath>
<ItemDestRecursiveDirIndex>$(ItemRecursiveDir.IndexOf($(ItemExcludePath))) </ItemDestRecursiveDirIndex>
<ItemExcludePathLength>$(ItemExcludePath.Length)</ItemExcludePathLength>
<ItemSkippingCount>$([MSBuild]::Add($(ItemDestRecursiveDirIndex), $(ItemExcludePathLength)))</ItemSkippingCount>
<ItemDestRecursiveDir>$(ItemRecursiveDir.Substring($(ItemSkippingCount)))</ItemDestRecursiveDir>
</PropertyGroup>
<Copy
SourceFiles="$(ItemFullPath)"
DestinationFolder="$(DestFolder)/MidTier/$(ItemDeploymentProject)/$(ItemDestRecursiveDir)" ContinueOnError="false" />
</Target>
See Property functions for more info.

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