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!