Repository Wiki
BeyondDimension/SteamTools

多平台分发渠道与打包脚本

本文介绍 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 矩阵 + 专用发布工具"的三层结构实现:

  1. 编排层:packaging/build.ps1 负责接收版本号、配置、是否正式发布三个参数,串联整个流程。本地开发可以只跑 Build-App 循环;正式发布(-isPublish $true)时才引入发布工具。
  2. 构建层:对每个 RID 调用 dotnet publish -p:PublishProfile=<rid>,输出到统一的 Publish 目录,fd- 前缀的 RID 单独进入 FrameworkDependent 子目录。
  3. 发布工具层: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

Loading diagram...

分层要点:

  • 编排层只做流程控制,不做打包细节。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)时的完整时序:

Loading diagram...

阶段一:参数与目录约定

脚本头部的参数块定义了三个参数:$version(版本号字符串)、$configuration(默认 Release)、$isPublish(默认 $false):

powershell
param([string]$version,[string]$configuration='Release',[bool]$isPublish=$false) $ErrorActionPreference = 'Stop'

Source: build.ps1

$ErrorActionPreference = 'Stop' 使任何 cmdlet 级错误立即终止脚本,配合后续每个外部命令后的 $LASTEXITCODE 检查,构成整体的 fail-fast 策略。

脚本随后以 $PSScriptRoot(即 packaging/)推导仓库根目录,并固定三个关键路径——主工程、输出目录、发布工具:

powershell
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 上的框架依赖变体:

powershell
$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-x86Windowsx64 / x86自包含win-x64 / win-x86
fd-win-x64 / fd-win-x86Windowsx64 / x86框架依赖FrameworkDependent\fd-win-x64 等
osx-x64 / osx-arm64macOSx64 / arm64自包含osx-x64 / osx-arm64
linux-x64 / linux-arm64Linuxx64 / arm64自包含linux-x64 / linux-arm64

Build-App 函数是构建的唯一执行点,其内部逻辑体现了两个设计意图:

powershell
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

  1. 先清理后构建:Remove-Item -Recurse -Force -Confirm:$false -ErrorAction Ignore 确保旧产物(可能来自上次失败构建或不同版本的文件)不会残留在发布目录中,-ErrorAction Ignore 容忍目录本来就不存在的情况。
  2. dev- 前缀隔离调试构建:当 $configuration 为 Debug 时,RID 会被改写为 dev-win-x64 这类形式。由于 -p:PublishProfile 与 -p:ExtraDefineConstants 都使用该值,调试构建会切换到独立的 dev 版 pubxml,并通过条件编译符号让代码感知调试发布模式——避免调试包与正式包在运行时行为(例如更新检查)上混淆。
  3. 三个 MSBuild 属性协同:PublishProfile 决定 pubxml 中的 RID/AOT/单文件等设置;DeployOnBuild=true 让发布动作在构建阶段即触发;ExtraDefineConstants 把 RID 本身注入条件编译符号。

阶段三:正式发布路径与发布工具契约

只有当 -isPublish $true 时,脚本才进入 Build-PublishTool 分支。进入前有一个强校验:$env:Token 必须非空,否则打印错误并 Exit 1——因为发布工具需要该 Token 访问远端(版本/上传服务):

powershell
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 契约(从调用方视角):

powershell
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.plistmacOS .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 阶段的产物目标格式:

text
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.apk

Source: 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 参数

参数类型默认值说明
$versionstring(必传)版本号字符串,用于日志与发布工具的版本登记
$configurationstringReleaseMSBuild 配置;Debug 时 RID 会被加 dev- 前缀、发布工具追加 -dev 参数、输出进入 bin\Debug\Publish
$isPublishbool$false是否走正式发布路径;$true 时校验 $env:Token 并调用 p.exe ver / p.exe full

环境变量

变量使用位置必需性说明
Tokenp.exe ver / p.exe full正式发布必需(为空即 Exit 1)访问远端版本/上传服务的凭据,以显式双引号包裹后传参
WIN_SIGN_PFX_PWDp.exe fullWindows 签名时必需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。

行为:

  1. 按 fd- 前缀决定输出目录(框架依赖进入 FrameworkDependent 子目录)。
  2. Remove-Item 递归清空旧输出目录(目录不存在时忽略错误)。
  3. Debug 配置下将 RID 改写为 dev-<rid>。
  4. 调用 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 脚本里追加打包逻辑——这保持了编排脚本与打包细节的解耦。

相关链接

Sources

(2 files)