MSBuild Property Functions

Have you ever wanted to do something simple in a build, like get a substring of a property value, and found that MSBuild didn't have syntax for it? You then had to write a task for it, which was tiresome to do for such a simple operation. What's more, if you wanted to do this during evaluation – outside of a target – you couldn't run a task there anyway.

In MSBuild 4.0 we addressed this by adding "property functions" which allow you to execute a variety of regular .NET API calls during evaluation or execution.

Here's an example. For the default VB or C# project, both the intermediate and final output directories are by default below the project's directory. Instead, I'm going to move the final outputs to c:\outputs\<some guid>\ followed by the usual path. You can see below how I did this. I removed the <OutputPath> property and replaced it with an expression that generated a guid for this project.

image

Now I reopen the project and hit build to show it worked:

image

Syntax

There are two syntaxes, as follows. They're intended to be fairly close to the existing Powershell syntax for calling .NET types. The first is for calling static members:

$([Namespace.Type]::Method(..parameters…))

$([Namespace.Type]::Property)

$([Namespace.Type]::set_Property(value))

The second is for instance members on the String class. You write it as if the property itself is a string.

$(property.Method(..parameters...))

$(property.Property)

$(property.set_Property(value))

Notice that when setting a property, you must use CLR syntax for properties ("set_XXX(value)").

The neat part is that these can all be nested – be sure to match your parentheses correctly of course. We attempt to coerce parameters as far as possible in order to find a method or overload that will work.

If you want to pass strings, quote with back-ticks.

When you pass the result of one expression to another, the types are maintained along the chain. This helps the binder find the member you are trying to call. Only when the final result of the expression needs to go into the build do we coerce it to a string.

Some examples may help:

Examples

image

Limitations

* You can't run instance methods on raw strings. For example $("c:\foo".get_Length()). They must go into a property first.

* Out parameters won't work – there are no intermediate values except for the return value. No delegates or generics either.

* If we coerce to the wrong overload, you may be able to use a Convert method to force the correct one.

* By default, you can only call certain members on certain types – selected to be free of side-effects. Here's the full list:

(1) All members on the following types:

System.Byte
System.Char
System.Convert
System.DateTime
System.Decimal
System.Double
System.Enum
System.Guid
System.Int16
System.Int32
System.Int64
System.IO.Path
System.Math
System.UInt16
System.UInt32
System.UInt64
System.SByte
System.Single
System.String
System.StringComparer
System.TimeSpan
System.Text.RegularExpressions.Regex
System.Version
MSBuild (see below)
Microsoft.Build.Utilities.ToolLocationHelper

(2) Selected members on certain other types:

System.Environment::CommandLine
System.Environment::ExpandEnvironmentVariables
System.Environment::GetEnvironmentVariable
System.Environment::GetEnvironmentVariables
System.Environment::GetFolderPath
System.Environment::GetLogicalDrives
System.IO.Directory::GetDirectories
System.IO.Directory::GetFiles
System.IO.Directory::GetLastAccessTime
System.IO.Directory::GetLastWriteTime
System.IO.Directory::GetParent
System.IO.File::Exists
System.IO.File::GetCreationTime
System.IO.File::GetAttributes
System.IO.File::GetLastAccessTime
System.IO.File::GetLastWriteTime
System.IO.File::ReadAllText

But I want to use other types and custom types ..

The reason we prevent this is to make it more safe to load Visual Studio projects. Otherwise, someone could give you a project that formatted your hard-disk during evaluation. Visual Studio load-time safety is actually more complicated than that – some targets will run and do arbitrary things – but we didn't want to make new opportunities for badness. We could have made this limitation only apply to Visual Studio, but then it would be possible to have your build work differently on the command line. I'd like to hear your feedback on this – is the list too constraining?

You can decide whether we made the correct call here. Meanwhile there is an unsupported way to call members on arbitrary types: set the environment variable MSBUILDENABLEALLPROPERTYFUNCTIONS=1. You can now use any type in any assembly. Of course, MSbuild has to know what assembly it is in (it knows them for the list above); and the CLR binder still has to be able to find it to load it.

To figure out the assembly, it tries to work up the name. So for this example (assuming the environment variable is set)

$([Microsoft.VisualBasic.FileIO.FileSystem]::CurrentDirectory)

it will look for Microsoft.VisualBasic.FileIO.dll, then Microsoft.VisualBasic.dll (which it will find and load from the GAC) and you will get the value of the current directory.

If that's not going to work for your assembly, it is possible to pass in a strong name. For example, the above could equivalently be written like this:

$([Microsoft.VisualBasic.FileIO.FileSystem, Microsoft.VisualBasic, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]::CurrentDirectory)

This means you can write your own functions for MSBuild to call – just put the assembly somewhere that the CLR can find it. By doing that, you can (if you set the environment variable) cause your build do do absolutely anything during property evaluation.

Here's a screenshot of these examples:

image

Have fun, and let me know what you think. I'd love to get suggestions on how we can improve this.

[Update] I'll post about this separately later, but here's one other property function that will be useful to some people:

$([MSBuild]::GetDirectoryNameOfFileAbove(directory, filename)

Looks in the designated directory, then progressively in the parent directories until it finds the file provided or hits the root. Then it returns the path to that root. What would you need such an odd function for? It's very useful if you have a tree of projects in source control, and want them all to share a single imported file. You can check it in at the root, but how do they find it to import it? They could all specify the relative path, but that's cumbersome as it's different depending on where they are. Or, you could set an environment variable pointing to the root, but you might not want to use environment variables. That's where this function comes in handy – you can write something like this, and all projects will be able to find and import it:

<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), EnlistmentInfo.props))\EnlistmentInfo.props" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), EnlistmentInfo.props))' != '' " />

Dan Moseley
Developer Lead - MSBuild