Home > Uncategorized > CreateFoldersItem Custom Task for MSBuild

CreateFoldersItem Custom Task for MSBuild

Last year I did a fair amount of MSBuild stuff, and really enjoyed it. And when I recently wrote an MSBuild task to clean up TFS source bindings in Visual Studio solutions and projects, it rekindled my interest and reminded me how much I like MSBuild.

That’s why today I decided to write another task to add to my library. I wanted to be able to get an item group containing folders, in particular the last name token each directory. For example, for the path “C:\Program Files\BitLocker\en-US\” I wanted to get the “en-US”.

I did a quick look to see if there was a way to do it with the existing ‘CreateItem’ task, or if anyone else had tried to do this and had a solution. But to be honest I didn’t look TOO hard because I really wanted to tackle this problem myself!

Enter: Snagy.Tasks.CreateFoldersItem

I’m actually writing this post while I work on the task. Think of it as a step by step account, and I’ll be writing in a present tense, so forgive me if it sounds daft. But if you’ve never created a custom task for MSBuild before, I’ll cover off all the basics.

First, I created a class library project in C# (actually I already had one from the last task I wrote, so I am just adding to that). Up front you’ll want to add some assemblies to your references list:

Microsoft.Build.Utilities
Microsoft.Build.Framework

Next, add a new class, call it ‘CreateFoldersItem’. At the top, add using statements for the 2 above namespaces, and add a using statement for System.IO while you are at it. Now, make your class inherit from ‘Task’ which is in the Microsoft.Build.Utilities namespace. You can also choose to implement ITask instead, but then you need to provide a little more information in your class. Its easier just to inherit from Task instead. One final thing you need to do to make it build is override the ‘Execute’ method. Do this now but don’t provide any implementation yet other than to ‘return true’. Build your task to make sure its all hunky dory.

Next we want to provide attributes on our task that people can use to set information to be used by the task. I was thinking of a syntax something like this:

<CreateFoldersItem Include=”C:\Projects;C:\Program Files”                    
                              Exclude=”C:\Projects\MSBuild\;C:\Projects\Lisp”
                              Recursive=”true”>
     <Output ItemName=”AllFolders” TaskParameter=”Include” />
</CreateFoldersItem>

This would take semi-colon delimited folders as input and provide a single ItemGroup of all sub-folders. If Recursive is false, only look 1 level deep, otherwise hit the bottom. The Exclude is a little tricky but I think just a semi-colon delimited list of folder names will suffice. Also I’ve deliberately used a semi-colon delimited list for Include and Exclude so that we can use ItemGroups as inputs in both those cases. That way you could use this task to create an include list and an exclude list, and then create a third list which is a delta of the two.

Ok so back to our class. We need to define 3 properties here. For my task, only the ‘Include’ attribute is mandatory. Exclude will be empty by default, and Recursive will be false by default. We can easily create the properties with C#3.0 syntax:

[Required] [Output] public ITaskItem[] Include { get; set; }
public bool Recursive { get; set; }
public ITaskItem[] Exclude { get; set; }

The ‘Required’ attribute means the Task cannot be called without mention of that property. And the ‘Output’ attribute means that the property can be used in the ‘TaskParameter’ attribute of the <Output> element. The ITaskItem interface stores individual items for passing around. It is best practise to use an array of these for accepting and returning ItemGroups.

Now we need to focus on our core functionality. We need to iterate through each Include item and add each sub-folder to a list. Since each include item might be a child or parent of another include item, we need to think about duplication. Until now I hadn’t thought if I want my returned ItemGroup to include duplicates. This could easily be configurable for the user by adding a ‘bool Distinct’ property but I’ll just assume that we don’t want duplicates to make it easy.

One of the things I want to be able to do is get some metadata out of my returned folders, specifically the last location in the folder path token (the ‘en-us’ in the example earlier). I’ll call this the folder ‘Name’ (since this is the convention used by the DirecoryInfo class in System.IO). This lets us get just the folder name using metadata like this:

<Message Text=”Folder name = %(AllFolders.Name)” />

Let’s start by creating a simple method that takes the string path of a folder and returns an ITaskItem which includes the above described metadata:

private TaskItem CreateTaskItemFromFolder(string folder)
{
    DirectoryInfo di = new DirectoryInfo(folder);
    Hashtable metadata = new Hashtable();
    metadata.Add("Name", di.Name);
    return new TaskItem(folder, metadata);
}

So what’s happening here? Well we use the DirectoryInfo class to provide us the last token in the directory path, via the ‘Name’ property (ie. di.Name). Metadata needs to be added via a non-generic implementation of IDictionary, in this case I used a System.Collections.Hashtable. The best implementation of an ITaskItem is the TaskItem class (surprised?) and its constructor lets us provide the folder path and the metadata as parameters.

Now we can use this method to create a TaskItem for each folder that we find. We need a method that will accept a base folder as a parameter, and return a List of TaskItems based on all child folders (with consideration to the ‘Recursive’ option). Here’s a method that does just that:

        public IList<TaskItem> GetChildFolders(string folder, bool recursive)
        {
            SearchOption searchOption = (recursive ?
                                         SearchOption.AllDirectories :
                                         SearchOption.TopDirectoryOnly);
            string[] subfolders = Directory.GetDirectories(folder, "*", searchOption);           
            IEnumerable<TaskItem> children =
                  subfolders.Select(s => CreateTaskItemFromFolder(s));
            return children.ToList();
        }

We utilise the existing ‘GetDirectories’ static method of the System.IO.Directory class. This method has an overload that accepts the ‘SearchOption’ enum which pretty much means recursive or not. Note that when specifying SearchOption.AllDirectories, the search will recursively search your whole tree from that point, even following folder shortcuts. This means if you have a shortcut to a higher level folder, you could potentially get stuck in an infinite loop. So be careful: if this task is called with Recursive=true, then ensure its not called on a folder that nests to itself via shortcuts.

Finally with a little bit of lambda magic, we create a task item from each folder returned by GetDirectories. That’s all the hard work done, we just need to bring it together in our Execute method and overwrite the ‘Include’ folder with the new results. Here’s what the first bit of code in our Execute method looks like:

IList<TaskItem> results = new List<TaskItem>();

          foreach (ITaskItem item in Include)
          {               
                IList<TaskItem> children = GetChildFolders(item.ItemSpec, Recursive);
                results = results.Union(children).ToList();
          }

Essentially the above code just creates one big list of task items by unioning the result of each folder call. After the loop completes we have all the included subfolders in one list. We now need to remove any exclude folders from that list. The next bit of code looks like this:

            foreach (ITaskItem item in Exclude)
            {
                 results = results.Where(x => x.ItemSpec.ToLower() != item.ItemSpec.ToLower()).ToList();
            }

This code is not as difficult as it looks. Once again we use a lambda to specify a condition on which we want to filter results. That condition is based on matching the folder strings from the exclude list against those in the current results list. The output is a new list without the matching items, and we just overwrite Results with this new list.

The last piece of the puzzle is to push our output back into the Include list of task items:

var r = results.Cast<ITaskItem>();
Include = r.ToArray();

Simply put, we need to cast our list of TaskItem to a list of ITaskItem and then push it out as an array.

And that’s it! A couple things to mention though. In the final version of my code there are lots of checks around whether folders exist. Its also worth mentioning that instead of using .Select and .Where methods, you can use standard LINQ syntax instead. I just found the lambda syntax more concise and easier to read.

So what’s the final result? Well you can do funky things like this:

<CreateFoldersItem Include="C:\Projects\Secret"
                              Recursive="true"
                              Exclude="C:\Projects\Secret\NotReallySecret">
             <Output ItemName="SecretFolders" TaskParameter="Include" />
</CreateFoldersItem>
<CreateFoldersItem Include="C:\Projects"
                              Recursive="true"
                              Exclude="@(SecretFolders)">
             <Output ItemName="AllFolders" TaskParameter="Include" />
</CreateFoldersItem>

In the above example, we get an item group for all folders under C:\Projects except for all the secret projects. Note that AllFolders will include C:\Projects\Secret\NotReallySecret\ because it was excluded from the first list!

I hope you find this task useful. You can download the DLL and example usage here.

About these ads
Categories: Uncategorized
  1. l
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: