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.
Create a directory in which we'll work on our package.
New-Item -ItemType Directory -Path "~/Documents/ChocoPkgs"
Change into this new directory.
Set-Location -Path "~/Documents/ChocoPkgs"
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 IDelk-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:
Install-ChocolateyInstallPackage
- Used when the software installer is embedded in the package and doesn't need to be downloaded.Install-ChocolateyZipPackage
- Downloads the specified archive file and extracts it to the specified location.Get-ChocolateyUnzip
- Extracts an archive file to the specified location, without downloading it first.Install-ChocolateyShortcut
- Used to create shortcuts, useful when the installer doesn't create any but users would expect to see one.Get-ChocolateyWebFile
- Download a file and save it to a given location. Used when you need files hosted online that aren't an installer.
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.
Popular Tags
- #news 69 Number of post with tag news
- #press release 56 Number of post with tag press release
- #chocolatey for business 44 Number of post with tag chocolatey for business
- #packaging 21 Number of post with tag packaging
- #open source 17 Number of post with tag open source
- #community 15 Number of post with tag community
- #tutorial 14 Number of post with tag tutorial
- #12 days of Chocolatey 2023 12 Number of post with tag 12 days of Chocolatey 2023
- #chocolatey community repository 12 Number of post with tag chocolatey community repository
- #podcast 11 Number of post with tag podcast