Sunday, March 22, 2015

Bringing the power of PowerShell to your build scripts

If you look back at the last couple of years, you'll notice an increasing attention for best practices that should make us more professional software developers. We design our classes using Test Driven Development, we review our code in pairs, and we apply all kinds of architectural principles such as those represented by the S.O.L.I.D. acronym. In other words, we really care about our code. But what about our build scripts? Do we care as much about those as we do for our regular code? I doubt it, but shouldn't we?

About a year ago we switched from Microsoft Team Foundation Server's build environment to JetBrains TeamCity, mostly because my client was moving to Git and GitHub. After being used to the cryptic MSBuild XML documents (I never bothered with the Windows Workflow Foundation stuff), being able to use TeamCity's elaborate build step system seemed like an attractive approach. But with that, we almost made the mistake of treating build scripts as something exotic again.

Luckily, my (then new) colleague Damian Hickey convinced us to look at how the open-source community solves the build problem. Quite a lot of these projects use either a .bat file that directly invokes the msbuild executable or use PowerShell to do the same thing. As the teams didn't have any prior experience with PowerShell, being able to use TeamCity's graphical user interface sounded like the way to go.

Tying the build process to TeamCity has some drawbacks however.
  • You can't run a build outside TeamCity. Being able to fully test whether your local changes are working correctly becomes an invaluable capability that you really start to appreciate once you have it.
  • A codebase evolves over time, potentially involving changes in the way the code is build, tested and/or deployed. You wouldn't want to have separate build definitions for different branches, or worse, align the build definition with the changes in the code-base.
  • Being able to treat your build scripts as first-class citizens of your code base also enables the capabilities you are used to such as merging changes from different contributions, supporting pull requests, and analyzing the history of your script. Although TeamCity supports auditing, it'll be a completely different experience.
  • Although TeamCity is a state-of-the-art build engine, especially compared to Microsoft Team Build, being able to switch to another build product (such as AppVeyor) is a big advantage.

So putting your build definition in source control, next the code base it builds, was a big advantage for us. And though we didn't have much prior PowerShell experience, being able to use .NET classes provides a lot of flexibility. We decided to use an open-source PowerShell library called PSake (pronounced as sake) that combines the concepts of the old make build language with the power of PowerShell.

After unzipping the release and adding the files to your source control repository, a simple default.ps1 file might look like this:
task default -depends Compile task Compile -description "Compiles the solution" { exec { msbuild /v:m /p:Platform="Any CPU" TestApplication.sln /p:Configuration=Release /t:Rebuild } }

Notice that both msbuild and exec are wrapper functions provided by PSake. The first ensures that the right version of msbuild for the appropriate .NET Framework is used. The second ensures that the last exit code of a DOS command-line is converted in the correct PowerShell exception. You can run this build script using psake.ps1 or psake.cmd, depending on whether you're running it from a PowerShell console or a Command Prompt.

Since the default behavior is to run the default task in the default.ps1 script, you don't need to provide any parameters. If you want, you can specify an explicit task to run. Just run psake.cmd -help or psake.ps1 -help to get the very extensive help page.

Now suppose you want to parameterize the project's solution configuration (from release to debug). PSake supports script properties for which you can pass values as part of the Psake.cmd call.
properties { $Configuration = "Release" } task default -depends Compile task Compile { exec { msbuild /v:m /p:Platform="Any CPU" TestApplication.sln /p:Configuration=$Configuration /t:Rebuild } }

Now when you run the following it'll build the debug version of the solution.

.\psake.ps1 -properties @{"Configuration"="Debug")

In most projects, your build script will consist of multiple tasks each depending on other tasks. In the previous example, the default task depends on the Compile task. Obviously, much more elaborate dependencies are possible, including conditions.

properties { $PackageName = "" } task default -depends Compile, BuildNugetPackage task Compile { } task BuildNugetPackage -depends Compile -precondition { $PackageName -ne "" } { }

So in addition to verifying that the $PackageName variable must be specified as a property, you can also run the BuildNugetPackage task directly. But since it depends on the Compile step, the net result is the same as just running the default task. It's good practice to add the essential dependencies. BTW, if you want to make that package name property mandatory, you can write the task like this:

task BuildNugetPackage -depends Compile -requiredVariables $PackageName { }

Notice that running this build script from a build product is just going to route the PowerShell output as the build output. However, both TeamCity and AppVeyor have ways to enable more deep integration. For instance, TeamCity will recognize certain phrases in the output and use that to interpret the phase of the build or the severity of certain build problems:

task Compile { "##teamcity[blockOpened name='Compilation']" # compilation steps "##teamcity[blockClosed name='Compilation']" }

But since PSake will scan and load PowerShell modules (.psm1 files) from under the Modules folder, you can make this a lot more readable by importing the TeamCity extensions provided by the Psake Contrib project:
task Compile { TeamCity-Block "Compiling the solution" { } }

AppVeyor offers PowerShell extensions to interact with their build system as well. And don't underestimate the capability of using PowerShell modules here. This is what will allow you to treat your PSake scripts as real code including opportunities for refactoring, code reuse, accepting open-source contributions, etc.

Anyway, this post was in no way meant to be a comprehensive discussion on Psake. Instead, I recommend you to checkout out the many examples from the GitHub repository or look at the build script that I'm using to build Fluent Assertions from CodeBetter's TeamCity environment.

So how do you organize your build environment? Does what I'm proposing here make sense? Let me know by commenting below or tweeting me at @ddoomen.