This post is translation of my article for czech Technet Flash Magazine.

Today I'd like to show you a module that got inspiration from other languages and that is mostly used for build automation. Besides that you can use it in many other situations when you need to perform several steps, one after another, that depend on each other. You can download the module from https://github.com/JamesKovacs/psake; and now it is obviously clear that its name is psake.

Why psake and not msbuild?

Msbuild can be used for build automation as well, right? The answer is simple: if your hobby is editing xml files, then you don't mind when you have to configure msbuild. For the others there is psake. Psake (or PowerShell) offers capabilities of scripting language that you can hardly find in xml-based languages. Msbuild of course offers many features, so the best choice is to combine PowerShell and psake together.

Besides that you can use psake for administration tasks. Everything what you can find in PowerShel, can be of course used in psake. It's because psake is written in PowerShell. The following examples will tell you more.

First steps

Go to psake homepage and after click on Download choose last item with label 4.00. Then unpack the downloaded zip file into a directory (e.g. c:\psake). Run PowerShell console and continue with:

PS> Set-Location c:\psake
import-module .\psake.psm1
# psake modul is imported now

Get-Help Invoke-psake -Full

Psake is very well documented module. That's why the last command shows parameters that you can use when calling Invoke-Psake. At the end you can find some examples from which you can pretty well imagine how psake works.

Simple example

I'll show you a very simple example. What it does:

  1. It copies directoryd:\temp\code to d:\temp\codebak (no recursion for the sake of simplicity).
  2. It lists all the files from d:\temp\codebak and stores their names to d:\temp\codebak\files.txt.
  3. It executes a commit command to VCS.

You can see that each of the steps follows the previous one. Usually it doesn't make sense to perform last two if the first one failed. However, in some situations you might need to run them in separate. Ok, your psake script would consist of these tasks:

task default -depends Full
task Full -depends Backup, ListFiles, Commit
task Backup {
  Write-Host Backup
  gci d:\temp\code | ? { !$_.PsIscontainer } | copy-Item -destination d:\temp\codebak 
}
task ListFiles {
  Write-Host Files list
  gci d:\temp\codebak | Select -exp FullName | sc d:\temp\codebak\files.txt
}
task Commit {
  Write-Host Commit
  Start-Process GitExtensions.exe -ArgumentList commit, d:\temp\codebak
}

Save this script as psake-build.ps1 and run. If you don't specify task name, psake uses task with name default:

PS> Invoke-psake -buildFile d:\temp\psake\psake-build.ps1 
Executing task: Backup
Backup
Executing task: ListFiles
Files list
Executing task: Commit
Commit

Build Succeeded!

----------------------------------------------------------------------
Build Time Report
----------------------------------------------------------------------
Name      Duration
----      --------
Backup    00:00:00.1396781
ListFiles 00:00:00.0548712
Commit    00:00:00.3255901
Full      00:00:00.5288634
Total:    00:00:00.6099274

After the script finishes you will see a window with GitExtensions prepared and waiting. Notice the nice summary at the end!

In case you would like to execute only some tasks, specify them as value for parameter -taskList:

PS> Invoke-psake -buildFile d:\temp\psake\psake-build.ps1 -task Backup, Commit

If any of the tasks fails or throws an exception, the script is stopped and the following tasks are not executed. You can try it very easily if you try to copy nonexisting directory: $codeDir = 'd:\temp\doesntexist':

PS> Invoke-psake d:\temp\psake\psake-build.ps1
Executing task: Backup
Backup
psake-build.ps1:Cannot find path 'D:\temp\doesntexist' because it does not exist.

In case the error is not serious and it is perfectly ok to continue, just use parameter -ContinueOnError:

PS> Invoke-psake d:\temp\psake\psake-build.ps1
Executing task: Backup
Backup
----------------------------------------------------------------------
Error in Task [Backup] Cannot find path 'D:\temp\doesntexist' because it does not exist.
----------------------------------------------------------------------
Executing task: ListFiles
....

Psake Parameters

As I said earlier, Psake script is written in PowerShell. Therefore we can save name of directories in script variables so that the script will be much clearer and more maintainable.
If you look into some psake scripts, you will see sometimes properties { $var1 = 'value'; ... }; should you use it?

If the directory names are only constants and there is no need to change them, use any approach. Variable in a script is maybe the best one. But if you need to define a default value in a script and override the value from command line, use construct properties {... } in combination with parameter -properties. File psake-build.ps1 would look like this:

#constant that can be changed only from here
$codeDir = 'd:\temp\code'
properties {
  #variable that can be changed from command line; this is just a default value
  $backupDir = 'd:\temp\codebak'
}

task default -depends Full
task Full -depends Backup, ListFiles, Commit
task Backup {
  Write-Host Backup
  gci $codeDir | ? { !$_.PsIscontainer } | copy-Item -destination $backupDir 
}
...

… and you would call Invoke-Psake with parameter -properties:

PS> Invoke-psake -buildFile d:\temp\psake\psake-build.ps1 -properties @{ backupdir = 'd:\temp\otherbackdir' }

Note the type of the value – it is hashtable, not scriptblock. Every item in the hashtable specifies a variable that will be evaluated in the same scope as properties {... } in the psake script (but later).

Sidenote: I didn't tell you whole truth. If you have a line $backupdir = 'some default path' outside of block properties { ... }, even this variable can be changed from command line via Invoke-Psake ... -properties @{backupdir= 'other path'}. Anyway, I wouldn't recommend this approach; the way how parameters are evaluated and working with scope could change in later versions and this script could stop working.

What are Parameters good for

Ok, we saw that there are some -properties. But you might note that psake allows you to specify parameters of function Invoke-Psake via -parameters. Type of -parameters is again hashtable with the same structure as -properties. So a new variable is created from each pair key-value. These variables can be used in function properties { ... } in our build script– that means that we parametrize our script. I hope you will see the difference from the example.

Let's suppose we have psake script like this:

properties { $s = get-service $services }
task default
task stop { $s | stop-service -whatif }
task start { $s | start-service -whatif }

And we pass name or names of services we would like to stop/start:

PS> Invoke-psake -buildFile d:\temp\psake\psake-services.ps1 -task start -parameters @{ services = 'W3SVC' }

What have we done? We stored list of services which names match variable $services and that was done block properties.
Somebody could complain that we get the same effect if we define an initialization task and that will be called as first task before the others. Look at the changed code:

task default
properties { $services = "noservice" }
task init { $s = get-service $services }
task stop -depends init { Write-Host stop service $s; $s | stop-service -whatif }
task start -depends init { Write-Host start service $s; $s | start-service -whatif }

And we would call the script without -parameters:

PS> Invoke-psake -buildFile d:\temp\psake\psake-services2.ps1 -task start -properties @{ services = 'W3SVC' }
Executing task: init
Executing task: start
start service
psake-services2.ps1:Cannot bind argument to parameter 'Name' because it is null.

As you can see the idea is not bad. It just doesn't work in psake. Every task (or better scriptblock that is represented by the task) runs in its own scope and that's why variables are not shared among the tasks. We could of course hack it with scope modificator script:, but I could hardly recommend it, because you change inner state of the module.

Psake for developers

I have been writing about psake only in general terms, but I mentioned almost everything what you will need for some automation tasks.
There is something more for programmers. The best feature is function exec, which terminates the psake script in case that application called inside exec finishes with error. The error is indicated by a return code. Body of exec is very simple, let's look at it with Get-Content function:\exec.

Task that builds solution is almost one liner when you use exec:

$framework = '4.0' 
...
task Build { 
  exec { msbuild $slnPath '/t:Build' }
}

You can of course pass more parameters to msbuild, that was only quick example. You have to tell psake which version of .NET framework you want to use and psake ensures that you call the right msbuild exe.

A simple example of build automation for us (developers) could cover clean, build, tests and copying to release directory:

$framework = '4.0'
$sln = 'c:\dev\.....sln'
$outDir = 'c:\dev\...'

task default -depends Rebuild,Test,Out
task Rebuild -depends Clean,Build
task Clean { 
  #exec { msbuild $slnPath '/t:Clean' }
  Write-Host Clean....
}
task Build { 
  #exec { msbuild $slnPath '/t:Build' }
  Write-Host Build....
}
task Test { 
  # run nunit console or whatever tool you want
  Write-Host Test....
}
task out {
  #gci somedir -include *.dll,*.config | copy-item -destination $outDir
  Write-Host Out....
}

Can this pure hapinness be even more beautiful? Yes – at least for those who use mouse more than keyboard. In some cases, this is much faster then typing on command line. Let's create a GUI for the build automation.

Psake and GUI

We will create GUI in .NET WinForms. Remember, this is just a sample that will show you that it is possible. So, the code will be as concise as possible.
PowerShell has to be run with -STA switch.

Add-type -assembly System.Windows.Forms
Add-type -assembly System.Drawing
if (! (get-module psake)) {
  sl D:\temp\psake\JamesKovacs-psake-b0094de\
  ipmo .\psake.psm1
}

$form = New-Object System.Windows.Forms.Form
$form.Text = 'Build'
$form.ClientSize = New-Object System.Drawing.Size 70,100

('build',10), ('test',30), ('out', 50) | % { 
  $cb = new-object Windows.Forms.CheckBox
  $cb.Text = $_[0]
  $cb.Size = New-Object System.Drawing.Size 60,20
  $cb.Location = New-Object System.Drawing.Point 10,$_[1]
  $form.Controls.Add($cb)
  Set-Item variable:\cb$($_[0]) -value $cb
}
$go = New-Object System.Windows.Forms.Button
$go.Text = "Run!"
$go.Size = New-Object System.Drawing.Size 60,20
$go.Location = New-Object System.Drawing.Point 10,70
$go.add_Click({
  $form.Close()
  if ($cbbuild.Checked) { $script:tasks += 'Rebuild' }
  if ($cbtest.Checked) { $script:tasks += 'Test' }
  if ($cbout.Checked) { $script:tasks += 'Out' }
})
$form.Controls.Add($go)

$script:tasks = @()
$form.ShowDialog() | Out-Null
if ($script:tasks) {
  Invoke-psake -buildFile d:\temp\psake\psake-devbuild.ps1 -task $tasks
}

Only several lines of code and you have a GUI. I use similar form to create quite complex msbuild/psake configuration and I like it mainly because I don't have to remember all the task names.

The part of code with module import just checks if psake is already imported. If you import psake twice and more, then calling Invoke-Psake finishes with failure. It's problem of psake itself. Generally there should be no problem with modules imported more than once.

Note: you probably saw how I work with $script:tasks outside of event handler. Why don't I call Invoke-Psake from the handler itself? Psake outputs results about the tasks (tables, timings, info about current task) to the pipeline so that you can redirect the output to file. In the event handler the output is processed differently; it is not sent to main pipeline so you don't see the output. The only messages you can see are produced by Write-Host (written of course to console).

Change psake

I'll show you, how you can change module behaviour without changing the file. It is general technique that is not used very often, because it changes the inner environment of the module. Without good knowledge how the module works it could stop working correctly. Anyway, why not to learn something?

Let's say we want to write current user and computer name at the end of psake output. So, you need to edit function Write-TaskTimeSummary. Actually, we don't change it:

PS> $module = Get-Module psake
PS> & $module { ${function:script:Write-TaskTimeSummaryBak} = (gi function:\Write-TaskTimeSummary).ScriptBlock }
PS> & $module { ${function:script:Write-TaskTimeSummary} = {
  . Write-TaskTimeSummaryBak
  "$env:USERNAME @$env:COMPUTERNAME"
}}

We created new function Write-TaskTimeSummaryBak as a backup of function Write-TaskTimeSummary. Then we changed definition of Write-TaskTimeSummary so that it calls the backuped function and then it adds user and computer names.

The general pattern how to edit module behaviour is :

& (Get-Module mymodule) { command that you want to execute in module scope }

And we are finished. You can find more info at psake wiki. For example ho to set up function called before/after each task or how to nest psake builds.
I hoped you enjoyed psake as well as I do!

Meta: 2011-03-19, Pepa