Because many Visual Studio project types are not supported in MSBuild, many Team Build users end up needing to invoke DevEnv directly. There is a fair amount of confusion about how to do this best / simplest - I've written two posts (here and here) on the issue already!
As such, I thought it would be helpful to write a DevEnv task that could be used to invoke DevEnv from MSBuild during the course of a Team Build build, along with some guidance on how to use it best. I'll go through how to use the attached DLL for those who just want to use the task, go into a few chunks of the source code for those interested in how the task works, and then go through an example of how to use the task to build a setup project (*.vdproj). A note for the lawyers: I can make no guarantees as to the awesomeness of this task or lack thereof - it is provided "as is" with no warranties and confers no rights.
How To Use It
Note that this task uses the Team Build Orcas (Visual Studio 2008) Object Model, and as such will not work with Team Build v1 (Visual Studio 2005). Beta 1 can be downloaded here, and Beta 2 will be available in fairly short order. To use the task, download the attached zip file and extract OrcasMSBuildTasks.dll. You'll then need to either:
You should then be able to use the DevEnv task in your Team Build build scripts (TfsBuild.proj files) just as you would any other task. Here is the recommended approach for incorporating the task into your build process - see the Building a Setup Project section below for a specific example and a discussion of the aproach:
<PropertyGroup> <CustomizableOutDir>true</CustomizableOutDir> </PropertyGroup> <Target Name="AfterCompileSolution"> <DevEnv TeamFoundationServerUrl="$(TeamFoundationServerUrl)" BuildUri="$(BuildUri)" Solution="$(Solution)" SolutionConfiguration="$(Configuration)" SolutionPlatform="$(Platform)" Target="Build" Version="9" /> <ItemGroup> <SolutionOutputs Condition=" '%(CompilationOutputs.Solution)' == '$(Solution)' " Include="%(RootDir)%(Directory)**\*.*" /> <!-- Add any additional outputs which need to be copied here. --> </ItemGroup> <Copy SourceFiles="@(SolutionOutputs)" DestinationFolder="$(TeamBuildOutDir)" /> </Target>
If you want to modify the task, the source code is also available in the attachment. Just open up the solution in Visual Studio, adjust the assembly references for Microsoft.TeamFoundation.Build.Client and Microsoft.TeamFoundation.Client if necessary, and you should be all set.
The DevEnv Task Details
Rather than implement this thing from scratch (as in my previous post), I decided to leverage the work the MSBuild team did in creating the ToolTask abstract class, which "...provides default implementations for the methods and properties of a task that wraps a command line tool". Various MSBuild tasks are derived from ToolTask, including AL, CSC, etc.
public class DevEnv : ToolTask
The important property/method overrides for classes that derive from ToolTask are:
The DevEnv task uses the registry key value for HKLM\Software\Microsoft\VisualStudio\<version>.0, where version is a property that can be set for the task. This way, you can execute VS 2005 if needed (version 8.0), VS 2008 if needed (version 9.0 - this is the default), etc. Here's the full method:/// <summary> /// Determines the full path to DevEnv.com, if this path has not been explicitly specified by the user. /// </summary> /// <returns>The full path to DevEnv.com, or just "DevEnv.com" if it's not found.</returns> protected override string GenerateFullPathToTool() { string regKey = string.Format(CultureInfo.InvariantCulture, m_vsInstallRegKeyTemplate, m_version); string path = string.Empty; using (RegistryKey key = Registry.LocalMachine.OpenSubKey(regKey)) { if (key != null) { path = key.GetValue("InstallDir") as String; } } if (string.IsNullOrEmpty(path)) { return ToolExe; } else { return Path.Combine(path, ToolExe); } }
The DevEnv task uses the registry key value for HKLM\Software\Microsoft\VisualStudio\<version>.0, where version is a property that can be set for the task. This way, you can execute VS 2005 if needed (version 8.0), VS 2008 if needed (version 9.0 - this is the default), etc. Here's the full method:
/// <summary> /// Determines the full path to DevEnv.com, if this path has not been explicitly specified by the user. /// </summary> /// <returns>The full path to DevEnv.com, or just "DevEnv.com" if it's not found.</returns> protected override string GenerateFullPathToTool() { string regKey = string.Format(CultureInfo.InvariantCulture, m_vsInstallRegKeyTemplate, m_version); string path = string.Empty; using (RegistryKey key = Registry.LocalMachine.OpenSubKey(regKey)) { if (key != null) { path = key.GetValue("InstallDir") as String; } } if (string.IsNullOrEmpty(path)) { return ToolExe; } else { return Path.Combine(path, ToolExe); } }
These few overrides would be enough to get the task up and running. To make it a bit more useful for Team Build users, I've added additional logic in an override of the LogEventsFromTextOutput method that detects the projects being built, errors and warnings encountered, etc. and adds the appropriate corresponding information to the Team Build database. Here's the full method:
/// <summary> /// Log standard error and standard out. Overridden to add build steps for important messages and to detect errors and warnings. /// </summary> /// <param name="singleLine">A single line of stderr or stdout.</param> /// <param name="messageImportance">The importance of the message. Controllable via the StandardErrorImportance and StandardOutImportance properties.</param> protected override void LogEventsFromTextOutput(String singleLine, MessageImportance messageImportance) { Match match; // Add build steps for important messages. if (messageImportance == MessageImportance.High) { BuildStep.Add("DevEnv Message", singleLine, DateTime.Now, BuildStepStatus.Succeeded); } // Detect project compilation and insert a build step and compilation summary. match = ProjectCompilationRegex.Match(singleLine); if (match.Success) { // Update the existing project build step, if we have one. UpdateProjectBuildStep(); CompilationSummary = ConfigurationSummary.AddCompilationSummary(); CompilationSummary.ProjectFile = match.Groups["Project"].Value; ProjectBuildStep = BuildStep.Add(CompilationSummary.ProjectFile, "DevEnv is building project " + CompilationSummary.ProjectFile, DateTime.Now); } // Detect static analysis errors and warnings and update the compilation summaries. else if (StaticAnalysisErrorRegex.IsMatch(singleLine)) { if (CompilationSummary != null) { CompilationSummary.StaticAnalysisErrors++; } m_errorEncountered = true; } else if (StaticAnalysisWarningRegex.IsMatch(singleLine)) { if (CompilationSummary != null) { CompilationSummary.StaticAnalysisWarnings++; } } // Detect errors and warnings and update the compilation summaries. else if (ErrorRegex.IsMatch(singleLine)) { if (CompilationSummary != null) { CompilationSummary.CompilationErrors++; } m_errorEncountered = true; } else if (WarningRegex.IsMatch(singleLine)) { if (CompilationSummary != null) { CompilationSummary.CompilationWarnings++; } } // Call the ToolTask implementation to make sure events get logged to the attached MSBuild loggers. base.LogEventsFromTextOutput(singleLine, messageImportance); }
Note the general approach here, which is to use regular expressions to match specific events in stdout and stderr and then take action accordingly. Obviously this approach is not nearly as powerful as the rich eventing MSBuild provides to attached loggers, but it's the best we can do when executing a command line tool like DevEnv! Here are the strings for the various regular expressions:
private const String m_projectCompilation = @"Build started: Project: (?<Project>[^,]+), Configuration:"; private const String m_caWarning = @"warning\s*:?\s*(?<Code>CA[^\s:]+)\s*:\s*(?<Text>.*)$"; private const String m_caError = @"error\s*:?\s*(?<Code>CA[^\s:]+)\s*:\s*(?<Text>.*)$"; private const String m_warning = @"warning\s*:?\s*(?<Code>[^\s:]+)\s*:\s*(?<Text>.*)$"; private const String m_error = @"error\s*:?\s*(?<Code>[^\s:]+)\s*:\s*(?<Text>.*)$";
Again, the full source is available in the attachment.
Building a Setup Project
One of the most common questions we get for Team Build is how to build a setup project. Blog posts can be found on the topic here and here. An MSDN walkthrough can be found here. I get 122 hits on our forums when I do a search for "setup project".
In trying to use my fancy new task to build a setup project, I first tried to copy the MSDN walkthrough and did something like this:
<UsingTask TaskName="DevEnv" AssemblyFile="OrcasMSBuildTasks.dll" /> <Target Name="AfterCompile"> <DevEnv TeamFoundationServerUrl="$(TeamFoundationServerUrl)" BuildUri="$(BuildUri)" Project="$(SolutionRoot)\Setup1\Setup1.vdproj" SolutionConfiguration="Debug" SolutionPlatform="Any CPU" Target="Build" Version="8" /> <Copy SourceFiles="$(SolutionRoot)\Setup1\Debug\Setup1.msi" DestinationFolder="$(OutDir)" /> <Copy SourceFiles="$(SolutionRoot)\Setup1\Debug\Setup.exe" DestinationFolder="$(OutDir)" /> </Target>
Unfortunately, I discovered that this did not do exactly what I expected... My setup project had been defined to copy the project outputs of another project in its solution to a particular spot on the target file system - I imagine this is a fairly common scenario in other setup projects out there! Well, the setup project in this case didn't find the project outputs where it expected them to be (since Team Build by default redirects them to its Binaries directory), and as such rebuilt the entire solution! So - not only did my build process waste time in compiling the solution twice, but it also built a setup from completely different binaries than the ones copied out to the drop location... This is obviously not ideal.
Luckily, due to some new features in Orcas there is a pretty decent workaround here. Here is my recommended approach for building setup projects (and any other projects not supported by MSBuild):
<PropertyGroup> <!-- Tell Team Build not to override $(OutDir), so that we can build once from MSBuild and not rebuild when DevEnv.com is executed. --> <CustomizableOutDir>true</CustomizableOutDir> </PropertyGroup> <Target Name="AfterCompileSolution"> <!-- Use the DevEnv task to build our setup project. --> <DevEnv TeamFoundationServerUrl="$(TeamFoundationServerUrl)" BuildUri="$(BuildUri)" Solution="$(Solution)" SolutionConfiguration="$(Configuration)" SolutionPlatform="$(Platform)" Target="Build" Version="9" /> <!-- Copy all compilation outputs for the solution AND the setup project to the Team Build out dir so that they are copied to the drop location, can be found by unit tests, etc. --> <ItemGroup> <SolutionOutputs Condition=" '%(CompilationOutputs.Solution)' == '$(Solution)' " Include="%(RootDir)%(Directory)**\*.*" /> <SolutionOutputs Include="$(SolutionRoot)\Setup1\$(Configuration)\**\*.*" /> </ItemGroup> <Copy SourceFiles="@(SolutionOutputs)" DestinationFolder="$(TeamBuildOutDir)" /> </Target>
This approach works quite nicely, and is extensible to any project type that cannot be built by MSBuild. Here's how it works:
Potential Extensions
I didn't add any logic to write errors and warnings to the various log files generated by Team Build - ErrorsWarningsLog.txt, which is linked to from created work items; Debug.txt (generated for Debug / Any CPU builds) and the other configuration specific files, which are linked to in the build report.
I didn't bother making the task usable outside of Team Build. It might be nice to create (a) a version which can be run from MSBuild in any environment, or (b) a version that interacts with Team Build when possible but doesn't require that interaction.
Please let me know what you think, post any issues you run into, etc.