Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / architecture

MSBuild Plugin. The Managing of Most Processes at Runtime and More. Projects Without Borders

4.75/5 (3 votes)
13 Apr 2016Apache11 min read 16.8K  
Explanation and architecture of powerful plugins system to MSBuild for flexible service of any projects and libraries, the build processes and processes at runtime.

# Introduction

I propose to consider the msbuild from a different angle -_* plınqsɯ (while you turn back your head, I'll tell you about this article below).

About this:

  • It describes the possibility of service of any projects and the solution, which may be not known at all, and also any other flexible interaction.
  • It also describes implementation of this service for MSBuild Tool as a common plugin connected during specific build (i.e., without of any additional manipulations with project files, etc.)

This is not a 'step by step' instruction, however it should help to understand - How to develop your own plugin for MSBuild on examples (not a Task libraries!) and to be detail oriented at all.

# Background

The example will be simple enough, but from real project and real problems which I had to fight 3-4 year ago.

Currently, it is already made in the common form of working and recommended free tool, primarily for Visual Studio and MSBuild Tool. And as I have information of the developments of this tool and similar, it would be nice to continue to describe the part of this tricks...

One of primarily problems at that moment was a need to have flexible service of all projects in loaded solution, about which nothing is known in advance. Mostly, such things are typical for some of the complete or box-solutions, but also for everyday tasks like automatic versioning, and in general, for any events of the solution-level. As part of this, several of you maybe should already see my answer in SO here.

So, we will consider only the msbuild component, however if you need a full picture of all this solution as a unified environment for Visual Studio, Devenv, MSBuild and other possible application like for CI, etc., then a little earlier I wrote another part of this solution here (only Russian lang)

Just keep in your mind, that can be prepared the unified environment interaction and handling as maintenance of all projects as the situation requires it, for current described methods.

And so, as example of interaction and maintenance of the build of projects, for simplicity, we take the most common task - Automatic or Custom Versioning. And as for the elements of interactions, we will try to get the main layers of events for msbuild, when he begins to build projects: i.e., Pre/Post-Build events of solution-level, projects-level, and other.

However, before start, please also note on possible alternatives like ITask, but the main flaw of this solution it is modifications of project files (+ <UsingTask ...), which can be highly unwelcome or not comfortable for some reasons at all... and the like.

# Plugin

Let's start the implementation of our plugin to MSBuild Tool.

For getting the described above control to msbuild.exe, we should register the developed logic (or minimal translator to other library - wrapper etc.), as simple logger.

Actually, the most accessible way (simple and easy) to interact with msbuild and manage of all its internal processes from different environments (where it can work) is a logging. For this, we will work with Microsoft.Build.Framework and partially with Microsoft.Build.Evaluation.

It will help solve all of our basic tasks without any modifications in any project files, and also partially manage threads of build.

It should be noted, the msbuild logger, of course, created not for this purpose or something like this : ) the logging and no more.
However, we'll try use it slightly differently and flexibly extend the range of possible tasks.

Firstly, you can find the example of a working plugin here:

The our interaction will implemented via logger, therefore we are strongly limited in any actions. But actually not so bad...

# Entry Point

Let's look at the basic entry point and IEventSource.

For work of our future library with MSBuild Tool, we should implement ILogger. As you already know, we will use it only to basic access (later, it will be powerful tool). The easiest way by extending the Logger class:

C#
public class EventManager: Logger
{
    public override void Initialize(IEventSource evt)
    {
        ... // our entry point
    }
    
    public override void Shutdown()
    {
        ...
    }
}

Inside entry point, we can implement loading of custom main logic, including from other library, for example:

C#
ILoader loader = new Loader(
                        new Provider.Settings()
                        {
                            DebugMode = log.IsDiagnostic,
                        }
                    );

Then, we must learn how to work with project data and their solution.

# Access to MSBuild Properties

For work with MSBuild Properties from projects, we need to extend our logger to normal plugin state.
If you're using .NET 4.0, you need to do it manually, i.e., you need to define the Configuration, Platform, SolutionDir,... for handler below. But for platform, .NET 4.5 is available ProjectStartedEventArgs.GlobalProperties.

If you still use the .NET 4.0 or you need a universal tool, then from project above is also available a small class SolutionProperties (b02d72c) which will help to quickly get all the necessary properties for the next step, like:

C#
(new SolutionProperties()).parse("path_to_sln");

The result will be:

C#
public sealed class Result
{
    // Configurations with platforms in solution file
    public List<SolutionCfg> configs;
    
    // Default Configuration for current solution
    public string defaultConfiguration;
    
    // Default Platform for current solution
    public string defaultPlatform;
    
    // All available global properties like for ProjectStartedEventArgs.GlobalProperties (net 4.5+)
    public Dictionary<string, string> properties;
}

# Evaluation New MSBuild Properties & Property Functions

Having access to GlobalProperties (see above), we can try to use the MSBuild engine at all, for example, to evaluate the new msbuild properties and use of complex property functions, prepare the BuildRequest and much more ...

We need only initialize the MSBuild engine for own plugin. The CIM example is a wrapper only for main plugin, therefore similar logic has located beyond the client and controlled via common interface as Provider.ILibrary. To see the full implementation of work with the msbuild engine, you need to look here:

It is implementation of the IEnvironment for isolated environments (yes it's our case). In general, in order to fully start work with all features of MSBuild, you can use the convenient Microsoft.Build.Evaluation:

C#
new Microsoft.Build.Evaluation.Project("path_to_project_file", 
properties, null, ProjectCollection.GlobalProjectCollection);

Where properties - it's our global properties above. You can also with Microsoft.Build.BuildEngine, etc.

Then, you can work with MSBuild as you want, for example, any incredible evaluation of properties:

simple bypass of available:

C#
foreach(ProjectProperty property in project.Properties) {
  ...
}

or to control additional build:

C#
    BuildRequestData request = new BuildRequestData(
                                    new ProjectInstance(root, propertiesByDefault(evt), 
				    root.ToolsVersion, ProjectCollection.GlobalProjectCollection),
                                    new string[] { ENTRY_POINT },
                                    new HostServices()
                               );

#if !NET_40

    // Using of BuildManager from Microsoft.Build.dll, v4.0.0.0 - .NETFramework\v4.5\Microsoft.Build.dll
    // you should see IDisposable, and of course you can see CA1001 for block as in #else section below.
    using(BuildManager manager = new BuildManager(Settings.APP_NAME_SHORT)) {
        return build(manager, request, evt.Process.Hidden);
    }

#else

    // Using of BuildManager from Microsoft.Build.dll, 
    // v4.0.30319 - .NETFramework\v4.0\Microsoft.Build.dll
    // It doesn't implement IDisposable, and voila:
    // https://ci.appveyor.com/project/3Fs/vssolutionbuildevent/build/build-103
    return build(new BuildManager(Settings.APP_NAME_SHORT), request, evt.Process.Hidden);

#endif

We are halfway ...

# Event Layer from MSBuild Tool

And so, now we can do all you need and more. We need only to solve when our plugin should work or follow to some behavior, i.e., events.

Firstly, the basic entry point already provides IEventSource.

C#
void Initialize(IEventSource evt)

Therefore first enough to subscribe on required events, and most requested to full work of your future plugin:

C#
evt.TargetStarted   += onTargetStarted;  // Pre/Post-Build events of project-level + custom actions for specific targets
evt.ProjectStarted  += onProjectStarted; // Pre-Build events of solution-level (deprecated, see below)
evt.AnyEventRaised  += onAnyEventRaised; // All Build information
evt.ErrorRaised     += onErrorRaised;    // All errors
evt.WarningRaised   += onWarningRaised;  // All warnings
evt.BuildStarted    += onBuildStarted;   // Pre-Build events of solution-level (recommended, see below)
evt.BuildFinished   += onBuildFinished;  // Post-Build & Cancel-Build events of solution-level

However, please note the following.

# Exceptions

To throw exceptions for MSBuild Tool, you can only in a specific context, i.e., when it will be ready for it, otherwise there is a risk of losing control at all. Process may remain in suspended state, that is critical for CI servers etc.

It's internal features of implementation ILogger for msbuild, therefore the very first opportunity to throw safe exception will be:

Starting from Initialize():

  1. First event StatusEventRaised is triggered with message ~ "Build Started" - We cannot throw any exception here!
  2. Then immediately should be AnyEventRaised that is triggered with same message ~ "Build Started" - We also can not throw any exception here !
  3. Then ProjectStarted - Here it is already possible, should be [safe].
  4. For StatusEventRaised & AnyEventRaised after ProjectStarted is also possible but first check the BuildContext that is not null!

A thrown exception will terminate execution of all msbuild tasks as:

BAT
_________________________________________________
Build started 12.04.2016 21:22:05.

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.03
MSBUILD : Logger error MSB1029: muahahahaaa

And also note, the following events will not triggered (never after exception above):

C#
evt.ErrorRaised     += onErrorRaised;
evt.WarningRaised   += onWarningRaised;

You should know about these features if you need control the build (we are logger and directly we can't control the any behavior of this - that is logical of course...)
Summing up the above, for IEventSource you should throw an exceptions from ProjectStarted and later as the first safe place, for example:

EventManager:137 (7896c05) - It supports simple logic of forwarding the control commands - EventManager:268 (7896c05)

C#
protected void command(object sender, CoreCommandArgs c)
{
    switch(c.Type)
    {
        case CoreCommandType.AbortCommand: {
            abortCommand(c);
            break;
        }
        case CoreCommandType.BuildCancel: {
            abort = true;
            break;
        }
        case CoreCommandType.Nop: {
            break;
        }
        case CoreCommandType.RawCommand: {
            rawCommand(c);
            break;
        }
    }
    receivedCommands.Push(c);
}

This allows us to continue to flexibly manage of forced stop etc. But it is details of implementation of specific program...

# The Current Project to Process

To fully work with any projects for all these events (TargetStarted, WarningRaised, ...), we need know - which is currently being processed.

And the ProjectStartedEventArgs from IEventSource.ProjectStarted will help us with these.

Actually this is the only place where throughout of all processing we can get minimal information about project, specifically: ProjectId and its properties.

So we need only to relate it with ProjectId, and as he is also available in other events, you can try like this:

C#
projects[e.ProjectId] = new Project() {
    Name        = properties["ProjectName"],
    File        = e.ProjectFile,
    Properties  = properties
};

Then refer to it as, for example in IEventSource.TargetStarted:

C#
int pid = e.BuildEventContext.ProjectInstanceId;
library.Event.onProjectPre(projects[pid].Name);

etc.

# Basic Events and their Problems

We are on the finish line. Let's teach our plugin the identification friend / foe. : )
We begin to implement the basic events.

# Pre-Build Events of Solution-level

Here, I made a small note that PRE events is possible to use only with IEventSource.ProjectStarted, and partially it is true, however this place can add a some problems for your logic. For example - CSC error (CS2001) if will be used multithreading for any build i.e. /m:2+ (2 or more concurrent processes to use). Later, this problem has been fixed for plugin above as:

C#
[CI.MSBuild] Fixes for Multi-Processor Environment

In general, the logic of calling of the PRE event has been moved from onProjectStarted.
However it also has required a lot of changes with manually detecting properties for our core.

Therefore the IEventSource.BuildStarted with all prepared msbuild properties (how to - has been shown above) a more correct place for processing of the Pre-Build events.

C#
protected void onBuildStarted(object sender, BuildStartedEventArgs e)
{
    try {
        ptargets.Clear();

        // yes, we're ready
        onPre(initializer.Properties.Targets);
    }
    catch(Exception) {
        abort = true;
    }
}

# Pre/Post-Build Events of Project-level

This is much easier, because we need only define control of all incoming targets, specifically:

C#
protected void onTargetStarted(object sender, TargetStartedEventArgs e)
{
    int pid = e.BuildEventContext.ProjectInstanceId;

    // the PreBuildEvent & PostBuildEvent should be only for condition '$(PreBuildEvent)'!='' ...
    switch(e.TargetName)
    {
        case "BeforeBuild":
        case "BeforeRebuild":
        case "BeforeClean":
        {
            if(!ptargets.ContainsKey(pid)) {
                ptargets[pid] = false; //pre
                library.Event.onProjectPre(projects[pid].Name);
            }
            break;
        }
        case "AfterBuild":
        case "AfterRebuild":
        case "AfterClean":
        {
            if(!ptargets.ContainsKey(pid) || !ptargets[pid]) {
                ptargets[pid] = true; //post
                library.Event.onProjectPost(projects[pid].Name, projects[pid].HasErrors ? 0 : 1);
            }                    
            break;
        }
    }
}

etc.

# Errors/Warnings + Build information

It is direct responsibilities of any loggers at all. : ) So there is no difficulty to work with it as you need, because we already can refer to specific project (see above) and everything is possible:

C#
protected void onErrorRaised(object sender, BuildErrorEventArgs e)
{
    if(projects.ContainsKey(e.BuildEventContext.ProjectInstanceId)) {
        projects[e.BuildEventContext.ProjectInstanceId].HasErrors = true;
    }
    
    library.Build.onBuildRaw(formatEW("error", e.Code, e.Message, e.File, e.LineNumber));
}

# Post-Build & Cancel-Build Events of Solution-level

It's also is very simply like for Errors/Warnings (worst is over). The IEventSource.BuildFinished and his BuildFinishedEventArgs has provided the Succeeded flag, so we can define our behavior as for Cancel-Build and as for Final Post operations.

C#
protected void onBuildFinished(object sender, BuildFinishedEventArgs e)
{
    if(!e.Succeeded) {
        library.Event.onCancel(); // Cancel-Build if it is
    }
    library.Event.onPost((e.Succeeded)? 1 : 0, 0, 0); // Post-Build, even if was the Cancel-Build before (like in VS)
}

# Sln-Opened/Closed

Finally, to the basic events we also have Sln-Opened/Closed event types as the equivalent of the following events in Visual Studio (that's another story, see for example here and related):

C#
int OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
int OnAfterCloseSolution(object pUnkReserved)

It is quite simply like for Post/Cancel-Build event types above. Suitable primarily: void Initialize(IEventSource evt) & void Shutdown()

C#
public override void Shutdown()
{
    library.Event.solutionClosed(pUnkReserved);
    detachCoreCommandListener(library);
}

In general, now we have access to all incoming targets and also to all required properties, therefore we can also extend range of possible events for what we need.

That's all, now you can begin to create main logic of your plugin, see below;

# We're Ready to Create Plugin. The Result and Possible Scenarios of Using

Now you have all you need to create simple or complex plugin to MSBuild.

Now, we are not simply logger, now we are powerful plugin, because now we can a lot:

  • Work with all MSBuild properties from available projects.
  • Complex work with MSBuild engine:
    • Allows new evaluation of properties at runtime.
    • Support of complex Property Functions and work with available.
  • Handlers with all basic events and more, for specific project or for all solution.
  • Partially manage threads of build and redefine their:
    • From the termination to the preparing additional BuildRequestData, etc.

As a basic example was considered wrapper https://github.com/3F/vsSolutionBuildEvent/tree/master/CI.MSBuild

It's only a translator of Events/Actions to the main plugin, and with external logic, we can also write additional user-scripts at runtime, as we want, affecting on the build processes in particular:

JavaScript
$(buildNumber = $([MSBuild]::Add($(buildNumber), 1)))
...
#[var tStart    = $([System.DateTime]::Parse("2015/12/02").ToBinary())]
#[var tNow      = $([System.DateTime]::UtcNow.Ticks)]
#[var revBuild  = $([System.TimeSpan]::FromTicks($([MSBuild]::Subtract($(tNow), $(tStart)))).TotalMinutes.ToString("0"))]

etc.

Now the our versioning (for example) only is a question of technique and preferences. From the very simple control of the build number to the using complex script engines, like from example of plugin above which provides the unified handler of different events for Visual Studio, MSBuild Tool, and other.

[And most important], all this may work without any additional manipulation or changes of any project files, which may be not known at all. Everything is very simply and automated:

BAT
> msbuild.exe "SolutionFile.sln" /l:"YourPlugin.dll"

and you are ready to conquer the world -_*

# References

# Source Code

# Points of Interest

This article was written without special editing, i.e. 'as is', so if you found errors, or have some question { relevant : ) }, write here, I'll try fix it and explain more in details if need it, as possible for my time...

Thanks to all, who read to the end, I hope it was helpful or interesting at least -_*

History

  • 13th April, 2016: Initial version

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0