Building Non-MSBuild Projects With Team Build

Building non-MSBuild projects in Team Build has never been a particularly nice experience...  You can make it work, typically by invoking DevEnv.exe or some other tool using an Exec task, but you don't get any output in your build report, can't control the output sent to your build log, etc.  Additionally, it was recently pointed out to me (by D. Omar Villareal from Notion Solutions) that when a build type is set up that only builds non-MSBuild projects, the resulting builds don't end up in default Build Reports generated by TFS, since the relevant information doesn't get generated for the Warehouse!

To solve some of these issues, I've put together a custom MSBuild task for compiling non-MSBuild projects in Team Build.  The basic idea here is to execute an external executable (like the Exec task does), redirect stdout and stderr to the build log, and insert some minimal data into the Team Build database.  I had it inherit from the task base class I put together way back in August of last year.  As always, no guarantees are made about the awesomeness, or lack thereof, of this sample code.

 using System;
using System.Web.Services;
using Microsoft.Build.Framework;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Build.Proxy;
using System.Diagnostics;

namespace CustomTasks
{
    public class NonMsBuildProject : TeamBuildTask
    {
        #region Properties

        [Required]
        public String ToolPath
        {
            get
            {
                return m_toolPath;
            }
            set
            {
                m_toolPath = value;
            }
        }

        [Required]
        public String ProjectName
        {
            get
            {
                return m_projectName;
            }
            set
            {
                m_projectName = value;
            }
        }

        public String Platform
        {
            get
            {
                return m_platform;
            }
            set
            {
                m_platform = value;
            }
        }

        public String Flavor
        {
            get
            {
                return m_flavor;
            }
            set
            {
                m_flavor = value;
            }
        }

        public String Arguments
        {
            get
            {
                return m_arguments;
            }
            set
            {
                m_arguments = value;
            }
        }

        public String WorkingDirectory
        {
            get
            {
                return m_workingDirectory;
            }
            set
            {
                m_workingDirectory = value;
            }
        }

        [Output]
        public int ExitCode
        {
            get
            {
                return m_exitCode;
            }
        }

        #endregion

        #region TeamBuildTask Methods

        protected override bool ExecuteInternal()
        {
            // Create process to compile the non-MSBuild project.
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.WorkingDirectory = WorkingDirectory;
            psi.FileName = ToolPath;
            psi.Arguments = Arguments;
            psi.UseShellExecute = false;
            psi.CreateNoWindow = true;
            psi.RedirectStandardError = true;
            psi.RedirectStandardOutput = true;

            Process p = new Process();
            p.StartInfo = psi;

            p.OutputDataReceived += new DataReceivedEventHandler(p_OutputDataReceived);
            p.ErrorDataReceived += new DataReceivedEventHandler(p_ErrorDataReceived);
            p.EnableRaisingEvents = true;

            BuildEngine.LogMessageEvent(new BuildMessageEventArgs(GetBuildStepMessage(), null, "NonMsBuildProject", MessageImportance.Normal));

            if (!p.Start())
            {
                return false;
            }

            p.BeginOutputReadLine();
            p.BeginErrorReadLine();

            do
            {
                p.WaitForExit(100);
                p.Refresh();
            }
            while (!p.HasExited);

            m_exitCode = p.ExitCode;

            // Create project details object and associated with the build.
            ProjectData projectData = new ProjectData();
            projectData.CompileErrors = m_errors;
            projectData.ProjectFile = ProjectName;
            projectData.PlatformName = Platform;
            projectData.FlavourName = Flavor;

            BuildStore.AddProjectDetailsForBuild(BuildUri, projectData);

            return (p.ExitCode == 0);
        }

        protected override string GetBuildStepName()
        {
            return "NonMsBuildProject";
        }

        protected override string GetBuildStepMessage()
        {
            return String.Format("Building a non-MSBuild project. Command-line: '{0} {1}'.", ToolPath, Arguments);
        }

        #endregion

        #region Private Members

        private void p_ErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            // Write errors to log file and increment compilation error count.
            if (e.Data != null &&
                !String.IsNullOrEmpty(e.Data.Trim()))
            {
                BuildEngine.LogMessageEvent(new BuildMessageEventArgs("Error: " + e.Data, null, "NonMsBuildProject", MessageImportance.High));
                m_errors++;
            }
        }

        private void p_OutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            // Write stdout to log file.
            BuildEngine.LogMessageEvent(new BuildMessageEventArgs(e.Data, null, "NonMsBuildProject", MessageImportance.Normal));
        }

        private String m_toolPath;
        private String m_projectName;
        private String m_arguments;
        private String m_workingDirectory;
        private String m_flavor;
        private String m_platform;
        private int m_exitCode = 0;
        private int m_errors = 0;

        #endregion
    }
}

To use this task, do something like the following in your TfsBuild.proj file:

   <UsingTask AssemblyFile="CustomTasks.dll" TaskName="NonMsBuildProject" />
  
  <Target Name="AfterCompile">

    <NonMsBuildProject TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
                       BuildUri="$(BuildUri)"
                       ToolPath="$(TeamBuildRefPath)\..\devenv.com"
                       Arguments="$(SolutionRoot)\VS2003Solution\VS2003Solution.sln /Build Debug|x86"
                       WorkingDirectory="$(SolutionRoot)\VS2003Solution\"
                       ProjectName="VS2003Solution"
                       Platform="x86"
                       Flavor="Debug" />                       

  </Target>

In the above example, you would still have some work to do getting the generated binaries copied to the appropriate locations, etc.  Hopefully this will be enough to get some folks started, however!