Modules/businessdev.ALbuild.Containers/Public/Get-BcContainerAppFileInfo.ps1

function Get-BcContainerAppFileInfo {
    <#
    .SYNOPSIS
        Reads the real identity (app id / name / publisher / version) of one or more .app files by
        asking the build container, for packages the host cannot read itself.
 
    .DESCRIPTION
        A committed **encrypted BC runtime package** has no host-readable ZIP payload, so its app id can
        not be extracted on the agent (Expand-BcAppFile throws, and the id is not recoverable from the
        file header). The container, however, decrypts the manifest: 'Get-NAVAppInfo -Path' returns the
        real identity. This copies the .app file(s) into the container's C:\run\my share and reads them
        there, so a locally-served runtime dependency can be pinned by its ACTUAL app id (matching the
        consuming app.json 'dependencies') instead of relying on a '{Publisher}_{Name}_{Version}.app'
        file-name convention.
 
    .PARAMETER ContainerName
        The build container to read through (its C:\run\my share is used to hand the files across).
 
    .PARAMETER Path
        One or more .app file paths on the host.
 
    .OUTPUTS
        For each readable file, a PSCustomObject with Id, Name, Publisher, Version and File (the original
        host path). Files the container cannot read are omitted.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [string] $ContainerName,
        [Parameter(Mandatory)] [string[]] $Path,
        [string] $DockerExecutable = 'docker'
    )

    $share = Get-BcContainerHostShare -Name $ContainerName
    $leaf = 'appinfo-' + [guid]::NewGuid().ToString('N').Substring(0, 8)
    $hostWork = Join-Path $share $leaf
    New-Item -ItemType Directory -Force -Path $hostWork | Out-Null

    $results = [System.Collections.Generic.List[object]]::new()
    try {
        # Stage under ASCII index names ('0.app', '1.app', ...) and map back by index: a source file name
        # can contain non-ASCII characters (umlauts, accents, ...) that do not round-trip cleanly through
        # docker exec / JSON, which would break a name-based match. Get-NAVAppInfo reads the app
        # id/name/version from the manifest, not the file name, so the staged name is irrelevant.
        $byIndex = @{}
        $idx = 0
        foreach ($p in $Path) {
            if (-not (Test-Path -LiteralPath $p)) { continue }
            Copy-Item -LiteralPath $p -Destination (Join-Path $hostWork "$idx.app") -Force
            $byIndex["$idx"] = (Resolve-Path -LiteralPath $p).Path
            $idx++
        }
        if ($byIndex.Count -eq 0) { return @() }

        # In-container reader: BC's Apps management module targets .NET 8, so it must run under pwsh
        # (Windows PowerShell 5.1 in the container cannot load it). Emit one JSON line per readable app.
        $reader = @'
$ErrorActionPreference = 'Stop'
$mod = Get-ChildItem 'C:\Program Files\Microsoft Dynamics NAV\*\Service\Admin\Microsoft.BusinessCentral.Apps.Management.psd1' -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $mod) { throw 'BC Apps management module not found in the container.' }
Import-Module $mod.FullName
$out = foreach ($f in (Get-ChildItem '__CWORK__' -Filter *.app -File)) {
    try {
        $i = Get-NAVAppInfo -Path $f.FullName -ErrorAction Stop
        [PSCustomObject]@{ Leaf = $f.Name; Id = "$($i.AppId)"; Name = "$($i.Name)"; Publisher = "$($i.Publisher)"; Version = "$($i.Version)" }
    }
    catch { }
}
@($out) | ConvertTo-Json -Compress -Depth 3
'@
 -replace '__CWORK__', ('C:\run\my\' + $leaf)
        $readerHost = Join-Path $hostWork 'read.ps1'
        Set-Content -LiteralPath $readerHost -Value $reader -Encoding UTF8

        $json = & $DockerExecutable exec $ContainerName pwsh -NoProfile -ExecutionPolicy Bypass -File "C:/run/my/$leaf/read.ps1" 2>$null | Out-String
        $parsed = $null
        try { if ($json.Trim()) { $parsed = $json | ConvertFrom-Json } } catch { Write-Verbose "Could not parse container app-info JSON: $($_.Exception.Message)" }

        foreach ($e in @($parsed)) {
            if (-not $e -or -not $e.Id) { continue }
            $orig = $byIndex[[System.IO.Path]::GetFileNameWithoutExtension("$($e.Leaf)")]
            if (-not $orig) { continue }
            $results.Add([PSCustomObject]@{ Id = "$($e.Id)"; Name = "$($e.Name)"; Publisher = "$($e.Publisher)"; Version = "$($e.Version)"; File = $orig })
        }
    }
    finally { Remove-Item -LiteralPath $hostWork -Recurse -Force -ErrorAction SilentlyContinue }

    return $results.ToArray()
}