In our second back-to-basics blog post, we're going to progress from consuming Chocolatey packages to creating them. What is a package, what is a package manager, and how do you go about creating your own packages?

What Is a Package?

This question was also answered in the previous back-to-basics blog post, but it is so pivotal to the topic of package creation that I will offer my own definition.

A package is a container that holds "stuff" and for a Chocolatey package it could include:

  • Software installers.
  • Portable executable files.
  • Zip/Compressed files.
  • Random files, such as fonts.
  • License keys.
  • Registry keys.
  • Scripts that do... anything.

Packages also have a manifest, like a "packing slip" on a physical package, that specifies information about the package and what is held inside it. This metadata generally includes:

  • Some way to identify the package (a package ID).
  • A version number, to distinguish a newer version of a package from previous versions.
  • A description of what the package contains.
  • Any dependencies, or other packages, that are required in order to make use of the package.

Before we move on, it's important to stress that "Packages" are not "Software" and vice versa. While a package can, and often do, install software, that's not their only use and conflating the two terms can cause confusion.

What Is a Package Manager?

We now know what a package is, but how do you use them? To actually work with packages, we use a tool called a package manager. This is a tool that:

  • Installs packages, executing any included instructions required to "unpack" the contents of the packages.
  • Uninstalls packages, rolling back any changes that occurred when a given package was installed.
  • Upgrades packages, replacing the "contents" of a previously installed package.
  • Manage dependencies, ensuring any packages that need to be installed before the package you're actually installing are, in fact, installed.

This is, in my opinion, the base line set of features that any given package manager should meet. It is not, however, and exhaustive list of possible features. A package manager could also do things like pin package versions so that you cannot accidentally upgrade them, help you discover available packages, or report on installed packages that have an upgrade available.

Chocolatey CLI is a package manager for Windows. If you've used Linux, then you're likely familiar with at least one of the various package managers available on that platform such as APT, Yum, DNF, or Pacman (among many others.) On macOS you can use Homebrew for your package management needs.

How Do You Create a Chocolatey Package?

The only thing left to do now is learn how to create our own Chocolatey packages. To illustrate the process, we're going to take an MSI installer and step through wrapping a package around it. MSI installers are a good option for your first package, as they are a standard installer type and generally one MSI will behave the same as another MSI. An EXE installer, on the other hand, could actually be one of a number of installer types under the hood and behave wildly differently from one another.

Get Your Installer

First things first, we're going to investigate our installer. For the purposes of this post, we're going to be packaging the installer for Elk Native, a light weight Mastodon client.

This project includes installers for different Operating Systems, including Windows, in their releases on GitHub. At the time of writing the latest release was v0.4.0 so we go to that release and download the file ending in .msi.

Our package will download this installer as part of its installation process, but we're downloading it manually now as we'll need to get some information from it later.

Creating a Scaffold

First you'll need to ensure that you have Chocolatey CLI installed and then open up a PowerShell console.

  1. Create a directory in which we'll work on our package.

    New-Item -ItemType Directory -Path "~/Documents/ChocoPkgs"

  2. Change into this new directory.

    Set-Location -Path "~/Documents/ChocoPkgs"

  3. Use the choco new command to create the starting point for your package, providing the ID which will uniquely represent it. In this example we're using the ID elk-native.

    choco new elk-native

Package Contents

You will now have a new directory specifically for your package with the same name as the ID provided. This directory contains all of the files required to complete your package as well as guidance to help you finish creating it.

The structure and contents of this directory will look like this:

elk-native
|- tools
|  | - chocolateyBeforeModify.ps1
|  | - chocolateyInstall.ps1
|  | - chocolateyUninstall.ps1
|  | - LICENSE.txt
|  | - VERIFICATION.txt
| - _TODO.txt
| - elk-native.nuspec
| - ReadMe.md

The _TODO.txt and ReadMe.md contain a wealth of information about package creation and they can be valuable resources, going further into depth than this post can. They are not required for our package, however, and so after reading them you can delete them.

:choco-info: If you're going to store your package source in source control, you may wish to repurpose the ReadMe.md file to display information about the package in your source control system.

The LICENSE.txt and VERIFICATION.txt files are for when you embed, with proper distribution rights, the installer inside the package and you're submitting your package to the Chocolatey Community Repository. We will not be doing that, so these files should be deleted.

elk-native.nuspec is the "packing slip" for our package, containing metadata about the package. If you open it, you'll find that the essential items that need to be filled out are uncommented and have place holder values. There are also lots of comments that explain each item, and a number of additional data points that can be uncommented, and filled out.

Please go through this file and fill out as much information as possible. Once finished, remove the comments (except the one that states it's not to be removed) and save it, the end result should look something like this:

<?xml version="1.0" encoding="utf-8"?>
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
  <metadata>
    <id>elk-native</id>
    <version>0.4.0</version>
    <packageSourceUrl>https://github.com/Windos/chocolatey/tree/master/elk-native</packageSourceUrl>
    <owners>Windos</owners>
    <title>Elk Native</title>
    <authors>Elk Native contributors</authors>
    <projectUrl>https://github.com/elk-zone/elk-native</projectUrl>
    <iconUrl>https://cdn.jsdelivr.net/gh/Windos/chocolatey@0bd79df6b3e7797bd2ba272356c1eb4491b9d661/elk-native/elk-native.png</iconUrl>
    <copyright>© 2022-PRESENT Elk Native contributors</copyright>
    <licenseUrl>https://github.com/elk-zone/elk-native/blob/main/LICENSE</licenseUrl>
    <requireLicenseAcceptance>true</requireLicenseAcceptance>
    <projectSourceUrl>https://github.com/elk-zone/elk-native</projectSourceUrl>
    <mailingListUrl>https://chat.elk.zone/</mailingListUrl>
    <bugTrackerUrl>https://github.com/elk-zone/elk-native/issues</bugTrackerUrl>
    <tags>elk mastodon social</tags>
    <summary>Native version of Elk, a nimble Mastodon web client.</summary>
    <description>Native version of [Elk](https://github.com/elk-zone/elk), a nimble Mastodon web client.

Elk Native is even more early alpha than the web version, but we would love your feedback and contributions. If you would like to help us with testing, feedback, or contributing, join our [discord](https://chat.elk.zone) and get involved.

![Screenshot of the app, showing the federated timeline home](https://github.com/elk-zone/elk-native/raw/main/Screenshot-dark.png#gh-dark-mode-only)</description>
    <releaseNotes>https://github.com/elk-zone/elk-native/releases/tag/elk-native-v0.4.0</releaseNotes>
  </metadata>
  <files>
    <file src="tools\**" target="tools" />
  </files>
</package>

One of the options I've filled out in this example is the iconUrl, please make sure you're aware of the Package Icon Guidelines. In short, ensure the image is hosted somewhere that you control, and if that is on GitHub then use a CDN to access the image rather than linking directly to it.

To round out the package, we have three PowerShell scripts that form the instructions Chocolatey executes when managing the package.

chocolateyInstall.ps1 is executed when installing or upgrading your package. By default, it is defaulting to installation of an MSI installer, which is perfect for us. Note that the comments in this file show you how to use other installer types, including listing common silent installation arguments.

Take a moment to fill out this file now. Note that in our example, we only have a 64-bit installer and so we will be putting the download URL and related information against the variables with the 64 suffix and removing the equivalent variables without this suffix (which is for 32-bit.)

:choco-info: If your installer can install both 32-bit and 64-bit versions of the software, then use the variables without the 64 suffix.

You will need to know the checksum of our installer, which you can find by running this PowerShell command against the installer downloaded earlier:

Get-FileHash 'C:\Path\To\Installer\Elk_0.4.0_windows_x86_64.msi' -Algorithm SHA256

Save the file, then run the PowerShell snippet from the top of the file to remove all of the comments:

# This example assumes you created your package in the same directory as described earlier.
# If you created your package in a different directory, you will need to update this path:
$f='~/Documents/ChocoPkgs/elk-native/tools/chocolateyInstall.ps1'
gc $f | ? {$_ -notmatch "^\s*#"} | % {$_ -replace '(^.*?)\s*?[^``]#.*','$1'} | Out-File $f+".~" -en utf8; mv -fo $f+".~" $f

The end result will look like this:

$ErrorActionPreference = 'Stop'
$toolsDir   = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$url64      = 'https://github.com/elk-zone/elk-native/releases/download/elk-native-v0.4.0/Elk_0.4.0_windows_x86_64.msi'

$packageArgs = @{
  packageName   = $env:ChocolateyPackageName
  unzipLocation = $toolsDir
  fileType      = 'MSI'
  url64bit      = $url64
  softwareName  = 'elk-native*'
  checksum64    = '1B8F025E5187E07D3807B46EE38DA46DAE8FFC6F04EE78F22EB9E9618DD570A8'
  checksumType64= 'sha256'
  silentArgs    = "/qn /norestart /l*v `"$($env:TEMP)\$($packageName).$($env:chocolateyPackageVersion).MsiInstall.log`""
  validExitCodes= @(0, 3010, 1641)
}

Install-ChocolateyPackage @packageArgs

chocolateyUninstall.ps1 is executed when uninstalling your package. If you were doing anything "custom" in your package, like setting keys in the registry, then you would undo that here. In many cases, this file isn't actually needed as Chocolatey can automatically handle the uninstallation of many software installer types. This is the case with the MSI installer used by our package, so we can go ahead and delete this file.

Finally, chocolateyBeforeModify.ps1 is executed before upgrading or uninstalling your package. You would use this if you needed to stop a running process, or perform some other action, prior to attempting the requested change to your package. Again, this isn't the case for our package, so we can go ahead and delete this file as well.

The contents of our directory should be much simpler at this point:

elk-native
|- tools
|  | - chocolateyInstall.ps1
| - elk-native.nuspec

Our package is now complete and ready to be packed up and "shipped," but first let's dive in to the PowerShell and the helping hand that Chocolatey CLI is providing.

PowerShell Best Practices

Firstly, things can go wrong. This is why your install script starts with $ErrorActionPreference = 'Stop'. This tells PowerShell that if there are any errors, that it should stop what it's doing rather than try and carry on and end up in an unknown state where somethings worked and other things didn't.

You'll also note that all of the information about your installer was entered into a "hashtable." This format is easy to update, and easy for moderators on the Chocolatey Community Repository to review.

This also enables "splatting" where you can pass the entire hashtable to a PowerShell command rather than writing out each parameter one after another. A truncated example of this from our install script is as follows:

# Splatting
$packageArgs = @{
  packageName   = $env:ChocolateyPackageName
  unzipLocation = $toolsDir
  fileType      = 'MSI'
  url64bit      = $url64
}

# Using splatting
Install-ChocolateyPackage @packageArgs

# Writing parameters one after the other
Install-ChocolateyPackage -packageName $env:ChocolateyPackageName -unzipLocation $toolsDir -fileType 'MSI' -url64bit $url64

As you can see, writing parameters one after the other, make for long line lengths. Updating a value would involve hunting out the position of that value in the middle of that long line. Splatting greatly increases the readability of your script.

The final thing I wanted to cover here is to limit the output from your script. Chocolatey CLI writes output that an install is happening, that it has succeeded or failed, and more. So your script doesn't need to output this information. The end user experience should be consistent across packages. If you're writing out the same information, then the user may think something has gone wrong.

Chocolatey Functions That Are There to Help

The final line of our script includes a command that you may not be familiar with, even if you've been scripting with PowerShell for years: Install-ChocolateyPackage.

This is a PowerShell function provided by Chocolatey CLI that handles the download and execution of software installers. It saves you having to write the logic to perform these functions and allows packages to be created in a standard and repeatable way. The documentation for this PowerShell function shows all the potential parameters you can provide.

There are a large number of these functions that you can make use of, so I will only highlight some of the more common ones:

Take Your Package for a Spin

Alright, it's time to pack your package and test that it installs the contained software as we expect it to. From your PowerShell session, make sure you're in the directory that your package contents lives in and run the choco pack command.

Set-Location -Path "~/Documents/ChocoPkgs/elk-native"
choco pack

This command looks for all nuspec files in the current directory and will create a completed package for each that it finds. We only have the one nuspec and are expecting to see only one package created. See the documentation for the command if you need to point to a specific nuspec file or need to control where the resulted package is saved.

In our current directory we will now see a file with the extension nupkg. That is the package we created, named using the package ID and the package version number. Under the hood this is just a special archive file, so you can open it up with tools like 7zip. In our example, the resulting file is: elk-native.0.4.0.nupkg

You can now go ahead and install this package, using the choco install command and telling it that the current directory (.) is the source from which to find your package:

choco install elk-native --source .

:choco-info: This will need to be done from a console with admin privileges.

You will be asked if you want to run the install script in your package, press y to allow this to happen. All going well, you will now have the Elk Native software installed.

Our final test is, can we uninstall the package? Go ahead and use the choco uninstall command. There is no need to specify a source as the package is already installed:

choco uninstall elk-native

If you were creating your own unique package, you could at this point submit your package to the Chocolatey Community Repository. For info about this and the moderation process your package will go through, I recommend reading the previous back-to-basics post.

Got Questions?

Now that you've created and tested your package, you may have some burning questions. I've tried to predict some here, but skip to the bottom of this post for a link to our Community Hub on Discord to ask any unanswered questions.

Do I Have to Use the PowerShell Commands Provided by Chocolatey CLI in My Scripts?

If you're only using your package internally, then you're free to do as you wish with it and create any custom logic you need.

However, if you're looking at sharing your package with the community via the Chocolatey Community Repository, you'd need a very good reason to recreate the logic already covered by a standard Chocolatey command. A moderator will likely pick up on this and ask you to switch to the appropriate command when they review your package.

How Can I Know My Package Is “Good Enough” for the Chocolatey Community Repository?

One of the first things that happens when you push your package to the Chocolatey Community Repository is that it is automatically checked against a set of rules to validate that it meets a baseline of quality. You can test your package against a subset of these rules locally using the Chocolatey Community Validation Extension. This will run those tests when you run choco pack, and will cancel the creation of your package if any errors are found.

Read more about this extension in our recent blog post.

I’m Going to Be Creating a Lot of Similar Packages; Do I Always Have to Start With the Same Template?

The default template is a great starting point when learning to create packages, but if you find yourself using it often then you'll come to realize you're removing a lot of help content that you don't need any more.

What's more, if you're creating the same sort of package over and over again, you may benefit from a more tailor-made starting point.

The way to do this is to create your own Package Template. This will allow you to set your own starting point for new packages, and also allow you to include templated values so you can complete more of your package by providing information to your template when calling the choco new command.

I Have More Questions!

Check the Chocolatey Packaging FAQ, the Package Creation Guide, the 12 Days of Chocolatey Packaging series, or reach out for community assistance on our Community Hub Discord Server.

Summary

This is the second post in our back-to-basics series. Packages are the reason for Chocolatey CLI, the package manager, and also the Chocolatey Community Repository to exist. I hope that this post has empowered you to create your own Chocolatey packages, regardless of if your end goal is to maintain them for the wider community, your organization, or your own personal use.

If you have any more questions, please reach out for community assistance on our Community Hub Discord Server.


comments powered by Disqus