Psake Builds

In Technical by jayden.macrae

For a while now I’ve been tearing my hair out (and I can little afford that these days) about a whole lot of inconsistencies in the build pipeline using msbuild. I’ve had a whole lot of unexplained, intermittent and un-resolved issues where at times my builds just don’t build (see postscript). What seem like file locking issues; regardless of cleans and deleting directories. I never had these problems with nant scripts, even when using msbuild for the actual compilation process; but alas nant just doesn’t seem to be supported anymore. With both nant and msbuild I also never really liked writing XML to define procedural tasks but I suffered through it.

Along comes Psake (pronounced like the Japanese rice wine sar-kay; the ‘p’ is silent). Psake is a Powershell build (make) tool; which presumably is where the name comes from (PowerShell mAKE).

Psake allows us to write procedural build scripts utilising all the functionality of Powershell.

Installing Psake

  1. Download the latest files from GitHub. The intent of the content of this zip is to add a new cmdlet to Powershell called with the invoke-psake command.
  2. Unzip the file contents to:%UserProfile%\Documents\WindowsPowerShell\Modules\psake

    You may need to create the WindowsPowerShell, Modules and psake directory. This will allow Powershell to find the invoke-psake command without importing it manually.

    There are a number of ways that you can use the module, either extracting it and using a manual import-mopdule command to import the command into the current session, or to extract the module to a PSModulePath. It is important the the containing folder is called psake to match the cmdlet so that Powershell recognises it correctly. Read more about installing modules at MSDN.

Build Script Structure

Psake build scripts are writen in files with a .ps1 extension. The basic structure of the script is:

# comments are denoted by the hash symbol #

properties {                    #-- you can define variables / properties here --#
  < property declarations >
}

task < task name > -depends < dependent task names > {
  < commands >
}

An example of a basic script may be :

properties {
  $configuration = "Release"
}

task default -depends Clean,Build

task Clean {
  msbuild project.csproj /t:Clean /p:Configuration=$configuration
}

task Build {
  msbuild project.csproj /t:Build /p:Configuration=$configuration
}

In this script, we define the configuration variable as “Release”. Although in this short script this may seem a bit pointless, this pattern will help with larger and longer scripts, where you may want to use a specific configuration multiple times. Assigning this to a property will help if you want to change the configuration of the build in the future.

We then define a default task. This is the task that is called by default if not specific target task is defined in the call to the psake script. This task has no implementation, but instead depends on two other tasks; Clean and Build.

You then see the definition of the Clean task, which calls msbuild, taking the project file as the target and the command line parameters appropriate to the action. The Build task is similar.

Of course, this script is a little pointless as it doesn’t do much more than Visual Studio would do, but of course, you can begin to add all your pre-build and post-build tasks as powershell commands. For example a more complex script may include:

properties {                                        #-- define your variables for the script here --#
  $configuration = "Release"                    

  $dir_base = split-path $psake.build_script_file   #-- get the directory where this psake script is located --#
  $dir_solution = "$dir_base\.."                    #-- solution directory is a level up --#
  $dir_build = "$dir_solution\build"                #-- the build directory is a sub directory of the solution directory --#
  $dir_bin = "$dir_build\bin"                       #-- the bin directory is a sub directory of the build directory --#
  $dir_final = "$dir_build\final"                   #-- the final directory (post obfuscation) is a sub-directory of the build dir --#

  $file_project = "$dir_solution\project\project.csproj"
}

task default -depends Clean, Build, Tidy

task Clean {
  if (test-path $dir_build) { remove-item $dir_build\* -Force -recurse}  #-- remove all the content of the build directory --#
  new-item $dir_final -type directory                                    #-- create the final directory --#
  msbuild $file_project /t:Clean /p:Configuration=$configuration
}

task Build { 
  msbuild $file_project /t:Build /p:Configuration=$configuration
}

task Tidy {
  remove-item $dir_bin -force -recurse
  get-childitem -path $dir_final | move-item -destination $dir_build    #-- move items from the final dir to the build dir as a final step --#
  remove-item $dir_final -force -recurse
}

This script starts to get a little more complex. You’ll note that we’ve defined a number of properties now, including the dynamic determination of the script location so that we can then build paths appropriate to our project relative to the build script location. I use the convention in my build script properties to define the class of information being held then with a specific descriptor (e.g. dir_solution tells me that the property holds information on a directory, and specifically the solution directory).

We then define the dependencies for the default build action; being Clean, Build, Tidy.

You’ll note that the Clean task here performs a couple of other actions as a part of the clean process. Powershell support conditional flow, in this case a test to see if the build directory exists, and if it does to remove all items inside it (tidying up after the last build). It then also creates a new directory final to setup for our build process (which is not entirely shown in the script here).

The last step of the Clean task is to invoke the msbuild Clean task.

The Build task is called next, no surprises here.

The Tidy task can then perform clean up, in this case, it removes directories, copies items from working directories and removes working directories. I’ve missed out a number of steps in this script to keep it readable; but basically the whole script performs obfuscation and versioning tasks that use working folders for binaries and other files being operated on.

I think that you’ll agree that the psake script is pretty readable and relatively easy to follow, even for the uninitiated to Powershell.

Invoking the Script

You can invoke the script ‘manually’ from a pwoershell prompt:

invoke-psake < script name >

This can be placed into a batch file if you want to run it easily from explorer:

powershell -noexit invoke-psake < script file name >

The no exit parameter tells Powershell to not close after completion so you can easily see the output. The invoke-psake command is what interpets and executes the psake script.

You’ll see the script output in the a cmd window.

That’s psake. I think it has a lot of potential, and it seems to have resolved many of the issues I was having using msbuild xml natively.

Post Script: Okay, so after re-engineering my builds using psake, I finally found what was causing the problems I was having with msbuild. It was actually the fact that I was building to a shared NAS drive (in order to save space on my desktop). The issues were not arising when building in Visual Studio, just msbuild from the command line. Psake ended up having similar issues when building to the shared NAS drive on the msbuild call; perhaps not as bad as using the msbuild script – but thats a bit subjective. I’m still going to keep using psake as I just like the fact I can read it a bit easier and it seems more natural from a procedural standpoint – however I’ve moved my project off the NAS onto a local SSD and all build issues have evaporated on that machine.