owenG
home learn tableau about
divider
SDLC VS 2010 Coded UI Test Coded UI Test and Fitnesse MSBuild 4.0 MSBuild and IronPython MSBuild and IronPython - TFS checkins MSBuild and IronPython - Custom SQL Data








previous next

MSBuild 4.0 Custom Task Walkabout Part 4


Takin' it Inline

One of the new features of MSBuild 4.0 itself is the availability of Inline Tasks, where the custom task code is embedded in the actual project file. Only C#, Visual Basic, and JScript languages are supported out of the box, though the Task Factory model is extensible and the future may bring support for Python, Perl etc. And/or you could write your own - someone at Microsoft did in fact begin work on an unofficial Task Factory for Powershell. Once more scripting languages are supported the benefits of inline tasks will grow (e.g. write custom tasks more quickly) but right now the primary advantage of going inline is probably ease of deployment, where you don't need to worry about having a custom tasks .dll available in a given location on the build server. For a relatively complex task like ProduceHashManifest, getting the syntax correct in the project file will almost certainly outweigh the positive aspects of going inline but I wanted to see what would be involved anyways.

The trickiest part of setting up the task is working with a Code element of Type=Class, where most of the current examples illustrate Type=Fragment usage. The MSDN documentation on the Type attribute of the Code element states:

  • If the value of Type is Class, then the Code element contains code for a class that derives from the ITask interface.
  • If the value of Type is Method, then the code defines an override of the Execute method of the ITask interface.
  • If the value of Type is Fragment, then the code defines the contents of the Execute method, but not the signature or the return statement.

From that description I should still be able to use Fragment if I refactor the code to essentially put everything into Execute(), but I don't want to. The working syntax, which I've added to the AllInOneTargets.targets file alongside the existing ProducHashManifest task, looks like the below:

.targets
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <UsingTask TaskName="CTBinaries.ProduceHashManifest" AssemblyFile="..\_compiled\CTBinaries.dll" />
  <Target Name="CustomAfterBuild"  >
    <ProduceHashManifest BinariesFolder="$(OutputDirectory)" />
  </Target>

  <UsingTask TaskName="ProduceHashManifest_Inline"  TaskFactory="CodeTaskFactory" 
        AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
    <ParameterGroup>
      <BinariesFolder ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="$(MSBuildBinPath)\Microsoft.Build.Framework.dll"/>
      <Reference Include="$(MSBuildBinPath)\Microsoft.Build.Utilities.v4.0.dll"/>
      <Using Namespace="Microsoft.Build.Framework" />
      <Using Namespace="Microsoft.Build.Utilities" />
      <Using Namespace="System"/>
      <Using Namespace="System.IO"/>
      <Using Namespace="System.Security.Cryptography" />
      <Code Type="Class" Language="cs">

        <![CDATA[
    public class ProduceHashManifest_Inline : Microsoft.Build.Utilities.Task
    {
        private string _binariesFolder;

        [Microsoft.Build.Framework.Required]
        public string BinariesFolder
        {
            get
            {
                return _binariesFolder;
            }
            set
            {
                _binariesFolder = value;
            }
        }

        public override bool Execute()
        {
            string contents =  HashOfFilesInThisDirectory(_binariesFolder);
            WriteContentsToFile(contents);
            Log.LogMessageFromText(contents, Microsoft.Build.Framework.MessageImportance.High);

            return true;
        }
        
        private string HashOfFilesInThisDirectory(string parentFolder)
        {
            string manifest = "(INLINE) Contents of " + parentFolder + "\r\n";
            System.Security.Cryptography.MD5 myHash = 
                new System.Security.Cryptography.MD5CryptoServiceProvider();
            string[] fileList = System.IO.Directory.GetFiles(parentFolder);

            foreach (string thisFile in fileList)
            {
                manifest += thisFile + ",";
                using (System.IO.FileStream fs = 
                    new System.IO.FileStream(System.IO.Path.Combine(parentFolder, thisFile), 
                    System.IO.FileMode.Open, System.IO.FileAccess.Read))
                {
                    using (System.IO.BinaryReader br = new System.IO.BinaryReader(fs))
                    {
                        myHash.ComputeHash(br.ReadBytes((int)fs.Length));
                        manifest += System.Convert.ToBase64String(myHash.Hash) + "\r\n";
                    }
                }
            }
            return manifest;
        }
        
        private void WriteContentsToFile(string contents)
        {
            using (System.IO.StreamWriter sw = new System.IO.StreamWriter
                (System.IO.Path.Combine(_binariesFolder, "hashManifest.txt"), false))
            {
                sw.Write("now: " + System.DateTime.Now);
                sw.WriteLine();
                sw.Write(contents);
            }
        }
    }
]]>

      </Code>
    </Task>
  </UsingTask>

  <Target Name="CustomAfterBuild_Inline" >
    <ProduceHashManifest_Inline BinariesFolder="$(OutputDirectory)" />
  </Target>

</Project>

Some things to point out about above:

  • The main Project element now has a 'ToolsVersion="4.0"' attribute, which was necessary in my environment so that the $(MSBuildToolsPath) correctly resolved to the 4.0 Framework directly and not the 2.0 one.
  • The new task is named ProduceHashManifest_Inline so as not to conflict with existing 'classic' custom task, same "_Inline" suffix was put on the Target name. The existing OutputDirectory MSBuild property is also used in the new task, for setting the BinariesFolder .Net property.
  • While troubleshooting the errors via the command-line, the errors returned often referred me to a location in my local temp directory where a .txt file had been generated. The file contained the contents of the 'inline class' but most of the problems I ran into were related to references, not syntax within the actual class.
  • I added Using elements with Namespace attributes set to the relevant classes but they didn't seem to have any effect on the class contents. I needed to write out the full namespace of the various .Net methods anyways and could have removed those statements. It is possible the Using elements work differently when dealing with Fragment code types.
  • As a sanity check, the manifest will now include marker text, "(INLINE)", in the header. That way I can look at the hashManifest.txt and know for sure which version of ProduceHashManifest was involved.

I confirmed the new inline task worked when run on the local command line but to test in team build the process template needs to be updated a little bit. For aesthetic and troubleshooting purposes, I go back to 'MSBuild of AllInOneTargets' in the Workflow .xaml and update DisplayName to 'MSBuild_AllInOneTargets_Inline, followed by setting the Targets property to 'New String() {"CustomAfterBuild_Inline"}'.

MSBuild Activity Properties - Inline Task

Check the updated .xaml and .targets files into TFS, Queue a new build ...Build Passes, build passes. Confirm we have a new hashManifest.txt file in the Drop folder, and that it contains the "(INLINE)" marker text.


Flowing forward

I tried to be relatively forward looking in the preceding approaches to custom tasks but the 2010 team build process is now moderately soaked in the Windows Workflow platform and there is no reason to believe integration won't become even more tightly coupled in the future. Therefore it makes sense to look at a Workflow-centered solution to custom build activities. The first step toward that direction involves reading How to Create a Custom Workflow Activity for TFS Build 2010 RTM. In my opinion the second and third steps involve re-reading that artice a few more times.


THE END.


previous next