How to access build variables from an MSBuild Task

I have been experimenting recently with ILWeaving. One thing that quickly became apparent was that the easiest way to expose ILWeaving as a re-usable tool was by writing a MSBuild Task. This way it can be integrated as part of the build and modify assemblies after compilation but before debug execution.

In writing these Tasks I wanted to ensure there was minimal setup and configuration. Preferably a user could place a small amount xml into their csproj file and it would work. I figured all of the configuration could be derived from the current “Build”. For example I would be able to derive, from with my Task, the following

  • All assemblies the current project references
  • The path to the current assembly being built

This would mean consumers are not required to inject these as part of the xml config.

So in essence I wanted this

<Target Name="AfterCompile">
<NotifyPropertyWeaverMsBuildTask.WeavingTask/>
</Target>

And not this

<Target Name="AfterCompile">;
<NotifyPropertyWeaverMsBuildTask.WeavingTask
TargetPath="@(IntermediateAssembly)"
References="$(ReferencePath)">;
</Target>;

However it turns out the MSBuild infrastructure does not let you do this. There is no way for a Task to get access to build environment variables.

I even raised a StackOverflow question http://stackoverflow.com/questions/3043531/when-implementing-a-microsoft-build-utilities-task-how-to-i-get-access-to-the-var

I initially took the advice of “Don't do it!” however after configuring the task on several of my own projects I was not satisfied. It annoyed me that I needed to re-type in config that should be able to be derived. So I got out the big hammer of the debugger and Reflector and managed to reverse engineer MSBuild enough to get this

#!cs
public static class BuildEngineExtensions
{
    const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public;

    public static IEnumerable GetEnvironmentVariable(this IBuildEngine buildEngine, string key,bool throwIfNotFound)
    {
        var projectInstance = GetProjectInstance(buildEngine);

        var items = projectInstance.Items
            .Where(x => string.Equals(x.ItemType, key, StringComparison.InvariantCultureIgnoreCase)).ToList();
        if (items.Count > 0)
        {
            return items.Select(x => x.EvaluatedInclude);
        }


        var properties = projectInstance.Properties
            .Where(x => string.Equals(x.Name, key, StringComparison.InvariantCultureIgnoreCase)).ToList();
        if (properties.Count > 0)
        {
            return properties.Select(x => x.EvaluatedValue);
        }

		if (throwIfNotFound)
		{
			throw new Exception(string.Format("Could not extract from '{0}' environmental variables.", key));
		}

        return Enumerable.Empty();
    }

    static ProjectInstance GetProjectInstance(IBuildEngine buildEngine)
    {
        var buildEngineType = buildEngine.GetType();
        var targetBuilderCallbackField = buildEngineType.GetField("targetBuilderCallback", bindingFlags);
        if (targetBuilderCallbackField == null)
        {
            throw new Exception("Could not extract targetBuilderCallback from " + buildEngineType.FullName);
        }
        var targetBuilderCallback = targetBuilderCallbackField.GetValue(buildEngine);
        var targetCallbackType = targetBuilderCallback.GetType();
        var projectInstanceField = targetCallbackType.GetField("projectInstance", bindingFlags);
        if (projectInstanceField == null)
        {
            throw new Exception("Could not extract projectInstance from " + targetCallbackType.FullName);
        }
        return (ProjectInstance)projectInstanceField.GetValue(targetBuilderCallback);
    }
}

And it can be used like this

string targetPath = buildEngine.GetEnvironmentVariable("TargetPath", true).First();
string intermediateAssembly = buildEngine.GetEnvironmentVariable("IntermediateAssembly", true).First();
IEnumerable referencePaths = buildEngine.GetEnvironmentVariable("ReferencePath", true);

Yes it is ugly and black magic but it works.

Performance

The overhead of using reflection in this case is insignificant compared to the other processing that is happening.

Compatibility in the future

Sure this may break in a future version of visual studio but I can handle fixing this code once every 2 years.

Might not work in all scenerios

Yes there are some edge cases where this will not work. For example using the task from mono build. In these cases the consumer can still pass in the properties directly and the variables will not be derived from the build engine.

Posted by: Simon Cropp
Last revised: 24 Dec, 2011 05:28 AM History

Comments

Dan
Dan
26 Mar, 2012 04:22 PM

The reasoning behind this design (tasks can't see properties) is to avoid coupling tasks to build process. If tasks could see arbitrary properties, they could avoid having any parameters: a task could be used by just putting an empty tag, like . It would be very difficult to tell what the build process was doing from reading it. The task would also be much less reuseable - what if you want it to process @(References) in one context, and @(AssemblyPaths) in another?

That's the theory anyway -- I can see how it can be frustrating if you don't want that. Perhaps there should be a global flag that breaks down this wall so tasks can see everything.

Dan

Simon Cropp
Simon Cropp
27 Mar, 2012 09:43 AM

@Dan

All the issues you raise should be design decisions that are left up to the write of the task.

No new comments are allowed on this post.