Private/New-LogCollectorScript.ps1

function New-LogCollectorScript {
    <#
    .SYNOPSIS
        Writes Sandbox\CollectLogs.ps1 — copies PSADT + MSI logs into the mapped project.
    .DESCRIPTION
        Creates a helper script that runs INSIDE Windows Sandbox after the deployment steps of a
        test scenario. Because the project folder is mapped read-write to C:\PSADT inside the
        sandbox, copying logs to C:\PSADT\Sandbox\Logs lands them in <ProjectPath>\Sandbox\Logs on
        the host, so they survive after the (disposable) sandbox is discarded — giving the operator
        the PSAppDeployToolkit and MSI logs needed to troubleshoot an install/uninstall failure.
 
        The collector gathers *.log/*.zip from the PSADT log locations (admin default
        %WinDir%\Logs\Software, non-admin %ProgramData%\Logs\Software, plus any custom LogPath read
        from the packaged Config\config.psd1) and MSI*.log dropped in the TEMP folders.
 
        The Sandbox\ folder is stripped from the Staging copy during packaging, so these logs never
        ship inside the .intunewin.
    .PARAMETER ProjectPath
        Full path to the PSADT project folder.
    .EXAMPLE
        $collector = New-LogCollectorScript -ProjectPath 'C:\Win32Apps\Projects\Git_x64_2.53.0'
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ProjectPath
    )

    $sandboxPath = Join-Path $ProjectPath 'Sandbox'
    if (-not (Test-Path $sandboxPath)) {
        New-Item -ItemType Directory -Path $sandboxPath -Force | Out-Null
    }

    # Single-quoted here-string: every $ is literal and evaluated INSIDE the sandbox at runtime.
    $collectorScript = @'
# Auto-generated by win32-toolkit. Runs inside Windows Sandbox after the deployment steps.
# Copies PSAppDeployToolkit + MSI logs into the mapped project (C:\PSADT = host project folder)
# so they survive after the disposable sandbox is discarded.
$ErrorActionPreference = 'SilentlyContinue'
 
$dest = 'C:\PSADT\Sandbox\Logs'
New-Item -ItemType Directory -Path $dest -Force | Out-Null
 
# Candidate PSADT log locations (admin + non-admin defaults).
$sources = [System.Collections.Generic.List[string]]::new()
$sources.Add((Join-Path $env:WinDir 'Logs\Software'))
$sources.Add((Join-Path $env:ProgramData 'Logs\Software'))
 
# Honor a custom LogPath from the packaged config.psd1, if present.
try {
    $cfg = Import-PowerShellDataFile 'C:\PSADT\Config\config.psd1'
    foreach ($section in @($cfg.Toolkit, $cfg.MSI)) {
        if ($section -and $section.LogPath) {
            $lp = [string]$section.LogPath
            $lp = $lp.Replace('$envWinDir', $env:WinDir).Replace('$envProgramData', $env:ProgramData).Replace('$envSystemDrive', $env:SystemDrive)
            $sources.Add($lp)
        }
    }
} catch { }
 
$collected = 0
foreach ($src in ($sources | Select-Object -Unique)) {
    if ($src -and (Test-Path -LiteralPath $src)) {
        Get-ChildItem -LiteralPath $src -Recurse -File -Include *.log, *.zip -ErrorAction SilentlyContinue | ForEach-Object {
            Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $dest $_.Name) -Force
            $collected++
        }
    }
}
 
# MSI verbose logs that some installers drop directly in a TEMP folder.
foreach ($tempDir in @($env:TEMP, (Join-Path $env:WinDir 'Temp'))) {
    if ($tempDir -and (Test-Path -LiteralPath $tempDir)) {
        Get-ChildItem -LiteralPath $tempDir -File -Filter 'MSI*.log' -ErrorAction SilentlyContinue | ForEach-Object {
            Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $dest $_.Name) -Force
            $collected++
        }
    }
}
 
Write-Host ''
Write-Host '========================================' -ForegroundColor Cyan
Write-Host "win32-toolkit: collected $collected log file(s)" -ForegroundColor Cyan
Write-Host ' -> project\Sandbox\Logs (on the host)' -ForegroundColor White
Write-Host '========================================' -ForegroundColor Cyan
'@


    $collectorPath = Join-Path $sandboxPath 'CollectLogs.ps1'
    # UTF-8 WITH BOM: this runs under Windows PowerShell 5.1 inside the sandbox, which decodes a BOM-less
    # file as ANSI (PS7's Set-Content -Encoding UTF8 writes no BOM) — non-ASCII paths/text would mojibake.
    [System.IO.File]::WriteAllText($collectorPath, $collectorScript, (New-Object System.Text.UTF8Encoding($true)))

    return $collectorPath
}