Skip to content

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 .nupkgs, 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.

Comments

Powered by Cactus Comments

Loading Comments...