Open Source.NET

Enable Coded Builds with PowerShell and psake

Open Source .NET columnist Ian Davis explores creating domain specific languages to run coded builds.

In an earlier column I explored the use of codified build scripts using tools like Albacore and Rake. There is a lot of focus and work going into the migration from XML- and GUI-based build systems to coded build scripts using tools like Rake and Albacore, FAKE, and psake. Leveraging existing programming languages, we can utilize their capabilities in creating domain specific languages (DSL) to run our builds.

The Power of PowerShell
PowerShell is a scripting language and task automation framework that was released by Microsoft in late 2006. If you haven't run into or used PowerShell yet, you are missing out on a lot of power, which could be employed in your everyday workflow. It allows .NET developers to take the blue pill and feel right at home using the command line they know and the .NET APIs they already use. PowerShell is a very flexible and expressive procedural language with pipeline support. PowerShell also allows blocks of script to be treated as first-class objects. All of these things allow PowerShell to be incredibly powerful, flexible, and a great target for creating a DSL.

We could use PowerShell directly to script our builds, and it would be functional. Assuming we have msbuild in our path, we can create a simple build script with the usual properties:

$solution_name = "PowerStudio"
$base_directory = resolve-path .
$build_configuration = "Release"
$solution_file = "$base_directory\$solution_name.sln"
msbuild /p:Configuration="$build_configuration" $solution_file

All of this could also be done in one line:

msbuild /p:Configuration=Release .\PowerStudio.sln

This is great, but we are missing a few things that we generally need in our build scripts:

  • Failure detection
  • Logging
  • Targets
  • Error handling
  • Task filtering
  • Pre/Post conditions
  • Overriding properties
  • Environment detection, usage, and variance
  • etc.
From PowerShell to psake
With the creation of PowerShell, we now have a powerful scripting system with which we can automate many tasks we encounter as administrators and developers. The psake build system is a DSL built on PowerShell by James Kovacs. Leveraging the expressiveness of PowerShell, psake allows us to write build scripts with minimal effort utilizing the command line, .NET Framework, and any additions/extensions you make or import.

The psake DSL is broken down into eight primary commands and a simple grammar. Only the Task verb is required when creating a script. The psake commands are actually method calls. Everything that you provide in the DSL is a set of predefined method calls and their arguments; this includes blocks of script, which are interpreted and executed by psake.

We can create a simple psake script that will define the build with a single dependent task and save it to build.ps1.

Task default -Depends Build
Task Build { Write-Host "Hello, World!" }

You can download psake from its GitHub download page, or via NuGet. Assuming NuGet.exe is in our path, we can simply execute the following:

>NuGet install psake
>Successfully installed 'psake'.
>.\psake.\tools\psake.cmd .\build.ps1
psake version 4.00
Copyright (c) 2010 James Kovacs

Executing Build
Hello, World!

Build Succeeded!

Build Time Report
Name   Duration
----   --------
Build  00:00:00.0074854
Total: 00:00:00.0335057

We can see psake loaded our script, executed our build, and reported the success of our build. You can verify action occurred in your scripts using Assert, PreCondition, and PostCondition. If we modified our script to have Assert($false) "Build failed!" after the Write-Host command, we would get the following output:

>.\psake.\tools\psake.cmd .\build.ps1
psake version 4.00
Copyright (c) 2010 James Kovacs

Executing Build
Hello, World!
7/14/2011 7:45:36 AM: An Error Occurred:
Assert: Build failed!

While this is a simple example, it illustrates the ease of verifying that a condition is met and will fail our build if needed. We can also use PreCondition to prevent tasks from executing if their requirements are not satisfied ahead of time. Let's change our build.ps1 to require xUnit for unit tests. Instead of failing the build as an Assert would do, we are just going to skip the task for now.

Task default -Depends Build, Test

function Test-CommandExists($command){
    ((Get-Command $command -ea SilentlyContinue) | Test-Path) -contains $true

Task Build {
    Write-Host "Building..."

Task Test -PreCondition { return Test-CommandExists("xunit") } {
    Exec { xunit ".\build\Unit.Tests.dll" }

We can see that our Test task was skipped.

>.\psake.\tools\psake.cmd .\build.ps1
psake version 4.00
Copyright (c) 2010 James Kovacs

Executing Build
Precondition was false not executing Test

Build Succeeded!

Build Time Report
Name   Duration
----   --------
Build  00:00:00.0086533
Test   0
Total: 00:00:00.0987873

DSL Features
You will notice that psake, while very useful, is missing features that other build DSLs have. The psake grammar is very complete in expressing what we need to do in a build, and PowerShell itself is usually capable of doing/executing anything you would expect to have been added in the other DSLs.

  • Conditional execution
  • Token replacement
  • Regex evaluation
  • CSC
  • NuGet
  • MSBuild
  • NAnt
  • MSTest
  • NUnit
  • XUnit
  • etc.

If you really want to flex your PowerShell usage, grab the open source PowerShell Community Extensions (PSCX), which will give you compression, network, virtual disk mounting, Active Directory, CD mastering and much more.

Here is a more flushed out build script outlining some unit tests, build cleanup, executing .NET code, command-line overridable properties and compilation:

properties {
  $solution_name = "PowerStudio"
  $base_directory = Resolve-Path .
  $build_directory = "$base_directory\build"
  $build_configuration = "Release"
  $tools_directory = "$base_directory\tools"
  $solution_file = "$base_directory\$solution_name.sln"
  $release_directory = "$base_directory\release"
  $max_cpu_count = [System.Environment]::ProcessorCount / 2
  $xunit = "$tools_directory\xunit.NET\xunit.console.exe"
  $build_in_parralel = $true

Import-Module ..\pscx\Pscx.psd1

# ?: is a PSCX alias for Invoke-Ternary filter to emulate ternary expressions
$vcargs = ?: {$Pscx:Is64BitProcess} {'amd64'} {'x86'}
$VS100VCVarsBatchFile = "${env:VS100COMNTOOLS}..\..\VC\vcvarsall.bat"
Invoke-BatchFile $VS100VCVarsBatchFile $vcargs

Task Build -depends Compile
Task Default -depends Build
Task Release -depends Default, IntegrationTest

task Test -depends Compile { 
  assert(Test-Path($xunit)) "xUnit must be available."
  exec { $xunit "$build_directory\Unit.Tests.dll" }

task IntegrationTest -depends Test { 
  assert(Test-Path($xunit)) "xUnit must be available."
  exec { $xunit "$build_directory\IntegrationTests.dll" }

task Init -depends Clean {
  new-item $release_directory -itemType directory | Out-Null
  new-item $build_directory -itemType directory | Out-Null

task Compile -depends Init {
  exec { msbuild /m:$max_cpu_count /p:BuildInParralel=$build_in_parralel /p:Configuration="$build_configuration" /p:Platform="Any CPU" /p:OutDir="$build_directory"\\ "$solution_file" }

task Clean { 
  remove-item -force -recurse $build_directory -ea SilentlyContinue | Out-Null
  remove-item -force -recurse $release_directory -ea SilentlyContinue | Out-Null

I highly recommend that you visit the psake wiki for more information and details on features that were not covered. There is also a psake contrib project for extending psake to provide tighter integration with other systems. Take a step over to James Kovacs' blog for news and info on psake and a great number of other topics.

About the Author

Ian Davis is the Master Code Ninja for software architecture and development consulting firm IntelliTechture. A C# MVP, Davis is an expert on the .NET Tramework and co-organizer of the Spokane .NET User Group who frequently speaks at industry events. He spends most of his free time as an open source author and advocate, publishing and working on many open source projects.

comments powered by Disqus


  • Xamarin.Forms 5 Preview Ships Ahead of .NET 6 Transition to MAUI

    Microsoft shipped a pre-release version of Xamarin.Forms 5 ahead of a planned transition to MAUI, which will take over beginning with the release of .NET 6 in November 2021.

  • ML.NET Improves Object Detection

    Microsoft improved the object detection capabilities of its ML.NET machine learning framework for .NET developers, adding the ability to train custom models with Model Builder in Visual Studio.

  • More Improvements for VS Code's New Python Language Server

    Microsoft announced more improvements for the new Python language server for Visual Studio Code, Pylance, specializing in rich type information.

  • Death of the Dev Machine?

    Here's a takeaway from this week's Ignite 2020 event: An advanced Azure cloud portends the death of the traditional, high-powered dev machine packed with computing, memory and storage components.

  • COVID-19 Is Ignite 2020's Elephant in the Room: 'Frankly, It Sucks'

    As in all things of our new reality, there was no escaping the drastic changes in routine caused by the COVID-19 pandemic during Microsoft's big Ignite 2020 developer/IT pro conference, this week shifted to an online-only event after drawing tens of thousands of in-person attendees in years past.

Upcoming Events