Automating NuGet package creation with MSBuild and PowerShell

With the upcoming release of NuGet 1.0, I modified the build scripts for FluentValidation to automatically produce NuGet packages as well as provide the ability to automatically publish them to the newly available NuGet Gallery.

Step 1 – Create the NuSpec files

First, you’ll need to create NuSpec files that describe the package. Mine live in the buildscripts subdirectory of the project’s source control tree. The FluentValidation.nuspec file looks like this:

<?xml version="1.0"?>
<package>
  <metadata>
    <id>FluentValidation</id>
    <version></version>
    <authors>Jeremy Skinner</authors>
    <licenseUrl>http://fluentvalidation.codeplex.com/license</licenseUrl>
    <projectUrl>http://fluentvalidation.codeplex.com</projectUrl>
    <iconUrl>http://download.codeplex.com/Project/Download/FileDownload.aspx?ProjectName=FluentValidation&amp;DownloadId=166226</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>...description goes here...</description>
    <summary>...summary goes here...</summary>
    <language>en-US</language>
  </metadata>
</package>

Note that the version element is empty – this will be replaced by the build script.

Step 2 – Create the build script

In this case I’ll be using MSBuild, although the same approach will also work with NAnt, Rake, Psake, Phantom or any other build engine. The build script is called “build.proj” and also lives in the buildscripts subdirectory.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTargets="default">
  <PropertyGroup>
    <BaseDir>$(MSBuildProjectDirectory)..</BaseDir>
    <Configuration Condition="'$(Configuration)'==''" >Release</Configuration>
    <BuildDir>$(BaseDir)build</BuildDir>
    <PackageDir>$(BuildDir)Packages</PackageDir>
    <SolutionFile>$(BaseDir)FluentValidation.sln</SolutionFile>
    <MSBuildExtensions>$(BaseDir)libmsbuildmsbuild.community.tasks.dll</MSBuildExtensions>
  </PropertyGroup>
 
  <UsingTask AssemblyFile="$(MSBuildExtensions)" TaskName="MSBuild.Community.Tasks.XmlUpdate" />
 
  <Target Name="default" DependsOnTargets="Compile; Package" />
 
  <Target Name="Compile">
    <MSBuild Projects="$(SolutionFile)" Properties="Configuration=$(Configuration)"  />
  </Target>
 
  <Target Name="Package">
    <ItemGroup>
      <MainBinaries Include="$(BaseDir)srcFluentValidationbin$(Configuration)***.*" />
    </ItemGroup>
 
    <!-- First copy the nuspec template files to the package dir -->
    <Copy SourceFiles="$(MSBuildProjectDirectory)FluentValidation.nuspec" DestinationFolder="$(PackageDir)tempFluentValidation" />
 
    <!-- Copy the source files to the package dir -->
    <Copy SourceFiles="@(MainBinaries)" DestinationFolder="$(PackageDir)tempFluentValidationlibNET35%(RecursiveDir)" />
 
    <!-- Get the version number of the main FV assembly to insert into the nuspec files -->
    <GetAssemblyIdentity AssemblyFiles="$(OutputDir)FluentValidationFluentValidation.dll">
      <Output TaskParameter="Assemblies" ItemName="AsmInfo" />
    </GetAssemblyIdentity>
 
    <!-- insert the version number into the nuspec files -->
    <XmlUpdate
      Namespace="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"
      XmlFileName="$(PackageDir)tempFluentValidationFluentValidation.nuspec"
      XPath="/package/metadata/version"
      Value="%(AsmInfo.Version)" />
 
    <Exec WorkingDirectory="$(BuildDir)Packages" 
          Command="$(BaseDir)libnugetnuget.exe pack $(PackageDir)tempFluentValidationFluentValidation.nuspec" />
  </Target>
</Project>

We begin by declaring some properties including the location of the solution file to build as well as where we want to place the generated packages. Next, we import the XmlUpdate task from the MSBuild Community Extensions project which will allow the nuspec files to be modified.

The “default” target simply calls the “Compile” and “Package” targets.

The “Compile” target invokes MSBuild on our solution file to compile the project.

The “Package” target begins by declaring an ItemGroup consisting of the assemblies we want to package (in this case, it’s just FluentValidation.dll). It copies these along with the nuspec file into a temporary directory that mirrors the NuGet package format.

Next, we use MSBuild’s “GetAssemblyIdentity” task to look up the version number of our assembly (defined in the AssemblyInfo.cs file).

We next use the XmlUpdate task from MSBuild Community Extensions to modify the copy of the nuspec file. The version number extracted from the assmbly file in the previous step is injected into the nuspec’s <version /> element.

Finally, we call nuget.exe’s “pack” command which generates the nupkg file.

Step 3 - Uploading the package

Now that we’ve created the package, it needs to be uploaded to the NuGet Gallery. The gallery allows you to manually upload the package through the browser, but we can also automatically publish it through the command line tool.

First, you’ll need to know what your NuGet Access Key is. This is a guid that allows you to upload packages to your NuGet account. Once signed up on http://nuget.org, this key can be found under the “My Account” page.

We’ll begin by storing this key in a text file. In my case, this file is kept in my DropBox at C:UsersJSkinnerDocumentsMy Dropboxnuget-access-key.txt (we don’t want to store this key in our repository or other users will be upload packages to our account).

Next, we can use a PowerShell script that makes use of this key to upload the package. This script also lives in the buildscripts directory as “publish-nuget-packages.ps1”. Here’s the contents of the script:

$keyfile = "$env:USERPROFILEDocumentsMy Dropboxnuget-access-key.txt"
$scriptpath = split-path -parent $MyInvocation.MyCommand.Path
$nugetpath = resolve-path "$scriptpath/../lib/nuget/nuget.exe"
$packagespath = resolve-path "$scriptpath/../build/packages"
 
if(-not (test-path $keyfile)) {
  throw "Could not find the NuGet access key at $keyfile. If you're not Jeremy, you shouldn't be running this script!"
}
else {
  pushd $packagespath
 
  # get our secret key. This is not in the repository.
  $key = get-content $keyfile
 
  # Find all the packages and display them for confirmation
  $packages = dir "*.nupkg"
  write-host "Packages to upload:"
  $packages | % { write-host $_.Name }
 
  # Ensure we haven't run this by accident.
  $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Uploads the packages."
  $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Does not upload the packages."
  $options = [System.Management.Automation.Host.ChoiceDescription[]]($no, $yes)
 
  $result = $host.ui.PromptForChoice("Upload packages", "Do you want to upload the NuGet packages to the NuGet server?", $options, 0) 
 
  # Cancelled
  if($result -eq 0) {
    "Upload aborted"
  }
  # upload
  elseif($result -eq 1) {
    $packages | % { 
        $package = $_.Name
        write-host "Uploading $package"
        & $nugetpath push -source "http://packages.nuget.org/v1/" $package $key
        write-host ""
    }
  }
  popd
}

We begin by declaring some variables that define where the access key file is located as well as where the built packages can be found (you’ll need to modify these if the directory structure for your project is different).

We ensure that the access key file exists (if not, the script throws an exception) and then finds all the nupkg files that have been created in the packages directory.

Next, we create a prompt (using $host.ui.PromptForChoice) that asks for confirmation before uploading (useful to prevent accidentally publishing packages).

Finally, the script loops over each package and uploads it by calling nuget.exe’s “push” command. We pass the URL to the NuGet package feed as the “source” parameter followed by the path to the package and the access key.

Now when we run this script from a PowerShell prompt, you’ll see some output something like this:

» ./buildscripts/publish-nuget-packages.ps1
Packages to upload:
FluentValidation.2.0.0.0.nupkg
 
Upload packages
Do you want to upload the NuGet packages to the NuGet server?
[N] No  [Y] Yes  [?] Help (default is "N"): y
 
Uploading FluentValidation.2.0.0.0.nupkg
Creating an entry for your package [ID:FluentValidation Ver:2.0.0.0]...
Your package has been uploaded to the server but not published.
Publishing your package [ID:FluentValidation Ver:2.0.0.0] to the live feed...
Your package has been published to the live feed.

The final versions of these files can all be found in the buildscripts directory of FluentValidation’s github repository.

Written on January 12, 2011