e0b45fc71d
Invoke-RemoteBuild.ps1: when BuildCommand succeeds (exit 0) but GuestArtifactSource (default: dist) does not exist, instead of silently skipping packaging, now creates a placeholder 'build-ok.txt' in the artifact output dir and zips it. This ensures Get-BuildArtifacts.ps1 always finds artifacts.zip regardless of whether the build produced real output files. Correct for burn-in, smoke, and delay-only build commands. Start-BurnInTest.ps1: revert default BuildCommand to 'ping 127.0.0.1 -n 31 > nul' (~30s delay, no artifact output) now that the pipeline handles missing dist gracefully. The previous 'if not exist dist mkdir dist' workaround is no longer needed.
543 lines
25 KiB
PowerShell
543 lines
25 KiB
PowerShell
#Requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Executes a build inside a guest VM (via WinRM). Supports two clone modes:
|
|
(1) Host-side clone: copy pre-cloned source via WinRM zip, or
|
|
(2) Guest-side clone: clone repo directly in VM via git (§3.3, requires Git)
|
|
|
|
.DESCRIPTION
|
|
Called by Invoke-CIJob.ps1 to orchestrate the build inside the VM.
|
|
|
|
**Mode 1: Host-side (default)** — Caller provides pre-cloned source:
|
|
1. Compresses the source tree on host, copies archive into VM via WinRM
|
|
2. Runs dotnet restore + dotnet build inside VM
|
|
3. Packages output as C:\CI\output\artifacts.zip
|
|
|
|
**Mode 2: Guest-side (§3.3, -UseGitClone in Invoke-CIJob)** — Repo cloned in VM:
|
|
1. Clone repo directly into VM via git (eliminates host-zip-transfer overhead)
|
|
2. Runs dotnet restore + dotnet build inside VM
|
|
3. Packages output same as Mode 1
|
|
|
|
Supply either -HostSourceDir (Mode 1) or -CloneUrl (Mode 2), not both.
|
|
|
|
In Mode 2, PAT for private repos is read from Credential Manager inside
|
|
this script's WinRM session (§1.5: no argv, no log, cleanup at end).
|
|
|
|
.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
|
|
[Mode 1] Full path on HOST to already-cloned repository (host-side clone mode).
|
|
Mutually exclusive with -CloneUrl. When omitted, -CloneUrl must be provided.
|
|
Example: C:\Users\user\AppData\Local\Temp\ci-src-run-42
|
|
|
|
.PARAMETER CloneUrl
|
|
[Mode 2, §3.3] Git URL to clone directly in VM (guest-side clone mode).
|
|
Mutually exclusive with -HostSourceDir. When provided, repo cloned via git inside VM.
|
|
PAT (if needed) read from Credential Manager at runtime.
|
|
Example: https://gitea.emulab.it/Simone/myrepo.git
|
|
|
|
.PARAMETER CloneBranch
|
|
[Mode 2] Branch to clone. Ignored in Mode 1. Default: main
|
|
|
|
.PARAMETER CloneCommit
|
|
[Mode 2] Specific commit SHA to checkout after clone. Ignored in Mode 1.
|
|
|
|
.PARAMETER CloneSubmodules
|
|
[Mode 2] Pass --recurse-submodules to git clone. Ignored in Mode 1.
|
|
|
|
.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()]
|
|
[System.Management.Automation.PSCredential] $Credential,
|
|
|
|
# [Mode 1] Host-side clone — pre-cloned source directory on HOST
|
|
[ValidateScript({
|
|
if ($_ -and -not (Test-Path $_ -PathType Container)) {
|
|
throw "HostSourceDir path does not exist: $_"
|
|
}
|
|
return $true
|
|
})]
|
|
[string] $HostSourceDir = '',
|
|
|
|
# [Mode 2, §3.3] Guest-side clone — Git URL for in-VM clone
|
|
[string] $CloneUrl = '',
|
|
|
|
# [Mode 2] Branch to clone (default: main). Ignored if HostSourceDir provided.
|
|
[string] $CloneBranch = 'main',
|
|
|
|
# [Mode 2] Specific commit SHA to checkout. Ignored if HostSourceDir provided.
|
|
[string] $CloneCommit = '',
|
|
|
|
# [Mode 2] Clone with --recurse-submodules. Ignored if HostSourceDir provided.
|
|
[switch] $CloneSubmodules,
|
|
|
|
# [Mode 2] Credential Manager target for Gitea PAT (private repos).
|
|
# Store with: New-StoredCredential -Target 'GiteaPAT' -UserName '<user>' -Password '<pat>' -Persist LocalMachine
|
|
# Leave empty for public repos (no auth needed).
|
|
[string] $GiteaCredentialTarget = 'GiteaPAT',
|
|
|
|
[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,
|
|
|
|
# Guest OS type — Linux uses SSH instead of WinRM.
|
|
[ValidateSet('Windows', 'Linux')]
|
|
[string] $GuestOS = 'Windows',
|
|
|
|
# SSH private key path (used when GuestOS=Linux).
|
|
[string] $SshKeyPath = 'F:\CI\keys\ci_linux',
|
|
|
|
# SSH username (used when GuestOS=Linux).
|
|
[string] $SshUser = 'ci_build',
|
|
|
|
# Persistent known_hosts file for SSH calls (CI jobs).
|
|
# Pass an empty string to disable host-key checking (template provisioning only).
|
|
[string] $SshKnownHostsFile = 'F:\CI\State\known_hosts',
|
|
|
|
# Work directory inside the Linux guest.
|
|
[string] $GuestLinuxWorkDir = '/opt/ci/build',
|
|
|
|
# Output directory inside the Linux guest.
|
|
[string] $GuestLinuxOutputDir = '/opt/ci/output',
|
|
|
|
# Extra environment variables injected into the guest before the build command (§6.5).
|
|
# Keys must be valid env var names. Values are plain strings (never logged).
|
|
[hashtable] $ExtraGuestEnv = @{}
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force
|
|
|
|
# ── Validate mutually exclusive modes ─────────────────────────────────────
|
|
$hostCloneMode = -not [string]::IsNullOrWhiteSpace($HostSourceDir)
|
|
$guestCloneMode = -not [string]::IsNullOrWhiteSpace($CloneUrl)
|
|
if ($hostCloneMode -and $guestCloneMode) {
|
|
throw "Provide either -HostSourceDir (host-side clone) or -CloneUrl (guest-side clone), not both."
|
|
}
|
|
if (-not $hostCloneMode -and -not $guestCloneMode) {
|
|
throw "Provide either -HostSourceDir (host-side clone) or -CloneUrl (guest-side clone)."
|
|
}
|
|
if ($guestCloneMode -and -not $CloneBranch) {
|
|
throw "CloneBranch is required when using -CloneUrl mode."
|
|
}
|
|
|
|
if ($GuestOS -eq 'Windows' -and $null -eq $Credential) {
|
|
throw "Credential is required when GuestOS is Windows."
|
|
}
|
|
|
|
if ($GuestOS -eq 'Linux') {
|
|
Import-Module (Join-Path $PSScriptRoot '_Transport.psm1') -Force
|
|
|
|
Write-Host "[Invoke-RemoteBuild] Linux build mode — SSH transport"
|
|
|
|
# Step 1: Clean work dir (parent permissions handled by template setup)
|
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile `
|
|
-Command "rm -rf '$GuestLinuxWorkDir' && mkdir -p '$GuestLinuxWorkDir'"
|
|
|
|
if ($hostCloneMode) {
|
|
# ── Mode 1 (Linux): tar host source -> scp -> untar in guest ──
|
|
Write-Host "[Invoke-RemoteBuild] Packaging host source ($HostSourceDir) ..."
|
|
$tarName = "ci-src-$([Guid]::NewGuid().ToString('N').Substring(0,8)).tar.gz"
|
|
$hostTar = Join-Path $env:TEMP $tarName
|
|
$guestTar = "/tmp/$tarName"
|
|
if (Test-Path $hostTar) { Remove-Item $hostTar -Force }
|
|
# bsdtar on Windows 10/11 supports -C and gz natively.
|
|
& tar -czf $hostTar -C $HostSourceDir .
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "[Invoke-RemoteBuild] tar failed packaging $HostSourceDir (exit $LASTEXITCODE)"
|
|
}
|
|
try {
|
|
Write-Host "[Invoke-RemoteBuild] Copying source archive to guest:$guestTar ..."
|
|
Copy-SshItem -Source $hostTar -Destination $guestTar `
|
|
-IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile -Direction ToGuest
|
|
|
|
Write-Host "[Invoke-RemoteBuild] Extracting source in guest ($GuestLinuxWorkDir) ..."
|
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile `
|
|
-Command "tar -xzf '$guestTar' -C '$GuestLinuxWorkDir' && rm -f '$guestTar'"
|
|
}
|
|
finally {
|
|
if (Test-Path $hostTar) { Remove-Item $hostTar -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
}
|
|
else {
|
|
# ── Mode 2 (Linux): in-guest git clone ──
|
|
$cloneCmd = "git clone --depth 1 --branch '$CloneBranch'"
|
|
if ($CloneSubmodules) { $cloneCmd += ' --recurse-submodules' }
|
|
|
|
$cloneCmd += " '$CloneUrl' '$GuestLinuxWorkDir'" # non-authed URL for logging
|
|
Write-Host "[Invoke-RemoteBuild] Cloning $CloneUrl into guest $GuestLinuxWorkDir ..."
|
|
|
|
# PAT injection via git http.extraHeader — PAT never appears in argv or /proc/environ
|
|
$authHeader = ''
|
|
if ($GiteaCredentialTarget -ne '') {
|
|
try {
|
|
Import-Module CredentialManager -ErrorAction Stop
|
|
$credObj = Get-StoredCredential -Target $GiteaCredentialTarget -ErrorAction Stop
|
|
if ($credObj) {
|
|
$b64 = [Convert]::ToBase64String(
|
|
[System.Text.Encoding]::UTF8.GetBytes(
|
|
"$($credObj.UserName):$($credObj.GetNetworkCredential().Password)"))
|
|
$authHeader = "Authorization: Basic $b64"
|
|
}
|
|
} catch {
|
|
Write-Warning "[Invoke-RemoteBuild] Could not load PAT from Credential Manager ($GiteaCredentialTarget) — cloning without auth (public repo?)."
|
|
}
|
|
}
|
|
|
|
if ($authHeader -ne '') {
|
|
$authCloneCmd = "git -c credential.helper= -c 'http.extraHeader=$authHeader' clone --depth 1 --branch '$CloneBranch'"
|
|
if ($CloneSubmodules) { $authCloneCmd += ' --recurse-submodules' }
|
|
$authCloneCmd += " '$CloneUrl' '$GuestLinuxWorkDir'"
|
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile -Command $authCloneCmd
|
|
} else {
|
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile -Command $cloneCmd
|
|
}
|
|
|
|
# Checkout specific commit if requested
|
|
if ($CloneCommit -ne '') {
|
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile `
|
|
-Command "git -C '$GuestLinuxWorkDir' checkout '$CloneCommit'"
|
|
}
|
|
}
|
|
|
|
# Step 3: Run build command (always cd into workdir first)
|
|
# Prepend extra env exports (§6.5) — scoped to this SSH command only
|
|
$envPrefix = ''
|
|
foreach ($envKey in $ExtraGuestEnv.Keys) {
|
|
$envVal = $ExtraGuestEnv[$envKey] -replace "'", "'\''" # escape ' for POSIX single-quoted string
|
|
$envPrefix += "export $envKey='$envVal'; "
|
|
}
|
|
if ($BuildCommand -ne '') {
|
|
$buildCmd = "${envPrefix}cd '$GuestLinuxWorkDir' && $BuildCommand"
|
|
} else {
|
|
$buildCmd = "${envPrefix}cd '$GuestLinuxWorkDir' && make"
|
|
}
|
|
$displayCmd = if ($BuildCommand -ne '') { $BuildCommand } else { 'make' }
|
|
Write-Host "[Invoke-RemoteBuild] Running build (env vars redacted): cd '$GuestLinuxWorkDir' && $displayCmd"
|
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile -Command $buildCmd
|
|
|
|
# Step 4: Stage artifact tree under $GuestLinuxOutputDir for SCP collection
|
|
$outDir = $GuestLinuxOutputDir
|
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile `
|
|
-Command "rm -rf '$outDir' && mkdir -p '$outDir'"
|
|
|
|
$artSrc = if ($GuestArtifactSource -ne '') { $GuestArtifactSource } else { 'dist' }
|
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile `
|
|
-Command "if [ -d '$GuestLinuxWorkDir/$artSrc' ]; then cp -r '$GuestLinuxWorkDir/$artSrc/.' '$outDir/'; elif [ -e '$GuestLinuxWorkDir/$artSrc' ]; then cp '$GuestLinuxWorkDir/$artSrc' '$outDir/'; else echo '[Invoke-RemoteBuild] WARNING: artifact source not found: $artSrc' >&2; fi"
|
|
|
|
Write-Host "[Invoke-RemoteBuild] Linux build complete."
|
|
return
|
|
}
|
|
|
|
function Compress-BuildArtifact {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string] $Path,
|
|
[Parameter(Mandatory)]
|
|
[string] $DestinationPath
|
|
)
|
|
$7zip = 'C:\Program Files\7-Zip\7z.exe'
|
|
if (Test-Path $7zip) {
|
|
Write-Host "[Compress-BuildArtifact] Using 7-Zip (multi-threaded)..."
|
|
& $7zip a -mmt=on -mx1 $DestinationPath $Path 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "7-Zip compression failed (exit $LASTEXITCODE)"
|
|
}
|
|
}
|
|
else {
|
|
Write-Host "[Compress-BuildArtifact] 7-Zip not found, using Compress-Archive (fallback)..."
|
|
Compress-Archive -Path $Path -DestinationPath $DestinationPath -CompressionLevel Fastest -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
|
|
|
|
# ── Source preparation: Host clone (compress+transfer) or Guest clone (git) ──
|
|
if ($hostCloneMode) {
|
|
# Mode 1: Copy pre-cloned source from host to guest via WinRM zip
|
|
# 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-BuildArtifact -Path "$HostSourceDir\*" -DestinationPath $hostZip
|
|
$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
|
|
}
|
|
}
|
|
else {
|
|
# Mode 2 (§3.3): Clone repo directly in guest via git
|
|
# PAT (if needed) read from Credential Manager inside session, never logged
|
|
Write-Host "[Invoke-RemoteBuild] Cloning repository in guest (§3.3)..."
|
|
|
|
# Read PAT from host Credential Manager (§1.5: never in argv or logs)
|
|
$gitPatUser = $null
|
|
$gitPatPass = $null
|
|
if (-not [string]::IsNullOrWhiteSpace($GiteaCredentialTarget)) {
|
|
try {
|
|
$stored = Get-StoredCredential -Target $GiteaCredentialTarget -ErrorAction Stop
|
|
$gitPatUser = $stored.UserName
|
|
$gitPatPass = $stored.GetNetworkCredential().Password
|
|
Write-Host "[Invoke-RemoteBuild] PAT loaded from Credential Manager ($GiteaCredentialTarget)."
|
|
} catch {
|
|
Write-Warning "[Invoke-RemoteBuild] Could not read PAT from Credential Manager '$GiteaCredentialTarget' — proceeding without auth (public repo only)."
|
|
}
|
|
}
|
|
|
|
# Clone in guest via WinRM
|
|
Invoke-Command -Session $session -ScriptBlock {
|
|
param($cloneUrl, $branch, $commit, $workDir, $patUser, $patPass, $submodules)
|
|
|
|
Set-Location (Split-Path $workDir -Parent)
|
|
$gitArgs = @('clone', '--depth', '1', '--branch', $branch)
|
|
if ($submodules) { $gitArgs += '--recurse-submodules' }
|
|
$gitArgs += @($cloneUrl, (Split-Path $workDir -Leaf))
|
|
|
|
# Disable interactive credential prompts — WinRM has no TTY
|
|
$env:GIT_TERMINAL_PROMPT = '0'
|
|
|
|
# Build git config args:
|
|
# - credential.helper= (empty) clears GCM and any system credential helper
|
|
# - http.extraHeader injects Basic auth without credential helpers, temp files,
|
|
# or modifying the clone URL (§1.5: token never in URL, argv display, or logs)
|
|
$gitConfigArgs = @('-c', 'credential.helper=')
|
|
if ($patUser -and $patPass) {
|
|
$b64 = [Convert]::ToBase64String(
|
|
[Text.Encoding]::UTF8.GetBytes("${patUser}:${patPass}"))
|
|
$gitConfigArgs += @('-c', "http.extraHeader=Authorization: Basic $b64")
|
|
}
|
|
|
|
$gitArgs = $gitConfigArgs + $gitArgs
|
|
|
|
Write-Host "[Guest] Cloning $cloneUrl (branch: $branch)..."
|
|
& git @gitArgs 2>&1 | ForEach-Object { Write-Host "[Guest Clone] $_" }
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "git clone failed in guest (exit $LASTEXITCODE)"
|
|
}
|
|
|
|
# Checkout specific commit if provided
|
|
if ($commit) {
|
|
Write-Host "[Guest] Checking out commit: $commit"
|
|
Set-Location $workDir
|
|
& git checkout $commit 2>&1 | ForEach-Object { Write-Host "[Guest Checkout] $_" }
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "git checkout $commit failed in guest (exit $LASTEXITCODE)"
|
|
}
|
|
}
|
|
} -ArgumentList $CloneUrl, $CloneBranch, $CloneCommit, $GuestWorkDir, $gitPatUser, $gitPatPass, $CloneSubmodules.IsPresent
|
|
|
|
Write-Host "[Invoke-RemoteBuild] Guest clone complete."
|
|
}
|
|
|
|
# ── 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, $extraEnv)
|
|
# 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'
|
|
}
|
|
# Inject extra env vars (§6.5 secret injection)
|
|
foreach ($pair in $extraEnv.GetEnumerator()) {
|
|
[System.Environment]::SetEnvironmentVariable($pair.Key, $pair.Value, 'Process')
|
|
}
|
|
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
|
|
if (-not (Test-Path $srcPath)) {
|
|
# No artifact source — create a placeholder zip so that
|
|
# Get-BuildArtifacts succeeds and the full pipeline is exercised.
|
|
# This is expected for burn-in / smoke builds that produce no output.
|
|
Write-Host "[Build] Artifact source '$srcPath' not found — creating placeholder artifact."
|
|
$placeholder = Join-Path $archiveDir 'build-ok.txt'
|
|
'No artifact source directory. Build command completed successfully.' |
|
|
Out-File -FilePath $placeholder -Encoding UTF8
|
|
Compress-Archive -Path $placeholder -DestinationPath $artifactZip -Force
|
|
}
|
|
else {
|
|
$7zip = 'C:\Program Files\7-Zip\7z.exe'
|
|
if (Test-Path $7zip) {
|
|
Write-Host "[Build] Compressing artifacts with 7-Zip..."
|
|
& $7zip a -mmt=on -mx1 $artifactZip $srcPath 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
|
|
if ($LASTEXITCODE -ne 0) { throw "7-Zip failed" }
|
|
}
|
|
else {
|
|
Compress-Archive -Path $srcPath -DestinationPath $artifactZip -Force
|
|
}
|
|
}
|
|
}
|
|
|
|
return $exit
|
|
} -ArgumentList $GuestWorkDir, $BuildCommand, $GuestArtifactZip, $GuestArtifactSource, $UseSharedCache.IsPresent, $ExtraGuestEnv
|
|
|
|
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
|
|
}
|
|
if (-not (Test-Path $outputDir)) {
|
|
Write-Host "[Build] Output dir '$outputDir' not found — skipping packaging."
|
|
}
|
|
else {
|
|
$7zip = 'C:\Program Files\7-Zip\7z.exe'
|
|
if (Test-Path $7zip) {
|
|
Write-Host "[Build] Compressing output with 7-Zip..."
|
|
& $7zip a -mmt=on -mx1 $artifactZip "$outputDir\*" 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
|
|
if ($LASTEXITCODE -ne 0) { throw "7-Zip failed" }
|
|
}
|
|
else {
|
|
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
|
|
}
|