Team Build DevEnv Task

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:

  1. Add OrcasMSBuildTasks.dll to source control in the same directory as your TfsBuild.proj file.  If you go this route, add the following <UsingTask> declaration to your TfsBuild.proj file:

    <UsingTask TaskName="DevEnv" AssemblyFile="OrcasMSBuildTasks.dll" />

  2. Install OrcasMSBuildTasks.dll to a known location on each of your build machines - C:\Program Files\MSBuild\Microsoft\VisualStudio\TeamBuild, for example.  If you go this route, add the following <UsingTask> declaration to your TfsBuild.proj file(s):

    <UsingTask TaskName="DevEnv" AssemblyFile="C:\Program Files\MSBuild\Microsoft\VisualStudio\TeamBuild\OrcasMSBuildTasks.dll" />

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:

  • ToolName , which gives the name of the command line tool to be executed.  In our case this is DevEnv.com (not DevEnv.exe - see here for why).
  • GenerateFullPathToTool , which locates the appropriate command line executable (when the user hasn't explicitly pointed to it, using the ToolPath property).

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:

  • The standard Team Build build process will build the solution, using MSBuild.  Because the new CustomizableOutDir property is set to true (see here for more info on this property) the generated outputs will all end up in their standard locations - crucially, in the same locations they would be in if the build had been done using DevEnv.
  • Any non-MSBuild projects will generate warnings here (which look something like: "warning MSB478: The project file 'foo.vdproj' is not supported by MSBuild and cannot be built."), but these can be safely ignored.
  • The customized AfterCompileSolution will then be executed, and the same solution will be built with the same configuration and platform, but this time it will be built by DevEnv.  The outputs of all the projects that could be built by MSBuild will already exist, and will not be re-generated.  The various projects that could not be built by MSBuild, on the other hand, including our setup project, will be built by DevEnv and should have no trouble finding their dependencies.
  • The outputs generated by the entire build process will be in their default locations (since we set CustomizableOutDir to true) so we then need to copy them to the spot where Team Build expects to find them...  Luckily, another new feature in Team Build for Orcas provides the solution here - the outputs of the build are aggregated into the CompilationOutputs item group and can then be accessed later in the build process.  The SolutionOutputs item group is used to gather up all these build outputs, which are then copied to $(TeamBuildOutDir), which specifies the value Team Build would have used for $(OutDir) if CustomizableOutDir had not been true.

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.

DevEnvTask.zip

Comments

  • Anonymous
    July 13, 2007
    Aaron has written a great post on using Visual Studio (devenv) from within Team Build as part of the
  • Anonymous
    July 13, 2007
    Aaron has written a great post on using Visual Studio (devenv) from within Team Build as part of the
  • Anonymous
    July 16, 2007
    Aaron,That's all wonderful, but what about the current version almost everyone is using? I mean, it would be nice to use your task in a year or so (and I am talking about early adopters), but for most of us it is pretty worthless.What's the reason it cannot be done for VS2005?Regards,
  • Anonymous
    July 17, 2007
    I appreciate the comment, and acknowledge that not too many people are using VS 2008 as of yet...  Beta 2 is being released here shortly, however, and it will be a Go Live license (meaning it will be fully supported).  I would strongly encourage any heavy users of Team Build to upgrade as soon as they can - there are a ton of new features in this new version (see my post http://blogs.msdn.com/aaronhallberg/archive/2007/03/21/orcas-changes.aspx, for example).For those of you for whom this is not an option, most of the logic of the task would remain the same for VS 2005.  The only bits that would need to change would be the VS 2008 OM code - obviously this would need to use the VS 2005 web service proxy instead.  If I have some time, I'll try to slap this together in a subsequent post.  If anybody else out there has some time, I would love to just post a link instead!-Aaron
  • Anonymous
    December 03, 2007
    Martin Woodward has written a post on using Ant within Team Build 2008 and 2005. He includes an initial
  • Anonymous
    December 03, 2007
    Martin Woodward has written a post on using Ant within Team Build 2008 and 2005. He includes an initial
  • Anonymous
    March 03, 2008
    Hi,What does the 'setup1' in the code snippet represent?   <!-- 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)." />   </ItemGroupJulius Angwenyi
  • Anonymous
    March 04, 2008
    The comment has been removed
  • Anonymous
    March 19, 2008
    Update: With Teamprise 3.0 we included this work into the freely downloadable Teamprise Extensions for Team Build. The source is also provided under the MS-PL if you are interested. You should definately look at the new version as it contains...
  • Anonymous
    May 27, 2008
    Has this work been published in a tfs power tools package yet? That would be great, also found a problem with the regex in matching warnings and errors. This resulted projects to be reported as failing in the build log. We had files with the term 'error' which matched the regex in the devenv output warnings. Used something like 'errors+' to require a space after the term. Though cleaning the warnings in our solution would have also resolved this problem! Cheers!
  • Anonymous
    August 08, 2008
    I've mentioned the CompilationOutputs item group we added in TFS 2008 before in passing (see this post
  • Anonymous
    September 24, 2008
    I've got some low-level (in the dependency graph) Fortran code so I'm using DevEnv to build a whole solution.  I put it in the BeforeCompileConfigurarion target and then we build a handful of smaller solutions using the SolutionsToBuild itemgroup.I've hacked in some decent support for multithreaded builds but I'm having a lot of trouble understanding the details of the Team Build progress reporting object model.  The documentation I've seen for it seems to be limited to the signatures of the methods and a few scattered vague hints as to the intent.A specific problem: with DevEnv in the BeforeCompileConfiguration target and non-empty solutions the corecompile target the progress records end up in the wrong order.  The CoreCompile entried appear later in time but textually above the DevEnv entries.Any hope for better documentation on the API?  Any idea how to fix the sorting problem?-swn
  • Anonymous
    September 28, 2008
    We’ve been talking with the BizTalk Server team about supporting MSBuild (and, transitively, Team Build)
  • Anonymous
    February 10, 2009
    The comment has been removed
  • Anonymous
    February 23, 2009
    The comment has been removed
  • Anonymous
    February 24, 2009
    Hi AaaronI am using ur task , really its goodcan you help me to make msi packagesin this task the exe file is generatingi want to make msi packageThanks in advance
  • Anonymous
    March 23, 2009
    The comment has been removed
  • Anonymous
    May 04, 2009
    We’ve been talking with the BizTalk Server team about supporting MSBuild (and, transitively, Team Build)
  • Anonymous
    June 09, 2009
    Hi Aaron,I have been using your DevEnv task and also the version that is maintained by the MS Build Extension Pack (http://msbuildextensionpack.codeplex.com/). Your blog discusses the way the task should be setup so that it doesn't cause a rebuild of the whole solution. I have followed your example closely yet I am still finding my entire solution is rebuilt. Are you sure your example doesn't also do this?Thanks,Neil
  • Anonymous
    July 13, 2009
    I have the same problem as Neil.I am using the codeplex version, and it certainly looks to me like the DevEnv task is rebuilding each project required for the Setup Project, even though MSBuild has already built it. Any suggestions?
  • Anonymous
    May 26, 2010
    Nice going!Is wonderful!
  • Anonymous
    March 01, 2012
    The comment has been removed
  • Anonymous
    April 25, 2012
    The comment has been removed