Files
Ant-Browser/publish/linux/publish-linux-docker.ps1
Ant Browser Release Bot fc5a6fadbe publish: 1.1.0 snapshot (ff46de5)
channel: master
version: 1.1.0
source-ref: master
published-at-utc: 2026-03-20T14:14:07Z
2026-03-20 22:14:08 +08:00

400 lines
13 KiB
PowerShell

param(
[Parameter(Mandatory = $true)]
[string]$RepoRoot,
[string]$ArchOutFile,
[string]$Version,
[string]$BuilderBaseImage
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$script:DockerContextArgs = @()
function Invoke-Docker {
param(
[Parameter(Mandatory = $true)]
[string[]]$Args,
[string[]]$ContextArgs,
[switch]$Quiet,
[switch]$AllowFailure
)
$effectiveContextArgs = @()
if ($PSBoundParameters.ContainsKey("ContextArgs")) {
$effectiveContextArgs = @($ContextArgs)
}
else {
$effectiveContextArgs = @($script:DockerContextArgs)
}
if ($Quiet) {
& docker @effectiveContextArgs @Args 2>$null
}
else {
& docker @effectiveContextArgs @Args
}
$code = $LASTEXITCODE
if ($code -ne 0 -and -not $AllowFailure) {
$contextSuffix = ""
if ($effectiveContextArgs.Count -ge 2) {
$contextSuffix = " [context: $($effectiveContextArgs[1])]"
}
throw "docker$contextSuffix $($Args -join ' ') failed with exit code $code"
}
return $code
}
function Invoke-DockerWithRetry {
param(
[Parameter(Mandatory = $true)]
[string[]]$Args,
[int]$MaxAttempts = 3,
[int]$DelaySeconds = 5
)
$attempt = 0
while ($attempt -lt $MaxAttempts) {
$attempt++
try {
Invoke-Docker -Args $Args | Out-Null
return
}
catch {
if ($attempt -ge $MaxAttempts) {
throw
}
Write-Host " Docker 命令失败,准备重试 ($attempt/$MaxAttempts): $($_.Exception.Message)"
Start-Sleep -Seconds $DelaySeconds
}
}
}
function Get-DockerContextCandidates {
$candidates = @(
[pscustomobject]@{
Label = "current"
ContextArgs = @()
}
)
$seen = @{}
$seen["current"] = $true
$extraNames = @()
$currentContextName = ((& docker context show 2>$null) | Out-String).Trim()
if ($LASTEXITCODE -eq 0 -and $currentContextName -ne "") {
$extraNames += $currentContextName
}
$extraNames += @("default", "desktop-linux")
foreach ($name in $extraNames) {
$trimmed = ([string]$name).Trim()
if ($trimmed -eq "" -or $seen.ContainsKey($trimmed)) {
continue
}
try {
& docker context inspect $trimmed *> $null
}
catch {
continue
}
if ($LASTEXITCODE -ne 0) {
continue
}
$seen[$trimmed] = $true
$candidates += [pscustomobject]@{
Label = $trimmed
ContextArgs = @("--context", $trimmed)
}
}
return $candidates
}
function Get-DockerInfo {
param(
[Parameter(Mandatory = $true)]
[string]$Label,
[AllowEmptyCollection()]
[string[]]$ContextArgs
)
$format = "{{.OSType}}|{{.Architecture}}"
$output = ""
$code = 1
try {
$output = (& docker @ContextArgs info --format $format 2>$null | Out-String).Trim()
$code = $LASTEXITCODE
}
catch {
return $null
}
if ($code -ne 0 -or $output -eq "") {
return $null
}
$parts = $output -split "\|", 2
$osType = ""
$architecture = ""
if ($parts.Length -ge 1) {
$osType = $parts[0].Trim().ToLowerInvariant()
}
if ($parts.Length -ge 2) {
$architecture = $parts[1].Trim()
}
return [pscustomobject]@{
Label = $Label
ContextArgs = @($ContextArgs)
OSType = $osType
Architecture = $architecture
}
}
function Resolve-DockerLinuxContext {
param(
[int]$Attempts = 1,
[int]$DelaySeconds = 0
)
$lastReadyContexts = @()
for ($attempt = 0; $attempt -lt $Attempts; $attempt++) {
$readyContexts = @()
foreach ($candidate in Get-DockerContextCandidates) {
$info = Get-DockerInfo -Label $candidate.Label -ContextArgs $candidate.ContextArgs
if ($null -eq $info) {
continue
}
$readyContexts += $info
if ($info.OSType -eq "linux") {
return [pscustomobject]@{
LinuxContext = $info
ReadyContexts = $readyContexts
}
}
}
$lastReadyContexts = $readyContexts
if ($attempt -lt ($Attempts - 1)) {
Start-Sleep -Seconds $DelaySeconds
}
}
return [pscustomobject]@{
LinuxContext = $null
ReadyContexts = $lastReadyContexts
}
}
function Get-EnvOrDefault {
param(
[string]$Name,
[string]$DefaultValue
)
$value = [Environment]::GetEnvironmentVariable($Name)
if ($null -eq $value) {
return $DefaultValue
}
$trimmed = $value.Trim()
if ($trimmed -eq "") {
return $DefaultValue
}
return $trimmed
}
try {
Write-Host "[Linux] 检查 Docker Desktop 环境..."
if (-not (Get-Command docker.exe -ErrorAction SilentlyContinue)) {
throw "未检测到 docker.exe,无法执行 Linux 打包`n 请先安装 Docker Desktop,并启用 Linux 容器引擎"
}
$dockerResolution = Resolve-DockerLinuxContext
if ($null -eq $dockerResolution.LinuxContext) {
$dockerDesktopExe = "C:\Program Files\Docker\Docker\Docker Desktop.exe"
if (Test-Path -LiteralPath $dockerDesktopExe) {
$dockerDesktopRunning = @(Get-Process -Name "Docker Desktop" -ErrorAction SilentlyContinue).Count -gt 0
if ($dockerDesktopRunning) {
Write-Host " Docker Desktop 已在运行,等待 Linux 引擎就绪..."
}
else {
Write-Host " Docker Linux 引擎未就绪,尝试启动 Docker Desktop..."
Start-Process -FilePath $dockerDesktopExe | Out-Null
}
$dockerResolution = Resolve-DockerLinuxContext -Attempts 24 -DelaySeconds 5
}
}
if ($null -eq $dockerResolution.LinuxContext) {
$readyContexts = @($dockerResolution.ReadyContexts)
if ($readyContexts.Count -gt 0) {
$readySummary = ($readyContexts | ForEach-Object { "$($_.Label) [$($_.OSType)]" }) -join ", "
throw "Docker 已启动,但当前可用上下文不是 Linux 容器: $readySummary`n 请在 Docker Desktop 中切换到 Linux containers 后重试"
}
throw "Docker 引擎未就绪`n 请先启动 Docker Desktop,并确认 Linux 容器引擎可用"
}
$script:DockerContextArgs = @($dockerResolution.LinuxContext.ContextArgs)
$contextLabel = $dockerResolution.LinuxContext.Label
$dockerArchRaw = ([string]$dockerResolution.LinuxContext.Architecture).Trim()
$linuxArch = switch ($dockerArchRaw.ToLowerInvariant()) {
"x86_64" { "amd64" }
"amd64" { "amd64" }
"aarch64" { "arm64" }
"arm64" { "arm64" }
default { "" }
}
if ($linuxArch -eq "") {
throw "无法识别 Docker Linux 架构`n 当前输出: $dockerArchRaw"
}
$repoDocker = ($RepoRoot -replace "\\", "/").Trim()
if ($repoDocker -eq "") {
throw "无法转换仓库路径为 Docker 挂载路径"
}
Write-Host "✓ Docker Desktop 就绪"
Write-Host " Docker 上下文: $contextLabel"
Write-Host " 仓库挂载: $repoDocker"
Write-Host " Linux 架构: $linuxArch"
Write-Host ""
$builderImage = "ant-browser-linux-builder:local"
$dockerfilePath = Join-Path $RepoRoot "publish/linux/linux-builder.Dockerfile"
$resolvedBuilderBaseImage = $BuilderBaseImage
if (-not $resolvedBuilderBaseImage -or $resolvedBuilderBaseImage.Trim() -eq "") {
$resolvedBuilderBaseImage = $env:ANT_BROWSER_LINUX_BUILDER_BASE_IMAGE
}
if (-not $resolvedBuilderBaseImage -or $resolvedBuilderBaseImage.Trim() -eq "") {
$resolvedBuilderBaseImage = "swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/golang:1.25-bookworm"
}
$resolvedBuilderBaseImage = $resolvedBuilderBaseImage.Trim()
$dockerfileHashRaw = (Get-FileHash -LiteralPath $dockerfilePath -Algorithm SHA256).Hash.ToLowerInvariant()
$dockerfileHash = [System.BitConverter]::ToString(
[System.Security.Cryptography.SHA256]::Create().ComputeHash(
[System.Text.Encoding]::UTF8.GetBytes("$dockerfileHashRaw|$resolvedBuilderBaseImage")
)
).Replace("-", "").ToLowerInvariant()
$builderHashLabel = "ant.browser.builder.hash"
$builderHashCurrent = ""
$builderInspectJson = ""
$builderInspectExitCode = 1
try {
$builderInspectJson = & docker @script:DockerContextArgs image inspect $builderImage 2>$null
$builderInspectExitCode = $LASTEXITCODE
}
catch {
$builderInspectExitCode = 1
}
if ($builderInspectExitCode -eq 0) {
$builderInspectItems = @($builderInspectJson | Out-String | ConvertFrom-Json)
if ($builderInspectItems.Count -gt 0) {
$config = $builderInspectItems[0].Config
if ($null -ne $config) {
$labelsProperty = $config.PSObject.Properties["Labels"]
if ($labelsProperty -and $null -ne $labelsProperty.Value) {
$labels = $labelsProperty.Value
if ($labels.PSObject.Properties.Name -contains $builderHashLabel) {
$builderHashCurrent = [string]$labels.PSObject.Properties[$builderHashLabel].Value
}
}
}
}
}
$needsBuilderImage = ($builderInspectExitCode -ne 0) -or ($builderHashCurrent -ne $dockerfileHash)
if ($needsBuilderImage) {
Write-Host "[Linux] 构建 Linux builder 镜像..."
Write-Host " Dockerfile: publish/linux/linux-builder.Dockerfile"
Write-Host " 基础镜像: $resolvedBuilderBaseImage"
Write-Host ""
Invoke-DockerWithRetry -Args @(
"build",
"--build-arg", "BUILDER_BASE_IMAGE=$resolvedBuilderBaseImage",
"--label", "$builderHashLabel=$dockerfileHash",
"-f", "publish/linux/linux-builder.Dockerfile",
"-t", $builderImage,
"publish/linux"
)
}
else {
Write-Host "[Linux] 复用已有 builder 镜像缓存"
Write-Host " 镜像: $builderImage"
Write-Host " 基础镜像: $resolvedBuilderBaseImage"
Write-Host ""
}
$npmCacheVolume = "ant-browser-linux-npm-cache"
$nodeModulesVolume = "ant-browser-linux-node-modules-$linuxArch"
$goModCacheVolume = "ant-browser-linux-go-mod-cache"
$goBuildCacheVolume = "ant-browser-linux-go-build-cache"
$runtimeCpuLimit = Get-EnvOrDefault -Name "ANT_BROWSER_LINUX_DOCKER_CPUS" -DefaultValue "2"
$runtimeMemoryLimit = Get-EnvOrDefault -Name "ANT_BROWSER_LINUX_DOCKER_MEMORY" -DefaultValue "1408m"
$runtimeMemorySwapLimit = Get-EnvOrDefault -Name "ANT_BROWSER_LINUX_DOCKER_MEMORY_SWAP" -DefaultValue "1792m"
$runtimeShmSize = Get-EnvOrDefault -Name "ANT_BROWSER_LINUX_DOCKER_SHM_SIZE" -DefaultValue "256m"
$nodeMaxOldSpace = Get-EnvOrDefault -Name "ANT_BROWSER_LINUX_NODE_MAX_OLD_SPACE" -DefaultValue "384"
$goMaxProcs = Get-EnvOrDefault -Name "ANT_BROWSER_LINUX_GOMAXPROCS" -DefaultValue "2"
$npmJobs = Get-EnvOrDefault -Name "ANT_BROWSER_LINUX_NPM_JOBS" -DefaultValue "1"
$versionArgText = ""
if ($Version -and $Version.Trim() -ne "") {
$versionArgText = " --version $Version"
}
$linuxPublishCommand = 'for f in publish/linux/*.sh tools/runtime/*.sh; do if [ -f "$f" ]; then sed -i ''s/\r$//'' "$f"; fi; done && bash publish/linux/publish-linux.sh --arch {0}{1}' -f $linuxArch, $versionArgText
Write-Host "[Linux] 通过 Docker Desktop 执行发布脚本..."
Write-Host " 镜像: $builderImage"
Write-Host " 挂载: $repoDocker > /workspace"
Write-Host " 缓存卷: $npmCacheVolume, $nodeModulesVolume, $goModCacheVolume, $goBuildCacheVolume"
Write-Host " 资源限制: CPUs=$runtimeCpuLimit, Memory=$runtimeMemoryLimit, Swap=$runtimeMemorySwapLimit, /dev/shm=$runtimeShmSize"
Write-Host " 进程限制: NODE_OPTIONS=--max-old-space-size=$nodeMaxOldSpace, GOMAXPROCS=$goMaxProcs, npm_config_jobs=$npmJobs"
Write-Host " 命令: 先清理 .sh 换行符,再执行 bash publish/linux/publish-linux.sh --arch $linuxArch$versionArgText"
Write-Host ""
Invoke-Docker -Args @(
"run",
"--rm",
"--cpus", $runtimeCpuLimit,
"--memory", $runtimeMemoryLimit,
"--memory-swap", $runtimeMemorySwapLimit,
"--shm-size", $runtimeShmSize,
"-e", "CI=1",
"-e", "NODE_OPTIONS=--max-old-space-size=$nodeMaxOldSpace",
"-e", "GOMAXPROCS=$goMaxProcs",
"-e", "npm_config_jobs=$npmJobs",
"-v", "${repoDocker}:/workspace",
"-v", "${npmCacheVolume}:/root/.npm",
"-v", "${nodeModulesVolume}:/workspace/frontend/node_modules",
"-v", "${goModCacheVolume}:/go/pkg/mod",
"-v", "${goBuildCacheVolume}:/root/.cache/go-build",
"-w", "/workspace",
$builderImage,
"bash",
"-c",
$linuxPublishCommand
) | Out-Null
if ($ArchOutFile -and $ArchOutFile.Trim() -ne "") {
Set-Content -LiteralPath $ArchOutFile -Value $linuxArch -NoNewline -Encoding ascii
}
Write-Host "✓ Linux 产物生成成功"
exit 0
}
catch {
Write-Host "✗ $($_.Exception.Message)"
exit 1
}