342 lines
14 KiB
PowerShell
342 lines
14 KiB
PowerShell
param(
|
|
[string[]]$Versions = @(),
|
|
[string]$SentryDsnFile = '',
|
|
[string]$OutputDir = (Join-Path $PSScriptRoot '..\out-fast-builds'),
|
|
[string]$WorktreeRoot = (Join-Path $PSScriptRoot '..\..\aionui-build-worktrees'),
|
|
[int]$TimeoutSeconds = 1800,
|
|
[switch]$Sequential
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
function Resolve-RepoRoot {
|
|
return (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
|
}
|
|
|
|
function Get-PackageVersion([string]$RepoRoot) {
|
|
$packageJsonPath = Join-Path $RepoRoot 'package.json'
|
|
$packageJson = Get-Content -LiteralPath $packageJsonPath -Raw | ConvertFrom-Json
|
|
if (-not $packageJson.version) {
|
|
throw "package.json is missing version: $packageJsonPath"
|
|
}
|
|
return [string]$packageJson.version
|
|
}
|
|
|
|
function Resolve-SentryDsn([string]$Path) {
|
|
if ($env:SENTRY_DSN) {
|
|
Write-Host 'Using SENTRY_DSN from the current environment.'
|
|
return $env:SENTRY_DSN.Trim()
|
|
}
|
|
|
|
if ($Path) {
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
throw "SENTRY_DSN file not found: $Path"
|
|
}
|
|
Write-Host "Using SENTRY_DSN from file: $Path"
|
|
return (Get-Content -LiteralPath $Path -Raw).Trim()
|
|
}
|
|
|
|
Write-Warning 'SENTRY_DSN is not set. Building without installer/app Sentry reporting.'
|
|
return ''
|
|
}
|
|
|
|
function ConvertTo-ProcessArgument([string]$Value) {
|
|
if ($null -eq $Value) {
|
|
return '""'
|
|
}
|
|
return '"' + ($Value -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"'
|
|
}
|
|
|
|
function Invoke-Git([string]$RepoRoot, [string[]]$GitArgs) {
|
|
$psi = [System.Diagnostics.ProcessStartInfo]::new()
|
|
$psi.FileName = 'git'
|
|
$psi.WorkingDirectory = $RepoRoot
|
|
$psi.UseShellExecute = $false
|
|
$psi.RedirectStandardOutput = $true
|
|
$psi.RedirectStandardError = $true
|
|
$allArgs = @('-C', $RepoRoot) + $GitArgs
|
|
$psi.Arguments = ($allArgs | ForEach-Object { ConvertTo-ProcessArgument $_ }) -join ' '
|
|
|
|
$process = [System.Diagnostics.Process]::Start($psi)
|
|
$stdout = $process.StandardOutput.ReadToEnd()
|
|
$stderr = $process.StandardError.ReadToEnd()
|
|
$process.WaitForExit()
|
|
$output = ($stdout + $stderr).Trim()
|
|
if ($process.ExitCode -ne 0) {
|
|
throw "git $($GitArgs -join ' ') failed:`n$output"
|
|
}
|
|
return $output
|
|
}
|
|
|
|
function Remove-LongPathTree([string]$Path) {
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
return
|
|
}
|
|
|
|
$reparsePoints = @(
|
|
Get-ChildItem -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint } |
|
|
Sort-Object FullName -Descending
|
|
)
|
|
foreach ($item in $reparsePoints) {
|
|
try {
|
|
Start-Process -FilePath 'cmd.exe' -ArgumentList ('/c rmdir "' + $item.FullName + '"') -WindowStyle Hidden -Wait -ErrorAction SilentlyContinue
|
|
} catch {
|
|
Write-Warning "Failed to remove reparse point $($item.FullName): $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
$fullPath = [System.IO.Path]::GetFullPath($Path)
|
|
$longPath = if ($fullPath.StartsWith('\\')) { '\\?\UNC\' + $fullPath.TrimStart('\') } else { '\\?\' + $fullPath }
|
|
$process = Start-Process -FilePath 'cmd.exe' -ArgumentList ('/c rmdir /s /q "' + $longPath + '"') -WindowStyle Hidden -Wait -PassThru
|
|
if ($process.ExitCode -ne 0 -and (Test-Path -LiteralPath $Path)) {
|
|
throw "cmd rmdir failed with exit code $($process.ExitCode): $Path"
|
|
}
|
|
}
|
|
|
|
function New-BuildCommandFile([string]$WorktreePath, [string]$Version, [string]$Dsn, [string]$LocalAioncoreBinary, [string]$LocalAioncoreBundleDir) {
|
|
$scriptPath = Join-Path $WorktreePath "build-$Version.ps1"
|
|
$dsnBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Dsn))
|
|
$lines = @(
|
|
'$ErrorActionPreference = ''Stop''',
|
|
'$buildTemp = Join-Path $PSScriptRoot ''.tmp''',
|
|
'New-Item -ItemType Directory -Force -Path $buildTemp | Out-Null',
|
|
'$env:TEMP = $buildTemp',
|
|
'$env:TMP = $buildTemp',
|
|
'$env:SENTRY_DSN = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(''' + $dsnBase64 + '''))',
|
|
'$env:AIONUI_DEBUG_AUTO_UPDATE_CURRENT_VERSION = ''' + $Version + '''',
|
|
'$env:ELECTRON_BUILDER_COMPRESSION_LEVEL = ''1''',
|
|
'$env:AIONUI_BACKEND_LOCAL_BINARY = ''' + ($LocalAioncoreBinary -replace "'", "''") + '''',
|
|
'$env:AIONUI_BACKEND_LOCAL_BUNDLE_DIR = ''' + ($LocalAioncoreBundleDir -replace "'", "''") + '''',
|
|
'$env:ELECTRON_CACHE = Join-Path $env:LOCALAPPDATA ''electron\Cache''',
|
|
'& bun run build-win:x64:fast',
|
|
'exit $LASTEXITCODE'
|
|
)
|
|
[System.IO.File]::WriteAllLines($scriptPath, $lines, (New-Object System.Text.UTF8Encoding $false))
|
|
return $scriptPath
|
|
}
|
|
|
|
function Start-BuildProcess([string]$WorktreePath, [string]$Version, [string]$Dsn, [string]$LogDir, [string]$LocalAioncoreBinary, [string]$LocalAioncoreBundleDir) {
|
|
$scriptPath = New-BuildCommandFile $WorktreePath $Version $Dsn $LocalAioncoreBinary $LocalAioncoreBundleDir
|
|
$stdoutPath = Join-Path $LogDir "build-$Version.out.log"
|
|
$stderrPath = Join-Path $LogDir "build-$Version.err.log"
|
|
$process = Start-Process -FilePath 'powershell.exe' `
|
|
-ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $scriptPath) `
|
|
-WorkingDirectory $WorktreePath `
|
|
-RedirectStandardOutput $stdoutPath `
|
|
-RedirectStandardError $stderrPath `
|
|
-WindowStyle Hidden `
|
|
-PassThru
|
|
|
|
return [pscustomobject]@{
|
|
version = $Version
|
|
worktreePath = $WorktreePath
|
|
process = $process
|
|
stdoutPath = $stdoutPath
|
|
stderrPath = $stderrPath
|
|
startedAt = Get-Date
|
|
}
|
|
}
|
|
|
|
function Copy-UntrackedBuildInputs([string]$RepoRoot, [string]$WorktreePath, [string[]]$UntrackedFiles) {
|
|
foreach ($relative in $UntrackedFiles) {
|
|
$normalized = $relative -replace '\\', '/'
|
|
if (-not $normalized.StartsWith('resources/')) { continue }
|
|
|
|
$source = Join-Path $RepoRoot $relative
|
|
$target = Join-Path $WorktreePath $relative
|
|
if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { continue }
|
|
|
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) | Out-Null
|
|
Copy-Item -LiteralPath $source -Destination $target -Force
|
|
}
|
|
}
|
|
|
|
function Wait-BuildProcess($Build, [int]$TimeoutSeconds) {
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
while (-not $Build.process.HasExited) {
|
|
if ((Get-Date) -gt $deadline) {
|
|
Stop-Process -Id $Build.process.Id -Force -ErrorAction SilentlyContinue
|
|
throw "build $($Build.version) timed out after $TimeoutSeconds seconds. Logs: $($Build.stdoutPath), $($Build.stderrPath)"
|
|
}
|
|
Start-Sleep -Seconds 5
|
|
$Build.process.Refresh()
|
|
}
|
|
|
|
$Build.process.WaitForExit()
|
|
$Build.process.Refresh()
|
|
$exitCode = $Build.process.ExitCode
|
|
$source = Join-Path $Build.worktreePath "out\AionUi-$($Build.version)-win-x64.exe"
|
|
if (-not (Test-Path -LiteralPath $source)) {
|
|
$tailParts = @()
|
|
if (Test-Path -LiteralPath $Build.stderrPath) {
|
|
$raw = Get-Content -LiteralPath $Build.stderrPath -Raw -ErrorAction SilentlyContinue
|
|
if ($raw.Length -gt 12000) {
|
|
$tailParts += "stderr tail:`n" + $raw.Substring($raw.Length - 12000)
|
|
} else {
|
|
$tailParts += "stderr tail:`n" + $raw
|
|
}
|
|
}
|
|
if (Test-Path -LiteralPath $Build.stdoutPath) {
|
|
$raw = Get-Content -LiteralPath $Build.stdoutPath -Raw -ErrorAction SilentlyContinue
|
|
if ($raw.Length -gt 12000) {
|
|
$tailParts += "stdout tail:`n" + $raw.Substring($raw.Length - 12000)
|
|
} else {
|
|
$tailParts += "stdout tail:`n" + $raw
|
|
}
|
|
}
|
|
$tail = $tailParts -join "`n`n"
|
|
throw "build $($Build.version) failed with exit code $exitCode and did not produce $source. Logs: $($Build.stdoutPath), $($Build.stderrPath)`n$tail"
|
|
}
|
|
|
|
if ($null -ne $exitCode -and $exitCode -ne 0) {
|
|
throw "build $($Build.version) produced an artifact but exited with code $exitCode. Logs: $($Build.stdoutPath), $($Build.stderrPath)"
|
|
}
|
|
|
|
$sourceItem = Get-Item -LiteralPath $source
|
|
if ($sourceItem.Length -lt 50MB) {
|
|
throw "build $($Build.version) produced an unexpectedly small installer ($($sourceItem.Length) bytes): $source. Logs: $($Build.stdoutPath), $($Build.stderrPath)"
|
|
}
|
|
|
|
$target = Join-Path $script:OutputDirResolved "AionUi-$($Build.version)-win-x64.exe"
|
|
Copy-Item -LiteralPath $source -Destination $target -Force
|
|
$item = Get-Item -LiteralPath $target
|
|
$hash = (Get-FileHash -LiteralPath $target -Algorithm SHA512).Hash
|
|
[pscustomobject]@{
|
|
version = $Build.version
|
|
artifact = $target
|
|
size = $item.Length
|
|
sha512 = $hash
|
|
stdoutLog = $Build.stdoutPath
|
|
stderrLog = $Build.stderrPath
|
|
}
|
|
}
|
|
|
|
$repoRoot = Resolve-RepoRoot
|
|
$dsn = Resolve-SentryDsn $SentryDsnFile
|
|
$runId = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$runRoot = Join-Path $WorktreeRoot $runId
|
|
$patchPath = Join-Path $runRoot 'current-worktree.patch'
|
|
$buildVersions = @($Versions | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
|
|
if ($buildVersions.Count -eq 0) {
|
|
$buildVersions = @(Get-PackageVersion $repoRoot)
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $runRoot, $OutputDir | Out-Null
|
|
$script:OutputDirResolved = (Resolve-Path -LiteralPath $OutputDir).Path
|
|
|
|
$untracked = @(git -C $repoRoot ls-files --others --exclude-standard)
|
|
$untrackedBuildInputs = @($untracked | Where-Object { ($_ -replace '\\', '/').StartsWith('resources/') })
|
|
$untrackedIgnored = @($untracked | Where-Object { -not (($_ -replace '\\', '/').StartsWith('resources/')) })
|
|
if ($untrackedBuildInputs.Count -gt 0) {
|
|
Write-Warning "Copying untracked build inputs into worktrees: $($untrackedBuildInputs -join ', ')"
|
|
}
|
|
if ($untrackedIgnored.Count -gt 0) {
|
|
Write-Warning "Untracked files are not copied into build worktrees: $($untrackedIgnored -join ', ')"
|
|
}
|
|
|
|
Invoke-Git $repoRoot @('diff', '--binary', 'HEAD', "--output=$patchPath") | Out-Null
|
|
$hasPatch = (Get-Item -LiteralPath $patchPath).Length -gt 0
|
|
$baseRef = (Invoke-Git $repoRoot @('rev-parse', 'HEAD')).Trim()
|
|
$nodeModules = Join-Path $repoRoot 'node_modules'
|
|
$localAioncoreBinary = Join-Path $repoRoot 'resources\bundled-aioncore\win32-x64\aioncore.exe'
|
|
$localAioncoreBundleDir = Join-Path $repoRoot 'out\win-unpacked\resources\bundled-aioncore\win32-x64'
|
|
if (-not (Test-Path -LiteralPath (Join-Path $localAioncoreBundleDir 'managed-resources') -PathType Container)) {
|
|
$localAioncoreBundleDir = Join-Path $env:LOCALAPPDATA 'Programs\AionUi\resources\bundled-aioncore\win32-x64'
|
|
}
|
|
if (Test-Path -LiteralPath (Join-Path $localAioncoreBundleDir 'managed-resources') -PathType Container) {
|
|
$localAioncoreBundleDir = (Resolve-Path -LiteralPath $localAioncoreBundleDir).Path
|
|
Write-Host "=== using local aioncore bundle fallback: $localAioncoreBundleDir ==="
|
|
} else {
|
|
$localAioncoreBundleDir = ''
|
|
Write-Warning 'Local aioncore bundle fallback was not found; builds may need to prepare managed resources.'
|
|
}
|
|
if (Test-Path -LiteralPath $localAioncoreBinary -PathType Leaf) {
|
|
$localAioncoreBinary = (Resolve-Path -LiteralPath $localAioncoreBinary).Path
|
|
Write-Host "=== using local aioncore fallback: $localAioncoreBinary ==="
|
|
} else {
|
|
$localAioncoreBinary = ''
|
|
Write-Warning 'Local aioncore fallback was not found; builds may need to download aioncore.'
|
|
}
|
|
$worktrees = @()
|
|
$builds = @()
|
|
$results = @()
|
|
$completed = $false
|
|
|
|
try {
|
|
foreach ($version in $buildVersions) {
|
|
$worktreePath = Join-Path $runRoot "AionUi-$version"
|
|
$worktrees += $worktreePath
|
|
Write-Host "=== prepare worktree ${version}: $worktreePath ==="
|
|
Invoke-Git $repoRoot @('worktree', 'add', '--detach', $worktreePath, $baseRef) | Out-Null
|
|
|
|
if ($hasPatch) {
|
|
Invoke-Git $worktreePath @('apply', '--whitespace=nowarn', $patchPath) | Out-Null
|
|
}
|
|
Copy-UntrackedBuildInputs $repoRoot $worktreePath $untrackedBuildInputs
|
|
|
|
if ((Test-Path -LiteralPath $nodeModules) -and -not (Test-Path -LiteralPath (Join-Path $worktreePath 'node_modules'))) {
|
|
New-Item -ItemType Junction -Path (Join-Path $worktreePath 'node_modules') -Target $nodeModules | Out-Null
|
|
}
|
|
}
|
|
|
|
foreach ($version in $buildVersions) {
|
|
$worktreePath = Join-Path $runRoot "AionUi-$version"
|
|
Write-Host "=== build $version start: $(Get-Date -Format o) ==="
|
|
$build = Start-BuildProcess $worktreePath $version $dsn $runRoot $localAioncoreBinary $localAioncoreBundleDir
|
|
if ($Sequential) {
|
|
$result = Wait-BuildProcess $build $TimeoutSeconds
|
|
$results += $result
|
|
Write-Host "=== build $version done: $(Get-Date -Format o) size=$($result.size) sha512=$($result.sha512) ==="
|
|
} else {
|
|
$builds += $build
|
|
}
|
|
}
|
|
|
|
if (-not $Sequential) {
|
|
foreach ($build in $builds) {
|
|
$result = Wait-BuildProcess $build $TimeoutSeconds
|
|
$results += $result
|
|
Write-Host "=== build $($build.version) done: $(Get-Date -Format o) size=$($result.size) sha512=$($result.sha512) ==="
|
|
}
|
|
}
|
|
|
|
$results
|
|
$completed = $true
|
|
} finally {
|
|
foreach ($build in $builds) {
|
|
if ($build.process -and -not $build.process.HasExited) {
|
|
Stop-Process -Id $build.process.Id -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
if ($completed) {
|
|
foreach ($worktreePath in $worktrees) {
|
|
try {
|
|
Invoke-Git $repoRoot @('worktree', 'remove', '--force', $worktreePath) | Out-Null
|
|
} catch {
|
|
Write-Warning "git worktree remove failed for $worktreePath; removing directory with long-path fallback. $($_.Exception.Message)"
|
|
try {
|
|
Remove-LongPathTree $worktreePath
|
|
} catch {
|
|
Write-Warning "long-path fallback cleanup failed for $worktreePath. $($_.Exception.Message)"
|
|
}
|
|
try {
|
|
Invoke-Git $repoRoot @('worktree', 'prune') | Out-Null
|
|
} catch {
|
|
Write-Warning "git worktree prune failed. $($_.Exception.Message)"
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
Remove-LongPathTree $runRoot
|
|
} catch {
|
|
Write-Warning "run root cleanup failed for $runRoot. $($_.Exception.Message)"
|
|
}
|
|
} else {
|
|
Write-Warning "Build failed; preserving worktrees and logs under $runRoot"
|
|
}
|
|
}
|