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