Possible to reverse order of ItemGroup elements? - msbuild

In MSBuild 3.5, is it possible to reverse the order elements in an ItemGroup?
Example
I have 2 projects. One can be built independently the other is dependent on the first. Each project references its specific items in a .targets file.
project_A.targets
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AssembliesToRemove Include="#(AssembliesToRemove)" />
<AssembliesToRemove Include="Assembly_A.dll">
<ApplicationName>App_A</ApplicationName>
</AssembliesToRemove>
</ItemGroup>
<ItemGroup>
<AssembliesToDeploy Include="#(AssembliesToDeploy)" />
<AssembliesToDeploy Include="Assembly_A.dll">
<AssemblyType>SomeType</AssemblyType>
<ApplicationName>App_A</ApplicationName>
</AssembliesToDeploy>
</ItemGroup>
</Project>
project_B.targets
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AssembliesToRemove Include="#(AssembliesToRemove)" />
<AssembliesToRemove Include="Assembly_B.dll">
<ApplicationName>App_B</ApplicationName>
</AssembliesToRemove>
</ItemGroup>
<ItemGroup>
<AssembliesToDeploy Include="#(AssembliesToDeploy)" />
<AssembliesToDeploy Include="Assembly_B.dll">
<AssemblyType>SomeType</AssemblyType>
<ApplicationName>App_B</ApplicationName>
</AssembliesToDeploy>
</ItemGroup>
</Project>
project_A.proj
<Project DefaultTargets="Start" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="project_A.targets" />
<Import Project="Common.targets" />
</Project>
project_B.proj
<Project DefaultTargets="Start" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="project_A.targets" />
<Import Project="project_B.targets" />
<Import Project="Common.targets" />
</Project>
The Problem
In this scenario the problem arises during the Task processing #(AssembliesToDeploy) because Assembly_B.dll needs to be deployed before Assembly_A.dll.
Processing #(AssembliesToRemove) works fine because here the assemblies are in the right order (remove Assembly_A.dll before Assembly_B.dll).
What I tried to do
I tried to influence the order of #(AssembliesToDeploy) by modifying project_B.targets like this:
<ItemGroup>
<AssembliesToDeploy Include="Assembly_B.dll">
<AssemblyType>SomeType</AssemblyType>
<ApplicationName>App_B</ApplicationName>
</AssembliesToDeploy>
<AssembliesToDeploy Include="#(AssembliesToDeploy)" />
</ItemGroup>
but when using project_B.targets inside project_B.proj the order inside #(AssembliesToDeploy) still remained Assembly_A.dll;Assembly_B.dll.
Edit
As MadGnome points out this cannot work because I'll end up with duplicates in #(AssembliesToDeploy)
Is there a solution which would allow to reuse my .targets i.e not copying all ItemGroup elements to all .targets files?

You just have to include project_B.targets before project_A.
<Project DefaultTargets="Start" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="project_B.targets" />
<Import Project="project_A.targets" />
<Import Project="Common.targets" />
</Project>
I think there is a mistake in this code :
<ItemGroup>
<!-- Generates duplicates if used with Import -->
<AssembliesToDeploy Include="#(AssembliesToDeploy)" />
<AssembliesToDeploy Include="Assembly_B.dll">
<AssemblyType>SomeType</AssemblyType>
<ApplicationName>App_B</ApplicationName>
</AssembliesToDeploy>
</ItemGroup>
You are using Import, so you if you use the code above you'll have duplicates in AssembliesToDeploy.

Adopting from MadGnomes answer I decided to split the ItemGroups into separate .target files.
project_A_REMOVE.targets
<Project xmlns="...">
<ItemGroup>
<AssembliesToRemove Include="#(AssembliesToRemove)" />
<AssembliesToRemove Include="Assembly_A.dll">
<ApplicationName>App_A</ApplicationName>
</AssembliesToRemove>
</ItemGroup>
</Project>
project_A_DEPLOY.targets
<Project xmlns="...">
<ItemGroup>
<AssembliesToDeploy Include="#(AssembliesToDeploy)" />
<AssembliesToDeploy Include="Assembly_A.dll">
<AssemblyType>SomeType</AssemblyType>
<ApplicationName>App_A</ApplicationName>
</AssembliesToDeploy>
</ItemGroup>
</Project>
and the same for project_B.targets.
The project_B.proj now looks like this
<Project DefaultTargets="Start" xmlns="...">
<Import Project="project_A_REMOVE.targets" />
<Import Project="project_B_REMOVE.targets" />
<Import Project="project_B_DEPLOY.targets" />
<Import Project="project_A_DEPLOY.targets" />
<Import Project="Common.targets" />
</Project>
Since my real solution consist of some 58 projects this will result in a lot of .targets. Even more so because I have to keep a common .targets for every project.

Related

Delete folders after publish with new ASP.NET CORE 1.1 csproj file format

I am publishing an ASP.NET Core 1.1 application and I need to delete from the output a few folders (fr;nl;pt) created by a library (Fluent Validation):
<ItemGroup>
<FluentValidationExcludedCultures Include="fr;nl;pt">
<InProject>false</InProject>
</FluentValidationExcludedCultures>
</ItemGroup>
<Target Name="RemoveTranslationsAfterBuild" AfterTargets="AfterBuild">
<RemoveDir Directories="#(FluentValidationExcludedCultures->'$(OutputPath)%(Filename)')" />
</Target>
But this does not work and the folders are still copied ... Then I tried:
<ItemGroup>
<Content Include="fr" CopyToPublishDirectory="Never" />
<Content Include="nl" CopyToPublishDirectory="Never" />
<Content Include="pt" CopyToPublishDirectory="Never" />
</ItemGroup>
But this didn't work either ...
Does anyone has any idea how to make this work?
Try to edit your csproj file and add the following section for each of the directories that you do not want to include when publishing:
<ItemGroup>
<PublishFile Remove="directory\**" />
</ItemGroup>
Another solution that works for build/publish
<!-- Removes FluentValidation localization folders -->
<Target Name="AfterPackage" AfterTargets="CopyAllFilesToSingleFolderForPackage" />
<ItemGroup>
<FluentValidationExcludedCultures Include="cs;da;de;es;fa;fi;fr;it;ko;mk;nl;pl;pt;ru;sv;tr;zh-CN">
<InProject>false</InProject>
</FluentValidationExcludedCultures>
</ItemGroup>
<Target Name="FluentValidationRemoveTranslationsAfterBuild" AfterTargets="AfterBuild">
<RemoveDir Directories="#(FluentValidationExcludedCultures->'$(OutDir)%(Filename)')" />
</Target>
<Target Name="FluentValidationRemoveTranslationsAfterPackage" AfterTargets="AfterPublish">
<RemoveDir Directories="#(FluentValidationExcludedCultures->'$(OutDir)%(Filename)')" />
</Target>

How to extract directory from property?

I have a property GroupProj storing a full path name. How can I extract the directory of the property?
I have the following code, but it doesn't work as expected:
<PropertyGroup>
<GroupProj>C:\development\project\default.groupproj</GroupProj>
</PropertyGroup>
<Target Name="Default">
<Message Text="Echo: $(GroupProj->'%(RootDir)')" />
</Target>
I will describe my actual intention of doing so. Perhaps there is a way to do the job that I am not aware of.
I have a Delphi groupproj (MSBuild project) file, C:\development\project\default.groupproj:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Projects Include="project1.dproj">
<Dependencies/>
</Projects>
<Projects Include="project2.dproj">
<Dependencies/>
</Projects>
<Projects Include="project3.dproj">
<Dependencies/>
</Projects>
</ItemGroup>
...
</Project>
There are other 3 MSBuild files (project1.dproj, project2.dproj and project3.dproj) stored in same folder as default.groupproj.
I create a MSBuild project file (c:\test.targets):
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build" ToolsVersion="3.5">
<Import Project="$(GroupProj)" />
<Target Name="Build">
<MSBuild BuildInParallel="True" Projects="project1.dproj;project2.dproj;project3.dproj"/>
</Target>
</Project>
And execute as:
c:\> msbuild /p:GroupProj="C:\development\project\default.groupproj" test.targets
The execution shall fail as MSBuild can't find projectN.dproj file. The issue shall be the working directory isn't set to default.groupproj.
One straight solution come into my mind is to extract directory of $(GroupProj) and concat to there projectN.dproj file.
That's the whole story of my question.
Try something like this:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<GroupProj>C:\development\project\default.groupproj</GroupProj>
</PropertyGroup>
<Target Name="Build">
<CreateItem Include="$(GroupProj)">
<Output TaskParameter="Include" ItemName="ItemFromProp"/>
</CreateItem>
<Message Text="1. #(ItemFromProp -> '%(RootDir)%(Directory)')"/>
<Message Text="2. %(ItemFromProp.RootDir)%(ItemFromProp.Directory)"/>
<Message Text="3. %(ItemFromProp.Identity)"/>
<Message Text="4. %(ItemFromProp.FullPath)"/>
<Message Text="5. %(ItemFromProp.FileName)"/>
<Message Text="6. %(ItemFromProp.Extension)"/>
</Target>
</Project>
EDIT:
To build the projects in parallel try this:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="GetGroupProjPath">
<ItemGroup>
<GroupProj Include="$(GroupProj)" />
<GroupProjPath Include="#(GroupProj->'%(Directory)')" />
</ItemGroup>
<PropertyGroup>
<GroupProjPath>#(GroupProjPath->'%(RootDir)%(Identity)')</GroupProjPath>
</PropertyGroup>
</Target>
<Import Project="$(GroupProj)" />
<Target Name="GetDProjs" DependsOnTargets="GetGroupProjPath">
<ItemGroup>
<DProjs Include="#(Projects->'$(GroupProjPath)%(FileName)%(Extension)')" />
</ItemGroup>
</Target>
<Target Name="Build" DependsOnTargets="GetDProjs">
<Message Text="#(DProjs)" />
</Target>
</Project>

msbuild to copy to multiple locations defined in item metadata

I have an item with metadata I want to copy to perform some actions on and the result to occur in multiple locations,
for example, I want to copy the file to multiple locations:
<ItemGroup>
<MyItem Include="myFile.txt">
<Out>c:\blah;c:\test</Out>
</MyItem>
</ItemGroup>
how would I setup a target to create c:\blah and c:\test if they dont exist, then copy myFile.txt to c:\blah\myFile.txt and c:\test\myFile.txt
I also want to get the list of full output paths (c:\blah\myFile.txt and c:\test\myFile.txt) if I want to clean them during a clean.
If you dont want to change the structure of you ItemGroup, you need to handle that you have a nested ItemGroup (the MetaDataElement Out). Therefor you will need to batch the ItemGroup MyItem to the target and inside you can batch Out. I made a small example project:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="CopyFiles" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<ItemGroup>
<MyItem Include="myFile.txt">
<Out>c:\blah;c:\test</Out>
</MyItem>
<MyItem Include="myFile2.txt">
<Out>c:\blah2;c:\test2</Out>
</MyItem>
</ItemGroup>
<Target Name="CopyFiles"
Inputs="%(MyItem.Identity)"
Outputs="%(MyItem.Identity)\ignore_this.msg">
<PropertyGroup>
<File>%(MyItem.Identity)</File>
</PropertyGroup>
<ItemGroup>
<Folders Include="%(MyItem.Out)" />
</ItemGroup>
<Message Text="%(Folders.Identity)\$(File)" />
</Target>
</Project>
The Output will be:
Project "D:\TEMP\test.proj" on node 1 (default targets).
CopyFiles:
c:\blah\myFile.txt
c:\test\myFile.txt
CopyFiles:
c:\blah2\myFile2.txt
c:\test2\myFile2.txt
Done Building Project "D:\TEMP\test.proj" (default targets).
Build succeeded.
0 Warning(s)
0 Error(s)
What you want to do is a concept called MSBuild Batching.
It allows you to divide item lists into different batches and pass each of those batches into a task separately.
<Target Name="CopyFiles">
<ItemGroup Label="MyFolders">
<Folder Include="c:\blah" />
<Folder Include="C:\test" />
</ItemGroup>
<Copy SourceFiles="myFile.txt" DestinationFolder="%(Folder.Identity)\">
<Output TaskParameter="CopiedFiles" ItemName="FilesCopy" />
</Copy>
</Target>
How about this:
<Target Name="CopyFiles">
<!--The item(s)-->
<ItemGroup>
<MyItem Include="myFile.txt"/>
</ItemGroup>
<!--The destinations-->
<ItemGroup>
<MyDestination Include="c:\blah"/>
<MyDestination Include="c:\test"/>
</ItemGroup>
<!--The copy-->
<Copy SourceFiles="#(MyItem)" DestinationFolder="%(MyDestination.FullPath)" />
<ItemGroup>
<FileWrites Include="%(MyDestination.FullPath)\*" />
</ItemGroup>
<!--The output -->
<Message Text="FileWrites: #(FileWrites)" Importance="high"/>
</Target>

Referencing well-known item metadata inside metadata definition in an ItemGroup in a target

Here's an MSBuild script:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="AugmentItemGroup" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<ItemGroup>
<ItmGrp Include="File1.txt">
<Dest>dest\%(FileName)%(Extension)</Dest>
</ItmGrp>
<ItmGrp Include="File2.txt">
<Dest>dest\%(FileName)%(Extension)</Dest>
</ItmGrp>
<ItmGrp Include="File3.txt">
<Dest>dest\%(FileName)%(Extension)</Dest>
</ItmGrp>
</ItemGroup>
<Target Name="AugmentItemGroup">
<ItemGroup>
<ItmGrp Include="File4.txt">
<Dest>dest\%(FileName)%(Extension)</Dest>
</ItmGrp>
</ItemGroup>
<Message Text="%(ItmGrp.FullPath) to %(ItmGrp.Dest)" />
</Target>
</Project>
The output I would expect from it is:
D:\t\File1.txt to dest\File1.txt
D:\t\File2.txt to dest\File2.txt
D:\t\File3.txt to dest\File3.txt
D:\t\File4.txt to dest\File4.txt
But the result is:
D:\t\File1.txt to dest\File1.txt
D:\t\File2.txt to dest\File2.txt
D:\t\File3.txt to dest\File3.txt
D:\t\File4.txt to dest\File1.txt
D:\t\File4.txt to dest\File2.txt
D:\t\File4.txt to dest\File3.txt
Why is the behavior of the %(FileName)%(Extension) well-known metadata reference is different when an ItemGroup is inside a target?
Is it possible to get the "outside a target" behavior inside a target?
This will give the output you desire. Though it may not be the correct approach in the general case, it does avoid the batching that occurs with "File4" by making the custom metadata a part of the item definition that is calculated:
<Project
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTargets="AugmentItemGroup"
ToolsVersion="4.0">
<ItemDefinitionGroup>
<ItmGrp>
<Dest>dest\%(FileName)%(Extension)</Dest>
</ItmGrp>
</ItemDefinitionGroup>
<ItemGroup>
<ItmGrp Include="File1.txt" />
<ItmGrp Include="File2.txt" />
<ItmGrp Include="File3.txt" />
</ItemGroup>
<Target Name="AugmentItemGroup">
<ItemGroup>
<ItmGrp Include="File4.txt" />
</ItemGroup>
<Message Text="%(ItmGrp.FullPath) to %(ItmGrp.Dest)" />
</Target>
</Project>
edit:
If (as your comment below says) each item has a different value for %(Dest), you just need to make the final value calculated:
<Project ...>
<ItemDefinitionGroup>
<ItmGrp>
<_Dest />
</ItmGrp>
</ItemDefinitionGroup>
<ItemGroup>
<ItmGrp Include="File1.txt"><Dest>dest1</Dest></ItmGrp>
<ItmGrp Include="File2.txt"><Dest>dest2</Dest></ItmGrp>
<ItmGrp Include="File3.txt"><Dest>dest3</Dest></ItmGrp>
</ItemGroup>
<Target Name="AugmentItemGroup">
<ItemGroup>
<ItmGrp Include="File4.txt"><Dest>dest4</Dest></ItmGrp>
<ItmGrp>
<_Dest>%(Dest)\%(FileName)%(Extension)</_Dest>
</ItmGrp>
</ItemGroup>
<Message Text="%(ItmGrp.FullPath) to %(ItmGrp._Dest)" />
</Target>
</Project>
Excerpted from MSBuild Trickery tricks #70, 71

How to give a different OutputPath per project per build configuration with MSBuild?

Multiple projects have to be build with one ore more configurations (debug/release/...).
The output of the build needs to be copied to a folder (BuildOutputPath).
There is a default BuildOutputFolder, but for some project you can indicate that the output needs to be put in a extra child folder.
For example:
Configuration are:
- debug
- release
The projects are:
Project1 (BuildOutputFolder)
Project2 (BuildOutputFolder)
Project3 (BuildOutputFolder\Child)
The end result should look like this:
\\BuildOutput\
debug\
project1.dll
project2.dll
Child\
Project3.dll
release\
project1.dll
project2.dll
Child\
Project3.dll
I got this far atm, but can't figure out how to override the OutputPath per project.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTargets="Build" >
<ItemGroup>
<ConfigList Include="Debug" />
<ConfigList Include="Release" />
</ItemGroup>
<PropertyGroup>
<BuildOutputPath>$(MSBuildProjectDirectory)\BuildOutput\</BuildOutputPath>
</PropertyGroup>
<ItemGroup>
<Projects Include="project1.csproj" />
<Projects Include="project2.csproj" />
<Projects Include="project3.csproj" />
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="Configuration=%(ConfigList.Identity);OutputPath=$(BuildOutputPath)%(ConfigList.Identity)" />
</Target>
</Project>
How would you accomplish this in a MSBuild project file ?
Your'e attempting to call a task recursively in two different contexts. 2 configurations and 3 projects requires 6 calls to the build task. You need to layout the project in such a way that for each item in ConfigList a call is made multiplied by each item in Projects.
Also use ItemDefinitionGroup to set default shared properties:
<ItemGroup>
<ConfigList Include="Debug" />
<ConfigList Include="Release" />
</ItemGroup>
<ItemDefinitionGroup>
<Projects>
<BuildOutputPath>$(MSBuildProjectDirectory)\BuildOutput\</BuildOutputPath>
</Projects>
</ItemDefinitionGroup>
<ItemGroup>
<Projects Include="project1.csproj" />
<Projects Include="project2.csproj" />
<Projects Include="project3.csproj" >
<Subfolder>Child</Subfolder>
</Projects>
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="$(MSBuildProjectFullPath)"
Targets="_BuildSingleConfiguration"
Properties="Configuration=%(ConfigList.Identity)" />
</Target>
<Target Name="_BuildSingleConfiguration">
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="Configuration=$(Configuration);OutputPath=%(Projects.BuildOutputPath)$(Configuration)\%(Projects.Subfolder)" />
</Target>
</Project>
Try to do it using Project metadata
<ItemGroup>
<Projects Include="project1.csproj">
<ChildFolder/>
</Project>
<Projects Include="project2.csproj">
<ChildFolder/>
</Project>
<Projects Include="project3.csproj">
<ChildFolder>Child</ChildFolder>
</Project>
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="Configuration=%(ConfigList.Identity);OutputPath=$(BuildOutputPath)%(ConfigList.Identity)%(Project.ChildFolder)" />