Files
ProjectAGiPrompt/13-构建专家-SHELL/构建专家/build-release.ps1
2026-05-27 17:44:02 +08:00

421 lines
14 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env pwsh
#Requires -Version 7.5
[CmdletBinding()]
param(
[ValidateSet("sync", "build", "all", "clean")]
[string]$Action = "all",
[ValidateSet("linux-x86_64", "linux-aarch64", "all")]
[string]$Target = "all",
[ValidateSet("dev", "release")]
[string]$BuildProfile = "dev",
[Alias("RunnableHostIp", "TargetHostIp")]
[string[]]$RuntimeHostIp = @(),
[string]$OutputDir = "build/release",
[Parameter(Mandatory = $true)]
[string]$LinuxHostUser,
[Parameter(Mandatory = $true)]
[string]$LinuxHostIp,
[Parameter(Mandatory = $true)]
[string]$LinuxRemoteWorkspaceDir,
[Parameter(Mandatory = $true)]
[string]$WindowsSshKeyPath,
[Parameter(Mandatory = $true)]
[string]$WindowsRsyncExe,
[string[]]$RsyncExcludes = @(
".git/",
".idea/",
".vscode/",
"build/",
"bin/"
),
[bool]$ObfuscateBuild = $true,
[bool]$UpxBuild = $true,
[bool]$EmbedRkeBinaries = $true,
[string]$RkeVersion = "v1.8.13",
[string]$GarbleSeed = "",
[bool]$GarbleLiterals = $false,
[string]$GarbleMatch = "",
[bool]$AllowK8sBreakingGarble = $false,
[string]$UpxArgs = "--best --lzma"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
if ($BuildProfile -eq "release") {
throw "本地 PowerShell 构建禁止 release 模式。release 构建仅允许在受控 Runner 中执行,并由超级管理员凭据授权。"
}
$ModuleName = "rmdc-watchdog"
$ModuleRoot = Split-Path -Parent $PSScriptRoot
$WorkspaceRoot = Split-Path -Parent $ModuleRoot
function Write-Log {
param(
[Parameter(Mandatory = $true)][ValidateSet("INFO", "WARN", "SUCCESS")][string]$Level,
[Parameter(Mandatory = $true)][string]$Message
)
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "[$ts] [$Level] $Message"
}
function Resolve-FullPath {
param([Parameter(Mandatory = $true)][string]$Path)
if (-not (Test-Path -LiteralPath $Path)) {
throw "路径不存在:$Path"
}
return (Resolve-Path -LiteralPath $Path).Path
}
function Assert-AbsoluteWindowsPath {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$Name
)
if (-not [System.IO.Path]::IsPathRooted($Path)) {
throw "$Name 必须是 Windows 绝对路径:$Path"
}
}
function Assert-AbsoluteLinuxPath {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$Name
)
if (-not $Path.StartsWith("/")) {
throw "$Name 必须是 Linux 绝对路径:$Path"
}
}
function Assert-IPAddress {
param(
[Parameter(Mandatory = $true)][string]$Value,
[Parameter(Mandatory = $true)][string]$Name
)
$parsed = [System.Net.IPAddress]::None
if (-not [System.Net.IPAddress]::TryParse($Value, [ref]$parsed)) {
throw "$Name 必须是有效 IP 地址:$Value"
}
}
function Normalize-IPAddressList {
param(
[Parameter(Mandatory = $true)][string[]]$Values,
[Parameter(Mandatory = $true)][string]$Name
)
$result = [System.Collections.Generic.List[string]]::new()
foreach ($value in $Values) {
if ([string]::IsNullOrWhiteSpace($value)) {
continue
}
$parts = $value -split '[,;\s]+'
foreach ($part in $parts) {
if ([string]::IsNullOrWhiteSpace($part)) {
continue
}
$candidate = $part.Trim()
Assert-IPAddress -Value $candidate -Name $Name
if (-not $result.Contains($candidate)) {
[void]$result.Add($candidate)
}
}
}
if ($result.Count -eq 0) {
throw "$Name 必须至少包含一个有效 IP 地址。"
}
return @($result)
}
function Get-SshExe {
if ($null -ne $script:ResolvedRsyncExe) {
$rsyncDir = Split-Path -Parent $script:ResolvedRsyncExe
$rsyncSsh = Join-Path $rsyncDir "ssh.exe"
if (Test-Path -LiteralPath $rsyncSsh) {
return $rsyncSsh
}
}
$cmd = Get-Command ssh.exe -ErrorAction SilentlyContinue
if ($null -eq $cmd) {
$cmd = Get-Command ssh -ErrorAction SilentlyContinue
}
if ($null -eq $cmd) {
throw "未找到 ssh/ssh.exe请安装 OpenSSH 客户端或使用 cwRsync 自带 ssh.exe。"
}
return $cmd.Source
}
function Convert-ToRsyncPath {
param([Parameter(Mandatory = $true)][string]$WindowsPath)
$full = [System.IO.Path]::GetFullPath($WindowsPath)
if ($full -match '^[A-Za-z]:\\') {
$drive = $full.Substring(0, 1).ToLowerInvariant()
$rest = $full.Substring(2) -replace '\\', '/'
return "/cygdrive/$drive$rest"
}
if ($full.StartsWith("/")) {
return $full
}
throw "无法转换为 rsync 可识别路径:$WindowsPath"
}
function Invoke-External {
param(
[Parameter(Mandatory = $true)][string]$Exe,
[Parameter(Mandatory = $true)][string[]]$Arguments,
[Parameter()][string]$StdinContent
)
Write-Log -Level INFO -Message ("执行命令: {0} {1}" -f $Exe, ($Arguments -join " "))
if ($PSBoundParameters.ContainsKey("StdinContent")) {
$tmp = [System.IO.Path]::GetTempFileName()
try {
$content = $StdinContent -replace "`r`n", "`n" -replace "`r", "`n"
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($tmp, $content, $utf8NoBom)
$proc = Start-Process -FilePath $Exe -ArgumentList $Arguments -RedirectStandardInput $tmp -Wait -NoNewWindow -PassThru
if ($null -eq $proc -or $proc.ExitCode -ne 0) {
$exitCode = if ($null -eq $proc) { -1 } else { $proc.ExitCode }
throw "命令执行失败,退出码:$exitCode"
}
}
finally {
Remove-Item -Path $tmp -Force -ErrorAction SilentlyContinue
}
return
}
& $Exe @Arguments
if ($LASTEXITCODE -ne 0) {
throw "命令执行失败,退出码:$LASTEXITCODE"
}
}
function New-RemoteShellScript {
param([Parameter(Mandatory = $true)][string]$Body)
@"
set -Eeuo pipefail
log() {
printf '[%s] [REMOTE] %s\n' "`$(date '+%F %T')" "`$*"
}
$Body
"@
}
function Invoke-RemoteBash {
param([Parameter(Mandatory = $true)][string]$ScriptContent)
$sshArgs = @(
"-i", $script:ResolvedSshKeyPath,
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"$script:LinuxHostUser@$script:LinuxHostIp",
"bash", "-s", "--"
)
Invoke-External -Exe $script:SshExe -Arguments $sshArgs -StdinContent $ScriptContent
}
function Convert-ToShellSingleQuoted {
param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Value)
return "'" + ($Value -replace "'", "'""'""'") + "'"
}
function Get-LocalBranch {
param([Parameter(Mandatory = $true)][string]$RepoPath)
$branch = (git -C $RepoPath symbolic-ref --quiet --short HEAD 2>$null)
if ([string]::IsNullOrWhiteSpace($branch)) {
$branch = (git -C $RepoPath rev-parse --short HEAD 2>$null)
}
if ([string]::IsNullOrWhiteSpace($branch)) {
return "detached"
}
return $branch.Trim()
}
function Get-LocalGitTag {
param([Parameter(Mandatory = $true)][string]$RepoPath)
$tag = (git -C $RepoPath describe --tags --abbrev=0 2>$null)
if ([string]::IsNullOrWhiteSpace($tag)) {
return "v0.0.0"
}
return $tag.Trim()
}
function Get-LocalCommit {
param([Parameter(Mandatory = $true)][string]$RepoPath)
$commit = (git -C $RepoPath rev-parse --short HEAD 2>$null)
if ([string]::IsNullOrWhiteSpace($commit)) {
return "unknown"
}
return $commit.Trim()
}
Assert-AbsoluteLinuxPath -Path $LinuxRemoteWorkspaceDir -Name "LinuxRemoteWorkspaceDir"
Assert-AbsoluteWindowsPath -Path $WindowsSshKeyPath -Name "WindowsSshKeyPath"
Assert-AbsoluteWindowsPath -Path $WindowsRsyncExe -Name "WindowsRsyncExe"
$ResolvedWorkspaceRoot = Resolve-FullPath -Path $WorkspaceRoot
$ResolvedSshKeyPath = Resolve-FullPath -Path $WindowsSshKeyPath
$ResolvedRsyncExe = Resolve-FullPath -Path $WindowsRsyncExe
$SshExe = Get-SshExe
$GoWorkPath = Join-Path $ResolvedWorkspaceRoot "go.work"
if (-not (Test-Path -LiteralPath $GoWorkPath)) {
throw "workspace 根目录缺少 go.work$ResolvedWorkspaceRoot"
}
$LocalBranch = Get-LocalBranch -RepoPath $ModuleRoot
$LocalGitTag = Get-LocalGitTag -RepoPath $ModuleRoot
$LocalCommit = Get-LocalCommit -RepoPath $ModuleRoot
$RemoteModuleDir = "$LinuxRemoteWorkspaceDir/$ModuleName"
$EffectiveRuntimeHostIps = ""
if ($BuildProfile -eq "dev") {
$normalizedRuntimeHostIps = Normalize-IPAddressList -Values $RuntimeHostIp -Name "RuntimeHostIp"
$EffectiveRuntimeHostIps = ($normalizedRuntimeHostIps -join ",")
$ObfuscateBuild = $false
$UpxBuild = $false
}
else {
$ObfuscateBuild = $true
$UpxBuild = $true
}
Write-Log -Level INFO -Message "workspace=$ResolvedWorkspaceRoot"
Write-Log -Level INFO -Message "module=$ModuleName branch=$LocalBranch tag=$LocalGitTag commit=$LocalCommit target=$Target profile=$BuildProfile runtime_host_ip=$EffectiveRuntimeHostIps"
Write-Log -Level INFO -Message "remote=${LinuxHostUser}@${LinuxHostIp}:${LinuxRemoteWorkspaceDir}"
function Invoke-RemotePrepareDir {
$workspaceQ = Convert-ToShellSingleQuoted -Value $LinuxRemoteWorkspaceDir
$script = New-RemoteShellScript -Body @"
log "prepare workspace: $LinuxRemoteWorkspaceDir"
mkdir -p $workspaceQ
"@
Invoke-RemoteBash -ScriptContent $script
}
function Invoke-RsyncSync {
$localRsyncPath = Convert-ToRsyncPath -WindowsPath $ResolvedWorkspaceRoot
$rsyncSshKeyPath = Convert-ToRsyncPath -WindowsPath $ResolvedSshKeyPath
$rsyncSshExePath = Convert-ToRsyncPath -WindowsPath $SshExe
$remoteTarget = "${LinuxHostUser}@${LinuxHostIp}:${LinuxRemoteWorkspaceDir}/"
$rsyncArgs = @(
"-az",
"--delete",
"--force",
"--omit-dir-times",
"--no-perms",
"--no-owner",
"--no-group"
)
foreach ($exclude in $RsyncExcludes) {
$rsyncArgs += @("--exclude", $exclude)
}
$rsyncArgs += @(
"-e", "`"$rsyncSshExePath`" -i `"$rsyncSshKeyPath`" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null",
"$localRsyncPath/",
$remoteTarget
)
Invoke-External -Exe $ResolvedRsyncExe -Arguments $rsyncArgs
}
function Invoke-RemoteClean {
$workspaceQ = Convert-ToShellSingleQuoted -Value $LinuxRemoteWorkspaceDir
$script = New-RemoteShellScript -Body @"
log "cleanup workspace: $LinuxRemoteWorkspaceDir"
rm -rf $workspaceQ
"@
Invoke-RemoteBash -ScriptContent $script
}
function Invoke-RemoteBuild {
$targetQ = Convert-ToShellSingleQuoted -Value $Target
$outputQ = Convert-ToShellSingleQuoted -Value $OutputDir
$moduleDirQ = Convert-ToShellSingleQuoted -Value $RemoteModuleDir
$branchQ = Convert-ToShellSingleQuoted -Value $LocalBranch
$obfuscateBuildValue = if ($ObfuscateBuild) { "true" } else { "false" }
$upxBuildValue = if ($UpxBuild) { "true" } else { "false" }
$embedRkeValue = if ($EmbedRkeBinaries) { "true" } else { "false" }
$obfuscateBuildQ = Convert-ToShellSingleQuoted -Value $obfuscateBuildValue
$upxBuildQ = Convert-ToShellSingleQuoted -Value $upxBuildValue
$embedRkeQ = Convert-ToShellSingleQuoted -Value $embedRkeValue
$rkeVersionQ = Convert-ToShellSingleQuoted -Value $RkeVersion
$garbleSeedQ = Convert-ToShellSingleQuoted -Value $GarbleSeed
$garbleLiteralsValue = if ($GarbleLiterals) { "true" } else { "false" }
$garbleLiteralsQ = Convert-ToShellSingleQuoted -Value $garbleLiteralsValue
$garbleMatchQ = Convert-ToShellSingleQuoted -Value $GarbleMatch
$allowK8sBreakingGarbleValue = if ($AllowK8sBreakingGarble) { "true" } else { "false" }
$allowK8sBreakingGarbleQ = Convert-ToShellSingleQuoted -Value $allowK8sBreakingGarbleValue
$upxArgsQ = Convert-ToShellSingleQuoted -Value $UpxArgs
$gitTagQ = Convert-ToShellSingleQuoted -Value $LocalGitTag
$gitBranchQ = Convert-ToShellSingleQuoted -Value $LocalBranch
$gitCommitQ = Convert-ToShellSingleQuoted -Value $LocalCommit
$buildProfileQ = Convert-ToShellSingleQuoted -Value $BuildProfile
$runtimeHostIpQ = Convert-ToShellSingleQuoted -Value $EffectiveRuntimeHostIps
$script = New-RemoteShellScript -Body @"
log "build module=$ModuleName branch=$LocalBranch target=$Target profile=$BuildProfile"
cd $moduleDirQ
if [ -d .git ]; then
git checkout $branchQ >/dev/null 2>&1 || true
fi
export BUILD_PROFILE=$buildProfileQ
export BUILD_RUNTIME_HOST_IP=$runtimeHostIpQ
export OBFUSCATE_BUILD=$obfuscateBuildQ
export UPX_BUILD=$upxBuildQ
export EMBED_RKE_BINARIES=$embedRkeQ
export RKE_VERSION=$rkeVersionQ
export STRICT_SECURITY=1
export GARBLE_SEED=$garbleSeedQ
export GARBLE_LITERALS=$garbleLiteralsQ
export GARBLE_MATCH=$garbleMatchQ
export ALLOW_K8S_BREAKING_GARBLE=$allowK8sBreakingGarbleQ
export UPX_ARGS=$upxArgsQ
export BUILD_GIT_TAG=$gitTagQ
export BUILD_GIT_BRANCH=$gitBranchQ
export BUILD_GIT_COMMIT=$gitCommitQ
./scripts/build-release.sh $targetQ $outputQ
log "build done: module=$ModuleName"
"@
Invoke-RemoteBash -ScriptContent $script
}
switch ($Action) {
"clean" {
Invoke-RemoteClean
Write-Log -Level SUCCESS -Message "远端清理完成"
}
"sync" {
Invoke-RemotePrepareDir
Invoke-RsyncSync
Write-Log -Level SUCCESS -Message "rsync 同步完成"
}
"build" {
Invoke-RemoteBuild
Write-Log -Level SUCCESS -Message "远端构建完成"
}
"all" {
Invoke-RemotePrepareDir
Invoke-RsyncSync
Invoke-RemoteBuild
Write-Log -Level SUCCESS -Message "rsync 同步 + 远端构建完成"
}
}