Building .NET Projects in Strider

After successfully getting code from Codeplex, I was eager to get one of my projects building. The two I care about are both .NET projects. I knew there was no .NET support in strider when I first started investigating it, so I knew I was going to have to make the plugin myself. This is (the beginnings of) my attempt at getting Strider to build my .NET projects.

The build system for .NET is pretty easy to use. It is all supported in msbuild. The problem is how to determine where it lives. And which version to use (as of now, these versions exist: 1.0, 1.1, 2.0, 3.0, 3.5, 4.0).

My first idea was to use the vcvarsall.bat file, as it will put msbuild in the path. Since it is included in Visual Studio, I was all set to give you a choice of which version of VS to use to run the build. And I didn't want to just execute vcvarsall.bat in the same context that strider was running in, as it would pollute the environment for all jobs. Instead, I used cmd.exe (the /c closes the cmd shell after the command has executed) to execute multiple items at once:

cmd /c '/path/to/vcvarsall.bat' & 'msbuild myproj'  

After getting all that working in strider (it was obviously a lot more complex than that), I realized that Visual Studio should not be installed on a build server. Ideally, it should be as unpolluted as possible, to give more confidence that a user's machine can run your project without additional software (besides the appropriate version of .NET).

Since msbuild is included with the framework, I was able to call it from the .NET installation directory. Which is: %WINDIR%\Microsoft.NET\Framework\{version} or %WINDIR%\Microsoft.NET\Framework64\{version} for 64 bit msbuild. Generally, this will be c:\windows, but using the environment variable is obviously safer than assuming windows is installed to the default directory and drive.

So, now I have a selection box that lets the user pick the version of .NET, and the build step will try to detect if msbuild exists in the given directory. Right now, it favors the 64 bit framework, but I am not entirely sure if that is correct, or if it even matters. For all I know, they are identical programs.

I used async's waterfall method, so I don't need a ton of nested function calls in the code that detects msbuild:

function findmsbuild(version, framework, callback) {  
  var root = process.env.windir + '\\Microsoft.NET\\' + framework;
  async.waterfall([
    function (next) {
      fs.readdir(root, next);
    },
    function (files, next) {
      var dir = _.find(files, function(f) {
        return f.indexOf('v' + version) >= 0;
      });

      if (dir) {
        next(null, root + '\\' + dir);
      } else {
        next('msbuild could not be found for .NET ' + version);
      }
    },
    function (dir, next) {
      var msbuild = dir + '\\msbuild.exe';
      fs.exists(msbuild, function(exists) {
        if (exists) {
          next(null, msbuild);
        } else {
          next('msbuild could not be found for .NET ' + version);
        }
      });      
    }
  ], callback);
}

Then, when I want to call msbuild:

if (config.netVersion && config.netVersion != 'whatever') {  
  findmsbuild(config.netVersion, 'Framework64', function (err, fullpath) {
    if (err || !fullpath) {
      findmsbuild(config.netVersion, 'Framework', function (err, fullpath) {
        if (err) {
          done(err);
        } else {
          msbuild(context, fullpath, args, screen, done);
        }
      });
    } else {
      msbuild(context, fullpath, args, screen, done);
    }
  });
} else {
  msbuild(context, 'msbuild', args, screen, done);
}

Where the msbuild function is:

function msbuild(context, path, args, screen, done) {  
  context.cmd({
    cmd: {
      command: path,
      args: args,
      screen: screen
    }
  }, done);
}

The error in strider when msbuild couldn't be found was pretty bad, so I decided to fix it. The context supports multiple output methods, but there doesn't seem to be any documentation about it. So, I cracked open strider-runner-core to see how to do so. It seems that the status method formats all the output based on the keyword passed as the first argument.

To print the command after the $ sign, I had to pass command.start:

var time = new Date();  
context.status('command.start', { command: screen, started: time, time: time, plugin: 'dotnet' });  

The error had to be passed with stderr:

context.status('stderr', err);  

And, I learned I could colorize it by using ANSI control codes. I don't really get how this works; I just ripped it from strider-runner-core:

context.status('stderr', '\u001b[31;1m' + err + '\u001b[0m');  

And if I didn't pass command.done, the execution timer never stopped:

context.status('command.done', { exitCode: 404, time: time, elapsed: 0 });  

I'm faking the exit code to be 404, because it isn't found. Makes sense, right?

All in all, I'm pretty happy with how easy this was to get setup. With the way strider manages projects, I think the next step is to add the ability to restore nuget packages automatically. I know you can setup nuget package restore to do this, but I think it is silly to store nuget.exe for every project you are going to create.

I have pushed the code to github at abe545/strider-dot-net.


View or Post Comments