Converting a Powershell script to a NuGet command
Last week I posted about the NuGet.Downloader package, which had begun life as a Powershell script. If you've got a Powershell commands that you'd like to make available in NuGet packages, here's how.
Write Powershell scripts that are easy to convert to NuGet packages
I've been looking at the contents of NuGet packages for a while. Since a .nupkg file is really just a zip file, you can extract them if you want, but it's just gotten even easier with the new NuGet Package Explorer. Download the Package Explorer, open the MvcScaffolding package (File / Open From NuGet Feed) and browse through the Powershell scripts in the tools folder.
The main lesson I'd learned from reading other peoples' scripts was that you add Package Manager commands by writing functions in the global namespace with parameters, like this:
function global:Verb-Noun { param ( [string] $question = "Can I kick it?", [string] $affirmativeResponse = "Yes you can!" ) # actual code goes here }
I'd written the original script with settings at the top so that they could easily be turned into parameters. Here's the structure of the original Powershell script:
# --- settings --- $feedUrlBase = "http://go.microsoft.com/fwlink/?LinkID=206669" # the rest will be params when converting to funclet $latest = $true $overwrite = $false $top = 500 #use $top = $null to grab all $destinationDirectory = join-path ([Environment]::GetFolderPath("MyDocuments")) "LocalNuGet" # --- locals --- $webClient = New-Object System.Net.WebClient # --- functions --- # download entries on a page, recursively called for page continuations function DownloadEntries { #code here } function GetPackageUrl { # code here } # --- do the actual work --- # one line of code to create the directory if not existing # a few lines of code to change feed based on settings DownloadEntries
Adding the function to the Init script
Adding that as a command that can be run from the Package Manager console is pretty simple (especially if you can get Eric to do most of the work - PROTIP!).
NuGet has conventions for a few specific Powershell scripts. From the "Creating a Package" docs on CodePlex:
- Init.ps1 runs the first time a package is installed in a solution. If the same package is installed into additional projects in the solution, the script is not run during those installations. The script also runs every time the solution is opened. For example, if you install a package, close Visual Studio, and then start Visual Studio and open the solution, the Init.ps1 script runs again
- Install.ps1 runs when a package is installed in a project. If the same package is installed in multiple projects in a solution, the script runs each time the package is installed. If a package is not installed into a project (such as the MvcScaffold package), the script runs when the package is installed into the solution. The package must have content/dll that will be added to the project for this to run. Just having something in the tools folder will not kick this off.
- Uninstall.ps1 runs every time a package is uninstalled.
The init script is where you add Package Manager commands to a package – although it can load scripts in other directories as well - you can really see this in action in the MvcScaffolding package, which adds a lot of commands to create scaffolds.
Adding a command in init.ps1 just requires wrapping it in a global function using parameters with default values, as shown below:
function global:Download-Packages { param ( [string] $feedUrlBase = "http://go.microsoft.com/fwlink/?LinkID=206669", [bool] $latest = $true, [bool] $overwrite = $false, $top = 500, #use $null to download all [string] $destinationDirectory = (join-path ([Environment]::GetFolderPath("MyDocuments")) "NuGetLocal" ) ) # --- locals --- $webClient = New-Object System.Net.WebClient $feedUrl = "" function DownloadEntries {
#### code excerpted ### } DownloadEntries $feedUrl "Complete. Local packages saved to $destinationDirectory" }
Getting an API key
Before you can publish a package on the NuGet feed, you'll need an API key. That's a very simple process:
- Go to http://nuget.org/Users/Account/Register and get an account
- Copy your API key from your account page
Build the package
The next step is to build a package. Honestly, the best thing to do here is just look at Scott Hansleman’s detailed post titled Creating a NuGet Package in 7 easy steps - Plus watch me drop Thusly in mid-post like it was Your Average Everyday Adverb. There's also plenty of documentation on the CodePlex, and you can watch Phil create and deploy packages in his mvcConf video. I'll tell you what I did for completeness, but I'm not going to take it to the Thusly level. No, sir.
There are two ways to go here - you can use the NuGet Package Explorer or you can go the manual way. I wanted experience with the manual approach first, so that's what I did. In the future, I think I'd probably just use the Package Explorer.
A .nupkg is just a zip file which contains some standard things:
- /lib contains binaries that you'll be adding as references
- /content contains code, configuration, and other non-dll files that you'll be adding to the project
- /tools contains command-line programs and scripts to support the package
This bundle also includes a manifest which is built from a simple XML specification file (.nuspec).
In this case, it looks like this:
Creating the nuspec file is pretty simple. You can just build one based on the specification, or you can create a shell nuspec file by running the NuGet.exe with the spec option:
C:\path\>nuget spec Created 'Package.nuspec' successfully.
That creates an empty Package.nuspec file in the same directory with the following contents:
<?xml version="1.0"?> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> <id>Package</id> <version>1.0</version> <authors>Author here</authors> <owners>Owner here</owners> <licenseUrl>http://LICENSE_URL_HERE_OR_DELETE_THIS_LINE</licenseUrl> <projectUrl>http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE</projectUrl> <iconUrl>http://ICON_URL_HERE_OR_DELETE_THIS_LINE</iconUrl> <requireLicenseAcceptance>false</requireLicenseAcceptance> <description>Package description</description> <tags>Tag1 Tag2</tags> <dependencies> <dependency id="SampleDependency" version="1.0" /> </dependencies> </metadata> </package>
From there, it's just fill in the blanks. Here's the nuspec file for the NuGet.Downloader package:
<?xml version="1.0"?> <package> <metadata> <id>Nuget.Downloader</id> <version>1.0.0.5</version> <authors>Jon Galloway,Eric Hexter</authors> <requireLicenseAcceptance>false</requireLicenseAcceptance> <description>Download packages from a remote feed to a local directory</description> <summary>Download packages from a remote feed to a local directory. Adds one command: Download-Packages By default, only pulls the top 100 packages by download count. Inspired by Steve Michelotti's local repository PowerShell script.</summary> <language>en-US</language> <projectUrl>http://weblogs.asp.net/jgalloway/archive/2011/02/02/downloading-a-local-nuget-repository-with-powershell.aspx</projectUrl> <tags>nuget</tags> </metadata> </package>
One important thing to notice here is the version - you'll need to update that when you build. There are all kinds of smart ways to automate this - e.g. adding this to you build process - but, hey, this is a one file package so I will mark that as Closed - Won't Fix for now.
Now it's time to build the package. With everything in place, we can just run NuGet.exe with the pack command. I'd recommend - at least as you're getting started - to pack test the package locally (adding it to your local repository), then do a push.
C:\my\crazy\file\structure\is\of\no\concern\here>..\nuget pack Attempting to build package from 'Nuget.Downloader.nuspec'. Successfully created package 'C:\Users\Jon\Documents\Visual Studio 2010\Projects\download-nugets\Package\Nuget.Downloader.1.0.0.5.nupkg'.
Once we're tested out, we can push this out to the feed. Since I want to make updates easy, I put everything in a folder and wrote a short batch file for the build. Here's how that looked:
The build.cmd is just 3 lines:
cd Package ..\Nuget.exe pack ..\Nuget.exe push nuget.downloader.1.0.0.5.nupkg APIKEYGOESHERE
Yes, I should probably use pushd and popd instead of cd. Running that builds and pushes the package, though:
C:\what\are\you\looking\at>..\Nuget.exe pack Attempting to build package from 'Nuget.Downloader.nuspec'. Successfully created package 'C:\path\Nuget.Downloader.1.0.0.5.nupkg'. C:\stop\looking\at\the\path\>..\Nuget.exe push nuget.downloader.1.0.0.6.nupkg APIKEYGOESHERE Publishing Nuget.Downloader 1.0.0.6 to the live feed... Your package was published to the feed.
And just like that, your Powershell command has become a NuGet package that's published for all the world to use.