The PowerShell based tool psake is a great way to orchestrate a build. It can also be used for many other kinds of orchestration. One of the trickiest parts is kicking off the process in the first place.

If your user has installed psake as a global tool, then it’s very easy to launch processing by using the invoke-psake cmdlet.

But what do you do if psake hasn’t been globally installed?

The approach I take is to have a utility script (normally called bootstrap.ps1) that takes care of loading the psake PowerShell module in a robust way. This is called at the start of each build script - here’s an example script, integration-build.ps1:

. .\scripts\bootstrap.ps1
invoke-psake -buildFile .\scripts\psake-build.ps1 -taskList Integration.Build

(I have a habit of stashing support scripts like this one in a separate \scripts\ folder so they don’t pollute the root folder of a project.)

The core of bootstrap.ps1 is a simple function that tries to load psake from a specified directory. That function is called multiple times, probing several likely locations for psake. Let’s look at how that function breaks down:

function TryLoad-Psake($path) {

    $toNatural = { [regex]::Replace($_, '\d+', { $args[0].Value.PadLeft(20) }) }

When we search for psake, it’s possible we’ll find multiple matches - if this happens, we want to ensure that we select the most recent release (highest version number). The $toNatural block transforms any numbers in the filename, ensuring we get a numeric sort, not an alphabetical one.

    $psakePath = `
        get-childitem $path\psake*\psake.psm1 `
            -ErrorAction SilentlyContinue -Recurse `
        | Sort-Object $toNatural | select-object -last 1

We use get-childitem to search for the module file psake.psm1 underneath the directory we’re passed, sorting the matches into order by their version, and selecting the highest one.

    if ($psakePath -eq $null) {
        Write-Output "[x] Psake not found within $path"
    } else {
        import-module $psakePath
    }

If we don’t find psake, we write a logline so that consumers know where we looked - this helps with the troubleshooting scenario where someone is trying to work out why the scripts aren’t working.

}

With that helper function in place, we can start trying to load psake, checking a number of possible locations. For each one, we check first to see if we’ve got the module already loaded.

We begin by trying to load it from a directory local to the project - this allows us to force a particular version by including directly in the folder structure:

if ((get-module psake) -eq $null) {
    TryLoad-Psake .\lib\psake
}

If that doesn’t work, we ask PowerShell to load it from the default module location:

if ((get-module psake) -eq $null) {
    # Don't have psake loaded, try to load it from PowerShell's default module location
    import-module psake -ErrorAction SilentlyContinue
}

If neither of those work, we try the local .\packages\ folder (in case NuGet has already downloaded it), as well as the global Chocolatey cache:

if ((get-module psake) -eq $null) {
    # Not yet loaded, try to load it from the packages folder
    TryLoad-Psake ".\packages\"
}

if ((get-module psake) -eq $null) {
    # Not yet loaded, try to load it from the chocolatey installation library
    TryLoad-Psake $env:ProgramData\chocolatey\lib
}

Newer versions of NuGet use a global machine cache, to save on disk space, so it’s worth looking there. There’s no environment variable; instead, we need to run a command to find the location. We use nuget.exe if we can, but fall back to dotnet.exe if needed.

function TryLoad-Psake-ViaNuGetCache()
{
    $locals = $null

Using get-command, we search the PATH on the machine to find the application we want. The SilentlyContinue option means that we don’t get an error, just a $null if it can’t be found.

    $nuget = get-command nuget -ErrorAction SilentlyContinue
    $dotnet = get-command dotnet -ErrorAction SilentlyContinue

If we found nuget.exe, we use it to get a list of cache folders to scan. If we didn’t find it, we’ll use dotnet.exe if that was found:

    if ($nuget -ne $null) {
        # nuget returns a list of the form "label : folder"
        Write-Output "[-] Finding NuGet caches via nuget.exe"
        $locals = & $nuget locals all -list
    } elseif ($dotnet -ne $null) {
        # dotnet returns a list of the form "info : label : folder"
        # So we strip "info : " from the start of each line
        Write-Output "[-] Finding NuGet caches via dotnet.exe"
        $locals = & $dotnet nuget locals all --list | % { $_.SubString(7) }
    }

Each command returns a list of locations, listing both a name and a folder. Unfortunately, the two lists are slightly different, so there’s a little bit of list mangling required.

Now we have a list of possible folders - we scan through them, trying to load psake from each one.

    foreach($local in $locals)
    {
        $index = $local.IndexOf(":")
        $folder = $local.Substring($index + 1).Trim()
        TryLoad-Psake $folder
    }
}

Before calling TryLoad-Psake-ViaNuGetCache, we need to set an environment variable so that dotnet doesn’t display a welcome message if it’s the first run ever (credit to Pete for finding this edge case).

$env:DOTNET_PRINT_TELEMETRY_MESSAGE = "false"

if ((get-module psake) -eq $null) {
    # Still not loaded, let's look in the various NuGet caches
    TryLoad-Psake-ViaNuGetCache
}

Once finished, we report on the location where psake was found, so that our user knows which version is being used. If we didn’t manage to load it, we abort with an error.

$psake = get-module psake
if ($psake -ne $null) {
    $psakePath = $psake.Path
    Write-Output "[+] Psake loaded from $psakePath"
}
else {
    Write-Output "[!]"
    Write-Output "[!] ***** Unable to load PSake *****"
    Write-Output "[!]"
    throw "PSake not loaded"
}

In use, the output of bootstrap.ps1 looks like this:

PS> .\build-site.ps1
[x] Psake not found within .\lib\psake
[x] Psake not found within .\packages\
[+] Psake loaded from C:\ProgramData\chocolatey\lib\psake\tools\psake\psake.psm1

(Sample taken from the output of the script that I use to generate the static HTML for this very site.)

In most cases, this script is able to load Psake for use; if it doesn’t, the user hopefully has enough information to try and troubleshoot the issue.

Prior post in this series:
Test Coverage History

Comments

blog comments powered by Disqus