Running PowerShell to change MSBuild scripts with NuGet

Running PowerShell to change MSBuild scripts with NuGet

One of the things I’ve missed from creating a number of NuGet packages, is not being able to add msbuild tasks to the .csproj file.  From running web.config transforms, versioning assemblies, to running unit tests, code analysis, or deployments – msbuild can add quite a lot of cherries to your builds.  In a web agency, I work with a lot of projects for fairly short periods of time, and even copying and pasting all those useful scripts means they often get missed / take too much time, or just plain break because I’ve forgotten something.

Re-using code still can take hours out of my project which I’d much rather be using to create something new.  Combine that with a larger team of people doing the same thing, and we need a better solution.

NuGet makes it very easy to install binaries or content/src files, and the nuspec syntax is beautifully simplistic.  In it’s absolute barest form, it looks a little like this:

[codesyntax lang=”xml”]

<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>VersionAssembly.Mvc</id>
    <version>1.6</version>
    <authors>Kevin Blake</authors>
    <description>Version Assembly</description>
  </metadata>
  <files>
    <file src="VersionController.cs.pp" target="content\Controllers" />
  </files>
</package>

[/codesyntax]

That doesn’t look like very much, but when we create a NuGet package (nuget.exe pack .), we’re going to get a package that will install a new controller, and the .pp file extensions means that file will be processed to include the correct namespace for our project.  In this case, the controller just outputs the assembly version number – but that’s not important.

We can also add dll’s to this, with target=”lib”, for all those libraries you’ve got hanging around on your server.  More excitingly, you can run PowerShell scripts along with your NuGet install, by using target=”tools\install.pl1″.  PowerShell is a very powerful scripting language from Microsoft (yep, another one), which among many other things, can give your NuGet packages all the extra power to do what you like…

Let’s start off with a really basic script that’ll run while installing your package by adding to your <files> node within your nuspec.

[codesyntax lang=”xml”]

<file src="install.ps1" target="tools\install.ps1" />

[/codesyntax]

Note that you can only have an install script if you’ve already got a content or lib file in there as well.  So go and add one (even a package_readme.txt) if you haven’t already.  You can also use init.ps1 instead, but that’s going to run every time your project loads.  For our sake, the controller we already have does the job just fine.

Now you’re going to need a PowerShell script, so let’s start out small with Hello World.

[codesyntax lang=”c”]

Write-Host "Hello World"

[/codesyntax]

You can probably guess that this won’t do very much, but you should be able to see that output when you install you NuGet package.  You also have at your command, your simplest form of echo/print debugging tool, for PowerShell beginners like me.

With that working, you can move onto something a little more useful… The following script will add a Version task to your .csproj file, which updates the Properties\AssemblyInfo.cs with the version number that (in our case) comes from CruiseControl.Net.  Most of the work for this task comes with the MSBuild Community Tasks, so you will need that installed if you’re going to try this out word-for-word.

[codesyntax lang=”c”]

param($installPath, $toolsPath, $package, $project)

$buildProject = Get-MSBuildProject
$import = $buildProject.Xml.AddImport("`$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets")
$target = $buildProject.Xml.AddTarget("Version")

$target.BeforeTargets = "BeforeBuild"
$target.Condition = "'`$(Configuration)' == 'Release'"
$task = $target.AddTask("AssemblyInfo")
$task.Condition = "'`$(CCNetLabel)' != ''"
$task.SetParameter("CodeLanguage", "CS")
$task.SetParameter("OutputFile", "properties\AssemblyInfo.cs")
$task.SetParameter("AssemblyVersion", "`$(CCNetLabel)")
$task.SetParameter("AssemblyFileVersion", "`$(CCNetLabel)")

$buildProject.Save()
$project.Save()

[/codesyntax]

That Get-MSBuildProject statement is actually supplied by another NuGet package, so we’ll have to change our NuSpec file to include that as a dependency by adding to the metadata block.

[codesyntax lang=”xml”]

    <dependencies>
      <dependency id="NuGetPowerTools" version="0.26" />
    </dependencies>

[/codesyntax]

It’s also a good idea to add an uninstall.ps1 script that will undo all of those changes if someone later decides not to use your package.  I’m not going to cover that here because it’s not something I’ve completed, and these packages will not be publicly posted (by all means, share your examples in the comments, if you have).

Your complete NuSpec file should now look a little bit like this

[codesyntax lang=”xml”]

<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>VersionAssembly.Mvc</id>
    <version>1.6</version>
    <authors>Kevin Blake</authors>
    <description>Version assemblies</description>
    <dependencies>
      <dependency id="NuGetPowerTools" version="0.26" />
    </dependencies>
  </metadata>
  <files>
    <file src="VersionController.cs.pp" target="content\Controllers" />
    <file src="tools\install.ps1" target="tools\install.ps1" />
</files>
</package>
[/codesyntax]

It’s a very simple example, I know – but these basic steps give almost limitless possibilities to your NuGet packages… Personally, I will be creating a lot more of these to include build scripts we would otherwise have had to include in a rather large bloated project template.  NuGet+PowerShell gives the flexibility to choose just what we need from the dessert cart, and that can only be a good thing.

Tags: , , , , , , ,

Leave a Reply?