Wednesday, February 8, 2017

Quick Tip/Hack: Getting MSBuild to Stop on First Error

One of the most annoying and common problems (as a bit of Googling soon reveals) when compiling a big project with MSBuild (multi-process) is that even if compilation fails on one of the files (due to a syntax error or some other error condition), it will keep compiling the rest of the codebase. This is bad for two reasons:
  1)  You won't be able to link the program in the end anyway. At least in my experience, even if you could link the program (and that's not a given in every case), it doesn't make much (if any) sense to have an unreproducable chimera of old + new code (though exactly which bits they are you probably won't remember a little down the track)

  2)  You'll probably have to recompile all the code again once you fix the error anyway. More often than not, I've mainly had these errors show up when changnig code in a popular header file that large swathes of the program used. So, if a compile error like this showed up, the computer would have already wasted a lot of time/energy compiling a lot of files (many of them unsuccessfully too), work that would have to be redone when you've fixed the error anyway.

After a few false starts, I ended up finding a neat (if somewhat evil) little hack to solve this problem once and for all.


My Solution
I found that simply calling Environment.Exit(-1) from within an error-event handler in a CustomLogger could be used to stop a build process dead in its tracks. Instantly.

The hack in action. 
See the full source code for this at: https://github.com/Aligorith/blender_msbuild_loggers

Someone more knowledgeable about how the MSBuild system might be able to tell me that doing so has certain downsides over more "officially sanctioned" ways of solving the problem. That said, IMO this should in theory be similar to just force-closing the terminal window anyway, or madly Ctrl-C'ing it into submission. Except this is more efficient and automated :)

So, here's a minimal example of how we could do this:
using System;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;


public class CustomLogger: Logger
{

    public override void Initialize(IEventSource eventSource)
    {

        eventSource.ErrorRaised   += new BuildErrorEventHandler(handleErrorRaised);
     }

     private void handleErrorRaised(object sender, BuildErrorEventArgs e)
     {
           /* Report the error */
           string line = String.Format("ERROR: {0}:{1} - {3}  [{2}]",
                                    e.File, e.LineNumber, e.Code, e.Message);
            Console.WriteLine(line);

                  
            /* Try to trigger a stop */
            Console.WriteLine("\nStopping build now!");

            Environment.Exit(-1);
        }
    }

}

(Warning: Untested code - Extracted from my full logger, but with just the key bits picked out)


Other Approaches That Didn't Work
* The "StopOnFirstFailure" option that's apparently available DOES NOT work when doing multi-process builds. The documentation tells us that much. But, even if it does work, it's hard to come across any info about how I'm supposed to pass/set that option when my build files were all generated by CMake

* Another option is to try commanding MSBuild via the programmatic api (i.e. Microsoft.Build.Execution -> BuildManager). In theory, that's how the VS IDE implements its Build.Cancel() functionality.

(That said, on the few occasions where I have used the VS IDE, I found that it too didn't have anything in place to exit the build on the first error encountered. Perhaps the folks in Redmond just don't care (or didn't think it was worth allocating the resources to solving this problem).)

Anyway, here's what I tried with the MSBuild Execution API's: In a CustomLogger, I added a handler for Build Errors (as above). But, instead of directly exiting, I tried to force a graceful shutdown by using the DefaultBuildManager singleton (which I assumed should be the same instance that's running the logger, and hence the current build job). Looking through the docs, I saw a few promising API's that looked like they could be used to stop things in their tracks - namely, ShutdownAllNodes(), Dismiss(), and CancelAllSubmissions().

Of these candidates, ShutdownAllNodes() looked to be the most promising; however, no matter what I tried, I couldn't get it to work, as I kept getting errors about missing symbols  (Note: I was compiling the code using csc directly on the commandline, and was passing it Microsoft.Build.dll as one of the "references", in addition to the existing logging ones).

All the other options could be compiled-into-the-logger without problems, but only caused the build process to stall (i.e. after the commands were issued, the build engine would continue to spawn whatever events it had pending, and then all the activity would then suddenly die out but not quit, with 2 MSBuild processes still active).

No comments:

Post a Comment