How to give a different OutputPath per project per build configuration with MSBuild? - 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)" />

Related

Execute a XCopy operation after all the projects build in MSBuild

I have a .proj file which is configured to execute a solution file which in turn build all the projects in the solution.
I want to add an XCopy operation which should copy the .dll files of all projects to another location only after all the projects build is completed.
I have tried with below, but it is not copying the dlls.
I am newbie in writing MSBuild tags, so it could be that I may be wrong in choosing this approach to write the task in this way.
Please provide a solution, if anyone knows.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition="'$(Configuration)' == 'Release|Debug'"/>
<Platform Condition="'$(Platform)' == ''">x64</Platform>
</PropertyGroup>
<ItemDefinitionGroup>
<SolutionToBuild>
<Properties>Configuration=$(Configuration);Platform=$(Platform)</Properties>
<Targets>Clean;Build</Targets>
</SolutionToBuild>
</ItemDefinitionGroup>
<ItemGroup>
<SolutionToBuild Include="..\Seg\Algorithms.sln" />
</ItemGroup>
<Target Name="Build" >
<MSBuild Projects="#(SolutionToBuild)" Targets="%(SolutionToBuild.Targets)" Properties="%(SolutionToBuild.Properties)" BuildInParallel="false" ContinueOnError="false" />
</Target>
<Target Name="Clean">
<MSBuild Projects="#(SolutionToBuild)" Targets="Clean" Properties="%(SolutionToBuild.Properties)" BuildInParallel="false" ContinueOnError="false" />
</Target>
<PropertyGroup>
<CopyDestination>..\Extern\Algo\bin\$(Configuration)\</CopyDestination>
<CopySource>..\Seg\Algorithms\$(Configuration)\DoBin\</CopySource>
</PropertyGroup>
<ItemGroup>
<FilesToCopy Include="$(CopySource)*.dll"/>
</ItemGroup>
<ItemGroup>
<CustomBuildStep Include ="#(FilesToCopy)">
<Message>Copying..</Message>
<Command> XCOPY %(Identity) $(CopyDestination) /f /y </Command>
</CustomBuildStep>
</ItemGroup>
<PropertyGroup>
<CustomBuildAfterTargets>Build</CustomBuildAfterTargets>
</PropertyGroup>
</Project>
Think of Targets as methods that are called. They run in sequence, so you just need to put your copy after the solution build:
<Target Name="Build">
<MSBuild Projects="#(SolutionToBuild)" Targets="%(SolutionToBuild.Targets)" Properties="%(SolutionToBuild.Properties)" BuildInParallel="false" ContinueOnError="false" />
<ItemGroup>
<FilesToCopy Include="..\Seg\Algorithms\$(Configuration)\DoBin\*.dll" />
</ItemGroup>
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="..\Extern\Algo\bin\$(Configuration)\" SkipUnchangedFiles="true" />
</Target>

MSBuild: how to include generated classes into compilation?

I have the following MSBuild script:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
<BuildDependsOn>
NSwag;
$(BuildDependsOn)
</BuildDependsOn>
<!--<AfterTransform>NSwag</AfterTransform>-->
</PropertyGroup>
<ItemGroup>
...
</ItemGroup>
<Target Name="NSwag" BeforeTargets="BeforeBuild">
<Message Text="Generating C# client code via NSwag" Importance="high" />
<!-- ISSUE HERE -->
<Copy SourceFiles="..\..\MyClient.cs" DestinationFiles="Gen\MyClient.cs" />
</Target>
</Project>
The Target "NSwag" above is going to be used for code generation tool. But to simplify things, I use here just a file copy command.
The issue is that the .cs files added within this Target are not visible in the MSBuild compilation:
The type or namespace name 'MyClient' does not exist in the namespace 'MyNamespace'
NOTE: The issue occurs only if the file didn't exist in the destination folder.
NOTE: I was trying to mangle with the following but with no success so far:
<Target Name="RemoveSourceCodeDuplicates" BeforeTargets="BeforeBuild;BeforeRebuild" DependsOnTargets="UpdateGeneratedFiles">
<RemoveDuplicates Inputs="#(Compile)">
<Output TaskParameter="Filtered" ItemName="Compile"/>
</RemoveDuplicates>
</Target>
and
<Target Name="UpdateGeneratedFiles" BeforeTargets="BeforeBuild;BeforeRebuild" DependsOnTargets="NSwag">
<ItemGroup>
<Compile Include="Gen\MyClient.cs" Condition="!Exists('Gen\MyClient.cs')" />
</ItemGroup>
</Target>
What am I missing here?
I think I found a workaround for that - check and include the files first (UpdateGeneratedFiles target), then generate them (NSwag target). See the script below:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
<BuildDependsOn>
NSwag;
$(BuildDependsOn)
</BuildDependsOn>
</PropertyGroup>
<Target Name="NSwag" BeforeTargets="BeforeBuild;BeforeRebuild"
DependsOnTargets="UpdateGeneratedFiles">
<Message Text="Generating C# client code via NSwag" Importance="high" />
<Copy SourceFiles="..\..\MyClient.cs" DestinationFiles="Gen\MyClient.cs" />
</Target>
<Target Name="UpdateGeneratedFiles" BeforeTargets="BeforeBuild;BeforeRebuild" >
<ItemGroup>
<Compile Include="Gen\MyClient.cs" Condition="!Exists('Gen\MyClient.cs')" />
</ItemGroup>
</Target>
</Project>

MSBuild: Output properties from imported projects

Let's say I have a build.proj like this:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0"
DefaultTargets="AfterBuild"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CustomAfterMicrosoftCSharpTargets>$(MSBuildThisFileDirectory)Common.Build.targets</CustomAfterMicrosoftCSharpTargets>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<ProjectProperties>
Configuration=$(Configuration);
Platform=$(Platform);
CustomAfterMicrosoftCSharpTargets=$(CustomAfterMicrosoftCSharpTargets);
</ProjectProperties>
</PropertyGroup>
<ItemGroup>
<ProjectToBuild Include="$(MSBuildThisFileDirectory)src\Proj\MyApp.csproj" />
</ItemGroup>
<Target Name="Build">
<MSBuild Targets="Build"
Projects="#(ProjectToBuild)"
Properties="$(ProjectProperties)" />
</Target>
<Target Name="AfterBuild" DependsOn="Build">
<Message Text="ChildProperty: $(ChildProperty)" />
</Target>
</Project>
In Common.Build.targets, I have a Target that creates a property:
<Target Name="DoSomethingUseful">
<!-- Do something useful -->
<CreateProperty Value="SomeComputedThingy">
<Output TaskParameter="Value" PropertyName="ChildProperty"/>
</CreateProperty>
</Target>
Now if I build build.proj, I do not see the value of ChildProperty in the message. The output is blank: ChildProperty:.
I was under the impression that any output for a target is merged back to global context after its execution. But it seems that it only applies to anything within that target file.
How do I make ChildProperty bubble up to the parent build.proj?
When you are calling <MSBuild> task on dependent projects, read TargetOutputs output parameter of the task. See example from MSDN:
<Target Name="BuildOtherProjects">
<MSBuild
Projects="#(ProjectReferences)"
Targets="Build">
<Output
TaskParameter="TargetOutputs"
ItemName="AssembliesBuiltByChildProjects" />
</MSBuild>
</Target>
You will also need to ensure the target you are calling in dependent projects correctly populates Returns or Output parameter (Returns takes precedence if used). E.g.:
<Target Name="MyTarget" Inputs="..." Outputs="..." Returns="$(MyOutputValue)">
<PropertyGroup>
<MyOutputValue>set it here</MyOutputValue>
</PropertyGroup>
</Target>

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>

TFS Build SourceTfs.Checkout

I am trying to get my build to checkout some files (using Microsoft.Sdc.Common.tasks) and then to check them in after the build has finished.
But I can't seem to get this working at all, let alone before and after the build.
Whaereabouts should this sort of code live?
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="DesktopBuild;MyProjectDbUpdate" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<PropertyGroup>
<TasksPath>C:\Program Files\MSBuild\sdc\</TasksPath>
</PropertyGroup>
<Import Project="$(TasksPath)\Microsoft.Sdc.Common.tasks" />
<Target Name="MyProjectDbUpdate">
<Message Text="MyProjectDbUpdate checkin start"/>
<SourceTfs.Checkout Path="$/MyProject/Code/MyProjectDbUpdate" TfsVersion="2008" workingDirectory="C:\buildagent\MyProject\ContinuousIntegration\Sources\Code" />
<SourceTfs.Checkin Path="$/MyProject/Code/MyProjectDbUpdate" workingDirectory="C:\buildagent\MyProject\ContinuousIntegration\Sources\Code" Comments="Build checkout/checkin." TfsVersion="2008" Override="Build overrides checkin policy" />
<Message Text="MyProjectDbUpdate checkin complete"/>
</Target>
<ProjectExtensions>
<ProjectFileVersion>2</ProjectFileVersion>
<Description>Build</Description>
<BuildMachine>MYSERVER</BuildMachine>
</ProjectExtensions>
<PropertyGroup>
<TeamProject>MyProject</TeamProject>
<BuildDirectoryPath>c:\buildagent\MyProject\ContinuousIntegration</BuildDirectoryPath>
<DropLocation>\\UNKNOWN\drops</DropLocation>
<RunTest>false</RunTest>
<RunCodeAnalysis>Never</RunCodeAnalysis>
<WorkItemType>Bug</WorkItemType>
<WorkItemFieldValues>System.Reason=Build Failure;System.Description=Start the build using Team Build</WorkItemFieldValues>
<WorkItemTitle>Build failure in build:</WorkItemTitle>
<DescriptionText>This work item was created by Team Build on a build failure.</DescriptionText>
<BuildlogText>The build log file is at:</BuildlogText>
<ErrorWarningLogText>The errors/warnings log file is at:</ErrorWarningLogText>
<UpdateAssociatedWorkItems>true</UpdateAssociatedWorkItems>
<AdditionalVCOverrides></AdditionalVCOverrides>
<CustomPropertiesForClean></CustomPropertiesForClean>
<CustomPropertiesForBuild></CustomPropertiesForBuild>
</PropertyGroup>
<ItemGroup>
<SolutionToBuild Include="$(BuildProjectFolderPath)/../../Code/MyProject.sln">
<Targets></Targets>
<Properties></Properties>
</SolutionToBuild>
</ItemGroup>
<ItemGroup>
<ConfigurationToBuild Include="Release|Any CPU">
<FlavorToBuild>Release</FlavorToBuild>
<PlatformToBuild>Any CPU</PlatformToBuild>
</ConfigurationToBuild>
</ItemGroup>
<ItemGroup>
</ItemGroup>
<PropertyGroup>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
</Project>
Specifying your target as a default target is not going to call it as Team build explicity sets the target it is going to call.
Try renaming the Target to AfterGet or overriding the the GetDependsOn property to include your target
<GetDependsOn>
$(GetDependsOn)
MyProjectDbUpdate;
</GetDependsOn>