Private/Build-ChocolateyPackage.ps1

function Build-ChocolateyPackage {
    <#
    .SYNOPSIS
        Builds one or more Chocolatey packages from a .nuspec file and tools folder, using template substitution for customization.
 
    .DESCRIPTION
        For each provided context object, this function copies the specified .nuspec file and its sibling ./tools folder to a temporary directory,
        performing template substitution on all files using the context.
 
        It then runs 'choco pack' to create a Chocolatey package.
 
        You can optionally specify an output directory for the resulting .nupkg file(s).
 
        Returns the path to each created .nupkg file.
 
    .PARAMETER Context
        The context object for template substitutions. Must include a 'version' property. Accepts pipeline input for processing multiple packages in one call.
 
    .PARAMETER NuspecPath
        Path to the .nuspec file to use as the package template.
 
    .PARAMETER OutputPath
        Optional. Directory where the resulting .nupkg file will be placed. If not specified, the package is created in a temporary directory.
 
    .EXAMPLE
        Build-ChocolateyPackage -Context $ctx -NuspecPath 'Samples/firebird.nuspec' -OutputPath 'out/'
 
        Creates a Chocolatey package using the provided context and nuspec file, placing the result in the 'out/' directory.
 
    .EXAMPLE
        $contexts | Build-ChocolateyPackage -NuspecPath 'Samples/firebird.nuspec'
 
        Builds packages for each context object in the pipeline, using the specified nuspec file.
 
    .OUTPUTS
        System.String. The path to the created .nupkg file for each context object.
 
    .NOTES
        Supports -WhatIf and -Confirm for safe execution.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]$Context,

        [Parameter(Mandatory)]
        [string]$NuspecPath,

        [string]$OutputPath
    )

    begin {
        $nuspecFull = (Resolve-Path -LiteralPath $NuspecPath).Path
        if (-not (Test-Path $nuspecFull)) {
            throw "Nuspec file not found: $nuspecFull"
        }

        $nuspecBase = [System.IO.Path]::GetFileNameWithoutExtension($nuspecFull)
        $srcDir = Split-Path -Parent $nuspecFull
        $toolsDir = Join-Path $srcDir 'tools'
        if (-not (Test-Path $toolsDir)) {
            throw "Expected 'tools' folder not found: $toolsDir"
        }
    }

    process {
        $ctx = $Context
        if (-not $ctx.PSObject.Properties['version']) {
            throw "Each context object must have a 'version' property."
        }

        $versionStr = $ctx.version.ToString()
        $tempRoot = Join-Path $env:TEMP 'chocoforge'
        $tempDir = Join-Path $tempRoot (Join-Path $nuspecBase $versionStr)

        $expectedOutputPath = $OutputPath ? $OutputPath : $tempDir
        $expectedPackageName = Join-Path $expectedOutputPath "$nuspecBase.$versionStr.nupkg"
        if (-not $PSCmdlet.ShouldProcess($expectedPackageName, 'Build Chocolatey package')) {
            # Return simulated package path. No need to output verbose messages in WhatIf mode.
            return $expectedPackageName
        }

        if (Test-Path $tempDir) {
            Remove-Item -Recurse -Force $tempDir
        }
        New-Item -ItemType Directory -Path $tempDir | Out-Null

        # Render nuspec file
        $nuspecContent = Get-Content -Raw -LiteralPath $nuspecFull
        $renderedNuspec = Expand-Template -Content $nuspecContent -Context $ctx
        $nuspecDest = Join-Path $tempDir ([System.IO.Path]::GetFileName($nuspecFull))
        Set-Content -Path $nuspecDest -Value $renderedNuspec -NoNewline

        # Render and copy tools folder recursively
        $srcToolsFiles = Get-ChildItem -Path $toolsDir -Recurse -File
        foreach ($file in $srcToolsFiles) {
            $relPath = $file.FullName.Substring($toolsDir.Length).TrimStart('/', '\')
            $destPath = Join-Path $tempDir (Join-Path 'tools' $relPath)
            $destDir = Split-Path -Parent $destPath
            New-Item -ItemType Directory -Path $destDir -Force | Out-Null
            $content = Get-Content -Raw -LiteralPath $file.FullName
            $rendered = Expand-Template -Content $content -Context $ctx
            Set-Content -Path $destPath -Value $rendered -NoNewline
        }

        # Prepare choco pack args
        $tempNuspec = Join-Path $tempDir ([System.IO.Path]::GetFileName($nuspecFull))
        $chocoArguments = @('pack', $tempNuspec, '--limit-output')
        if ($OutputPath) {
            $chocoArguments += '--output-directory'
            $chocoArguments += $OutputPath
        }

        Write-VerboseMark -Message "Packing Chocolatey package in $tempDir"
        $result = Invoke-Chocolatey -Arguments $chocoArguments -WorkingDirectory $tempDir
        if ($result.ExitCode -ne 0) {
            throw "choco pack failed for version $($versionStr): $($result.StdOut)"
        }

        # Extract the filename from the output
        if ($result.StdOut -match "Successfully created package '([^']+)'") {
            $fileName = $Matches[1]
            Write-VerboseMark -Message "Chocolatey package '$fileName' created."
            return $fileName
        } else {
            throw 'Unexpected output from "choco pack": No package path found in message.'
        }
    }
}