If you are working in a VisualStudio/.NET
environment, MSBuild
seems a natural build tool since VS solutions and projects files are MSBuild
files. To build your solution you can just call msbuild
in your solution directory without having to write any additional script or code. Nevertheless, as soon as you need something more advanced (like running an SQL
query), MSBuild
quickly shows its limits.
Why XML is not a good build technology
The example below shows an MSBuild
target for executing tests and creating reports.
<Target Name="UnitTest"> <Exec Command="del $(ProjectTestsUnit)\results.trx" ContinueOnError="true" /> <MSBuild Projects="$(ProjectFileTestsUnit)" Targets="Clean;Build" Properties="Configuration=$(Configuration)"/> <Exec Command="mstest /testcontainer:$(ProjectTestsUnit)\bin\$(Configuration)\$(ProjectTestsUnit).dll /testsettings:local.testsettings /resultsfile:$(ProjectTestsUnit)\results.trx" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode"/> </Exec> <Exec Command="$(Libs)\trx2html\trx2html $(ProjectTestsUnit)\results.trx" ContinueOnError="true" /> <Error Text="The Unit Tests failed. See results HTML page for details" Condition="'$(ErrorCode)' == '1'" /> </Target>
The problem with MSBuild
, like any other XML
-based build tool (such as ANT
or NANT
), is that XML
is designed to structure and transport data, and not to be used as a procedural language. Hence, you can’t easily manipulate files, handle environment variables, or seed a database. Even calling native commands or executables becomes cumbersome. Although I used to like ANT
, I now think it is foolish to use XML
for scripting builds, even if it comes with extra APIs for the basic tasks (e.g. the copy
target in ANT
, plus all the custom ANT
tasks).
A good build tool should be based on a native or popular scripting language such as Shell
, PowerShell
, Perl
, or Ruby
. Note that I did not mention Batch
and I would strongly recommend not using it. Maybe it is because I am not very good at it. Even if you are used to Batch
, it is well overdue to move to PowerShell
.
The last time I tried to use Batch
for my build file I ended up with things like the example below. The unit
or intg
targets are short enough, but as soon as you want to do more complex stuffs like in the seed
target, then the inability to break things into function makes your script very long, hard to read, and unmaintainable.
if [%1] EQU [unit] ( call msbuild build.xml /t:unittest if errorlevel 1 ( goto end ) call .\Libs\trx2html\trx2html JRProject.Tests.Unit\results.trx goto end ) if [%1] EQU [intg] ( call msbuild build.xml /t:intgtest if errorlevel 1 ( goto end ) call .\Libs\trx2html\trx2html JRProject.Tests.Integration\results.trx goto end ) [...] if [%1] EQU [seed] ( if [%2] EQU [desktop] ( call JRProject.Database/seed %3 %dbserver% 26333 %dbuser% %dbpassword% %desktop% %version% goto end ) if [%2] EQU [mobile] ( call JRProject.Database/seed %3 %dbserver% 26333 %dbuser% %dbpassword% %mobile% %version% goto end ) if [%2] EQU ( call JRProject.Database/seed %3 %dbserver% 26333 %dbuser% %dbpassword% %facebook% %version% goto end ) if [%2] EQU [allapps] ( call %~dp0go seed desktop %3 if errorlevel 1 ( goto end ) call %~dp0go seed mobile %3 if errorlevel 1 ( goto end ) call %~dp0go seed facebook %3 goto end ) call JRProject.Database/seed %2 %dbserver% 26333 %dbuser% %dbpassword% goto end )
So if MSBuild
or ANT
are no good, and Batch
does not fit the bill either, what is the alternative? Psake!
It is built on PowerShell
, which has some really cool functional capabilities, and heaps of CmdLets
to do whatever you need in Windows
7, 8, 2008, or SQL
Server.
I’ll show you some of its features by walking you through a simple example.
Example project
To follow my example, please create a new VisualStudio C# Console project/solution called PsakeTest
in a directory of your choice. Let’s implement the main
program to output a simple trace:
using System; namespace PsakeTest { class Program { static void Main(string[] args) { Console.WriteLine("This is the PsakeTest console output"); } } }
After building the project in VisualStudio, you should be able to run the resulting PsakeTest.exe
as follows:
Running Psake
With Psake comes one single PowerShell
module file: psake.psm1
. To get started, I recommend you do 2 things:
- place the
psake.psm1
module file in your project root directory - create 3 additional scripts:
psake.cmd,
psake.ps1
, andbuild.ps1.
psake.cmd
The main entry point, written as a batch file as a convenience so that you don’t have to start a PowerShell
console. It mostly starts a PowerShell
subprocess and delegates all calls the psake.ps1
script. You can also use this script to create or set environment variables as we’ll see later.
@echo off powershell -ExecutionPolicy RemoteSigned -command "%~dp0psake.ps1" %* echo WILL EXIT WITH RCODE %ERRORLEVEL% exit /b %ERRORLEVEL%
psake.ps1
This script does the following:
- Sets the execution policy so that
PowerShell
scripts can be executed - Import the
Psake.psm1
module - Invoke the Psake build file
psake.ps1
with all program arguments, in order to execute your build tasks - Exit the program with the return code from
build.ps1
param([string]$target) function ExitWithCode([string]$exitCode) { $host.SetShouldExit($exitcode) exit } Try { Set-ExecutionPolicy RemoteSigned Import-Module .\psake.psm1 Invoke-Psake -framework 4.0 .\build.ps1 $target ExitWithCode($LastExitCode) } Catch { Write-Error $_ Write-Host "GO.PS1 EXITS WITH ERROR" ExitWithCode 9 }
build.ps1
This is the actual build file where you will implement the build tasks. Let’s start with a simple compilation task.
##### ##### # PsakeTest Build File # ##### ##### Task compile { msbuild }
Now, if you open a command prompt, cd
to your project directory, and execute psake compile
you should see the following output:
Default Task
Psake
(like most build tools) has the concept of a default task, which will be executed when the Psake
build is run with no argument. So let’s add a default
task that depends on the existing compile
task and run the command psake
instead of psake compile
.
##### ##### # PsakeTest Build File # ##### ##### Task default -depends compile Task compile { msbuild }
Properties
Properties are variables used throughout your script to configure its behaviour. In our case, let’s create a property for our VS project’s $configuration
, which we use when executing msbuild
.
##### ##### # PsakeTest Build File # ##### ##### ########################## # PROPERTIES # ########################## properties { $configuration = Debug } ########################## # MAIN TARGETS # ########################## Task default -depends compile Task compile { msbuild /p:configuration=$configuration }
Functions
Because we use PowerShell
, we can implement and call functions to make sure the build tasks are kept small, clean, readable, and free of implementation details. In this instance, we will set the $configuration
property using the environment variable PSAKETESTCONFIG
.
##### ##### # PsakeTest Build File # ##### ##### ########################## # PROPERTIES # ########################## properties { $configuration = GetEnvVariable PSAKETESTCONFIG Debug } ########################## # MAIN TARGETS # ########################## Task default -depends compile Task compile { msbuild /p:configuration=$configuration } ########################## # FUNCTIONS # ########################## function GetEnvVariable([string]$variableName, [string]$defaultValue) { if(Test-Path env:$variableName) { return (Get-Item env:$variableName).Value } return $defaultValue }
We have created the GetEnvVariable
function that returns the value of an existing environment variable, or returns a user-defined default value if the environment variable does not exist. We use it to set the $configuration
property with the PSAKECONFIG
environment variable value.
We can now compile our code for the Release
configuration of the PsakeTest
project as follows:
set PSAKETESTCONFIG=Release psake
And this time the output trace will show you that the project is built in bin/Release
instead of bin/Debug
. This is a convenient way to drive the build using different configurations for different environments, like you would normally do in automated build tools.
Error Handling
Psake
error handling is like PowerShell
. It is based on throwing errors. This is one of the reasons why I chose to wrap all calls to psake.ps1
with the psake.cmd
batch file, so that I get a non-zero return code everytime the Psake
build fails.
Additionally, if your Psake
build executes a command line program (such as msbuild
, aspnet_compiler
, pskill
) rather than a PowerShell
function, it will not throw an exeception on failure, but return a non-zero error code. Psake
adds the exec
helper, which takes care of checking the error code and throwing an exception for command line executables.
In our case, we’ll modify the compile
task as follows:
Task compile { exec { msbuild /p:configuration=$configuration } }
Final Words
For me Psake
is the best alternative to write maintainable and flexible build scripts on Windows
(Rake
could be another one but I never tried it on Windows). In my current project we are moving all builds away from Batch/MSBuild
to Psake
, which is a relief.
I do recommend you use the setup with the 3 files that I have described here, since it provides the scaffolding for being able to call your Psake
build from any Windows
prompt.
Source code and downloads for Psake
can be found here.