Modules/businessdev.ALbuild.Feeds/Public/New-BcNuGetPackage.ps1

function New-BcNuGetPackage {
    <#
    .SYNOPSIS
        Creates a NuGet package (.nupkg) that contains a Business Central .app.
 
    .DESCRIPTION
        Reads the .app manifest, derives the package id from the id scheme (or uses -PackageId),
        and produces a valid OPC NuGet package (with [Content_Types].xml and relationships)
        containing the .app and a generated .nuspec. Application/platform dependency ranges and
        extra dependencies can be declared (used by, for example, runtime packages).
 
    .PARAMETER AppFile
        Path to the .app (or .runtime.app) file.
 
    .PARAMETER PackageId
        Explicit package id. If omitted, derived from -IdScheme and the app identity.
 
    .PARAMETER IdScheme
        Package id template when -PackageId is not supplied. Default '{publisher}.{name}.{id}'.
 
    .PARAMETER Version
        Package version. Default: the app version.
 
    .PARAMETER Authors
        Package authors. Default: the app publisher.
 
    .PARAMETER Description
        Package description. Default: the app name.
 
    .PARAMETER Dependency
        Additional NuGet dependencies as @{ id = ...; version = ... } hashtables.
 
    .PARAMETER OutputFolder
        Output folder for the .nupkg. Default: the app file's folder.
 
    .EXAMPLE
        New-BcNuGetPackage -AppFile .\Publisher_App_1.0.0.0.app -IdScheme '{publisher}.{name}.symbols.{id}'
 
    .OUTPUTS
        System.String - the path to the created .nupkg.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $AppFile,
        [string] $PackageId,
        [string] $IdScheme = '{publisher}.{name}.{id}',
        [string] $Version,
        [string] $Authors,
        [string] $Description,
        [object[]] $Dependency = @(),
        [string] $OutputFolder
    )

    if (-not (Test-Path -LiteralPath $AppFile)) { throw "App file not found: '$AppFile'." }
    $info = Expand-BcAppFile -Path $AppFile
    if ($info.ExtractedPath -and (Test-Path $info.ExtractedPath)) {
        Remove-Item $info.ExtractedPath -Recurse -Force -ErrorAction SilentlyContinue
    }

    $normalize = { param($v) ($v -replace '[^A-Za-z0-9_\-]', '') }
    if (-not $PackageId) {
        $PackageId = $IdScheme.Replace('{publisher}', (& $normalize $info.Publisher)).Replace('{name}', (& $normalize $info.Name)).Replace('{id}', $info.Id)
        $PackageId = ($PackageId -replace '\.\.', '.').Trim('.')
    }
    if (-not $Version)     { $Version = "$($info.Version)" }
    if (-not $Authors)     { $Authors = $info.Publisher }
    if (-not $Description) { $Description = $info.Name }
    if (-not $OutputFolder) { $OutputFolder = Split-Path -Path (Resolve-Path -LiteralPath $AppFile).ProviderPath -Parent }

    $depXml = ''
    foreach ($dep in $Dependency) {
        $depXml += " <dependency id=`"$($dep.id)`" version=`"$($dep.version)`" />`n"
    }

    $nuspec = @"
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
  <metadata>
    <id>$PackageId</id>
    <version>$Version</version>
    <title>$($info.Name)</title>
    <authors>$Authors</authors>
    <owners>$Authors</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>$Description</description>
    <dependencies>
$depXml </dependencies>
  </metadata>
</package>
"@


    $contentTypes = @'
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="nuspec" ContentType="application/octet" />
  <Default Extension="app" ContentType="application/octet" />
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
</Types>
'@


    $relsId = 'R' + ([Guid]::NewGuid().ToString('N').Substring(0, 16))
    $rels = @"
<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Type="http://schemas.microsoft.com/packaging/2010/07/manifest" Target="/$PackageId.nuspec" Id="$relsId" />
</Relationships>
"@


    $nupkgPath = Join-Path $OutputFolder "$PackageId.$Version.nupkg"
    if (-not $PSCmdlet.ShouldProcess($nupkgPath, 'Create NuGet package')) { return }
    if (-not (Test-Path -LiteralPath $OutputFolder)) { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null }
    if (Test-Path -LiteralPath $nupkgPath) { Remove-Item -LiteralPath $nupkgPath -Force }

    Add-Type -AssemblyName System.IO.Compression -ErrorAction SilentlyContinue
    Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue

    $utf8 = [System.Text.UTF8Encoding]::new($false)
    $zip = [System.IO.Compression.ZipFile]::Open($nupkgPath, [System.IO.Compression.ZipArchiveMode]::Create)
    try {
        $addText = {
            param($entryName, $text)
            $entry = $zip.CreateEntry($entryName)
            $stream = $entry.Open()
            try { $bytes = $utf8.GetBytes($text); $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Dispose() }
        }
        & $addText '[Content_Types].xml' $contentTypes
        & $addText '_rels/.rels' $rels
        & $addText "$PackageId.nuspec" $nuspec

        # Payload .app under the package root.
        $appEntryName = [System.IO.Path]::GetFileName($AppFile)
        [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, (Resolve-Path -LiteralPath $AppFile).ProviderPath, $appEntryName) | Out-Null
    }
    finally {
        $zip.Dispose()
    }

    Write-ALbuildLog -Level Success "Created NuGet package '$nupkgPath'."
    return $nupkgPath
}