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:
- Create a new class library project (in my case in C#, but VB or other languages will do as well).
- Add any references you might need (in my case I needed references to Tamir.sharpSsh which contains the SCP implementation I want to use).
- Add references to Microsoft.Build.Framework and Microsoft.Build.Utilities.
- 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.
- Add properties to the class which will be used as parameters to the task.
- 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:
- 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"/>
- 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