<# .SYNOPSIS 构建 gitcat 发布二进制:多平台交叉编译 + 校验和 + 升级清单。 .DESCRIPTION 产物落在 dist\,包含: - gitcat-<版本>--[.exe] 各平台单文件(静态,依赖 CGO_ENABLED=0) - checksums.txt sha256sum 兼容格式 - manifest.json internal/upgrade 可直接消费的升级源清单 升级功能会用 manifest.json 里的 sha256 校验下载到的二进制,因此这个脚本 同时也是「发布」动作:把 dist\ 整个目录传到任意 HTTP 静态托管, 再把 manifest.json 的 URL 填进后台「系统设置 → 升级源」即可。 .PARAMETER Version 语义化版本号,如 1.1.0。不传则自动取 git describe。 .PARAMETER OutDir 产物目录,默认 dist\。 .EXAMPLE .\scripts\build-release.ps1 -Version 1.1.0 .\scripts\build-release.ps1 -Version 1.1.0 -Targets linux/amd64,windows/amd64 #> [CmdletBinding()] param( [string]$Version = '', [string]$OutDir = 'dist', [string[]]$Targets = @('linux/amd64', 'linux/arm64', 'windows/amd64', 'darwin/amd64', 'darwin/arm64') ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest # Windows PowerShell 5.1 的 `Out-File -Encoding utf8` 会写 BOM,那会让 # manifest.json 被 Go 的 json.Unmarshal 拒绝(BOM 不是合法 JSON), # checksums.txt 也会多出 BOM 而无法通过 sha256sum -c。一律走 .NET 无 BOM 写入。 $Utf8NoBom = New-Object System.Text.UTF8Encoding($false) function Write-TextNoBom { param([string]$Path, [string]$Text) [System.IO.File]::WriteAllText($Path, $Text, $Utf8NoBom) } # git 在 Windows 上会把 LF/CRLF 警告写进 stderr;配合 $ErrorActionPreference='Stop' # 会被 PowerShell 5.1 当成终止性错误。探测版本信息时临时放开错误策略。 function Invoke-GitQuiet { param([string[]]$Arguments) $prev = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $out = & git @Arguments 2>$null return ($out | Out-String).Trim() } finally { $ErrorActionPreference = $prev } } $root = Split-Path -Parent $PSScriptRoot Push-Location $root try { # ---------------------------------------------------------------- 版本信息 $commit = Invoke-GitQuiet @('rev-parse', '--short', 'HEAD') if (-not $commit) { $commit = 'unknown' } # 未提交的改动如实写进 commit 字段,但不动 version——version 保持干净 # 才能让升级页面正常工作(IsDev 只看 Version)。 $dirty = Invoke-GitQuiet @('status', '--porcelain') if ($dirty) { $commit = "$commit-dirty" } if (-not $Version) { $tag = Invoke-GitQuiet @('describe', '--tags', '--abbrev=0') if ($tag) { $Version = $tag.TrimStart('v') } else { $Version = '0.0.0-dev' } } $buildDate = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') Write-Host "版本 : $Version" -ForegroundColor Cyan Write-Host "提交 : $commit" Write-Host "构建时间: $buildDate" Write-Host "目标 : $($Targets -join ', ')" Write-Host '' # ---------------------------------------------------------------- 前置检查 & go build ./... if ($LASTEXITCODE -ne 0) { throw '源码编译失败,已中止打包' } & go vet ./... if ($LASTEXITCODE -ne 0) { Write-Warning 'go vet 有告警,继续打包' } if (Test-Path $OutDir) { Remove-Item -Recurse -Force $OutDir } New-Item -ItemType Directory -Path $OutDir | Out-Null # ---------------------------------------------------------------- 交叉编译 # -trimpath 去掉构建机的绝对路径;-s -w 去符号表,体积小一个量级。 $ldflags = "-s -w -X main.version=$Version -X main.commit=$commit -X main.buildDate=$buildDate" $assets = @() foreach ($t in $Targets) { $os, $arch = $t -split '/' $ext = if ($os -eq 'windows') { '.exe' } else { '' } $name = "gitcat-$Version-$os-$arch$ext" Write-Host "编译 $os/$arch ..." -NoNewline $env:CGO_ENABLED = '0' $env:GOOS = $os $env:GOARCH = $arch & go build -trimpath -ldflags $ldflags -o (Join-Path $OutDir $name) . if ($LASTEXITCODE -ne 0) { throw "编译 $os/$arch 失败" } $file = Get-Item (Join-Path $OutDir $name) $hash = (Get-FileHash $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() $assets += [pscustomobject]@{ name = $name os = $os arch = $arch size = $file.Length sha256 = $hash } Write-Host (" {0,7:N1} MB" -f ($file.Length / 1MB)) -ForegroundColor DarkGray } # 还原环境变量,别把 GOOS/GOARCH 泄漏给后续命令 Remove-Item Env:GOOS, Env:GOARCH -ErrorAction SilentlyContinue # ---------------------------------------------------------------- 校验和 # 格式与 sha256sum -c 兼容:每行 " <文件名>" $lines = ($assets | ForEach-Object { "$($_.sha256) $($_.name)" }) -join "`n" Write-TextNoBom (Join-Path $OutDir 'checksums.txt') "$lines`n" # ---------------------------------------------------------------- 升级清单 # 结构必须与 internal/upgrade.Manifest 一致,否则后台「检查更新」读不懂。 $releases = @($assets | ForEach-Object { [ordered]@{ version = $Version url = $_.name sha256 = $_.sha256 size = $_.size os = $_.os arch = $_.arch notes = '' } }) $manifest = [ordered]@{ latest = $Version min_supported = '1.0.0' # 低于此版本需先中转,避免一次跨度过大的结构变更 releases = $releases } Write-TextNoBom (Join-Path $OutDir 'manifest.json') ` (($manifest | ConvertTo-Json -Depth 5) + "`n") # ---------------------------------------------------------------- 自检 # 1) 清单必须是无 BOM 的合法 JSON,否则 Go 端 json.Unmarshal 会直接报错 $mpath = Join-Path $OutDir 'manifest.json' $mb = [System.IO.File]::ReadAllBytes($mpath) if ($mb[0] -eq 0xEF -and $mb[1] -eq 0xBB) { throw 'manifest.json 带 BOM,必须去掉' } $null = [System.Text.Encoding]::UTF8.GetString($mb) | ConvertFrom-Json # 2) 拿本机那份产物实跑一次,确认版本注入、静态链接、可执行性都正常 $native = $assets | Where-Object { $_.os -eq 'windows' -and $_.arch -eq 'amd64' } | Select-Object -First 1 if ($native) { Write-Host '' Write-Host '本机产物自检:' $out = & (Join-Path $OutDir $native.name) -version 2>&1 if ($LASTEXITCODE -ne 0) { throw "产物无法执行:$out" } $out | ForEach-Object { Write-Host " $_" } if ("$out" -notmatch [regex]::Escape($Version)) { throw "版本号未正确注入(期望 $Version)" } } # ---------------------------------------------------------------- 汇总 $total = ($assets | Measure-Object -Property size -Sum).Sum Write-Host '' Write-Host "产物目录: $OutDir" -ForegroundColor Green Write-Host "文件数 : $($assets.Count) 个二进制 + checksums.txt + manifest.json" Write-Host ("合计 : {0:N1} MB" -f ($total / 1MB)) Write-Host '' Write-Host '校验和:' Get-Content (Join-Path $OutDir 'checksums.txt') | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } Write-Host '' Write-Host '用法: 把整个 dist 目录传到静态托管,再把 manifest.json 的地址' -ForegroundColor Yellow Write-Host ' 填入 后台 → 系统设置 → 升级源,即可一键升级。' -ForegroundColor Yellow } finally { Remove-Item Env:GOOS, Env:GOARCH -ErrorAction SilentlyContinue Pop-Location }