Get Project Path From An MSBuild Task

I am writing an MSBuild Task and want to get access to the current project. Specifically the path to the project that is executing my task.

According to the Microsoft doco I should be using IBuildEngine.ProjectFileOfTaskNode

> Gets the full path to the project file that contained the call to this task.

So this would give us something like this

public class WriteProjectContentsTask : Task
{
    public override bool Execute()
    {
        var projectPath = BuildEngine.ProjectFileOfTaskNode;
        var projectContents = File.ReadAllText(projectPath);
        Debug.WriteLine(projectContents);
        return true;
    }
}

However it turns out there is a bug/feature with using ProjectFileOfTaskNode.

When you rename a project in Visual Studio ProjectFileOfTaskNode does not get updated.

This means the above WriteProjectContentsTask will get a nasty FileNotFoundException.

The Work-around

Note: Stop reading here if you are afraid of black magic

So wading into the inner working of the Visual Studio BuildEngine allows this code to be written.

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

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

    public static string GetProjectPath(this IBuildEngine buildEngine)
    {
        var projectFilePath = buildEngine.ProjectFileOfTaskNode;
        if (File.Exists(projectFilePath))
        {
            return projectFilePath;
        }
        return buildEngine.GetProjectInstance().FullPath;
    }
}

Which means WriteProjectContentsTask becomes

public class WriteProjectContentsTask : Task
{
    public override bool Execute()
    {
        var projectPath = BuildEngine.GetProjectPath();
        var projectContents = File.ReadAllText(projectPath);
        Debug.WriteLine(projectContents);
        return true;
    }
}

But this could break in future versions of MSBuild

This code uses reflection and hence bypasses the public MSBuild API. This means there is the risk that this code wont work in future updates of MSBuild.

So use at your own risk.

However there are several points that make me less concerned about this.

  • This is compile time. So any failures would happen to a developer after they have upgraded to a new version of MSBuild. I think the risk of an MSBuild task not being initially compatible with a new version of MSBuild is acceptable.
  • If it breaks it should consistently break and hence the problem can be identified quickly.
  • MSBuild has not been the focus of any recent improvements efforts from Microsoft. It is unlikely to receive any love in the next revamp of Visual Studio. (I home I am wrong on this point)
  • If the initial "project rename bug" is fixed in the next version of MSBuild the reflection code will not be called due to the File.Exists check.
Posted by: Simon Cropp
Last revised: 20 Dec, 2011 07:38 PM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.