By default Team Build spews all compilation output into a single directory. Although web projects are output in deployable form into a directory called _PublishedWebsites\<name of project> the same is not true for exe or dll projects. A while back I wrote a post showing how you could grab the output for exe projects and place them in a similar _PublishedApplications directory, and this worked fine for simple cases.
However that solution relied on getting the correct files from the single flat compile output directory. Now, we have exe projects that output various helper files, such as XSLT documents, in subdirectories. So we may end up with paths like this: MyProject\bin\Release\Transforms\ImportantTransform.xslt. But because these subdirectories get flattened by the default TFS build we loose our output directory structure.
This begs the question: why do we need to output everything in this big flat directory anyway? Why can’t we just have our CI build do the same as our Visual Studio build and simply output the build products into the <project name>\bin\Release folders? Then we can simply copy the compilation output to our build output directory.
There’s an easy way to do this with TFS introduced with 2008; simply change the property CustomizableOutDir to true and the TFS build will behave just like a Visual Studio build. Put the following in your TFSBuild.proj file somewhere near the top under the Project element:
<PropertyGroup> <CustomizableOutDir>true</CustomizableOutDir> </PropertyGroup>
Aaron Hallberg has a great blog post explaining exactly how this all works. Aaron’s blog is essential reading if you’re doing pretty much anything with TFS. You can still get the directory where TFS would have put the output from the new TeamBuildOutDir property.
Now the TFS build outputs into bin/Release in exactly the same way as a standard Visual Studio build and we can just grab the outputs for the projects we need and copy them to our build output directory. I do this by including a CI.exe.targets file near the end of the .csproj file of any project that I want to output:
<Import Project="..\..\Build\CI.build.targets\CI.exe.targets" />
My CI.exe.targets looks like this:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <PublishedApplicationOutputDir Condition=" '$(TeamBuildOutDir)'!='' ">$(TeamBuildOutDir)_PublishedApplications\$(MSBuildProjectName)</PublishedApplicationOutputDir> <PublishedApplicationOutputDir Condition=" '$(TeamBuildOutDir)'=='' ">$(MSBuildProjectDirectory)</PublishedApplicationOutputDir> </PropertyGroup> <PropertyGroup> <PrepareForRunDependsOn> $(PrepareForRunDependsOn); _CopyPublishedApplication; </PrepareForRunDependsOn> </PropertyGroup> <!-- ============================================================ _CopyPublishedApplication This target will copy the build outputs This Task is only necessary when $(TeamBuildOutDir) is not empty such as is the case with Team Build. ============================================================ --> <Target Name="_CopyPublishedApplication" Condition=" '$(TeamBuildOutDir)'!='' " > <!-- Log tasks --> <Message Text="Copying Published Application Project Files for $(MSBuildProjectName)" /> <Message Text="PublishedApplicationOutputDir is: $(PublishedApplicationOutputDir)" /> <!-- Create the _PublishedWebsites\app\bin folder --> <MakeDir Directories="$(PublishedApplicationOutputDir)" /> <!-- Copy compile output to publish directory --> <ItemGroup> <ApplicationBinContents Include="$(OutputPath)\**\*.*" /> </ItemGroup> <Copy SourceFiles="@(ApplicationBinContents)" DestinationFiles="$(PublishedApplicationOutputDir)\%(RecursiveDir)%(Filename)%(Extension)"></Copy> </Target> </Project>
First of all we define a new property PublishedApplicationOutputDir to hold the directory that we want our exe’s build output to be published to. If the TeamBuildOutDir is empty it means that this build has been triggered by Visual Studio, so we don’t really want to do anything. In the target _CopyPublishedApplication we create a list of everything in the build output directory called ApplicationBinContents, and copy it all to to PublishedApplicationOutputDir. Simple when you know how.