多平台分发渠道与打包脚本
本文介绍 SteamTools(Watt Toolkit)仓库中面向多平台(Windows / Linux / macOS / Android)的打包脚本与分发渠道体系:以 packaging/build.ps1 为核心入口、以 RID(Runtime Identifier)矩阵驱动的 dotnet publish 批量构建,以及由发布工具 ST.Tools.Publish(p.exe)完成的重打包、签名与多渠道产物生成。
Purpose and Scope
本页面覆盖以下内容:
packaging/build.ps1构建脚本的完整控制流:参数解析、发布工具引导、逐 RID 的dotnet publish调用、产物目录布局。- RID 矩阵(
fd-*框架依赖 vs 自包含)与dev-前缀调试机制。 - 多平台分发产物清单与命名约定(
msix、7z、tar.zst、deb、rpm、pkg、apk)。 - 打包脚本依赖的敏感环境变量(
Token、WIN_SIGN_PFX_PWD)与失败即停(fail-fast)策略。 packaging/目录下其余打包辅助文件(build.cmd、build.v1.ps1、build-osx-app.sh、Info.plist、SHA256.ps1)的定位说明。
以下内容有意留给兄弟页面,本页不展开:
- 程序安装后的文件目录结构细节:参见
doc/program-file-structure/Windows.md、Linux.md、macOS.md。 - 发布工具
ST.Tools.Publish内部的签名与上传实现:本页仅从调用方(build.ps1)视角描述其 CLI 契约。 - 应用自身运行时功能:与本页无关。
Overview
SteamTools 是跨平台桌面应用,同一个主工程(src/ST.Client.Desktop.Avalonia.App/ST.Client.Avalonia.App.csproj)需要为 Windows x64/x86、macOS x64/arm64、Linux x64/arm64 等多个目标平台产出安装包与压缩包。仓库通过一套"PowerShell 编排 + dotnet publish RID 矩阵 + 专用发布工具"的三层结构实现:
- 编排层:
packaging/build.ps1负责接收版本号、配置、是否正式发布三个参数,串联整个流程。本地开发可以只跑Build-App循环;正式发布(-isPublish $true)时才引入发布工具。 - 构建层:对每个 RID 调用
dotnet publish -p:PublishProfile=<rid>,输出到统一的Publish目录,fd-前缀的 RID 单独进入FrameworkDependent子目录。 - 发布工具层:
src/ST.Tools.Publish编译为p.exe,提供ver(生成版本号)与full(重打包/签名/上传,接收-win_sign_pfx_pwd)两个子命令。
doc/publish.md 记录了最终面向用户的分发资产矩阵,即每个平台/架构对应的扩展名:
- Windows:
Steam++_vx.y.z_win_x64.msix/.7z(x86、arm64 同理) - Linux:
Steam++_vx.y.z_linux_x64.tar.zst/.deb/.rpm(arm64 同理) - macOS:
Steam++_vx.y.z_macos_x64.pkg/Steam++_vx.y.z_macos_arm64.pkg - Android:
Steam++_vx.y.z_android_arm64.apk
Architecture
分层要点:
- 编排层只做流程控制,不做打包细节。
build.ps1中所有"产物如何变成 msix/deb/pkg"的逻辑都委托给p.exe full,脚本本身只负责把每个 RID 的原始发布目录准备好。 fd-前缀是框架依赖(Framework Dependent)发布的显式标记。脚本用字符串前缀判断($rid.StartsWith("fd-"))决定输出目录,避免自包含与框架依赖产物混放。p.exe采用惰性构建:只有当p.exe不存在时才执行dotnet build,本地反复跑脚本时不会重复编译发布工具。
核心流程:build.ps1 控制流详解
packaging/build.ps1 的整体执行路径可以分为三个阶段:参数与前置校验 → 发布工具引导(仅正式发布)→ 逐 RID 构建。下图展示了正式发布(-isPublish $true)时的完整时序:
阶段一:参数与目录约定
脚本头部的参数块定义了三个参数:$version(版本号字符串)、$configuration(默认 Release)、$isPublish(默认 $false):
param([string]$version,[string]$configuration='Release',[bool]$isPublish=$false)
$ErrorActionPreference = 'Stop'Source: build.ps1
$ErrorActionPreference = 'Stop' 使任何 cmdlet 级错误立即终止脚本,配合后续每个外部命令后的 $LASTEXITCODE 检查,构成整体的 fail-fast 策略。
脚本随后以 $PSScriptRoot(即 packaging/)推导仓库根目录,并固定三个关键路径——主工程、输出目录、发布工具:
1$RootPath = Split-Path $PSScriptRoot -Parent
2$output_dir = "$RootPath\src\ST.Client.Desktop.Avalonia.App\bin\$configuration\Publish"
3$proj_path = "$RootPath\src\ST.Client.Desktop.Avalonia.App\ST.Client.Avalonia.App.csproj"
4
5$publishtool_dir = "$RootPath\src\ST.Tools.Publish"
6$publishtool_exe = "$publishtool_dir\bin\Release\net6.0\p.exe"Source: build.ps1
注意输出目录随 $configuration 变化:Debug 构建产物落在 bin\Debug\Publish,Release 构建落在 bin\Release\Publish,两者互不干扰。
阶段二:RID 矩阵与框架依赖区分
构建目标由一个硬编码的 pubxml 名单驱动,覆盖 3 个 OS、2 种 CPU 架构,以及 Windows 上的框架依赖变体:
$build_pubxmls = "fd-win-x64","win-x64","fd-win-x86","win-x86","osx-x64","linux-x64","linux-arm64","osx-arm64"Source: build.ps1
| pubxml | 平台 | 架构 | 发布模式 | 输出目录(相对 Publish/) |
|---|---|---|---|---|
win-x64 / win-x86 | Windows | x64 / x86 | 自包含 | win-x64 / win-x86 |
fd-win-x64 / fd-win-x86 | Windows | x64 / x86 | 框架依赖 | FrameworkDependent\fd-win-x64 等 |
osx-x64 / osx-arm64 | macOS | x64 / arm64 | 自包含 | osx-x64 / osx-arm64 |
linux-x64 / linux-arm64 | Linux | x64 / arm64 | 自包含 | linux-x64 / linux-arm64 |
Build-App 函数是构建的唯一执行点,其内部逻辑体现了两个设计意图:
1function Build-App
2{
3 param([string]$rid)
4
5 Write-Host "Building $version $rid"
6
7 if($rid.StartsWith("fd-"))
8 {
9 $publishDir = "$output_dir\FrameworkDependent\$rid"
10 }else
11 {
12 $publishDir = "$output_dir\$rid"
13 }
14
15 Remove-Item $publishDir -Recurse -Force -Confirm:$false -ErrorAction Ignore
16
17 if($configuration -eq 'Debug'){ $rid = "dev-$rid" }
18
19 & dotnet publish $proj_path -c $configuration -p:PublishProfile=$rid -p:DeployOnBuild=true -p:ExtraDefineConstants=$rid --nologo
20
21 if ($LASTEXITCODE) { exit $LASTEXITCODE }
22}Source: build.ps1
- 先清理后构建:
Remove-Item -Recurse -Force -Confirm:$false -ErrorAction Ignore确保旧产物(可能来自上次失败构建或不同版本的文件)不会残留在发布目录中,-ErrorAction Ignore容忍目录本来就不存在的情况。 dev-前缀隔离调试构建:当$configuration为Debug时,RID 会被改写为dev-win-x64这类形式。由于-p:PublishProfile与-p:ExtraDefineConstants都使用该值,调试构建会切换到独立的 dev 版 pubxml,并通过条件编译符号让代码感知调试发布模式——避免调试包与正式包在运行时行为(例如更新检查)上混淆。- 三个 MSBuild 属性协同:
PublishProfile决定 pubxml 中的 RID/AOT/单文件等设置;DeployOnBuild=true让发布动作在构建阶段即触发;ExtraDefineConstants把 RID 本身注入条件编译符号。
阶段三:正式发布路径与发布工具契约
只有当 -isPublish $true 时,脚本才进入 Build-PublishTool 分支。进入前有一个强校验:$env:Token 必须非空,否则打印错误并 Exit 1——因为发布工具需要该 Token 访问远端(版本/上传服务):
1if($isPublish)
2{
3 if([String]::IsNullOrEmpty($env:Token))
4 {
5 Write-Error "$version Undefined Token : $env:Token"
6 Exit 1
7 }
8
9 Build-PublishTool
10}else
11{
12 Foreach ($pubxml in $build_pubxmls)
13 {
14 Build-App $pubxml
15 }
16}Source: build.ps1
Build-PublishTool 展示了发布工具的完整 CLI 契约(从调用方视角):
1function Build-PublishTool
2{
3 if(-not (Test-Path $publishtool_exe))
4 {
5 & dotnet build $publishtool_dir\ST.Tools.Publish.csproj -c Release
6 if ($LASTEXITCODE) { exit $LASTEXITCODE }
7 }
8
9 $dev=''
10 if($configuration -eq 'Debug')
11 {
12 $dev = "-dev"
13 }
14
15 & $publishtool_exe ver -token $('"')$env:Token$('"') $dev
16 if ($LASTEXITCODE) { exit $LASTEXITCODE }
17
18 # build App
19
20 Foreach ($pubxml in $build_pubxmls)
21 {
22 Build-App $pubxml
23 }
24
25 & $publishtool_exe full -token $('"')$env:Token$('"') $dev -win_sign_pfx_pwd $('"')$env:WIN_SIGN_PFX_PWD('"')
26 if ($LASTEXITCODE) { exit $LASTEXITCODE }
27}Source: build.ps1
关键细节:
- 惰性构建:
Test-Path $publishtool_exe为真时跳过dotnet build,本地迭代时节省时间;CI 每次全新工作区则会重新编译。 $('"')拼接引号:Token 通过$('"')$env:Token$('"')显式包裹双引号后传给p.exe,防止包含特殊字符的 Token 被 PowerShell/原生参数解析截断。ver先于full:ver子命令负责生成/登记版本号(Debug 追加-dev参数),full子命令在所有 RID 构建完成后统一执行重打包、Windows PFX 签名(密码来自$env:WIN_SIGN_PFX_PWD)与产物分发。- 每步检查
$LASTEXITCODE:外部进程(dotnet、p.exe)失败不会自动抛出 PowerShell 异常,因此脚本在每个调用后显式检查退出码并exit,确保 CI 能第一时间捕获失败。
打包辅助文件
packaging/ 目录下还包含若干面向特定平台或辅助用途的文件(本次未逐一展开源码,定位依据目录结构):
| 文件 | 用途 |
|---|---|
build.cmd / build.v1.ps1 | 供 Windows 环境快速调用 build.ps1 的包装入口与旧版脚本 |
build-osx-app.sh | 在 macOS 上把发布输出组装为 .app bundle 的 shell 脚本(配合 Info.plist) |
Info.plist | macOS .app bundle 的元数据(Bundle ID、版本、图标等),供 macOS 打包流程读取 |
SHA256.ps1 | 为发布产物计算 SHA256 校验和,供分发页面校验 |
macOS 侧需要专用脚本的原因在 doc/publish.md 的注释中有记录:子服务进程二进制通过 PublishFolderType="Assembly" 指定复制到 MonoBundle 目录(这是 .NET 在 macOS bundle 中放置非主可执行文件的唯一途径),而尝试改成 Windows 式目录结构会在 AOT AppHost 中触发 "Initialization for self-contained components is not supported"(参见 dotnet/runtime#35329),因此 macOS 产物布局必须由脚本按 bundle 规则单独组装:
Source: publish.md
分发产物与命名约定
doc/publish.md 给出了各平台的最终分发资产命名规范,即 p.exe full 阶段的产物目标格式:
1Windows x64/x86/arm64 : Steam++_vx.y.z_win_<arch>.msix / .7z
2Linux x64/arm64 : Steam++_vx.y.z_linux_<arch>.tar.zst / .deb / .rpm
3macOS x64/arm64 : Steam++_vx.y.z_macos_<arch>.pkg
4Android arm64 : Steam++_vx.y.z_android_arm64.apkSource: publish.md
命名约定说明:
- 命名模板为
Steam++_v{semver}_{os}_{arch}.{ext},中划线分隔的小写 OS/架构标识与 RID 命名保持一致(win/linux/macos+x64/x86/arm64)。 - Windows 同时提供安装器(
msix)与解压即用(7z)两种形态;Linux 同时提供tar.zst(选择 zstd 压缩以获得比 gzip 更好的体积/速度平衡)、deb、rpm三种形态,覆盖不同发行版的包管理器。 - macOS 统一为
pkg安装器;Android 为apk(arm64 单架构)。 - 注意此清单与
build.ps1的 RID 矩阵并非一一对应:win-arm64与 Android 产物不在build.ps1的$build_pubxmls中,说明这些目标由其他构建路径(发布工具/其他流水线)产出,本页脚本只覆盖列出的 8 个 RID。
配置选项(脚本参数与环境变量)
build.ps1 参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
$version | string | (必传) | 版本号字符串,用于日志与发布工具的版本登记 |
$configuration | string | Release | MSBuild 配置;Debug 时 RID 会被加 dev- 前缀、发布工具追加 -dev 参数、输出进入 bin\Debug\Publish |
$isPublish | bool | $false | 是否走正式发布路径;$true 时校验 $env:Token 并调用 p.exe ver / p.exe full |
环境变量
| 变量 | 使用位置 | 必需性 | 说明 |
|---|---|---|---|
Token | p.exe ver / p.exe full | 正式发布必需(为空即 Exit 1) | 访问远端版本/上传服务的凭据,以显式双引号包裹后传参 |
WIN_SIGN_PFX_PWD | p.exe full | Windows 签名时必需 | Windows 代码签名 PFX 证书密码 |
派生路径
| 路径 | 推导规则 |
|---|---|
src/ST.Client.Desktop.Avalonia.App/bin/<Configuration>/Publish/<rid> | 自包含发布输出(win-x64、linux-x64 等) |
src/ST.Client.Desktop.Avalonia.App/bin/<Configuration>/Publish/FrameworkDependent/<rid> | fd- 前缀 RID 的框架依赖输出 |
src/ST.Tools.Publish/bin/Release/net6.0/p.exe | 发布工具可执行文件(惰性构建产物) |
API 参考(脚本函数)
Build-PublishTool()
执行正式发布全流程:必要时构建发布工具 → p.exe ver → 逐 RID Build-App → p.exe full。
参数: 无(读取脚本级变量)。
副作用: 调用外部进程 dotnet build、p.exe;任一子进程退出码非 0 时立即以该码退出脚本。
Build-App(string $rid)
针对单个 RID 执行清理与发布。
参数:
$rid(string): pubxml 名称/RID,如win-x64或fd-win-x86。
行为:
- 按
fd-前缀决定输出目录(框架依赖进入FrameworkDependent子目录)。 Remove-Item递归清空旧输出目录(目录不存在时忽略错误)。- Debug 配置下将 RID 改写为
dev-<rid>。 - 调用
dotnet publish -c <cfg> -p:PublishProfile=<rid> -p:DeployOnBuild=true -p:ExtraDefineConstants=<rid>。
退出: dotnet publish 失败时以 $LASTEXITCODE 终止脚本。
失败模式、边界情况与注意事项
- Fail-fast 全链路生效:
$ErrorActionPreference = 'Stop'处理 cmdlet 错误;所有外部进程(dotnet、p.exe)之后紧跟if ($LASTEXITCODE) { exit $LASTEXITCODE },任何一步失败都会带着真实退出码终止,避免"部分成功的产物"被继续签名或上传。 - Token 缺失的显式失败:正式发布前若
$env:Token为空,脚本用Write-Error报出包含版本号的诊断信息并Exit 1,而不是把空 Token 传给发布工具后在远端才失败。 - 目录幂等性:每个 RID 构建前先强制删除旧目录,保证产物目录内容即当前构建的完整快照,防止陈旧文件(例如上次构建多出的 DLL)被误打包。
- Debug/Release 双通道隔离:输出目录、pubxml(
dev-前缀)、发布工具参数(-dev)三处同时区分调试与正式发布,避免调试包进入正式分发渠道。 - 凭据传递方式:Token 与 PFX 密码均通过环境变量 + 显式双引号包裹传参,不落盘到命令行历史之外;脚本本身不包含任何硬编码密钥。
- 已知平台限制(macOS 布局):受 .NET 运行时限制,macOS 上子服务二进制必须位于
MonoBundle(由PublishFolderType="Assembly"控制),无法采用 Windows 式扁平结构,否则 AOT AppHost 报 "Initialization for self-contained components is not supported"。因此 macOS 产物必须依赖build-osx-app.sh+Info.plist的 bundle 组装流程。
运维与扩展要点
- 新增目标平台/架构:在
$build_pubxmls中追加对应 pubxml 名(并在工程Properties/PublishProfiles提供同名 pubxml),Build-App的目录与命令逻辑无需改动——RID 驱动的设计使新增目标只需数据变更。 - 新增框架依赖目标:使用
fd-前缀命名即可自动进入FrameworkDependent输出目录;无需修改任何判断逻辑。 - 发布工具升级:
p.exe的惰性构建意味着修改src/ST.Tools.Publish源码后需要手动删除旧p.exe(或清理其bin)才会触发重编译,这是本地调试发布工具时最易踩的坑。 - CI 集成:CI 环境应注入
Token与WIN_SIGN_PFX_PWD两个密钥,并以-isPublish $true调用;本地默认-isPublish $false只做纯构建,不触碰任何凭据。 - 扩展包格式:
p.exe full是所有格式化/签名/上传的唯一收口,新增分发格式(如 winget 清单、AppImage)应在发布工具层扩展,而非在 PowerShell 脚本里追加打包逻辑——这保持了编排脚本与打包细节的解耦。
相关链接
- packaging/build.ps1 — 主编排脚本
- doc/publish.md — 分发资产矩阵与命名约定
- packaging/build-osx-app.sh — macOS bundle 组装脚本
- packaging/Info.plist — macOS bundle 元数据
- doc/program-file-structure/Windows.md、Linux.md、macOS.md — 安装后文件结构(兄弟页面主题)