Thursday, April 13, 2006 12:06 AM bart

Custom MSBuild tasks

Introduction

I guess most of my blog readers are familiar with the new build system in .NET 2.0 and Visual Studio 2005. In the past, project files were only consumable by Visual Studio .NET. Today, project files (which are XML-based) are VS-independent and can be used to build the project using MSBuild at the command line. To do this, you only need to have the .NET Framework 2.0 SDK on your machine. A typical MSBuild session looks as follows:

C:\temp\ReflectionDemo\ReflectionDemo>dir
 Volume in drive C is Windows XP Professional MCE
 Volume Serial Number is 3003-9177

 Directory of C:\temp\ReflectionDemo\ReflectionDemo

04/11/2006  09:03 PM    <DIR>          .
04/11/2006  09:03 PM    <DIR>          ..
04/11/2006  09:01 PM    <DIR>          bin
04/11/2006  08:55 PM    <DIR>          obj
04/11/2006  09:02 PM               600 Program.cs
04/11/2006  08:52 PM    <DIR>          Properties
04/11/2006  09:01 PM             2,112 ReflectionDemo.csproj
04/11/2006  09:03 PM               168 ReflectionDemo.csproj.user
04/11/2006  08:53 PM               257 ReflectionDemo.csproj.vspscc
               4 File(s)          3,137 bytes
               5 Dir(s)  28,914,020,352 bytes free

C:\temp\ReflectionDemo\ReflectionDemo>msbuild
Microsoft (R) Build Engine Version 2.0.50727.42
[Microsoft .NET Framework, Version 2.0.50727.42]
Copyright (C) Microsoft Corporation 2005. All rights reserved.

Build started 4/13/2006 12:06:58 AM.
__________________________________________________
Project "C:\temp\ReflectionDemo\ReflectionDemo\ReflectionDemo.csproj" (default targets):

Target CoreCompile:
  Skipping target "CoreCompile" because all output files are up-to-date with respect to the input files.
Target CopyFilesToOutputDirectory:
    ReflectionDemo -> C:\temp\ReflectionDemo\ReflectionDemo\bin\Debug\Reflection
Demo.exe

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.06

Notice that the output is colored in reality, indicating the success or failure of something (among other status indicator colors and highlighting colors), leveraging the richness of the new System.Console class in .NET 2.0.

Now, let's take a look at the project file for this:

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProductVersion>8.0.50727</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{0C53AE2F-B541-4A05-9B75-A4A0CB0B0156}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>ReflectionDemo</RootNamespace>
    <AssemblyName>ReflectionDemo</AssemblyName>
    <SccProjectName>SAK</SccProjectName>
    <SccLocalPath>SAK</SccLocalPath>
    <SccAuxPath>SAK</SccAuxPath>
    <SccProvider>SAK</SccProvider>
  </PropertyGroup>
  <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>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

When executing MSBuild, the system looks for a project file (if not specified) and uses that to perform the build. The way it works is by crawling through a tree of tasks that have to be performed, based on the specified target (or the default one). The default targets can be found in %windir%\Microsoft.NET\Framework\v2.0.50727 and are XML-files with the extension .targets. An example is Microsoft.Common.targets, a 3757 lines file with target definitions. Check it out to get a good idea of what targets are and how those work.

 

Building custom tasks

Last week I had to write a custom task to run as part of daily builds in Team Foundation Server. TFS also uses MSBuild files to drive automatic builds, so my job came down to writing a new MSBuild task to perform the work. In my case, I needed to create a SCP copy task to copy binaries from the build server to a *nix based machine running Mono (actually I'm not promoting Mono at all; this was just the project case I was involved in and I needed to show the power and richness of MSBuild - in this case by writing a task to perform a file copy over SSH/SCP).

Luckily I found an SSH/SCP client implementation for C# on Code Project, called SharpSSH. Download it and build it if you're interested in this.

Now, how to create a custom MSBuild task. An overview:

  1. Create a new class library project (in my case in C#, but VB or other languages will do as well).
  2. Add any references you might need (in my case I needed references to Tamir.sharpSsh which contains the SCP implementation I want to use).
  3. Add references to Microsoft.Build.Framework and Microsoft.Build.Utilities.
  4. Implement Microsoft.Build.Framework.ITask either by implementing the interface directly or by inheriting from Microsoft.Build.Utilities.Task. Sample code can be found below.
  5. Add properties to the class which will be used as parameters to the task.
  6. Write the core code in the method Execute().

Sample code (for SCP file copy):

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class ScpUpload : Task
{
    private string user;

    [Required]
    public string User
    {
        get { return user; }
        set { user = value; }
    }

    private string password;

    [Required]
    public string Password
    {
        get { return password; }
        set { password = value; }
    }

    private string host;

    [Required]
    public string Host
    {
        get { return host; }
        set { host = value; }
    }

    private ITaskItem[] sourceFiles;

    [Required]
    public ITaskItem[] SourceFiles
    {
        get { return sourceFiles;}
        set { sourceFiles = value;}
    }
 
    private string destinationFolder;

    [Required]
    public string DestinationFolder
    {
        get { return destinationFolder;}
        set { destinationFolder = value;}
    }

    public override bool Execute()
    {
        Tamir.SharpSsh.Scp scp = new Tamir.SharpSsh.Scp();

        foreach (ITaskItem item in sourceFiles)
        {
            if (item.ItemSpec.Length > 0)
            {
                string source = item.ItemSpec;
                Log.LogMessage(item.ItemSpec);
                string target = destinationFolder + "/" + System.IO.Path.GetFileName(item.ItemSpec);

                try
                {
                    scp.To(source, host, target, user, password);
                }
                catch (Exception ex)
                {
                    Log.LogError(ex.Message);
                }
            }
        }

        return !Log.HasLoggedErrors;
    }
}

Actually I've stripped down my code because I have slightly more complex - but for this post irrelevant - code. Notice that required parameters are marked with the attribute Required. Now compile the code which will result in a .dll assembly file.

 

Using custom tasks

Now, how to use our new task? Go to a csproj or vbproj or ... project file and modify it as follows:

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <UsingTask TaskName="ScpUpload" AssemblyFile="C:\temp\DemoTask\DemoTask\bin\Debug\scpupload.dll"/>

  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProductVersion>8.0.50727</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{0C53AE2F-B541-4A05-9B75-A4A0CB0B0156}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>ReflectionDemo</RootNamespace>
    <AssemblyName>ReflectionDemo</AssemblyName>
    <SccProjectName>SAK</SccProjectName>
    <SccLocalPath>SAK</SccLocalPath>
    <SccAuxPath>SAK</SccAuxPath>
    <SccProvider>SAK</SccProvider>
  </PropertyGroup>
  <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>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  -->
  <Target Name="AfterBuild">
<ScpUpload Host="somemachine" User="someuser" Password="somepwd" DestinationFolder="www" SourceFiles="bin\Debug\ReflectionDemo.exe" />

  </Target>
</Project>

You have to do two things:

  1. Add a <UsingTask> reference to the project file (you can do this in .targets files as well. The tag has two parameters: a TaskName and an AssemblyFile or AssemblyName attribute. The former one defines a friendly name (in this case ScpUpload), the latter one points to the assembly (if you're using AssemblyFile, specify a path; if you're using AssemblyName, use an assembly name, e.g. to point to a strongly-named assembly in the GAC).

    <UsingTask TaskName="ScpUpload" AssemblyFile="C:\temp\DemoTask\DemoTask\bin\Debug\scpupload.dll"/>
  2. Use the task somewhere in a target. In this case, I'm just using AfterBuild and specifying the to-be-copied file manually (you could use build variables as well using $(...) syntax):

    <ScpUpload Host="somemachine" User="someuser" Password="somepwd" DestinationFolder="www" SourceFiles="bin\Debug\ReflectionDemo.exe" />

    In here, you have to specify every [Required] parameter.

Now, when executing MSBuild on the project, the output will be copied to the server over SCP. Just a little demo to show how you can extend MSBuild with your own custom tasks. I'm not putting the sources online because it relies on 3rd party components (see SharpSSH) and because it hasn't been tested thoroughly. However, using the code mentioned in this post, you can create a similar MSBuild task yourself. Enjoy!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under: ,

Comments

# re: Custom MSBuild tasks

Saturday, April 15, 2006 7:41 PM by Jonathan de Halleux

Hi Bart,

To retreive the file name, path or other properties in ITaskItem, you can use the 'well known item metadata' ( http://msdn2.microsoft.com/en-us/library/ms164313(VS.80).aspx ).

Cool blog btw

# re: Custom MSBuild tasks

Wednesday, April 26, 2006 5:39 PM by Peter Mounce

I'm trying to write a task that asks the user a question, then returns true or false depending on his answer - I want to put this in the BeforeBuild target of my Web Deployment Projects so that I can put a confirmation on running the build when the Release configuration is selected.

My Task class is as follows:

using System;
using System.Windows.Forms;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace MSBuildConfirmDialog
{
public class UserConfirm : Task
{
private string question;
private string caption;

[Required]
public string Question
{
get { return question; }
set { question = value; }
}

public string Caption
{
get { return caption; }
set { caption = value; }
}

public UserConfirm(string question) : this(question, String.Empty) {}

public UserConfirm(string question, string caption) : base()
{
this.question = question;
this.caption = caption;
}

public override bool Execute()
{
try
{
return MessageBox.Show(question, caption, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes;
}
catch
{
return false;
}
}
}
}

And then I've put in a UsingTask element into my WDP, and put the following into my BeforeBuild target:

<UserConfirm Condition=" $(Configuration == 'Debug' " Question="Are you sure you want to deploy in the $(Configuration) Configuration?" />

(I'm testing it in Debug mode at the moment).

I don't see the expected MessageBox.  I want the build to be stopped if the user clicks "No" and makes the task return false.

Am I going about this in the right way?

# The custom MSBuild task cookbook

Friday, February 15, 2008 11:03 PM by B# .NET Blog

A few years ago I wrote about building custom MSBuild tasks . I wanted to bring the topic back in the

# Update Version MSBuild Task

Saturday, August 13, 2011 2:24 AM by Null Reference Exception

Update Version MSBuild Task