edit-chocolatey-packages
The Right Way To Edit Chocolatey Packages
Intro
So, you need to edit a Chocolatey package. Maybe you need to fix a bug, update the software version, or add a feature to the install script.
Ideally, if you are maintaining the package you would not need to edit the compiled package itself. It is best to store the package source files in a source control repository (e.g. git), and edit the files in the repository. That way, a history of changes can be saved, collaboration is much easier in the long run, and package updates can be automated.
But, a source repository is not always available. Perhaps the maintainer of the package did not commit the files to a source repository in the first place, or perhaps the repository is unavailable.
So, then, how best to edit a package? First, a little background on the packages themselves.
The anatomy of a .nupkg
Chocolatey builds on top of the NuGet package manager. One of the things it brings over from NuGet is the .nupkg
, which is the file format for NuGet packages.
What is a .nupkg
? It is zip file, with a couple of specific files inside it, and follows the Open Packaging Conventions.
First, you have the *.nuspec
. This is the manifest, which is the primary file contains metadata about the package itself.
Next, there are three items which are created inside the package as a part of pack/compilation process.
[Content_Types].xml
- A file which contains information about file types_rels
- A folder which contains a.rels
file with relationships, which is basically metadata internal to the package in this context.package
- A folder which contains metadata for NuGet, mostly a mashup of information in the.nuspec
.
Finally, there is anything that that got specifically packed into the package. It is possibly to stick pretty much anything else you want inside the package, although do not add a folder called "content". This is controlled by files
section in the .nuspec
. This specifies the source and destination for any files you want to include. If the files
element is missing, choco pack
will automatically include all files in the same folder as the .nuspec
Also, packing will strip the files
element, so it no longer exists inside the .nupkg
.
Chocolatey nupkg additions
Chocolatey has added a couple of things on top of the original NuGet .nupkg
.
The first are the chocolateyInstall.ps1
, chocolateyBeforeModify.ps1
and chocolateyUninstall.ps1
scripts. These are all optional, but if included, will be run during install, before upgrade/uninstall, and during uninstall respectively. In many packages, they are put in a folder called tools
, however it is not required that they go inside that folder.
The second thing that has been added is a few more fields for metadata in the .nuspec
:
projectSourceUrl
packageSourceUrl
docsUrl
mailingListUrl
bugTrackerUrl
provides
(not implemented in choco yet)conflicts
(not implemented in choco yet)replaces
(not implemented in choco yet)
There are plans for more fields to be added at some point.
Extracting the nupkg
So, now that you know a bit about the .nupkg
, now it is time to describe to how to edit it.
Editing the package itself, while possible in some ways such as through 7zip, is hardly ideal. First as the automatically created extra files may have to be updated, and second, unpacking/extracting the .nupkg
allows you to put it into source control. Not to mention editing files inside a zip archive is not necessarily the easiest thing.
Therefore, extracting the files is what you want to do.
It is possible to use the standard windows tools for extracting zips if you change the extension from .nupkg
to .zip
. Both the standard Windows explorer zip file support and PowerShell Expand-Archive
work. If you have 7zip installed, that will work directly with .nupkg
s, no need to change the extension.
However, these will also extract the extra metadata items that are created automatically, namely the [Content_Types].xml
, _rels
folder, and the package
folder. These should not be manually included if you re-pack the package, so they should be deleted after you extract the .nupkg
.
However, I have created a better way; the a PowerShell function Expand-Nupkg
. This will not extract the metadata files, so you can just extract in one command and be done.
Note, this uses Add-NuspecFilesElement
to add back in the files element, so make sure that function from below is also available.
<#
.SYNOPSIS
Extract NuGet/Chocolatey .nupkg packages.
.DESCRIPTION
Extract NuGet/Chocolatey .nupkg packages.
Does not extract the automatically created metadata files.
Adds back in the files element to .nuspec which is stripped out during the pack process using Add-NuspecFilesElement
.PARAMETER Path
Path to .nupkg file to extract
.PARAMETER Destination
Location for where to extract files.
If not specified, extracts to the same folder as the .nupkg
.PARAMETER NoAddFilesElement
Do not add the files element back into the .nuspec
.EXAMPLE
PS> Expand-Nupkg .\chocolatey.0.10.15.nupkg
.EXAMPLE
PS> Expand-Nupkg -Path "C:\packages\chocolatey.0.10.15.nupkg" -Destination "C:\packageSources\chocolatey" -NoAddFilesElement
.LINK
Add-NuspecFilesElement
#>
Function Expand-Nupkg {
[CmdletBinding()]
[Alias("Extract-Nupkg")]
param (
[parameter(Mandatory = $true, Position = 0)]
[ValidateScript( {
if (!(Test-Path -Path $_ -PathType Leaf) ) {
throw "The Path parameter must be a file. Folder paths are not allowed."
}
if ($_ -notmatch "(\.nupkg)") {
throw "The file specified in the Path parameter must be .nupkg"
}
return $true
} )]
[string]$Path,
[parameter(Position = 1)]
[ValidateScript( {
if (!(Test-Path -Path $_ -IsValid) ) {
throw "The Destination parameter must be a valid path"
}
return $true
} )]
[string]$Destination,
[Parameter(Position = 2)]
[switch]$NoAddFilesElement
)
Begin {
#needed for accessing dotnet zip functions
Add-Type -AssemblyName System.IO.Compression.FileSystem
}
Process {
Try {
$Path = (Resolve-Path $Path).Path
if (!($PSBoundParameters.ContainsKey('Destination'))) {
Write-Verbose "Extracting next to nupkg"
$Destination = Split-Path $Path
}
$null = New-Item -Type Directory $Destination -ea 0
$Destination = (Resolve-Path $Destination).Path
$archive = [System.IO.Compression.ZipFile]::Open($Path, 'read')
#Making sure that none of the extra metadata files in the .nupkg are unpacked
$filteredArchive = $archive.Entries | `
Where-Object Name -NE '[Content_Types].xml' | Where-Object Name -NE '.rels' | `
Where-Object FullName -NotLike 'package/*' | Where-Object Fullname -NotLike '__MACOSX/*'
$filteredArchive | ForEach-Object {
$OutputFile = Join-Path $Destination $_.fullname
$null = New-Item -Type Directory $(Split-Path $OutputFile) -ea 0
Write-Verbose "Extracting $($_.fullname) to $OutputFile"
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $outputFile, $true)
}
if (!$NoAddFilesElement) {
$toplevelFiles = $filteredArchive.fullname | ForEach-Object { $_ -split "[/\\]" | Select-Object -first 1 } | Select-Object -Unique
$nuspecPath = Join-Path $Destination ($toplevelFiles | Where-Object { $_ -like "*.nuspec" })
[array]$filesElementList = Get-Item ($toplevelFiles | Where-Object { $_ -notlike "*.nuspec" } | ForEach-Object { Join-Path $Destination $_ })
Add-NuspecFilesElement -NuspecPath $nuspecPath -FilesList $filesElementList
}
} Finally {
#Always be sure to cleanup
$archive.dispose()
}
}
}
The nuspec files element
Remember the files
element which gets stripped during pack?
So, choco pack
is capable of packing when the .nuspec
does not have a file element, it includes everything that is in the directory the .nuspec
is in. But, you do not always want that. There could be files you want to exclude, such as AU automatic update files, or files from other directories to include.
Therefore, adding back in the files element is ideal, and guess what, I have another PowerShell function to do just that: Add-NuspecFilesElement
Also, there is a reference for the files element available at the Microsoft NuGet documentation
<#
.SYNOPSIS
Add the files element to a .nuspec file
.DESCRIPTION
Adds the files element to a .nuspec file.
Will replace the current element if the .nuspec file already has one.
By default, will include all files in the folder the .nuspec is in.
Exclude AU files with the AuExclude parameter.
Alternatively, specify files to include with the FilesList parameter.
.PARAMETER NuspecPath
Path to .nuspec to add the files element too.
.PARAMETER FilesList
Array of files to include in the files element.
.PARAMETER AuExclude
Do not include AU specific files in the files element.
.EXAMPLE
PS> Add-NuspecFilesElement .\chocolatey.nuspec
.EXAMPLE
PS> Add-NuspecFilesElement -NuspecPath "C:\packageSources\chocolatey\chocolatey.nuspec" -AuExclude
.LINK
Extract-Nupkg
.LINK
https://docs.microsoft.com/en-us/nuget/reference/nuspec
#>
Function Add-NuspecFilesElement {
[CmdletBinding()]
param (
[alias("Path")]
[parameter(Mandatory = $true, Position = 0)]
[ValidateScript( {
if (!(Test-Path -Path $_ -PathType Leaf) ) {
throw "The NuspecPath parameter must be a file. Folder paths are not allowed."
}
if ($_ -notmatch "(\.nuspec)") {
throw "The file specified in the NuspecPath parameter must be .nuspec"
}
return $true
} )]
[string]$NuspecPath,
[System.IO.FileSystemInfo[]]$FilesList,
[switch]$AuExclude
)
$NuspecPath = (Resolve-Path $NuspecPath).path
if ($PSVersionTable.PSVersion.major -ge 6) {
[xml]$nuspecXML = Get-Content $NuspecPath
} else {
[xml]$nuspecXML = Get-Content $NuspecPath -Encoding UTF8
}
if (!($PSBoundParameters.ContainsKey('FilesList'))) {
$packageDir = Split-Path $NuspecPath
if ($AuExclude) {
$filesList = Get-ChildItem $packageDir -Exclude "*.nupkg", "*.nuspec", "update.ps1", "readme.md"
} else {
$filesList = Get-ChildItem $packageDir -Exclude "*.nupkg", "*.nuspec"
}
}
# Remove current files element if it exists
if ($null -ne $nuspecXML.package.files) {
$nuspecXML.package.RemoveChild($nuspecXML.package.files) | Out-Null
}
$filesElement = $nuspecXML.CreateElement("files", $nuspecXML.package.xmlns)
foreach ($file in $filesList) {
$fileElement = $nuspecXML.CreateElement("file", $nuspecXML.package.xmlns)
if ($file.PSIsContainer) {
$srcString = "$($file.name){0}**" -f [IO.Path]::DirectorySeparatorChar
$fileElement.SetAttribute("src", "$srcString")
} else {
$fileElement.SetAttribute("src", "$($file.name)")
}
$fileElement.SetAttribute("target", "$($file.name)")
$filesElement.AppendChild($fileElement) | Out-Null
}
$nuspecXML.package.AppendChild($filesElement) | Out-Null
Try {
[System.Xml.XmlWriterSettings] $XmlSettings = New-Object System.Xml.XmlWriterSettings
$XmlSettings.Indent = $true
# Save without BOM
$XmlSettings.Encoding = New-Object System.Text.UTF8Encoding($false)
[System.Xml.XmlWriter] $XmlWriter = [System.Xml.XmlWriter]::Create($nuspecPath, $XmlSettings)
$nuspecXML.Save($XmlWriter)
} Finally {
$XmlWriter.Dispose()
}
}
Packing it back up
Once you are done making any changes to need to the extracted package, pack it back up with choco pack
or nuget pack
.
Conclusion
Hopefully you have a bit of a better understanding of the .nupkg
now, and the PowerShell functions are useful to you.