danielwertheim

danielwertheim


notes from a passionate developer

Share


Sections


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

First meeting with Phantom - a mature and extendable .Net build engine

So far I’m a Windows guy and I mainly spend my time within .Net. Historically I’ve been using Ruby, Rake and Albacore to manage my simple builds, like: building, copying, running tests, zipping and creating NuGet packages. No more! That era has come to an end. Meet Phantom – a mature and extendable .Net build engine.

Don’t get me wrong. It’s easy to get started with Ruby and Rake and Albacore on a Windows machine, but, I kind of lost interest in that world a couple of days ago when trying to get Jekyll working nicely on my machine. After fiddling around with different versions of different gems and installing Python etc. I lost it. I don’t want all these dependencies for one simple thing. And during this "incident" I made a decision to try and stick to stuff that works simple enough and works with .Net. I was looking into PSake (PowerShell based build script solution) and Phantom. And for me, I just liked the syntax and how easy it was to get started with Phantom and extending it. Although, there are two things I miss:

  1. Distribute it via Chocolatey.
  2. Make the core dll available via NuGet so that you more easily can write your extensions against it.

Meet Phantom

Phantom is created by Jeremmy Skinner and it "lives" on GitHub:JeremySkinner/Phantom. There’s really not much to it to get started. Just download it and start using the included phantom.exe located under libphantom. For me, it would have been a bit easier if it was distributed via e.g. Chocolatey, but nothing that I can’t live without and also nothing that I can’t fix myself.

On my environment I’ve chosen to keep some tools in my environment path. Both the nuget.exe, nunit-console.exe and phantom.exe are registrered in the environment path on my machine.

The syntax of Phantom is Boo, that is, you write your build scripts using Boo, but if you like to extend Phantom, you just write a simple .Net C# class library targeting .Net 4.0, and then you drop that dll into the folder where you have your copy of phantom.exe.

You execute a build script by calling phantom.exe accordingly:

phantom -file=a_file.boo target_name

This can be shortened if you go with some default values. You don’t have to specify a file if you name the file: build.boo. You can also leave out a specific target, then Phantom will look for one named: default.

Lets have a look at a fairly simple build script I have to manage the builds of one of my open source projects Ensure.That. Please note. This is my first script written for Phantom so there is most likely room for improvements. The tasks it will perform are:

  1. Clean the build folder
  2. Compile the solution
  3. Copy binaries etc to build folder
  4. Run tests
  5. Create a Zip
  6. Create a NuGet package

None existing folders will be created automatically. So for me, it will create e.g: deploybuildsEnsureThat-v1.0.0-Release. The script look like this:

import MyPhantom

sln_name = "Ensure.That"
sln_dir = "../src"
prj_name = "EnsureThat"
builds_dir = "builds"
bld_version = "1.0.0"
bld_config = "Release"
bld_name = "${prj_name}-v${bld_version}-${bld_config}"
bld_dir = "${builds_dir}/${bld_name}"

target default, (clean, compile, copy, test, zip, nuget_pack):
  pass

target clean:
  rm(bld_dir)

target compile:
  msbuild(
    file: "${sln_dir}/${sln_name}.sln",
    targets: ("Clean", "Build"),
    configuration: bld_config)

target copy:
  with FileList(
      "${sln_dir}/Projects/${prj_name}/bin/${bld_config}"):
    .Include("*.*")
    .ForEach def(file):
      file.CopyToDirectory(bld_dir)

target test:
  mynunit(
    assemblies: MyFileList(
      "${sln_dir}/Tests/*.UnitTests/bin/${bld_config}",
      "*.UnitTests.{dll}"),
    options: "
      /framework=v4.0.30319
      /xml=${bld_dir}/NUnit-Report-${bld_name}-UnitTests.xml")

target zip:
  zip(bld_dir, "${builds_dir}/${bld_name}.zip")

target nuget_pack:
  mynuget(options: "
    pack ${sln_name}.nuspec
    -version ${bld_version}
    -basepath ${builds_dir}
    -outputdirectory ${builds_dir}")

Lets have a look at some parts of this script. For more information, just head over to the Phantom Wiki.

imports MyPhantom

This imports a custom dll that I’ve created and put in the Phantom folder. It contains some simple extensions. There is builtin support for both NUnit and NuGet but they perform a check that the specified exe exists, and since I have it in my environment path that did not work for me. So I just created two simple extensions: mynunit and mynuget. I also extended the existing FileList and created MyFileList so that I can shorten the code a bit. The code for them exists here.

Then there’s a section of arbitrary variables that I use. Followed by:

target default, (clean, compile, copy, test, zip, nuget_pack):
  passtarget default

This defines the default target that will be invoked by Phantom, but only if you haven’t specified one already. The only thing it does is to, sequentially, invoke other targets that are defined in the script.

The script is kind of self explaining but lets look at one more target:

target test:
  mynunit(
    assemblies: MyFileList(
      "${sln_dir}/Tests/*.UnitTests/bin/${bld_config}",
      "*.UnitTests.{dll}"),
    options: "
      /framework=v4.0.30319
      /xml=${bld_dir}/NUnit-Report-${bld_name}-UnitTests.xml")

This uses my custom mynunit runner that accepts assemblies to run tests in and gives you the opurtunity to pass options to the nunit-console.exe. The code for my custom NUnit runner is really simple:

using Phantom.Core.Builtins;
using Phantom.Core.Language;

namespace MyPhantom
{
  public class mynunit : IRunnable<mynunit>;
  {
    public static string tool_path { get; set; }
    public static MyFileList assemblies { get; set; }
    public static string options { get; set; }

    public mynunit()
    {
      tool_path = "nunit-console.exe";
    }

    public mynunit Run()
    {
      IOFunctions.exec(
        tool_path, 
        string.Concat(assemblies.ToPathString(), " ", options));

      return this;
    }
  }
}

It would have been a bit easier if the Phantom.Core was distributed via NuGet, but as of now, I had to reference it "the old way".

Anyway, that’s it. Simple, intuitive and it works. Just what I like.

//Daniel

View Comments