gitcat
1<#
2.SYNOPSIS
3 构建 gitcat 发布二进制:多平台交叉编译 + 校验和 + 升级清单。
4
5.DESCRIPTION
6 产物落在 dist\,包含:
7 - gitcat-<版本>-<os>-<arch>[.exe] 各平台单文件(静态,依赖 CGO_ENABLED=0)
8 - checksums.txt sha256sum 兼容格式
9 - manifest.json internal/upgrade 可直接消费的升级源清单
10
11 升级功能会用 manifest.json 里的 sha256 校验下载到的二进制,因此这个脚本
12 同时也是「发布」动作:把 dist\ 整个目录传到任意 HTTP 静态托管,
13 再把 manifest.json 的 URL 填进后台「系统设置 → 升级源」即可。
14
15.PARAMETER Version
16 语义化版本号,如 1.1.0。不传则自动取 git describe。
17
18.PARAMETER OutDir
19 产物目录,默认 dist\。
20
21.EXAMPLE
22 .\scripts\build-release.ps1 -Version 1.1.0
23 .\scripts\build-release.ps1 -Version 1.1.0 -Targets linux/amd64,windows/amd64
24#>
25[CmdletBinding()]
26param(
27 [string]$Version = '',
28 [string]$OutDir = 'dist',
29 [string[]]$Targets = @('linux/amd64', 'linux/arm64', 'windows/amd64', 'darwin/amd64', 'darwin/arm64')
30)
31
32$ErrorActionPreference = 'Stop'
33Set-StrictMode -Version Latest
34
35# Windows PowerShell 5.1 的 `Out-File -Encoding utf8` 会写 BOM,那会让
36# manifest.json 被 Go 的 json.Unmarshal 拒绝(BOM 不是合法 JSON),
37# checksums.txt 也会多出 BOM 而无法通过 sha256sum -c。一律走 .NET 无 BOM 写入。
38$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
39
40function Write-TextNoBom {
41 param([string]$Path, [string]$Text)
42 [System.IO.File]::WriteAllText($Path, $Text, $Utf8NoBom)
43}
44
45# git 在 Windows 上会把 LF/CRLF 警告写进 stderr;配合 $ErrorActionPreference='Stop'
46# 会被 PowerShell 5.1 当成终止性错误。探测版本信息时临时放开错误策略。
47function Invoke-GitQuiet {
48 param([string[]]$Arguments)
49 $prev = $ErrorActionPreference
50 $ErrorActionPreference = 'Continue'
51 try {
52 $out = & git @Arguments 2>$null
53 return ($out | Out-String).Trim()
54 }
55 finally { $ErrorActionPreference = $prev }
56}
57
58$root = Split-Path -Parent $PSScriptRoot
59Push-Location $root
60try {
61 # ---------------------------------------------------------------- 版本信息
62 $commit = Invoke-GitQuiet @('rev-parse', '--short', 'HEAD')
63 if (-not $commit) { $commit = 'unknown' }
64
65 # 未提交的改动如实写进 commit 字段,但不动 version——version 保持干净
66 # 才能让升级页面正常工作(IsDev 只看 Version)。
67 $dirty = Invoke-GitQuiet @('status', '--porcelain')
68 if ($dirty) { $commit = "$commit-dirty" }
69
70 if (-not $Version) {
71 $tag = Invoke-GitQuiet @('describe', '--tags', '--abbrev=0')
72 if ($tag) { $Version = $tag.TrimStart('v') } else { $Version = '0.0.0-dev' }
73 }
74 $buildDate = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
75
76 Write-Host "版本 : $Version" -ForegroundColor Cyan
77 Write-Host "提交 : $commit"
78 Write-Host "构建时间: $buildDate"
79 Write-Host "目标 : $($Targets -join ', ')"
80 Write-Host ''
81
82 # ---------------------------------------------------------------- 前置检查
83 & go build ./...
84 if ($LASTEXITCODE -ne 0) { throw '源码编译失败,已中止打包' }
85 & go vet ./...
86 if ($LASTEXITCODE -ne 0) { Write-Warning 'go vet 有告警,继续打包' }
87
88 if (Test-Path $OutDir) { Remove-Item -Recurse -Force $OutDir }
89 New-Item -ItemType Directory -Path $OutDir | Out-Null
90
91 # ---------------------------------------------------------------- 交叉编译
92 # -trimpath 去掉构建机的绝对路径;-s -w 去符号表,体积小一个量级。
93 $ldflags = "-s -w -X main.version=$Version -X main.commit=$commit -X main.buildDate=$buildDate"
94 $assets = @()
95
96 foreach ($t in $Targets) {
97 $os, $arch = $t -split '/'
98 $ext = if ($os -eq 'windows') { '.exe' } else { '' }
99 $name = "gitcat-$Version-$os-$arch$ext"
100
101 Write-Host "编译 $os/$arch ..." -NoNewline
102 $env:CGO_ENABLED = '0'
103 $env:GOOS = $os
104 $env:GOARCH = $arch
105 & go build -trimpath -ldflags $ldflags -o (Join-Path $OutDir $name) .
106 if ($LASTEXITCODE -ne 0) { throw "编译 $os/$arch 失败" }
107
108 $file = Get-Item (Join-Path $OutDir $name)
109 $hash = (Get-FileHash $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
110 $assets += [pscustomobject]@{
111 name = $name
112 os = $os
113 arch = $arch
114 size = $file.Length
115 sha256 = $hash
116 }
117 Write-Host (" {0,7:N1} MB" -f ($file.Length / 1MB)) -ForegroundColor DarkGray
118 }
119
120 # 还原环境变量,别把 GOOS/GOARCH 泄漏给后续命令
121 Remove-Item Env:GOOS, Env:GOARCH -ErrorAction SilentlyContinue
122
123 # ---------------------------------------------------------------- 校验和
124 # 格式与 sha256sum -c 兼容:每行 "<hash> <文件名>"
125 $lines = ($assets | ForEach-Object { "$($_.sha256) $($_.name)" }) -join "`n"
126 Write-TextNoBom (Join-Path $OutDir 'checksums.txt') "$lines`n"
127
128 # ---------------------------------------------------------------- 升级清单
129 # 结构必须与 internal/upgrade.Manifest 一致,否则后台「检查更新」读不懂。
130 $releases = @($assets | ForEach-Object {
131 [ordered]@{
132 version = $Version
133 url = $_.name
134 sha256 = $_.sha256
135 size = $_.size
136 os = $_.os
137 arch = $_.arch
138 notes = ''
139 }
140 })
141 $manifest = [ordered]@{
142 latest = $Version
143 min_supported = '1.0.0' # 低于此版本需先中转,避免一次跨度过大的结构变更
144 releases = $releases
145 }
146 Write-TextNoBom (Join-Path $OutDir 'manifest.json') `
147 (($manifest | ConvertTo-Json -Depth 5) + "`n")
148
149 # ---------------------------------------------------------------- 自检
150 # 1) 清单必须是无 BOM 的合法 JSON,否则 Go 端 json.Unmarshal 会直接报错
151 $mpath = Join-Path $OutDir 'manifest.json'
152 $mb = [System.IO.File]::ReadAllBytes($mpath)
153 if ($mb[0] -eq 0xEF -and $mb[1] -eq 0xBB) { throw 'manifest.json 带 BOM,必须去掉' }
154 $null = [System.Text.Encoding]::UTF8.GetString($mb) | ConvertFrom-Json
155
156 # 2) 拿本机那份产物实跑一次,确认版本注入、静态链接、可执行性都正常
157 $native = $assets | Where-Object { $_.os -eq 'windows' -and $_.arch -eq 'amd64' } |
158 Select-Object -First 1
159 if ($native) {
160 Write-Host ''
161 Write-Host '本机产物自检:'
162 $out = & (Join-Path $OutDir $native.name) -version 2>&1
163 if ($LASTEXITCODE -ne 0) { throw "产物无法执行:$out" }
164 $out | ForEach-Object { Write-Host " $_" }
165 if ("$out" -notmatch [regex]::Escape($Version)) {
166 throw "版本号未正确注入(期望 $Version)"
167 }
168 }
169
170 # ---------------------------------------------------------------- 汇总
171 $total = ($assets | Measure-Object -Property size -Sum).Sum
172 Write-Host ''
173 Write-Host "产物目录: $OutDir" -ForegroundColor Green
174 Write-Host "文件数 : $($assets.Count) 个二进制 + checksums.txt + manifest.json"
175 Write-Host ("合计 : {0:N1} MB" -f ($total / 1MB))
176 Write-Host ''
177 Write-Host '校验和:'
178 Get-Content (Join-Path $OutDir 'checksums.txt') | ForEach-Object {
179 Write-Host " $_" -ForegroundColor DarkGray
180 }
181 Write-Host ''
182 Write-Host '用法: 把整个 dist 目录传到静态托管,再把 manifest.json 的地址' -ForegroundColor Yellow
183 Write-Host ' 填入 后台 → 系统设置 → 升级源,即可一键升级。' -ForegroundColor Yellow
184}
185finally {
186 Remove-Item Env:GOOS, Env:GOARCH -ErrorAction SilentlyContinue
187 Pop-Location
188}