#Requires -Version 5.1 <# .SYNOPSIS Copies pre-cloned source from host into a build VM and runs dotnet build. .DESCRIPTION The caller (Invoke-CIJob.ps1) is responsible for cloning the repository on the HOST before calling this script. This script then: 1. Compresses the local source tree on the host, copies the archive into the VM via WinRM (single file = much faster than recursive Copy-Item), then expands it inside the VM. 2. Runs dotnet restore and dotnet build inside the VM 3. Packages the output as C:\CI\output\artifacts.zip inside the VM The build VM does NOT need internet access or git. All network I/O happens on the host before this script is called. .PARAMETER IPAddress IP of the build VM (e.g. 192.168.11.101). .PARAMETER Credential PSCredential for the build user inside the VM. Retrieve from Windows Credential Manager in production: $cred = Get-StoredCredential -Target 'BuildVMGuest' .PARAMETER HostSourceDir Full path on the HOST to the already-cloned repository directory. This directory is copied into the VM as the build working directory. Example: C:\Users\user\AppData\Local\Temp\ci-src-run-42 .PARAMETER Configuration MSBuild/dotnet build configuration. Default: Release .PARAMETER GuestWorkDir Working directory inside the VM where source will be placed. Default: C:\CI\build .PARAMETER GuestArtifactZip Full path inside the VM where the artifact archive will be written. Default: C:\CI\output\artifacts.zip .EXAMPLE $cred = Get-Credential .\Invoke-RemoteBuild.ps1 ` -IPAddress "192.168.11.101" ` -Credential $cred ` -HostSourceDir "C:\Temp\ci-src-run-42" #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')] [string] $IPAddress, [Parameter(Mandatory)] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Container })] [string] $HostSourceDir, [string] $Configuration = 'Release', # Custom build command to run inside the VM (working dir = GuestWorkDir). # When empty, defaults to: dotnet restore + dotnet build --configuration $Configuration # Example: 'python build_plugin.py --final' [string] $BuildCommand = '', # Glob pattern (relative to GuestWorkDir) of files to include in the artifact zip. # Used only when BuildCommand is set. Default: collect everything under 'dist'. # Example: 'dist\**' or 'Contrib\nsInnoUnp\Build\**' [string] $GuestArtifactSource = 'dist', [string] $GuestWorkDir = 'C:\CI\build', [string] $GuestArtifactZip = 'C:\CI\output\artifacts.zip', # Redirect dotnet NuGet package store and pip download cache to VMware shared # folders on the host (F:\CI\Cache\NuGet and F:\CI\Cache\pip). # Requires the template VMX to have been configured by Set-TemplateSharedFolders.ps1 # and VMware Tools HGFS driver present in the guest. [switch] $UseSharedCache ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force $sessionOptions = New-CISessionOption Write-Host "[Invoke-RemoteBuild] Connecting to VM at $IPAddress..." $session = New-PSSession ` -ComputerName $IPAddress ` -Credential $Credential ` -SessionOption $sessionOptions ` -UseSSL ` -Port 5986 ` -Authentication Basic ` -ErrorAction Stop try { # ── Ensure working directories exist on guest ──────────────────────── Invoke-Command -Session $session -ScriptBlock { param($workDir, $outputDir) # Ensure artifact output parent exists $outParent = Split-Path $outputDir -Parent if (-not (Test-Path $outParent)) { New-Item -ItemType Directory -Path $outParent -Force | Out-Null } # Clean and recreate build dir (ephemeral VM, but guard against partial prior run) if (Test-Path $workDir) { Remove-Item $workDir -Recurse -Force } New-Item -ItemType Directory -Path $workDir -Force | Out-Null } -ArgumentList $GuestWorkDir, $GuestArtifactZip # ── Copy source tree: compress on host, transfer zip, expand in guest ── # Single-file transfer over WinRM is far faster than recursive Copy-Item. $hostZip = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.zip' try { Write-Host "[Invoke-RemoteBuild] Compressing source on host..." Compress-Archive -Path "$HostSourceDir\*" -DestinationPath $hostZip -CompressionLevel Fastest -Force $zipSize = [math]::Round((Get-Item $hostZip).Length / 1MB, 1) Write-Host "[Invoke-RemoteBuild] Transferring archive to guest ($zipSize MB)..." $guestZip = 'C:\CI\src-transfer.zip' Copy-Item -Path $hostZip -Destination $guestZip -ToSession $session -Force Write-Host "[Invoke-RemoteBuild] Expanding archive in guest..." Invoke-Command -Session $session -ScriptBlock { param($zip, $dest) Expand-Archive -Path $zip -DestinationPath $dest -Force Remove-Item $zip -Force } -ArgumentList $guestZip, $GuestWorkDir Write-Host "[Invoke-RemoteBuild] Source transfer complete." } finally { Remove-Item $hostZip -Force -ErrorAction SilentlyContinue } # ── Build ────────────────────────────────────────────────────────── if ($BuildCommand -ne '') { # ── Custom build command (e.g. python, msbuild, cmake…) ────────── Write-Host "[Invoke-RemoteBuild] Running build command: $BuildCommand" $buildExit = Invoke-Command -Session $session -ScriptBlock { param($workDir, $cmd, $artifactZip, $artifactSrc, $useCache) # Unbuffered Python output — lines appear in real-time instead of being held in buffer $env:PYTHONUNBUFFERED = '1' if ($useCache) { $env:PIP_CACHE_DIR = '\\vmware-host\Shared Folders\pip-cache' } Set-Location $workDir # Stream each output line via Write-Host — WinRM forwards Write-Host in real-time & cmd /c "$cmd 2>&1" | ForEach-Object { Write-Host $_ } $exit = $LASTEXITCODE if ($exit -eq 0) { $archiveDir = Split-Path $artifactZip -Parent if (-not (Test-Path $archiveDir)) { New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null } $srcPath = Join-Path $workDir $artifactSrc Compress-Archive -Path $srcPath -DestinationPath $artifactZip -Force } return $exit } -ArgumentList $GuestWorkDir, $BuildCommand, $GuestArtifactZip, $GuestArtifactSource, $UseSharedCache.IsPresent if ($buildExit -ne 0) { throw "Build command failed (exit $buildExit)" } } else { # ── Default: dotnet restore + dotnet build ──────────────────────── Write-Host "[Invoke-RemoteBuild] Running dotnet restore..." $restoreExit = Invoke-Command -Session $session -ScriptBlock { param($workDir, $useCache) if ($useCache) { $env:NUGET_PACKAGES = '\\vmware-host\Shared Folders\nuget-cache' } Set-Location $workDir dotnet restore 2>&1 | ForEach-Object { Write-Host $_ } return $LASTEXITCODE } -ArgumentList $GuestWorkDir, $UseSharedCache.IsPresent if ($restoreExit -ne 0) { throw "dotnet restore failed (exit $restoreExit)" } Write-Host "[Invoke-RemoteBuild] Running dotnet build (configuration: $Configuration)..." $buildExit = Invoke-Command -Session $session -ScriptBlock { param($workDir, $config, $artifactZip, $useCache) if ($useCache) { $env:NUGET_PACKAGES = '\\vmware-host\Shared Folders\nuget-cache' } Set-Location $workDir $outputDir = Join-Path $workDir 'bin\CIOutput' dotnet build --configuration $config --output $outputDir 2>&1 | ForEach-Object { Write-Host $_ } $exit = $LASTEXITCODE if ($exit -eq 0) { $archiveDir = Split-Path $artifactZip -Parent if (-not (Test-Path $archiveDir)) { New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null } Compress-Archive -Path "$outputDir\*" -DestinationPath $artifactZip -Force } return $exit } -ArgumentList $GuestWorkDir, $Configuration, $GuestArtifactZip, $UseSharedCache.IsPresent if ($buildExit -ne 0) { throw "dotnet build failed (exit $buildExit)" } } Write-Host "[Invoke-RemoteBuild] Build succeeded. Artifact at $GuestArtifactZip" } finally { Remove-PSSession $session -ErrorAction SilentlyContinue }