Fondue.psm1
|
#Region '.\Prefix.ps1' -1 $currentPrincipal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Fondue must be imported in an elevated (Administrator) PowerShell session.' } #EndRegion '.\Prefix.ps1' 6 #Region '.\private\Assert-LicenseValid.ps1' -1 function Assert-LicenseValid { [CmdletBinding()] Param( [Parameter()] [String] $LicenseFile = "$env:ChocolateyInstall\license\chocolatey.license.xml" ) end { $licenseFound = Test-Path $LicenseFile $xmlDoc = [System.Xml.XmlDocument]::new() $xmlDoc.Load($LicenseFile) $licenseNode = $xmlDoc.SelectSingleNode('/license') $expirationDate = [datetime]::Parse($licenseNode.Attributes["expiration"].Value) $LicenseExpired = $expirationDate -lt (Get-Date) if($licenseFound -and (-not $LicenseExpired)){ return $true } else { return $false } } } #EndRegion '.\private\Assert-LicenseValid.ps1' 28 #Region '.\private\Scaffold-Nuspec.ps1' -1 function Scaffold-Nuspec { Param( [Parameter(Mandatory)] [String] $Path ) $settings = [System.Xml.XmlWriterSettings]::new() $settings.Indent = $true $utf8WithoutBom = [System.Text.UTF8Encoding]::new($false) $stream = [System.IO.StreamWriter]::new($Path, $false, $utf8WithoutBom) try { $writer = [System.Xml.XmlWriter]::Create($stream, $settings) $writer.WriteStartDocument() $writer.WriteComment("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.") $writer.WriteStartElement('', 'package', 'http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd') $writer.WriteStartElement('metadata') $writer.WriteFullEndElement() # metadata $writer.WriteEndElement() # package $writer.WriteEndDocument() } finally { $writer.Flush() $writer.Close() $stream.Close() $stream.Dispose() $writer.Dispose() } return (Get-Item $Path) } #EndRegion '.\private\Scaffold-Nuspec.ps1' 34 #Region '.\private\Write-Metadata.ps1' -1 function Write-Metadata { [CmdletBinding()] Param( [Parameter(Mandatory)] [Hashtable] $Metadata, [Parameter(Mandatory)] [String] $NuspecFile ) process { [xml]$xmlDoc = Get-Content $NuspecFile $namespaceManager = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) $namespaceManager.AddNamespace("ns", "http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd") $metadataNode = $xmlDoc.SelectSingleNode("//*[local-name()='metadata']", $namespaceManager) $Metadata.GetEnumerator() | ForEach-Object { $node = $xmlDoc.SelectSingleNode("//*[local-name()='$($_.Key)']", $namespaceManager) if (-not $node) { $node = $xmlDoc.CreateElement($_.Key) } else { 'Node exists: {0}, updating' -f $_.Key } $null = $node.InnerText = $_.Value $null = $metadataNode.AppendChild($node) } #we don't need the namespace on all the nodes, so strip it off $xmlDoc = $xmlDoc.OuterXml -replace 'xmlns=""', '' $settings = New-Object System.Xml.XmlWriterSettings $settings.Indent = $true $settings.Encoding = [System.Text.Encoding]::UTF8 $writer = [System.Xml.XmlWriter]::Create($NuspecFile, $settings) try { $xmlDoc.WriteTo($writer) } finally { $writer.Flush() $writer.Close() $writer.Dispose() } } } #EndRegion '.\private\Write-Metadata.ps1' 48 #Region '.\public\Convert-Xml.ps1' -1 Function Convert-Xml { <# .SYNOPSIS Converts XML from a URL or a file to a hash table. .DESCRIPTION The Convert-Xml function takes a URL or a file path as input and converts the XML content to a hash table. If a URL is provided, the function downloads the XML content from the URL. If a file path is provided, the function reads the XML content from the file. The function then converts the XML content to a hash table and returns it. .PARAMETER Url The URL of the XML content to convert. If this parameter is provided, the function will download the XML content from the URL. .PARAMETER File The file path of the XML content to convert. If this parameter is provided, the function will read the XML content from the file. .EXAMPLE Convert-Xml -Url "http://example.com/data.xml" This example downloads the XML content from the specified URL and converts it to a hash table. .EXAMPLE Convert-Xml -File "C:\path\to\data.xml" This example reads the XML content from the specified file and converts it to a hash table. .NOTES The function does not support XML content that contains dependencies or comments. #> [cmdletBinding(HelpUri='https://chocolatey-solutions.github.io/Fondue/Convert-Xml')] Param( [Parameter()] [String] $Url, [Parameter()] [String] $File ) process { if ($url) { [xml]$xml = [System.Net.WebClient]::new().DownloadString($url) } if ($File) { [xml]$xml = Get-Content $File } $hash = @{} foreach ($node in ($xml.package.metadata.ChildNodes | Where-Object {$_.Name -notmatch 'dependencies|#comment'})) { $hash.Add($node.Name, $node.'#text') } return $hash } } #EndRegion '.\public\Convert-Xml.ps1' 60 #Region '.\public\New-Dependency.ps1' -1 function New-Dependency { <# .SYNOPSIS Injects one or more <dependency> nodes into a Chocolatey package nuspec file. .DESCRIPTION New-Dependency reads an existing .nuspec file and appends one or more <dependency> elements to the <dependencies> section. If the <dependencies> node does not yet exist it is created automatically. Optionally the package can be recompiled immediately after the dependency is injected, with the resulting .nupkg saved to an output directory of your choice. .PARAMETER Nuspec The full path to the .nuspec file to which the dependency will be added. .PARAMETER Dependency An array of hashtables, each containing a mandatory 'id' key and a mandatory 'version' key that specifies the NuGet version range for the dependency (e.g. @{id='git'; version='2.44.0'}). .PARAMETER Recompile When specified, runs 'choco pack' against the .nuspec file after the dependency has been injected, producing a .nupkg in the current directory or in -OutputDirectory. .PARAMETER OutputDirectory The directory to which the recompiled .nupkg should be saved. Only used with -Recompile. The path must already exist. .EXAMPLE Add a single versioned dependency New-Dependency -Nuspec 'C:\packages\foo.1.1.1.nuspec' -Dependency @{id='baz'; version='3.4.2'} .EXAMPLE Add multiple dependencies, one with a version range New-Dependency -Nuspec 'C:\packages\foo.1.1.0.nuspec' -Dependency @{id='baz'; version='1.1.1'}, @{id='boo'; version='[1.0.1,2.9.0)'} .EXAMPLE Add a dependency and immediately recompile the package $newDependencySplat = @{ Nuspec = 'C:\packages\foo.1.1.1.nuspec' Dependency = @{id='baz'; version='3.4.2'} Recompile = $true } New-Dependency @newDependencySplat .EXAMPLE Add a dependency, recompile, and save the .nupkg to a specific directory $newDependencySplat = @{ Nuspec = 'C:\packages\foo.1.1.1.nuspec' Dependency = @{id='baz'; version='3.4.2'} Recompile = $true OutputDirectory = 'C:\recompiled' } New-Dependency @newDependencySplat #> [CmdletBinding(HelpUri = 'https://chocolatey-solutions.github.io/Fondue/New-Dependency')] Param( [Parameter(Mandatory)] [String] $Nuspec, [Parameter(Mandatory)] [ValidateScript({ foreach ($d in $_) { if (-not $d.ContainsKey('id')) { throw "Each dependency hashtable must contain an 'id' key." } if (-not $d.ContainsKey('version')) { throw "Each dependency hashtable must contain a 'version' key. Dependency '$($d['id'])' is missing a version." } } $true })] [Hashtable[]] $Dependency, [Parameter()] [Switch] $Recompile, [Parameter()] [String] [ValidateScript({ Test-Path $_ })] $OutputDirectory ) process { [xml]$xmlContent = Get-Content $Nuspec # Define the XML namespace $namespaceManager = New-Object System.Xml.XmlNamespaceManager($xmlContent.NameTable) $namespaceManager.AddNamespace("ns", "http://schemas.microsoft.com/packaging/2015/08/nuspec.xsd") # Check if the package node exists and verify its namespace $packageNode = $xmlContent.SelectSingleNode("//*[local-name()='package']", $namespaceManager) if ($null -eq $packageNode) { Write-Error "Package node not found. Exiting." -Category ObjectNotFound break } else { Write-Verbose "Package node found." } # Check if the metadata node exists within the package node $metadataNode = $xmlContent.SelectSingleNode("//*[local-name()='metadata']", $namespaceManager) if ($null -eq $metadataNode) { Write-Error "Metadata node not found." -Category ObjectNotFound break } else { Write-Verbose "Metadata node found." } # Find the dependencies node $dependenciesNode = $xmlContent.SelectSingleNode("//*[local-name()='dependencies']", $namespaceManager) if ($null -eq $dependenciesNode) { $null = $dependenciesNode = $xmlContent.CreateElement('dependencies') $null = $metadataNode.AppendChild($dependenciesNode) } else { Write-Verbose "Dependencies node found." } #Loop over the given dependencies and create new nodes for each foreach ($D in $Dependency) { # Create a new XmlDocument $newDoc = New-Object System.Xml.XmlDocument # Create a new dependency element in the new document $newDependency = $newDoc.CreateElement("dependency") $newDependency.SetAttribute("id", "$($D['id'])") if ($D.version) { # Check if the version string contains invalid characters # Valid ranges: https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#version-ranges if ($($D['version']) -match '\([^,]*?\)') { Write-Error "Invalid version string: $($D['version']) for package $($D['id'])" continue } $newDependency.SetAttribute("version", "$($D['version'])") } # Import the new dependency into the original document $importedDependency = $xmlContent.ImportNode($newDependency, $true) # Append the imported dependency to the dependencies node $null = $dependenciesNode.AppendChild($importedDependency) } # Save the xml back to the nuspec file $settings = New-Object System.Xml.XmlWriterSettings $settings.Indent = $true $settings.Encoding = [System.Text.Encoding]::UTF8 $writer = [System.Xml.XmlWriter]::Create($Nuspec, $settings) try { $xmlContent.WriteTo($writer) } finally { $writer.Flush() $writer.Close() $writer.Dispose() } # Stupid hack to get rid of the 'xlmns=' part of the new dependency nodes. .Net methods are "overly helpful" $content = Get-Content -Path $Nuspec -Raw $content = $content -replace ' xmlns=""', '' Set-Content -Path $Nuspec -Value $content if ($Recompile) { if (-not (Get-Command choco)) { Write-Error "Choco is required to recompile the package but was not found on this system" -Category ResourceUnavailable } else { $OD = if ($OutputDirectory) { $OutputDirectory } else { Split-Path -Parent $Nuspec } $chocoArgs = ('pack', $Nuspec, $OD) $choco = (Get-Command choco).Source $null = & $choco @chocoArgs if ($LASTEXITCODE -eq 0) { 'Package is ready and available at {0}' -f $OD } else { throw 'Recompile had an error, see chocolatey.log for details' } } } } } #EndRegion '.\public\New-Dependency.ps1' 200 #Region '.\public\New-MetaPackage.ps1' -1 function New-Metapackage { <# .SYNOPSIS Creates a new Chocolatey meta (virtual) package. .DESCRIPTION New-Metapackage generates the .nuspec file needed to define a Chocolatey metapackage — a package that contains no software itself but declares a set of dependencies that will be installed together. The nuspec is written to -Path (defaults to the current directory) and can optionally be given an explicit semantic version. .PARAMETER Id The package id for the metapackage (e.g. 'dev-tools'). Used as the nuspec <id> element. .PARAMETER Summary A short, one-line summary of what the metapackage installs. .PARAMETER Description A longer description of the metapackage. Must be at least 30 characters. .PARAMETER Dependency An array of hashtables, each with an 'id' key and an optional 'version' key, describing the packages that will be pulled in when this metapackage is installed (e.g. @{id='git'; version='2.44.0'}). .PARAMETER Path The directory in which to write the generated .nuspec file. Defaults to the current working directory. .PARAMETER Version A valid semantic version for the package (e.g. '1.0.0' or '1.0.0-pre'). Defaults to '0.1.0'. .EXAMPLE Create a minimal metapackage using only mandatory parameters $newMetapackageSplat = @{ Id = 'dev-tools' Summary = 'Common developer tools' Description = 'Installs a curated set of developer tooling for new machines.' Dependency = @{id='git'}, @{id='vscode'}, @{id='nodejs'} } New-Metapackage @newMetapackageSplat .EXAMPLE Create a metapackage with a pre-release version saved to a custom path $newMetapackageSplat = @{ Id = 'dev-tools' Summary = 'Common developer tools' Description = 'Installs a curated set of developer tooling for new machines.' Dependency = @{id='git'; version='2.44.0'}, @{id='putty'} Version = '1.0.0-pre' Path = 'C:\chocopackages' } New-Metapackage @newMetapackageSplat .NOTES This function requires Chocolatey to be installed so that the metapackage template can be placed in the standard templates directory. Alias: New-VirtualPackage #> [Alias('New-VirtualPackage')] [CmdletBinding(HelpUri = 'https://chocolatey-solutions.github.io/Fondue/New-Metapackage')] Param( [Parameter(Position = 0, Mandatory)] [String] $Id, [Parameter(Position = 1, Mandatory)] [String] $Summary, [Parameter(Position = 2, Mandatory)] [Hashtable[]] $Dependency, [Parameter(Mandatory)] [ValidateScript({ if ($_.Length -ge 30) { $true } else { throw "Description must be at least 30 characters long." } })] [String] $Description, [Parameter()] [String] $Path = $PWD, [Parameter()] [ValidateScript({ $matcher = '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' $_ -match $matcher })] [String] $Version = '0.1.0' ) begin { $chocoTemplatesPath = 'C:\ProgramData\chocolatey\templates' if (-not (Test-Path $chocoTemplatesPath)) { $null = New-Item -ItemType Directory -Path $chocoTemplatesPath -Force } $copyItemSplat = @{ Path = Join-Path $PSScriptRoot 'template\metapackage' Destination = $chocoTemplatesPath Recurse = $true Force = $true } Copy-Item @copyItemSplat } end { $chocoArgs = @('new', "$Id", '--template="metapackage"', "Id=$Id", "Summary=$Summary", "Description=$Description", "Version=$Version") & choco @chocoArgs } } #EndRegion '.\public\New-MetaPackage.ps1' 126 #Region '.\public\New-Package.ps1' -1 function New-Package { <# .SYNOPSIS Generates a new Chocolatey package. .DESCRIPTION New-Package scaffolds a new Chocolatey package directory and .nuspec file. Three parameter sets are supported: - Default : creates a basic FOSS-style package using only a package name. - File : internalises a local installer (exe/msi/msu/zip) into the package. Requires a Chocolatey for Business license and the Licensed Extension. - Url : downloads and embeds an installer from a URL. Requires a Chocolatey for Business license and the Licensed Extension. Optional dependencies and nuspec metadata can be injected at scaffold time. Pass -Recompile to immediately pack the scaffolded directory into a .nupkg. .PARAMETER Name The package id (e.g. 'myapp'). Used as both the directory name and the nuspec <id>. Required for the Default parameter set. .PARAMETER File Path to a local installer file (exe, msi, msu, or zip) to embed in the package. Required for the File parameter set. Requires a Chocolatey for Business license. .PARAMETER Url URL of an installer to download and embed in the package. Required for the Url parameter set. Requires a Chocolatey for Business license. .PARAMETER Dependency One or more dependency hashtables to inject into the nuspec at creation time. Each hashtable must contain an 'id' key and a 'version' key. .PARAMETER Metadata A hashtable of additional nuspec metadata fields to populate (e.g. authors, version, description, projectUrl). Keys must match valid nuspec element names. .PARAMETER OutputDirectory The directory in which to create the package scaffold. Defaults to the current working directory. .PARAMETER Recompile When specified, runs 'choco pack' against the generated .nuspec immediately after scaffolding. Only available with the File and Url parameter sets. .EXAMPLE Create a basic package New-Package -Name 'myapp' .EXAMPLE Create a package from a local installer New-Package -File 'C:\installers\myapp-setup.exe' .EXAMPLE Create a package from a download URL New-Package -Url 'https://example.com/myapp-setup.exe' .EXAMPLE Create a package with rich metadata $newPackageSplat = @{ Name = 'myapp' Metadata = @{ Authors = 'Acme Corp' Version = '2.1.0' Description = 'The best app ever made' ProjectUrl = 'https://example.com' } } New-Package @newPackageSplat .EXAMPLE Scaffold and immediately pack to .nupkg New-Package -Url 'https://example.com/myapp-setup.exe' -Recompile .EXAMPLE Write output to a specific directory New-Package -Name 'myapp' -OutputDirectory 'C:\chocopackages' .NOTES The -File and -Url parameter sets require a valid Chocolatey for Business license and the Chocolatey Licensed Extension ('chocolatey.extension') to be installed. .LINK https://docs.chocolatey.org/en-us/guides/create/ #> [CmdletBinding(DefaultParameterSetName = 'Default', HelpUri = 'https://chocolatey-solutions.github.io/Fondue/New-Package')] Param( [Parameter(Mandatory, ParameterSetName = 'Default')] [String] $Name, [Parameter(Mandatory, ParameterSetName = 'File')] [ValidateScript({ Test-Path $_ })] [String] $File, [Parameter(Mandatory, ParameterSetName = 'Url')] [String] $Url, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'File')] [Parameter(ParameterSetName = 'Url')] [ValidateScript({ foreach ($d in $_) { if (-not $d.ContainsKey('id')) { throw "Each dependency hashtable must contain an 'id' key." } if (-not $d.ContainsKey('version')) { throw "Each dependency hashtable must contain a 'version' key. Dependency '$($d['id'])' is missing a version." } } $true })] [Hashtable[]] $Dependency, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'File')] [Parameter(ParameterSetName = 'Url')] [Hashtable] $Metadata, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'File')] [Parameter(ParameterSetName = 'Url')] [String] $OutputDirectory = $PWD, [Parameter(ParameterSetName = 'FIle')] [Parameter(ParameterSetName = 'Url')] [Switch] $Recompile ) process { switch ($PSCmdlet.ParameterSetName) { 'File' { $licenseValid = Assert-LicenseValid $extensionInstalled = Test-Path "$env:ChocolateyInstall\lib\chocolatey.extension" if (-not $licenseValid) { throw 'A valid Chocolatey license is required to use -File but was not found on this system.' } if (-not $extensionInstalled) { throw 'A valid license file was found, but the Chocolatey Licensed Extension is not installed. The Chocolatey Licensed Extension is required to use -File.' } $chocoArgs = @('new', "--file='$file'", "--output-directory='$OutputDirectory'", '--build-package') $i = 4 } 'Url' { $licenseValid = Assert-LicenseValid $extensionInstalled = Test-Path "$env:ChocolateyInstall\lib\chocolatey.extension" if (-not $licenseValid) { throw 'A valid Chocolatey license is required to use -Url but was not found on this system.' } if (-not $extensionInstalled) { throw 'A valid license file was found, but the Chocolatey Licensed Extension is not installed. The Chocolatey Licensed Extension is required to use -Url.' } $chocoArgs = @('new', "--url='$url'", "--output-directory='$OutputDirectory'", '--build-package', '--no-progress') $i = 7 } default { $chocoArgs = @('new', "$Name", "--output-directory='$OutputDirectory'") $i = 3 } } $matcher = "(?<nuspec>(?<=').*(?='))" $choco = & choco @chocoArgs Write-Verbose -Message $('Matching against {0}' -f $choco[$i]) $null = $choco[$i] -match $matcher if ($matches.nuspec) { 'Adding dependencies to package {0}, if any' -f $matches.nuspec } else { throw 'Something went wrong, check the chocolatey.log file for details!' } if ($Dependency) { $newDependencySplat = @{ Nuspec = $matches.nuspec Dependency = $Dependency OutputDirectory = $OutputDirectory } New-Dependency @newDependencySplat } if ($Metadata) { Write-Metadata -Metadata $Metadata -NuspecFile $matches.nuspec } if ($Recompile) { $chocoArgs = ('pack', $matches.nuspec, $OutputDirectory) $choco = (Get-Command choco).Source $null = & $choco @chocoArgs if ($LASTEXITCODE -eq 0) { 'Package is ready and available at {0}' -f $OutputDirectory } else { throw 'Recompile had an error, see chocolatey.log for details' } } } } #EndRegion '.\public\New-Package.ps1' 227 #Region '.\public\Open-FondueHelp.ps1' -1 function Open-FondueHelp { <# .SYNOPSIS Opens the Fondue module documentation in the default browser. .DESCRIPTION Open-FondueHelp launches the Fondue documentation website (https://chocolatey-solutions.github.io/Fondue/) in the system's default web browser. Use this as a quick shortcut to the full command reference without leaving your PowerShell session. .EXAMPLE Open the documentation website Open-FondueHelp #> [CmdletBinding(HelpUri = 'https://chocolatey-solutions.github.io/Fondue/Open-FondueHelp')] Param() end { Start-Process https://chocolatey-solutions.github.io/Fondue/ } } #EndRegion '.\public\Open-FondueHelp.ps1' 24 #Region '.\public\Remove-Dependency.ps1' -1 function Remove-Dependency { <# .SYNOPSIS Removes a dependency from a NuGet package specification file. .DESCRIPTION The Remove-Dependency function takes a NuGet package specification (.nuspec) file and an array of dependencies as input. It removes the specified dependencies from the .nuspec file. .PARAMETER PackageNuspec The path to the .nuspec file from which to remove dependencies. This parameter is mandatory. .PARAMETER Dependency An array of dependencies to remove from the .nuspec file. This parameter is mandatory. .EXAMPLE Remove-Dependency -PackageNuspec "C:\path\to\package.nuspec" -Dependency "Dependency1", "Dependency2" This example removes the dependencies "Dependency1" and "Dependency2" from the .nuspec file at the specified path. .NOTES The function does not support removing dependencies that are not directly listed in the .nuspec file. #> [CmdletBinding(HelpUri = 'https://chocolatey-solutions.github.io/Fondue/Remove-Dependency')] Param( [Parameter(Mandatory)] [String] $PackageNuspec, [Parameter(Mandatory)] [String[]] $Dependency ) process { $xmlDoc = [System.Xml.XmlDocument]::new() $xmlDoc.Load($PackageNuspec) # Create an XmlNamespaceManager and add the namespace $nsManager = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) $nsManager.AddNamespace('ns', $xmlDoc.DocumentElement.NamespaceURI) # Use the XmlNamespaceManager when selecting nodes $dependenciesNode = $xmlDoc.SelectSingleNode('//ns:metadata/ns:dependencies', $nsManager) foreach ($d in $Dependency) { if ($null -ne $dependenciesNode) { $dependencyToRemove = $dependenciesNode.SelectSingleNode("ns:dependency[@id='$d']", $nsManager) if ($null -ne $dependencyToRemove) { $null = $dependenciesNode.RemoveChild($dependencyToRemove) } } } $settings = New-Object System.Xml.XmlWriterSettings $settings.Indent = $true $settings.Encoding = [System.Text.Encoding]::UTF8 $writer = [System.Xml.XmlWriter]::Create($PackageNuspec, $settings) try { $xmlDoc.WriteTo($writer) } finally { $writer.Flush() $writer.Close() } } } #EndRegion '.\public\Remove-Dependency.ps1' 69 #Region '.\public\Sync-Package.ps1' -1 function Sync-Package { <# .SYNOPSIS Brings installed software under Chocolatey management using choco sync. .DESCRIPTION Sync-Package wraps the 'choco sync' command provided by the Chocolatey Licensed Extension to bring software already visible in Programs and Features under Chocolatey management without reinstalling it. Three modes are supported: - Default : syncs all unmanaged programs found in Programs and Features. - Package : syncs a single application identified by its Programs and Features display name, mapped to a specific Chocolatey package id. - Map : syncs multiple applications from a hashtable of DisplayName → PackageId pairs in one call. After syncing, Fondue checks for a TODO.txt file in each synced package folder and surfaces its contents as a warning if found. .PARAMETER Id The Chocolatey package id to assign to the synced application. Required for the Package parameter set. .PARAMETER DisplayName The exact display name of the application as it appears in Programs and Features. Required for the Package parameter set. .PARAMETER Map A hashtable of DisplayName → PackageId pairs, used to sync multiple applications in a single call. Required for the Map parameter set. .PARAMETER OutputDirectory The directory to which synced package files are written. Defaults to the current working directory. .EXAMPLE Sync all unmanaged programs in Programs and Features Sync-Package .EXAMPLE Sync a single application by display name and package id Sync-Package -Id 'googlechrome' -DisplayName 'Google Chrome' .EXAMPLE Sync multiple applications from a hashtable Sync-Package -Map @{ 'Google Chrome' = 'googlechrome' 'Notepad++' = 'notepadplusplus' } .EXAMPLE Sync and save package files to a custom output directory Sync-Package -Map @{'Notepad++' = 'notepadplusplus'} -OutputDirectory 'C:\synced' .NOTES Requires a Chocolatey for Business license and the Chocolatey Licensed Extension ('chocolatey.extension') to be installed. #> [CmdletBinding(DefaultParameterSetName = 'Default', HelpUri = 'https://chocolatey-solutions.github.io/Fondue/Sync-Package')] Param( [Parameter(Mandatory, ParameterSetName = 'Package')] [String] $Id, [Parameter(Mandatory, ParameterSetName = 'Package')] [String] $DisplayName, [Parameter(Mandatory, ParameterSetName = 'Map')] [hashtable] $Map, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'Map')] [Parameter(ParameterSetName = 'Package')] [String] $OutputDirectory = $PWD ) begin { $licenseValid = Assert-LicenseValid $extensionInstalled = Test-Path "$env:ChocolateyInstall\lib\chocolatey.extension" if (-not $licenseValid) { throw 'A valid Chocolatey license is required to use -File but was not found on this system.' } if (-not $extensionInstalled) { throw 'A valid license file was found, but the Chocolatey Licensed Extension is not installed. The Chocolatey Licensed Extension is required to use -File.' } } end { switch ($PSCmdlet.ParameterSetName) { 'Package' { choco sync --id="$DisplayName" --package-id="$Id" --output-directory="$OutputDirectory" $packageFolder = Join-path $OutputDirectory -ChildPath "sync\$Id" $todo = Join-Path $packageFolder -ChildPath 'TODO.txt' if (Test-Path $todo) { Write-Warning (Get-Content $todo) } } 'Map' { $map.GetEnumerator() | Foreach-Object { choco sync --id="$($_.Key)" --package-id="$($_.Value)" --output-directory="$OutputDirectory" $packageFolder = Join-path $OutputDirectory -ChildPath $_.Value $todo = Join-Path $packageFolder -ChildPath 'TODO.txt' if (Test-Path $todo) { Write-Warning (Get-Content $todo) } } } default { choco sync --output-directory="$OutputDirectory" } } } } #EndRegion '.\public\Sync-Package.ps1' 128 #Region '.\public\Test-NupkgFile.ps1' -1 function Test-NupkgFile { <# .SYNOPSIS Tests a NuGet package for compliance with specified rules. .DESCRIPTION The Test-NupkgFile function takes a NuGet package and a set of rules as input. It tests the package for compliance with the specified rules. The function can test for compliance with only the required rules, or it can also test for compliance with all system rules. Additional tests can be specified. .PARAMETER PackagePath The path to the NuGet package to test. This parameter is mandatory and validated to ensure that it is a valid path. .PARAMETER OnlyRequiredRules A switch that, when present, causes the function to test for compliance with only the required rules. .PARAMETER AdditionalTest An array of additional tests to run. This parameter is optional. .EXAMPLE Test-NupkgFile -PackagePath "C:\path\to\package.nupkg" -OnlyRequiredRules This example tests the NuGet package at the specified path for compliance with only the required rules. .EXAMPLE Test-NupkgFile -PackagePath "C:\path\to\package.nupkg" -AdditionalTest "Test1", "Test2" This example tests the NuGet package at the specified path and runs the additional tests "Test1" and "Test2". .NOTES The function uses the Fondue module to perform the tests. #> [CmdletBinding(HelpUri = 'https://chocolatey-solutions.github.io/Fondue/Test-NupkgFile')] Param( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ })] [String] $PackagePath, [Parameter()] [Switch] $OnlyRequiredRules, [Parameter()] [String[]] $AdditionalTest ) process { $Data = @{ PackagePath = $PackagePath } $moduleRoot = (Get-Module Fondue).ModuleBase $SystemTests = (Get-ChildItem (Join-Path $moduleRoot -ChildPath 'module_tests') -Recurse -Filter package*.tests.ps1) | Select-Object Name, FullName $containerCollection = [System.Collections.Generic.List[psobject]]::new() if ($OnlyRequiredRules) { $tests = ($SystemTests | Where-Object Name -match 'required').FullName $containerCollection.Add($tests) } else { $tests = ($SystemTests).FullName $containerCollection.Add($tests) } if ($AdditionalTest) { $AdditionalTest | ForEach-Object { $containerCollection.Add($_) } } $containers = $containerCollection | Foreach-object { New-PesterContainer -Path $_ -Data $Data } $configuration = [PesterConfiguration]@{ Run = @{ Container = $Containers Passthru = $true } Output = @{ Verbosity = 'Detailed' } TestResult = @{ Enabled = $false } } $results = Invoke-Pester -Configuration $configuration } } #EndRegion '.\public\Test-NupkgFile.ps1' 89 #Region '.\public\Test-NuspecFile.ps1' -1 function Test-NuspecFile { <# .SYNOPSIS Validates a .nuspec file or metadata hashtable against Fondue's built-in rule set. .DESCRIPTION Test-NuspecFile runs Pester-based validation against a Chocolatey .nuspec file or a raw metadata hashtable. By default all built-in nuspec tests are executed. Pass -SkipBuiltinTests to bypass the built-in suite and run only your own tests via -AdditionalTest. Both can be combined to layer organisation-specific rules on top of the built-in suite. .PARAMETER NuspecFile Path to the .nuspec file to validate. The file is converted to a metadata hashtable internally before testing. Mutually exclusive with -Metadata. .PARAMETER Metadata A hashtable of nuspec metadata to validate directly (e.g. as returned by Convert-Xml). Mutually exclusive with -NuspecFile. .PARAMETER SkipBuiltinTests When specified, the built-in test suite is not run. Requires -AdditionalTest to be supplied, otherwise an error is thrown. .PARAMETER AdditionalTest One or more paths to Pester test scripts (.tests.ps1) to run alongside (or instead of) the built-in suite. .EXAMPLE Run all built-in tests against a nuspec file Test-NuspecFile -NuspecFile 'C:\packages\myapp.nuspec' .EXAMPLE Test a raw metadata hashtable $meta = Convert-Xml -File 'C:\packages\myapp.nuspec' Test-NuspecFile -Metadata $meta .EXAMPLE Run only custom tests, skipping the built-in suite Test-NuspecFile -NuspecFile 'C:\packages\myapp.nuspec' -SkipBuiltinTests -AdditionalTest 'C:\tests\my-rules.tests.ps1' .EXAMPLE Add extra tests on top of the built-in suite Test-NuspecFile -NuspecFile 'C:\packages\myapp.nuspec' -AdditionalTest 'C:\tests\my-rules.tests.ps1' .NOTES Uses Pester 5 internally. Results are returned as a Pester TestResult object. #> [CmdletBinding(HelpUri = 'https://chocolatey-solutions.github.io/Fondue/Test-NuspecFile')] Param( [Parameter()] [ValidateScript({ Test-Path $_ })] [String] $NuspecFile, [Parameter()] [Hashtable] $Metadata, [Parameter()] [Switch] $SkipBuiltinTests, [Parameter()] [String[]] $AdditionalTest ) process { $data = if ($NuspecFile) { $Metadata = Convert-Xml -File $NuspecFile @{ metadata = $Metadata } } else { @{ Metadata = $Metadata } } $moduleRoot = (Get-Module Fondue).ModuleBase $SystemTests = (Get-ChildItem (Join-Path $moduleRoot -ChildPath 'module_tests') -Recurse -Filter nuspec*.tests.ps1) | Select-Object Name, FullName $containerCollection = [System.Collections.Generic.List[psobject]]::new() if(-not $SkipBuiltinTests){ $SystemTests |ForEach-Object{ $containerCollection.Add($_.FullName)} } if ($AdditionalTest) { $AdditionalTest | ForEach-Object { $containerCollection.Add($_) } } if($SkipBuiltinTests -and (-not $AdditionalTest)){ throw '-SkipBuiltinTests was passed, but not additional tests. Please pass additional tests, or remove -SkipBuiltinTests' } $containers = $containerCollection | Foreach-object { New-PesterContainer -Path $_ -Data $data } $configuration = [PesterConfiguration]@{ Run = @{ Container = $Containers Passthru = $true } Output = @{ Verbosity = 'Detailed' } TestResult = @{ Enabled = $false } } $results = Invoke-Pester -Configuration $configuration } } #EndRegion '.\public\Test-NuspecFile.ps1' 117 #Region '.\public\Update-ChocolateyMetadata.ps1' -1 function Update-ChocolateyMetadata { <# .SYNOPSIS Updates metadata fields in a Chocolatey .nuspec file. .DESCRIPTION Update-ChocolateyMetadata accepts a hashtable of metadata key/value pairs and writes each value into the matching element of the specified .nuspec file. Keys must correspond to valid nuspec element names (e.g. 'version', 'authors', 'releaseNotes'). Only the elements present in the hashtable are modified; all other elements in the file remain untouched. The function accepts pipeline input, making it easy to chain with Convert-Xml. .PARAMETER Metadata A hashtable of nuspec element names and their new values. Accepts pipeline input. .PARAMETER NuspecFile The full path to the .nuspec file to update. The file must already exist. .EXAMPLE Bump the version and add a release note $updateMetadataSplat = @{ NuspecFile = 'C:\packages\myapp.nuspec' Metadata = @{ version = '2.2.0' releaseNotes = 'Fixed a critical bug.' } } Update-ChocolateyMetadata @updateMetadataSplat .EXAMPLE Copy metadata from one nuspec into another via the pipeline Convert-Xml -File 'C:\packages\source.nuspec' | Update-ChocolateyMetadata -NuspecFile 'C:\packages\target.nuspec' .NOTES Uses Convert-Xml internally when reading source metadata from a file. #> [CmdletBinding(HelpUri = 'https://chocolatey-solutions.github.io/Fondue/Update-ChocolateyMetadata')] Param( [Parameter(Mandatory,ValueFromPipeline,ValueFromRemainingArguments)] [Hashtable] $Metadata, [Parameter(Mandatory)] [ValidateScript({Test-Path $_})] [String] $NuspecFile ) process { Write-metadata -MetaData $Metadata -Nuspecfile $NuspecFile } } #EndRegion '.\public\Update-ChocolateyMetadata.ps1' 58 #Region '.\Suffix.ps1' -1 #EndRegion '.\Suffix.ps1' 1 |