421 lines
14 KiB
PowerShell
421 lines
14 KiB
PowerShell
#!/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 同步 + 远端构建完成"
|
||
}
|
||
}
|