- 桌面应用
【免费下载链接】SoundSwitch
C# application to switch default playing device. Download: https://soundswitch.aaflalo.me/
本文聚焦 SoundSwitch 仓库
tools/目录下的 PowerShell 7+ 发布工具链,逐一拆解环境准备(Install-BuildTools.ps1)、安装包编译与签名(Build-Installer.ps1 / Sign-Binary.ps1)、完整发布编排(Publish-Release.ps1)、Markdown 文档转 HTML(markdown_to_html.py)与夜间构建上传(upload_nightly_r2.py)五个环节。读完本文,你将掌握 SoundSwitch 从源码/草稿 Release 一路走到正式发布、再到夜间构建分发的完整自动化路径,并理解每一条命令背后的源码级实现细节。
工具链概览与运行前提
tools/目录是 SoundSwitch 发布流程的核心所在,目录入口说明见 tools/README.md,其中包含以下工具:
| 工具脚本 | 职责 |
|---|---|
| Install-BuildTools.ps1 | 在全新 Windows 11 机器上一键安装全部构建与签名工具 |
| Sign-Binary.ps1 | 用 signtool 对可执行文件做 SHA-256 签名与 RFC 3161 时间戳 |
| Build-Installer.ps1 | 编译并签名 Inno Setup 安装包(不负责源码构建与发布) |
| Publish-Release.ps1 | 完整发布编排:产物准备 → 文档生成 → 安装包 → 上传草稿 Release → 发布 |
| markdown_to_html.py | 将 Markdown 转为独立 HTML 文档(替代旧的markdown-htmlnpm 包) |
| upload_nightly_r2.py | 上传夜间构建压缩包到 Cloudflare R2 并通知 Discord |
vswhere.exe | 定位 Visual Studio 安装位置(发布流程辅助) |
运行前提(硬性要求):所有 PowerShell 脚本都要求PowerShell 7+(Windows 11 自带),与 Windows PowerShell 5.1 不兼容。脚本开头均有#Requires -Version 7.0指令(参见 Install-BuildTools.ps1 第 34 行),在 5.1 下运行会直接报错。Python 脚本则要求 Python 3 及markdown包。
一、环境准备:Install-BuildTools.ps1 一键安装
Install-BuildTools.ps1 面向"全新 Windows 11 机器",建议只运行一次。它基于 winget 安装以下组件,并通过-Scope参数控制安装范围(machine默认、需管理员权限;user免提权):
| 组件 | winget 包 ID | 用途 |
|---|---|---|
| GitHub CLI | GitHub.cli | Publish-Release.ps1通过gh与 GitHub Releases 交互 |
| Inno Setup 6 | JRSoftware.InnoSetup | 安装包编译器(ISCC.exe) |
| Certum SimplySign Desktop | Certum.SmartSignSimplySignDesktop | 代码签名用的云证书提供商 |
| Python 3.14 | Python.Python.3.14 | 运行 markdown-to-HTML 文档生成 |
| .NET SDK | Microsoft.DotNet.SDK.<major> | 编译与测试应用,版本从工程自动推导 |
其中.NET SDK的版本号并非写死,而是通过Get-DotNetSdkVersion函数从 SoundSwitch/SoundSwitch.csproj 的<TargetFramework>net10.0-windows10.0.17763.0</TargetFramework>正则提取主版本号(当前为 10),再安装对应的Microsoft.DotNet.SDK.10。这一点在 Install-BuildTools.Tests.ps1 中有专门的Get-DotNetSdkVersion测试组验证(如net10.0-windows→ 10、net9.0→ 9)。
signtool 的四级获取策略
签名工具signtool.exe的获取是脚本中最讲究的部分(Install-SignTool函数),依次尝试:
- 已存在于 PATH 中,直接复用;
- 在
C:\Program Files (x86)\Windows Kits\10\bin下递归查找x64目录中的 signtool.exe,取版本号最高者(Find-SignToolInWindowsKits,排序逻辑见 Install-BuildTools.Tests.ps1 中"返回最高版本"的测试用例); - 从 GitHub 上的 Delphier/SignTool 发布页下载独立轻量版signtool.exe,解压后执行无参冒烟测试(
Test-SignToolWorks:退出码 0 或 1 均视为可运行,因为无参 signtool 会打印用法并以 1 退出)确认可用,缓存在%LOCALAPPDATA%\SignTool,下次直接复用; - 兜底:用 winget 安装完整 Windows SDK(
Microsoft.WindowsSDK.10.0.26100),再回到第 2 步查找。
无论哪条路径成功,脚本都会把 signtool 目录同时加入当前会话 PATH与用户级 PATH([System.Environment]::SetEnvironmentVariable('Path', ..., 'User')),保证后续Sign-Binary.ps1/Build-Installer.ps1能找到它。安装完成后脚本会提示重启终端以刷新 PATH。
二、代码签名:Sign-Binary.ps1 的现代签名实践
Sign-Binary.ps1 是签名操作的最小封装,被Build-Installer.ps1自动调用,也可独立使用。其核心参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
-Path | 必填 | 一个或多个待签名文件,支持管道输入与-FullName别名 |
-CertificateName | Open Source Developer Antoine Aflalo | 证书主题名(CN),按名称在证书库中定位证书 |
-TimestampUrl | http://timestamp.digicert.com | RFC 3161 时间戳服务器 |
-MaxRetries | 3 | 单文件最大签名尝试次数 |
-RetryDelaySec | 5 | 失败重试间隔(秒) |
实际签名的 signtool 命令(第 119-124 行)为:
signtool.exe sign /n $CertificateName /fd sha256 /tr $TimestampUrl /td sha512 /v $resolvedPath要点解读:
- 文件摘要用 SHA-256(
/fd sha256),时间戳摘要用 SHA-512(/td sha512),完全弃用已过时的 SHA-1; - 时间戳走 RFC 3161 协议(
/tr),即使证书过期签名依然有效; - 每个文件最多重试
-MaxRetries次,主要应对时间戳服务器临时不可用等瞬时故障(见脚本第 105-133 行的重试循环); - 定位 signtool 的方式与
Install-BuildTools.ps1一致:先查 PATH,再回退到 Windows Kits 目录,找不到则直接抛错提示先运行Install-BuildTools.ps1。
独立用法示例:
# 签名单个可执行文件 .\tools\Sign-Binary.ps1 -Path Final\SoundSwitch.exe # 一次签名多个文件 .\tools\Sign-Binary.ps1 -Path Final\SoundSwitch.exe, Final\SoundSwitch.CLI.exe # 使用自定义证书名签名安装包 .\tools\Sign-Binary.ps1 -Path Final\Installer\SoundSwitch_Installer.exe -CertificateName "My Cert"三、安装包编译:Build-Installer.ps1
Build-Installer.ps1 职责单一:仅编译并签名安装包,不涉及源码构建、文档生成或 GitHub 交互(这些归Publish-Release.ps1)。其参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
-FinalDir | 仓库根下Final\ | 存放二进制、文档、资源的目录;必须是规范的Final\目录 |
-SkipSigning | 关 | 跳过签名(即使 signtool 可用) |
-CertificateName | OpenSource Developer, Antoine Aflalo | 传递给Sign-Binary.ps1 |
-InstallerReleaseState | Release | 传给 Inno Setup 的发布状态标签(Release/Beta/Nightly 等) |
-Architectures | win-x64, win-arm64 | 逗号分隔字符串或数组均可,每个架构单独跑一次 ISCC |
为什么-FinalDir必须是规范目录
脚本第 87-94 行做了硬校验:-FinalDir必须解析为仓库根下的Final\,否则直接抛错。原因在于 Installer/scripts/app_defines.iss 通过硬编码相对路径..\Final\引用打包载荷(ExeDir定义)。若允许非规范目录,ISCC 实际打包的目录会与脚本签名/清理的目录不一致——校验发生在任何破坏性操作(清理、签名)之前,避免误清误签。
工作流程
- 校验载荷:
Final\必须存在且非空,否则抛错提示先用Publish-Release.ps1填充; - 签名二进制(未跳过且找到 signtool 时):递归查找
Final\下文件名匹配*SoundSwitch*.exe/*SoundSwitch*.dll的文件(排除Installer\子目录),交给Sign-Binary.ps1; - 定位 Inno Setup:与 CI 工作流(test-installer-build.yml)逻辑一致,先查注册表
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1与 WOW6432Node 变体,再回退查 PATH; - 逐架构编译:先清理
Final\下旧安装包,再对每个架构映射win-x64 → x64、win-arm64 → arm64,执行ISCC.exe setup.iss /DReleaseState=<状态> /DTargetArch=<架构>,输出从Final\移动到Final\Installer\; - 签名安装包:对
Final\Installer\*Installer*.exe再次调用Sign-Binary.ps1。
与 setup.iss 的联动
Installer/setup.iss 顶部强制要求编译时通过/DTargetArch传入x64或arm64,未定义或非法值直接#error。#if TargetArch == "x64"分支还决定:
- 载荷目录:
Final\win-x64\或Final\win-arm64\(app_defines.iss); - 安装包文件名后缀:x64 保留无后缀的传统名(
OutputBaseFilename为SoundSwitch_v<版本>_<ReleaseState>_Installer.exe),arm64 追加_arm64(SoundSwitch_v<版本>_<ReleaseState>_Installer_arm64.exe); - 架构约束:x64 用
x64compatible,arm64 用arm64(ArchitecturesAllowed/ArchitecturesInstallIn64BitMode,即只支持 64 位 Windows,x86 不在支持范围); - 应用版本号:
MyAppVersion通过GetVersionNumbersString从所选架构载荷中的SoundSwitch.exe读取。
setup.iss 还内置了多语言支持(英文、德文、法文、西班牙文、意大利文、葡萄牙文、俄文、波兰文、荷兰文、简体中文、韩文,语言文件见 Installer/Languages)、桌面图标/加入 PATH/删除旧设置三个可选任务、卸载时移除根证书与发布者证书的certutil步骤等。注意:代码签名已不在 setup.iss 中通过SignTool=指令完成,而是统一由Build-Installer.ps1调用Sign-Binary.ps1事后签名(setup.iss 第 52-54 行有注释说明)。
四、完整发布编排:Publish-Release.ps1
Publish-Release.ps1 是发布流程的总指挥,把前面各工具串成一条流水线。参数如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
-Channel | release | release(找非预发布草稿)或beta(找预发布草稿);仅下载模式使用 |
-Repository | Belphemur/SoundSwitch | GitHub 仓库(owner/repo) |
-BuildFromSource | 关 | 改为从源码构建,跳过发布步骤且不要求gh |
-Configuration | Release | 构建配置:Release/Debug/Nightly;仅源码构建模式有效 |
-SkipSigning | 关 | 跳过签名 |
-CertificateName | Open Source Developer Antoine Aflalo | 传递给Build-Installer.ps1/Sign-Binary.ps1 |
-InstallerReleaseState | 自动 | 未显式指定时由-Channel推导:beta →Beta,release →Release |
-Architectures | win-x64, win-arm64 | 支持的架构,逗号分隔或数组;夜间构建只传win-x64 |
两种入口模式
默认模式(下载草稿 Release):要求已安装并登录 GitHub CLI(gh auth login)。Find-DraftRelease用gh release list --repo ... --json tagName,isDraft,isPrerelease,name --limit 30拉取最近 30 个 Release,按通道筛选出最新的草稿(beta 通道选isPrerelease的草稿,release 通道选非预发布草稿),找不到就抛错提示"Has semantic-release run?"——说明草稿由 CI 侧的 semantic-release 预先创建,脚本消费它而非创建它。
-BuildFromSource模式:不依赖gh,直接dotnet publish -c <Configuration> -r <rid> --self-contained true编译SoundSwitch.CLI与SoundSwitch两个项目,产物归入Final\<rid>\;同时执行第 2 步文档生成与资源打包,并跳过第 4-6 步(没有草稿可发布)。
六步流水线详解
Step 1:填充Final\目录。默认模式用gh release download <tag> --pattern 'SoundSwitch-v*.zip'下载 CI 构建产物 zip(要求恰好一个匹配文件,多个会抛错),解压到Final\;源码构建模式则先清理bin/obj/Release/Final再做上述自包含发布。
Step 2:生成 HTML 文档并打包资源(仅源码构建模式)。调用markdown_to_html.py将以下文件转为独立 HTML:
| 源文件 | 输出 |
|---|---|
CHANGELOG.md | Final\Changelog.html |
README.md | Final\Readme.html |
Terms.md | Final\Terms.html |
README.de.md | Final\Readme.de.html |
同时把img\soundSwitched.png、SoundSwitch.CLI\README.md、LICENSE.txt、Terms.txt复制进Final\。文件缺失时打印"跳过"而不中断。下载模式中这些内容已由 CI 打进 zip,因此跳过本步。
Step 3:委托Build-Installer.ps1,透传FinalDir、InstallerReleaseState、CertificateName、Architectures(和可选的SkipSigning)。
Step 4:上传安装包到草稿 Release。对Final\Installer\*Installer*.exe逐个执行gh release upload <tag> <file> --repo <repo> --clobber,--clobber用于覆盖同名旧资产。
Step 5:用 CHANGELOG 设置 Release 正文。Get-LatestChangelogEntry从 CHANGELOG.md 提取第一个## [段直到下一个## [段;随后交互式询问是否在正文前追加额外说明(直接回车跳过),写入临时文件后gh release edit --notes-file更新。
Step 6:确认后发布。打印 Release 名称、Tag、通道与安装包数量,等待(y/N)确认后执行gh release edit <tag> --draft=false正式发布;输入N则保持草稿状态,并提示可稍后用同一条命令手动发布。
常用命令速查
# 稳定版完整发布(默认走最新 release 草稿) .\tools\Publish-Release.ps1 # 发布最新 beta 草稿 .\tools\Publish-Release.ps1 -Channel beta # 仅从源码构建安装包,不发布 .\tools\Publish-Release.ps1 -BuildFromSource # 完整发布但不签名 .\tools\Publish-Release.ps1 -SkipSigning # 源码构建,只打 x64 安装包 .\tools\Publish-Release.ps1 -BuildFromSource -Architectures win-x64脚本质量保障
tools/Install-BuildTools.Tests.ps1 中的 Pester 测试不仅覆盖Install-BuildTools.ps1的辅助函数(通过解析 AST 只加载函数定义、mock 掉 winget/signtool 实现完全隔离),还包含对Publish-Release.ps1/Build-Installer.ps1的语法解析校验(ParseFile断言无错误、含#Requires -Version 7.0),以及"-InstallerReleaseState未显式指定时由-Channel推导"、"Build-Installer.ps1不再使用遗留的Make-Installer.bat、直接调用 ISCC.exe、通过Sign-Binary.ps1签名"等行为断言——这些测试固化了工具链的契约,防止回归。
五、文档转换:markdown_to_html.py
markdown_to_html.py 取代了此前流水线使用的markdown-htmlnpm 包,纯 Python 实现,依赖 PyPI 的markdown包:
pip install markdown用法:
# 单文件转单 HTML python tools/markdown_to_html.py README.md -o Final/Readme.html # 多文件批量转到一个目录 python tools/markdown_to_html.py CHANGELOG.md Terms.md -d Final实现要点:
- 启用
extra(表格、围栏代码块、脚注)、codehilite(代码高亮,无 Pygments 时优雅降级)、toc([toc]占位符支持)、sane_lists四个扩展; - 输出为独立 HTML5 文档,内嵌完整的 GitHub 风格 CSS(
_HTML_TEMPLATE),不需要外部样式表即可直接浏览; - 标题由文件名推导:
path.stem.replace("-", " ").replace("_", " ").title(); -o与-d互斥(argparse 互斥组);-o只允许单个输入文件,多文件配-o直接报错退出;- 文件不存在或不可读时以错误信息退出,输出统一打印
输入 -> 输出映射行。
六、夜间构建分发:upload_nightly_r2.py
upload_nightly_r2.py 负责把夜间构建(nightly)压缩包上传到Cloudflare R2(S3 兼容对象存储),并可选向 Discord 发送通知。夜间构建在本仓库中的定位可从 website/src/advanced/nightly.md 印证:它们是未经测试、未签名的最新快照,供尝鲜、翻译验证或开发者指定测试,列表展示最近 5 个构建并带 SHA-512 校验和。
命令行参数(全部必填除标注外):
| 参数 | 必填 | 说明 |
|---|---|---|
--file | 是 | 要上传的构建压缩包 |
--version | 是 | 版本号 |
--bucket | 是 | R2 bucket 名 |
--account-id | 是 | Cloudflare 账户 ID |
--access-key-id/--secret-access-key | 是 | R2 API 凭据 |
--public-base-url | 否 | 公共下载 URL 前缀,空则跳过 URL 生成 |
--prefix | 否 | 对象前缀,默认nightly |
--metadata-file | 否 | 版本元数据 JSON 输出路径 |
--discord-webhook | 否 | Discord webhook URL,空则跳过通知 |
--repository | 否 | 用于提交链接格式化 |
--commit-count | 否 | 通知里展示的近期提交数,默认 10 |
--commit | 否 | 当前构建对应的 commit |
核心流程与细节:
- 客户端:用 boto3 构造 S3 客户端,endpoint 为
https://<account-id>.r2.cloudflarestorage.com,region 固定auto; - 读取既有元数据:从
nightly/version.json读取历史 artifact 列表(兼容新旧两种元数据格式),用于计算增量 changelog 与清理旧文件; - SHA-512 校验和:分块计算(1 MiB 块),存入 artifact 记录,供 SoundSwitch 更新器下载前校验(对应 nightly 文档中"updater 依据 SHA-512 校验"的说明);
- Changelog 生成:用
git log --no-merges --oneline取上一个 commit 到当前 commit 的提交(最多 20 条),按 Conventional Commits 格式正则^([\da-f]{7,})\s(\w+)\((.+)\)(.+)$解析并渲染为带链接的列表; - 上传与保留策略:上传
nightly/<文件名>,同时只保留最近 9 个 artifact,多余的从 bucket 删除(retained_artifacts[:9]+ 计算 delete_keys);随后更新nightly/version.json(含latest、published、url、artifacts数组); - 通知与 CI 输出:写本地元数据文件(若指定);Discord webhook 发送带 embed 的消息(标题
New Build: <version>、最近 N 条提交、时间戳);若环境变量GITHUB_OUTPUT存在,追加version与download-url两个输出供 CI 下游步骤使用。
七、工具链在发布流水线中的整体协作
把上述脚本串起来,SoundSwitch 的发布流程可以概括为三条路径:
路径 A(默认,草稿 Release 驱动): CI(semantic-release 创建草稿 + 构建产物 zip) → Publish-Release.ps1 下载 zip 到 Final\ → Build-Installer.ps1(内部调用 Sign-Binary.ps1 签二进制与安装包) → gh release upload --clobber 上传安装包 → 提取 CHANGELOG 最新段落作为正文(可追加用户说明) → 确认后 gh release edit --draft=false 发布 路径 B(本地从源码构建): Publish-Release.ps1 -BuildFromSource → dotnet publish 自包含发布 CLI + 主程序(按架构) → markdown_to_html.py 生成 Changelog/Readme/Terms 的 HTML → 复制 README/LICENSE/Terms/图片资源 → Build-Installer.ps1 编译并签名安装包 (不涉及 GitHub,无发布步骤) 路径 C(夜间构建分发): 构建产物 → upload_nightly_r2.py → Cloudflare R2(保留最近 9 个 + version.json 元数据 + SHA-512) → Discord 通知 + GITHUB_OUTPUT 输出值得一提的是,仓库根目录还保留了 Make.bat 与 Installer/Make-Installer.bat 作为遗留的批处理构建入口(前者同样执行自包含发布与markdown-html文档生成,后者按x64,arm64逐架构调用 ISCC),而tools/下的 PowerShell 脚本是其现代化替代——测试用例明确断言Build-Installer.ps1"does not use Make-Installer.bat (legacy)"。同时注意,这些脚本面向 Windows 开发机,与 docs/architecture.md 描述的 .NET/WinForms 应用结构互为表里:发布链路服务于主应用(SoundSwitch/)、CLI(SoundSwitch.CLI/)以及多语言安装包这一整套交付物。
参考文件索引
- 工具说明总览:tools/README.md
- 环境安装:tools/Install-BuildTools.ps1、测试 tools/Install-BuildTools.Tests.ps1
- 代码签名:tools/Sign-Binary.ps1
- 安装包编译:tools/Build-Installer.ps1、脚本 Installer/setup.iss、定义 Installer/scripts/app_defines.iss
- 发布编排:tools/Publish-Release.ps1
- 文档转换:tools/markdown_to_html.py
- 夜间构建:tools/upload_nightly_r2.py、说明 website/src/advanced/nightly.md
- 遗留批处理:Make.bat、Installer/Make-Installer.bat
- 桌面应用
【免费下载链接】SoundSwitch
C# application to switch default playing device. Download: https://soundswitch.aaflalo.me/
相关推荐
基于 release 工具链的 AutoMQ 版本发布全流程指南:从环境准备到 RC 投票
基于 release 工具链的 AutoMQ 版本发布全流程指南:从环境准备到 RC 投票 本指南围绕仓库 release/ 目录下的发布工具链,系统讲解如何为
消息队列后端云原生存储ServerBox 源码开发环境搭建与构建实战:从工具链准备到发布的全流程指南
ServerBox 源码开发环境搭建与构建实战:从工具链准备到发布的全流程指南 ServerBox(即本仓库 server_box )是一个「服务器状态与工具箱
运维观测指标监控监控大盘运维3分钟打造专属桌面监控中心:让闲置USB-C屏幕焕发新生
3分钟打造专属桌面监控中心:让闲置USB C屏幕焕发新生 你是否曾想过,桌面上那个闲置的小屏幕可以变成实时监控电脑性能的智能仪表盘?Turing Smart S
桌面应用智能硬件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考