Files

134 lines
8.1 KiB
PowerShell

. (Join-Path $PSScriptRoot 'Build.Resources.ps1')
function Read-PveCleanupChoice {
while ($true) {
$answer=([string](Read-Host '是否清理本轮构建的临时资源?(Y/N) [Y]')).Trim()
if (-not $answer -or $answer -in @('y','yes')) { return $true }
if ($answer -in @('n','no')) { return $false }
Write-Host '请输入 Y 或 N,回车默认选择是。' -ForegroundColor Yellow
}
}
function Test-PveBuildCancellationKey {
if ([Console]::IsInputRedirected) { return $false }
while ([Console]::KeyAvailable) {
$key=[Console]::ReadKey($true)
if ([int]$key.KeyChar -eq 3 -or ($key.Key -eq [ConsoleKey]::C -and ($key.Modifiers -band [ConsoleModifiers]::Control))) { return $true }
}
return $false
}
function Write-PveBuildOutput {
param([string]$Line)
if ($Line -eq '#< CLIXML') { return }
if ($Line.StartsWith('<Objs ') -and $Line.Contains('http://schemas.microsoft.com/powershell/2004/04')) {
try {
$document=[Xml.XmlDocument]::new()
$document.XmlResolver=$null
$document.LoadXml($Line)
foreach ($node in $document.DocumentElement.ChildNodes) {
if ($node.GetAttribute('S') -in @('error','warning','verbose','debug')) {
$value=$node.InnerText
$value=[regex]::Replace($value,'_x([0-9a-fA-F]{4})_',[Text.RegularExpressions.MatchEvaluator]{param($match) [string][char][Convert]::ToInt32($match.Groups[1].Value,16)})
Write-Host $value
}
}
return
} catch {}
}
Write-Host $Line
}
function Set-PveCancelledBuildState {
param([string]$ContextPath,[string]$Cleanup,[string]$Detail)
$context=Get-PveBuildResources -ContextPath $ContextPath
$context.Cleanup=$Cleanup
Save-PveBuildResources -ContextPath $ContextPath -Context $context
$path=Join-Path $context.LogDirectory 'status.json'
$state=[ordered]@{Phase='cancelled';OutputPath=$context.OutputPath;LogDirectory=$context.LogDirectory;Cleanup=$Cleanup;CancelledUtc=[DateTime]::UtcNow.ToString('o')}
if ($Detail) { $state.Error=$Detail }
[IO.File]::WriteAllText($path,($state|ConvertTo-Json -Depth 5),[Text.UTF8Encoding]::new($false))
return [PSCustomObject]$state
}
function Invoke-PveCancelledBuild {
param([string]$ContextPath)
try { Stop-PveOwnedBuildVms -ContextPath $ContextPath }
catch { Write-Host ('停止本轮临时 VM 时遇到问题:'+$_.Exception.Message) -ForegroundColor Yellow }
if (Read-PveCleanupChoice) {
try {
Remove-PveOwnedBuildResources -ContextPath $ContextPath
Write-Host '构建已取消,本轮临时资源已清理。ISO、安装包和日志已保留。' -ForegroundColor Green
return Set-PveCancelledBuildState -ContextPath $ContextPath -Cleanup 'complete'
} catch {
Write-Host ('构建已取消,部分资源未能清理:'+$_.Exception.Message) -ForegroundColor Yellow
return Set-PveCancelledBuildState -ContextPath $ContextPath -Cleanup 'failed' -Detail $_.Exception.Message
}
}
Write-Host '构建已取消,临时资源已保留。' -ForegroundColor Yellow
return Set-PveCancelledBuildState -ContextPath $ContextPath -Cleanup 'declined'
}
function Invoke-PveControlledBuild {
param([string]$ScriptPath,[string]$ConfigPath,$Settings,$Config,[string]$ServicingIsoPath,[switch]$SkipBootVerification)
if (-not ('PveImageBuilder.BuildProcess' -as [type])) { Add-Type -Path (Join-Path $PSScriptRoot 'Build.Process.cs') }
$outputLock=$null
try {
$lockDirectory=Join-Path $Settings.ProjectRoot 'local\build-locks'
New-Item -ItemType Directory -Path $lockDirectory -Force | Out-Null
$hasher=[Security.Cryptography.SHA256]::Create()
try { $lockName=([BitConverter]::ToString($hasher.ComputeHash([Text.Encoding]::UTF8.GetBytes([IO.Path]::GetFullPath($Config.image_path).ToLowerInvariant())))).Replace('-','')+'.lock' }
finally { $hasher.Dispose() }
try { $outputLock=[IO.File]::Open((Join-Path $lockDirectory $lockName),[IO.FileMode]::OpenOrCreate,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None) }
catch { throw '另一个构建正在使用同一输出路径,请选择新的输出文件名。' }
$runId=(Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N')
$logDirectory=Join-Path $Settings.AssetsRoot ('logs\builds\wizard-'+$runId)
New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null
$contextPath=Join-Path $logDirectory 'resources.json'
$context=[PSCustomObject]@{SchemaVersion=1;RunId=$runId;AssetsRoot=[IO.Path]::GetFullPath($Settings.AssetsRoot);OutputPath=[IO.Path]::GetFullPath($Config.image_path);LogDirectory=[IO.Path]::GetFullPath($logDirectory);Completed=$false;Cleanup='not_requested';Files=@();Directories=@();VMs=@();SourceIsos=@()}
foreach ($iso in @($Config.wim_file_path,$Config.virtio_iso_path,$ServicingIsoPath) | Select-Object -Unique) {
if ([IO.Path]::GetExtension($iso) -ieq '.iso') {
$image=Get-DiskImage -ImagePath $iso -ErrorAction Stop
$context.SourceIsos+= [PSCustomObject]@{Path=[IO.Path]::GetFullPath($iso);AttachedBefore=[bool]$image.Attached}
}
}
Save-PveBuildResources -ContextPath $contextPath -Context $context
Register-PveBuildFile -ContextPath $contextPath -Path $context.OutputPath
Register-PveBuildFile -ContextPath $contextPath -Path ([IO.Path]::ChangeExtension($context.OutputPath,'.vhdx')) -Kind Disk
Register-PveBuildFile -ContextPath $contextPath -Path ([IO.Path]::ChangeExtension($context.OutputPath,'.raw')) -Kind Disk
$quote={param($value) "'"+$value.Replace("'","''")+"'"}
$snapshot=Join-Path $logDirectory 'build.ini'
Register-PveBuildFile -ContextPath $contextPath -Path $snapshot
Register-PveBuildFile -ContextPath $contextPath -Path ($snapshot+'.offline')
Copy-Item -LiteralPath ([IO.Path]::GetFullPath($ConfigPath)) -Destination $snapshot
$command='[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false); $ProgressPreference=''SilentlyContinue''; try { & '+(& $quote $ScriptPath)+' -ConfigPath '+(& $quote $snapshot)+' -InternalWorker -ResourceContextPath '+(& $quote $contextPath)+' -Confirm:$false'
if ($SkipBootVerification) { $command+=' -SkipBootVerification' }
$command+=' } catch { [Console]::Error.WriteLine($_.ToString()); exit 1 }; if ($?) { exit 0 } else { exit 1 }'
$encoded=[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))
$worker=$null
$consoleChanged=$false
$previous=$false
$cancelled=$false
try {
if (-not [Console]::IsInputRedirected) {
$previous=[Console]::TreatControlCAsInput
[Console]::TreatControlCAsInput=$true
$consoleChanged=$true
}
$worker=[PveImageBuilder.BuildProcess]::new((Join-Path $env:windir 'System32\WindowsPowerShell\v1.0\powershell.exe'),('-NoProfile -ExecutionPolicy Bypass -OutputFormat Text -EncodedCommand '+$encoded),(Split-Path -Parent $ScriptPath))
Write-Host '构建已启动。按 Ctrl+C 中断后,可选择是否清理本轮临时资源(默认是)。' -ForegroundColor Cyan
while (-not $worker.HasExited) {
foreach ($line in $worker.ReadOutput()) { Write-PveBuildOutput -Line $line }
if (Test-PveBuildCancellationKey) { $cancelled=$true; $worker.Cancel(); break }
Start-Sleep -Milliseconds 150
}
$worker.Wait()
foreach ($line in $worker.ReadOutput()) { Write-PveBuildOutput -Line $line }
$exitCode=$worker.ExitCode
} finally {
if ($consoleChanged) { [Console]::TreatControlCAsInput=$previous }
if ($worker) { $worker.Dispose() }
}
if ($cancelled) {
$latest=Get-PveBuildResources -ContextPath $contextPath
if (-not $latest.Completed) { return Invoke-PveCancelledBuild -ContextPath $contextPath }
}
if ($exitCode -ne 0 -and -not $cancelled) { throw ('构建失败,日志:'+$logDirectory) }
return Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $logDirectory 'status.json') | ConvertFrom-Json
} finally { if ($outputLock) { $outputLock.Dispose() } }
}