Psake automates your .NET build using PowerShell. Here's how.

8 minute read Published:

What is Psake and why do you want it? Psake is a build automation tool written in PowerShell. That means you don’t have to mess around with MSBuild scripts and trying to force procedural code into a declarative XML format - which is just weird. Psake frees you from the shackles of awkwardness. But how does it work?

With Psake, you write a PowerShell script consisting of one or more build “tasks” which you can invoke from PowerShell, or even from a continuous integration build server. You invoke your build task via the Invoke-Psake function or, more commonly, via a build.ps1 or build.bat script. Each build task can depend on zero or more other build tasks, and it’s possible to set a default build task. Each build task can do whatever you’d like, including cleaning your solution, building your assemblies, running your acceptance tests, running your unit tests, generating code coverage reports, packaging the binaries in a zip file, a NuGet package, etc. If you dream it, Psake will build it.

In 18 seconds Psake builds it, tests it, and packages it. Boom.

Before we continue, “Psake” is pronounced SAH-key, like the Japanese rice wine. As stated on their project readme, “it does NOT rhyme with make, bake, or rake.” Maybe I should add that it doesn’t really start with a “P” either. :)

Psake in action: A working example

Here’s a fun little project on Github called Resfit. It’s “a multipurpose tool for working with and mangling your resources (*.resx) in .NET.” I was interested in crafting a project from the ground up using Acceptance Test Driven Development in order to see what sort of difficulties I would run into, both technical and otherwise. To keep the TDD feedback loop nice and tight, I wanted to run all of the acceptance tests and unit tests early and often. I started the project with a trial of NCrunch, which I loved, but the trial ran out. So I thought it’d be great to at least run all of the tests with each build. That’s when Psake came in swinging like a Psamurai.

(Check out the full version of the build script here.)

For starters, here is a barebones Psake build script, which will build your solution from the command line, assuming msbuild.exe is found in your command path. Notice how simple it is. You could even forego the explaining variables and just hardcode everything if you really wanted to get down to bare metal.

# Filename: default.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path

properties {
    $solutionDirectory = Split-Path -Parent $here
    $solutionFile = $(Get-ChildItem -Path $solutionDirectory -Filter *.sln | Select -First 1).FullName
    $outputDirectory = Join-Path $solutionDirectory ".build"
    $buildConfiguration = "Release"
    $buildPlatform = "Any CPU"
}

Task Default -depends Build -description "Default task"

Task Build -description "Build solution" {
    Exec {
        msbuild $solutionFile /verbosity:quiet /maxcpucount "/property:Configuration=$buildConfiguration;Platform=$buildPlatform;OutDir=$outputDirectory"
    }
}

In this example you can see the basic layout of a Psake script. You have an array of properties{ }, which are initialized to default values, but may be overridden by the call to Invoke-psake (more on that later).

The Default task gets executed if you invoke psake without any parameters. It has no script body but depends on one or more other build tasks. In this sample script the Default task depends on Build.

Inside the Build task, notice that script block preceded by Exec. This is very important. Exec makes sure that the build fails if the code inside the script block returns a nonzero exit code. If you don’t wrap the call to msbuild inside the Exec block, your psake build won’t fail even though the code fails to compile.

So there you have the basics. Pretty simple, right?

More Psake, please.

As you’ll notice if you’ve taken a peek at Resfit’s psake script, there’s a lot more you can do.

For one thing, you can import additional PowerShell scripts with the Include command. Here I import a poorly named script containing a loose assortment of helper functions. For example, if you look inside the helper script, you’ll notice a useful function called Find-PackagePath, which gets the directory path of the most recent version of a NuGet package.

Include ".\psake_helpers.ps1"

The Framework function allows you to target a specific version of the .Net framework.

Framework "4.5.2"

FormatTaskName allows you to configure the way the task name appears when displayed during the build. Notice that it’s a format string with that ‘{0}’ in the middle.

FormatTaskName ">>>-- Executing {0} Task -->"

You probably noticed that each task in my script has a -description parameter. While this can serve as a helpful comment when you’re reading the psake script, it is also the text used to document your build tasks when you call WriteDocumentation. Note the build task called ?. It can be called anything (Help, List, Docs, …) All it does is call WriteDocumentation, which just lists all of your build tasks along with their dependencies and descriptions.

Task ? -description "List tasks" { WriteDocumentation }

As you continue to read through the script, you’ll notice the Check-Environment task. This is the task where I make sure that all of our required dependencies are in place. To that end, Task has a useful parameter, -requiredVariables, which lets you specify variables which must have values for the task to succeed. You’ll also notice a series of Assert statements. With the first few I validate the build configuration and platform variables, and the remaining Asserts make sure the necessary executables are in place, such as the NUnit test runner, OpenCover, etc.

Task Check-Environment `
    -description "Verify parameters and build tools" `
    -requiredVariables $outputDirectory, $testResultsDirectory, $solutionFile, $testCoverageDirectory, $coreNuspec `
{
    Assert ("Debug", "Release" -contains $buildConfiguration) `
        "Invalid build configuration '$buildConfiguration'. Valid values are 'Debug' or 'Release'"
    Assert ("x86", "x64", "Any CPU" -contains $buildPlatform) `
        "Invalid build platform '$buildPlatform'. Valid values are 'x86', 'x64', or 'Any CPU'"
    Assert (Test-Path $nunit) `
        "NUnit console test runner could not be found"
    Assert (Test-Path $openCover) `
        "OpenCover console could not be found"
    Assert (Test-Path $reportGenerator) `
        "ReportGenerator console could not be found"
    Assert (Test-Path $7zip) `
        "7-Zip console could not be found"
    Assert (Test-Path $nuget) `
        "NuGet CommandLine could not be found"
    Assert (Test-Path $pester) `
        "Pester.bat could not be found"
    Assert ($git -ne $null) `
        "Git is not in your command path. Install Git."
}

As you continue to browse the script, you’ll notice tasks for running the unit tests, acceptance tests, and pester tests. The unit and acceptance tests are run through OpenCover, so you also get a nice code coverage report, which was another feature that I missed from NCrunch. All of the tasks are run by the default task, which is Package in this case. How does that work? Well, notice that Package depends on PackageZip and PackageNuget, which in turn depend on BuildAndRunAllTests, which in turn depends on Build and Tests… You get the idea.

Where do I get my first sip of Psake?

Like most things these days, Psake is available as a NuGet package. I recommend creating a separate “Build” project and installing Psake in it. That keeps all of your build stuff in one place and doesn’t contaminate your core domain with build artifacts.

Getchas and gotchas

What could go wrong?

Remember the `s

My biggest tip is don’t forget the backticks ( ``` ) when breaking a line for readability. Look carefully at the following build task declaration and you’ll notice a backtick placed at the end of each line, even a backtick on the line preceding the opening curly brace.

Task Clean `
    -description "Clean up build cruft and initialize build folder structure" `
    -depends Check-Environment `
{
    New-Directory $outputDirectory
    Remove-Contents $outputDirectory
    # ...
}

When you forget those EOL backticks, really weird things happen that don’t seem at all related. Consider keeping each task declaration on one line until you’re confident that everything is working. Later on you can reformat the code for clarity if you wish.

If they build it, will it come?

Another tip is, make sure your solution builds from the command line after a fresh clone, or after a git clean -fxd. It’s easy to forget some bootstrapping that causes your build to fail from a pristine state.

For instance, you’ll notice that the solution contains a build.ps1 script, which is very handy. Rather than having to import the psake module and then call Invoke-Psake -buildFile .. -taskList.. -framework .. -properties .. -yadayada, you wrap all of that into a single script, build.ps1, making executing your build script from the command line as easy as typing “build”. But I digress. After cleaning my solution, running build.ps1 failed miserably, complaining that it couldn’t find the Psake module. facepalm. Cleaning the solution blew away my NuGetpackages folder, including the Psake package. The solution was to restore the necessary NuGet packages in the build.ps1 script, before invoking Psake. The current implementation relies on PowerShell finding nuget.exe in the user’s path, but I plan on including a standalone version of the nuget executable so that the user has one less dependency to worry about.

Conclusion

Well there you have it, folks: Psake in a nutshell. Or more appropriately, Psake distilled. Feel free to leave your comments below. What’s worked for you? Any Psake “gotchas” you’ve run into?