Preserving Output Directory Structures in Orcas Team Build

A common complaint with Team Build v1 was that it ignored the output paths specified for individual projects and just dumped all binaries and other compilation outputs into a flat directory structure...  In previous posts (e.g. this one) I have discussed various methods for getting around this problem in v1.  In Orcas we've tried to fix this problem altogether...

Before jumping into what we've done in Orcas to fix the issue, it probably makes sense to delve into the specifics a bit first. 

OutputPath vs OutDir

When you set the Output Path for a project (e.g. a C# project) in Visual Studio, you are actually setting the value of an MSBuild property called OutputPath.  If you open up a C# project in notepad, you will se something like this:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
</PropertyGroup>

The OutputPath property specifies the final location of the compilation outputs for the project - dlls, pdbs, exes, etc. all end up here.  There is a bit more to the story, however - OutputPath is actually used in Microsoft.Common.targets to initialize another property, OutDir.  From Microsoft.Common.targets:

<!--
OutDir:
Indicates the final output location for the project or solution. When building a solution,
OutDir can be used to gather multiple project outputs in one location. In addition,
OutDir is included in AssemblySearchPaths used for resolving references.

OutputPath:
This property is usually specified in the project file and is used to initialize OutDir.
OutDir and OutputPath are distinguished for legacy reasons, and OutDir should be used if at all possible.

-->

<PropertyGroup>
    <OutDir Condition=" '$(OutDir)' == '' ">$(OutputPath)</OutDir>
</PropertyGroup>

Team Build uses OutDir to, as Microsoft.Common.targets put it, gather multiple project outputs in one location - namely the binaries directory and then the drop location. 

MSBuild and Global Properties

This doesn't seem so bad, you're thinking - I can just override OutDir in my project files!  That is, you might try modifying the above chunk of your C# project as follows:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <OutDir>$(OutputPath)</OutDir>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
</PropertyGroup>

The trouble here, however, is that Team Build builds solutions by invoking the MSBuild task on them.  To set the OutDir property for these solutions, then, it has to use the Properties property of the MSBuild task, at which point it becomes a global property.  Global properties cannot be overridden declaratively - see this blog post for some more specifics on this topic - so this will not have any effect.  You could override the value Team Build specifies for OutDir programmatically using the CreateProperty task, but this is rather painful to have to do in all your project files for each individual configuration.

The Solution

So - in Orcas we added two new properties to help you get around all of these issues. 

CustomizableOutDir.  This property defaults to false (to preserve the Team Build v1 behavior), but if you set it to true Team Build will not pass a value for OutDir into the MSBuild task when it compiles your solutions.  At this point, your project-specific OutputPath properties should start working as you expect them to. 

TeamBuildOutDir.  This property stores the path that Team Build would have used for OutDir had CustomizableOutDir been false. 

Between these two properties, you should be in good shape.  If your build process already copies outputs to wherever they are needed (using post build events, for example) all you should have to do is set CustomizableOutDir to true.  If you still want Team Build to copy your binaries to the drop location for you, run unit tests, etc. you can use TeamBuildOutDir either directly in your OutputPath property values or in a post build step to copy your binaries. 

For example, if you have two projects - foo and bar - whose binaries should end up in the following directory structure:

$(TeamBuildOutDir)
    -> foo
        -> bin
            -> debug
                -> foo.dll, foo.pdb
    -> bar
        -> bin
            -> debug
                -> bar.dll, bar.pdb

Their OutputPath properties probably both started as "bin\debug".  To have them compiled directly into the TeamBuildOutDir directory while preserving this structure, just do something like the following in foo.csproj (and similarly in bar.csproj):

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<OutputPath Condition=" '$(TeamBuildOutDir)'=='' ">bin\debug\</OutputPath>
    <OutputPath Condition=" '$(TeamBuildOUtDir)'!='' ">$(TeamBuildOutDir)foo\bin\debug</OutputPath>
</PropertyGroup>

Alternatively, if you want your binaries to end up in just bin\debug to start with you could then copy them to $(TeamBuildOutDir) by overriding the AfterCompile target in foo.csproj and bar.csproj as follows:

<Target Name="AfterCompile">
    <ItemGroup>
        <CompileOutputs Include="$(OutDir)\**\*" />
    </ItemGroup>
    <Copy SourceFiles="@(CompileOutputs)" DestinationFolder="$(TeamBuildOutDir)foo\%(RecursiveDir)" />
</Target>

One difference between these two approaches for those of you who use post build events in your projects - these are really just command-lines that get passed to an Exec task as follows:

<Exec WorkingDirectory="$(OutDir)" Command="$(PostBuildEvent)" />

Note the WorkingDirectory - in the first approach, the working directory for the command-line would end up being under $(TeamBuildOutDir), while in the latter approach it would just be under bin\debug (i.e. in your source tree).

Please try out the Orcas Betas as they come up (Beta1 is already available, and Beta2 is following shortly) and let us know what you think!

Comments

  • Anonymous
    June 11, 2007
    Aaron Hallberg on Preserving Output Directory Structures in Orcas Team Build. Juan J. Perez on Thanks...

  • Anonymous
    July 09, 2007
    Various issues arise when trying to use Team Build with Web Deployment Projects (which are a Visual Studio

  • Anonymous
    July 12, 2007
    Because many Visual Studio project types are not supported in MSBuild, many Team Build users end up needing

  • Anonymous
    February 17, 2008
    The comment has been removed

  • Anonymous
    February 23, 2008
    I have been meaning to blog this for a long time and not too long ago the question came up again. So

  • Anonymous
    April 28, 2008
    In an earlier post I described how one can, in Orcas, preserve the output directory structure used in

  • Anonymous
    June 10, 2008
    Aaron:I've read your excellent post regarding Preserving Output Directory Structures in Orcas Team Build, and I have implemented the use of CustomizableOutDir into my TFSBuild.proj. I've also been able to successfully modify my .csproj files to take advantage of the TeamBuildOutDir property.All is well. Project assemblies are being placed into project specified customizable directories during the build. However, I have one more question for you regarding this approach.We use a Master Solution for our Team Project (one Build Solution for the entire Team Project). Therefore, we have many projects in our Master Solution. By using the CustomizableOutDir property it seems as if I'm being forced to have developers update every .csproj in order to have their assemblies as part of the build output. If I leave a .csproj file untouched then I don't see its output.I only need to customize the output for a small number of my projects in my Solution. I'd like the majority of my .csproj files to go untouched - to keep things simple for my development team (i.e. every time they create a new project they would be forced to hand edit the .csproj file).Is there a way to customize only some of the projects within one solution while still generating output for un-customized projects within the same solution?

  • Anonymous
    June 11, 2008
    I cannot think of anything to help here...  You could, of course, split out those few projects that do need to customize their output directory into a separate solution and/or a custom configuration within the solution (see my blog post on solution configurations - the idea here would be to create one custom configuration for all the standard projects and another for all the customized projects).  This would enable you to explicitly set OutDir yourself in the BeforeCompileSolution or BeforeCompileConfiguration target for the standard projects.Beyond that, you could try playing around with setting OutDir to the empty string in your customized csproj files in a really early target (you could even explicitly add an InitialTargets element to the project node to make sure your initialization gets called before anything else).  I don't think this would work, however, since setting OutDir does most of its damage during the declarative phase of property evaluation (see my post on msbuild property evaluation).A final possibility to explore might be setting CustomizableOutDir to true for everything and then trying to add some standard logic to AfterCompileSolution to copy outputs to the standard location.Best of luck!

  • Anonymous
    June 24, 2008
    Aaron:Thanks for the feedback. I like your AfterCompileSolution idea (with CustomizableOutDir=true). Two more followup questions:Would this approach allow ALL of the unit tests to run and be reported (this is the main reason why I care about the other standard projects)?Where would I be copying the outputs from? I know I want them to end up in the normal TeamBuildOutDir. --Tony

  • Anonymous
    June 30, 2008
    The comment has been removed

  • Anonymous
    August 07, 2008
    Sorry for the massively delayed response - this fell off my radar for a while.  The whole point of the CustomizableOutDir property is to cause Team Build to behave like a development build.  That is, when you set this property to true build outputs will go exactly where your individual projects tell them to.  The trouble is that this typically causes various other problems:(1) If the build process doesn't "know" where the outputs of individual projects have gone, it cannot copy them appropriately to the drop location.(2) Again, if the location of individual outputs is unknown, the build process cannot point mstest to the appropriate location such that it can find the unit tests it is supposed to run.You could focus on solving these problems rather than on getting all your project outputs into the standard output location (under TeamBuildOutDir).  Solving these problems is going to be quite difficult, however, if individual projects are allowed to place their outputs anywhere.  And, of course, once you standardize on putting project outputs into some common location you are right back at your first issue - each developer has to conform to the standard.  Alternatively, you could focus on enforcing the standard such that individual developers know immediately if they are not conforming to it.  Perhaps a common targets file that everyone is required to import which checks for appropriate values for OutDir/OutputPath?  -Aaron

  • Anonymous
    June 19, 2009
    Aaron,Thank you very much for lots of useful information about Team Build/MSBuild.  I'm in the process of configuring Team Build for several of our major applications and I was able to get most of it set up as I like thanks to almost exclusively to your blog.Igor

  • Anonymous
    December 06, 2010
    This is the worst poem I've ever read!(Seriously, the content of the site is aligned to the center.)

  • Anonymous
    July 17, 2013
    Can you please put a big note at the top of this to say it no longer works in TFS 2010.  I just spent ages doing and testing this (without success) before finding that it no longer works!