0 Comments

I know just enough of the windows scripting language (i.e. batch files) to get by. I’ve written a few scripts using it (at least one of which I’ve already blogged about), but I assume there is a world of deeper understanding and expertise there that I just haven’t fathomed yet. I’m sure its super powerful, but it just feels so…archaic.

Typically what happens is that I will find myself with the need to automate something, and two options will come to mind:

  1. I could write a batch file. This is always a struggle, because I write C# code all day, and going back to the limited functionality of the windows scripting language is painful. I know that if I want to do something past a certain point of complexity, I’ll need to combine this approach with something else.
  2. I could write a C# application of some sort (probably console) that does the automation. C# is where most of my skills lie, but it seems like I’m overcomplicating a situation when I build an entire C# console application. Additionally, if I’m automating something, I want it to be as simple and readable as possible for the next person (which may be me in 3 months) and encapsulating a bunch of automation logic into a C# application is not immediately discoverable.

I usually go with option 1 (batch file), which to me, is the lesser of two evils.

Enter Powershell

I’ve always secretly known that there are more than 2 options, probably many more.

At the very least there is a 3rd option:

  1. Use Powershell. Its just like a batch file, except completely different. You can leverage the .NET framework, and you get a lot more useful built in commands.

For me, the downside of using Powershell has always been the learning curve.

Every time I go to solve a problem that Powershell might be useful for, I can never justify spending the time to learn Powershell, even just the minimum needed to get the job done.

I finally got past that particular mental block recently when I wanted to write an automated build script that did the following:

  1. Update a version.
  2. Commit changes to git.
  3. Build a C# library project.
  4. Package the library into a NuGet package.
  5. Track the package in git for later retrieval.

There’s a lot of complexity hidden in those 5 statements, enough such that I knew I wouldn’t be able to accomplish it using just the vanilla windows scripting language. I resolved that this was definitelynot something that should be hidden inside the source code of an application, so it was time to finally go nuclear and learn how to do the Powershell.

Getting Started

The last time I tried to use Powershell was a…while ago. Long enough such that it wasn’t guaranteed that a particular computer would have Powershell installed on it. That’s pretty much not true anymore, so you can just run the “powershell” command from the command line to enter the Powershell repl. Typing “exit” leaves Powershell and returns you back to your command prompt.

Using Powershell on the command line is all well and good for exploration, but how can I use it for scripting?

powershell -Executionpolicy remotesigned -File [FileName]

The –Executionpolicy flag makes it so that you can actually run the script file. By default Powershell has the Restricted policy set, meaning scripts will not run.

Anyway, seems straightforward enough, so without further ado, I’ll show you to the finished Powershell script to accomplish the above, and then go through it in more detail.

The Script

param ( [switch]$release ) $gitTest = git if($gitTest -match "'git' is not recognized as an internal or external command") { write-error "Cannot find Git in your path. You must have Git in your path for this package script to work." exit } # Check for dirty git index (i.e. uncommitted, unignored changes). $gitStatus = git status --porcelain if($gitStatus.Length -ne 0) { write-error "There are uncommitted changes in the working directory. Deal with them before you package, or the tag that's made in git as a part of a package will be incorrect." exit } $currentUtcDateTime = (get-date).ToUniversalTime() $assemblyInfoFilePath = "[PROJECT PATH]\Properties\AssemblyInfo.cs" $assemblyVersionRegex = "(\[assembly: AssemblyVersion\()(`")(.*)(`"\))" $assemblyInformationalVersionRegex = "(\[assembly: AssemblyInformationalVersion\()(`")(.*)(`"\))" $existingVersion = (select-string -Path $assemblyInfoFilePath -Pattern $assemblyVersionRegex).Matches[0].Groups[3] $existingVersion = new-object System.Version($existingVersion) "Current version is [" + $existingVersion + "]." $major = $existingVersion.Major $minor = $existingVersion.Minor $build = $currentUtcDateTime.ToString("yy") + $currentUtcDateTime.DayOfYear $revision = [int](([int]$currentUtcDateTime.Subtract($currentUtcDateTime.Date).TotalSeconds) / 2) $newVersion = [System.String]::Format("{0}.{1}.{2}.{3}", $major, $minor, $build, $revision) "New version is [" + $newVersion + "]." "Replacing AssemblyVersion in [" + $assemblyInfoFilePath + "] with new version." $replacement = '$1"' + $newVersion + "`$4" (get-content $assemblyInfoFilePath) | foreach-object {$_ -replace $assemblyVersionRegex, $replacement} | set-content $assemblyInfoFilePath if ($release.IsPresent) { $newInformationalVersion = $newVersion } else { write-host "Building prerelease version." $newInformationalVersion = [System.String]::Format("{0}.{1}.{2}.{3}-pre", $major, $minor, $build, $revision) } "Replacing AssemblyInformationalVersion in [" + $assemblyInfoFilePath + "] with new version." $informationalReplacement = '$1"' + $newInformationalVersion + "`$4" (get-content $assemblyInfoFilePath) | foreach-object {$_ -replace $assemblyInformationalVersionRegex, $informationalReplacement} | set-content $assemblyInfoFilePath "Committing changes to [" + $assemblyInfoFilePath + "]." git add $assemblyInfoFilePath git commit -m "SCRIPT: Updated version for release package." $msbuild = 'C:\Program Files (x86)\MSBuild\12.0\bin\msbuild.exe' $solutionFile = "[SOLUTION FILENAME]" .\tools\nuget.exe restore $solutionFile & $msbuild $solutionFile /t:rebuild /p:Configuration=Release if($LASTEXITCODE -ne 0) { write-host "Build FAILURE" -ForegroundColor Red exit } .\tools\nuget.exe pack [PATH TO PROJECT FILE] -Prop Configuration=Release -Symbols write-host "Creating git tag for package." git tag -a $newInformationalVersion -m "SCRIPT: NuGet Package Created."

[Wall of text] crits [reader] for [astronomical amount of damage].

To prevent people from having to remember to run Powershell with the correct arguments, I also created a small batch file that you can just run by itself to execute the script.

@ECHO OFF

powershell -Executionpolicy remotesigned -File _Package.ps1 %*

As you can see, the batch script is straightforward. All it does is call the Powershell script, passing in any arguments that were passed to the batch file (that’s the %* at the end of the line).

Usage is:

package // To build a prerelease, mid-development package.

package –release // To build a release package, intended to be uploaded to NuGet.org.

Parameters

The first statement at the top defines parameters to the script. In this case, there is only one parameter, and it defines whether or not the script should be run in release mode.

I wasn’t comfortable with automatically making every single package built using the script a release build, because it meant that if I automated the upload to NuGet.org at some later date, I wouldn’t be able to create a bunch of different builds during development without potentially impacting on people actually using the library (they would see new versions available and might update, which would leave me having to support every single package I made, even the ones I was doing mid-development). That’s less than ideal.

The release flag determines whether or not the AssemblyInformationalVersion has –pre appended to the end of the version string. NuGet uses the AssemblyInformationalVersion in order to define whether or not the package is a prerelease build, which isolates it from the normal stream of packages.

Checks

Because the script is dependent on a couple of external tools that could not be easily included in the repository (git in particular, but also MSBuild) I wanted to make sure that it failed fast if those tools were not present.

I’ve only included a check for git because I assume that the person running the script has Visual Studio 2013 installed, whereas git needs to be in the current path in order for the script to do what it needs to do.

The other check that the script does is check to see whether or not there are any uncommitted changes.

I do this because one of the main purposes of this build script is to build a library and then mark the source control system so that the source code for that specific version can be retrieved easily. Without this check, someone could use the script with uncommitted local changes and the resulting tag would not actually represent the contents of the package. Super dangerous!

Versioning

In this particular case, versioning is (yet again) a huge chunk of the script, as it is intended to build a library for distribution.

The built in automatic versioning for .NET is actually pretty good. The problem is, I have never found a way to use that version easily from a build script and the version is never directly stated in the AssemblyInfo file, so you can’t see the version at a glance just by reading the code. I need more control than that.

The algorithm that the .NET framework uses is (partially) explained in the documentation for AssemblyVersion.

To summarise:

  1. The version is of the form [MAJOR].[MINOR].[BUILD].[REVISION].
  2. You can substitute a * for either (or both of) BUILD and REVISION.
  3. BUILD is automatically set to the number of days since 1 January 2000.
  4. REVISION is automatically set to the number of seconds since midnight / 2.

The algorithm I implemented in the script is a slight modification of that, where BUILD is instead set to YYDDD.

Anyway, Powershell makes the whole process of creating this new version much much easier than it would be a normal batch file, primarily because of the ability to use the types and functions in the .NET framework. Last time I tried to give myself more control over versioning I had to write a custom MSBuild task.

The script grabs the version currently in the AssemblyVersion attribute of the specified AssemblyInfo file using the select-string cmdlet. It extracts the MAJOR and MINOR numbers from the existing version (using the .NET Version class) and then creates a string containing the new version.

Finally, it uses a brute force replacement approach to jam the new version back into the AssemblyVersion attribute, using the same regular expression. I’ll be brutally honest, I don’t understand the intricacies of the way in which it does the replacement, just that it reads all of the lines from the file, modifies any that match the regular expression, then writes them all back, effectively overwriting the entire file. I wouldn’t recommend this approach for any serious replacement, but AssemblyInfo is a very small file, so it doesn’t matter all that much here.

Some gotchas here. Initially I broke the regular expression into 3 groups. Left of the version, the version and right of the version. However, when it came time to do the replace, I could not create a replacement string using the first capture group + the new version because the resulting string came out like this “$11.2.14309.2306”. When Powershell/.NET tried to substitute the capture groups in, it tried to substitute the $11 group, which didn’t exist. Simply adding whitespace would have broken the version in the file, so I had to break the regular expression into 3 groups, one of which is just the single double quotes to the left of the version. When it comes time to do the replacement, I just manually insert that quote and that worked. A bit nastier than I would like, but ah well.

The version update is then duplicated for the AssemblyInformationalVersion, with the previously mentioned release/prerelease changes.

Source Control

A simple git add and commit to ensure that the altered AssemblyInfo file is in source control, ready to be tagged after the build is complete. I prepended the commit message with “SCRIPT:” so that its easy to tell which commits were done automatically when looking at the git log output.

Build

Nothing fancy, just a normal MSBuild execution, preceded by a NuGet package restore.

I struggled with calling MSBuild correctly from the script for quite a while. For some reason Powershell just would not let me call it with the appropriate parameters. Eventually I stumbled onto this solution. & is simply the call operator.

The script checks that there weren’t any errors during the build, because there would be no point in going any further if there was. The $LASTEXITCODE variable is a handy little variable that tracks the last exit code from a call.

Packaging

Simple NuGet package command. I use –Symbols because I prefer NuGet packages that include source code, so that they are easier to debug. This is especially useful for a library.

Source Control (again)

If we got this far, we need to create a record that the package was created. A simple git tag showing the same version as the AssemblyInformationalVersion is sufficient.

Summary

As you can clearly see, the script is not perfect. Honestly, I’m not even sure if its good. At the very least it gets the job done. I’m sure as I continue to use it for its intended purpose I will come up with ways to improve it and make it clearer and easier to understand.

Regardless of that, Powershell is amazing! The last time I tried to solve this problem I wrote a custom MSBuild task to get the versioning done. That was a lot more effort than the versioning in this script, and much harder to maintain moving forward. The task was better structured though, so that’s definitely an area where this script could use some improvement. Maybe I can extract the versioning code out into a function? Another file maybe? I should almost certainly run the tests before packaging as well, no point in making a package where the library has errors that could have been picked up. Who knows, I’m sure I’ll come up with something.

You may also ask why I went to all this trouble when I should be using a build server of some description.

I agree, I should be using a build server. For the project that this build script was written for I don’t really have the time or resources to put one into place…yet.