Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/sdk-coverage.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
name: sdk-coverage

on:
# push 只对 master 生效:分支上的提交已经由 pull_request 触发一次,
# 两边都开会把同一份 commit 跑两遍(14 分钟 × 2,等待与配额都浪费)。
# 需要单独验证某个分支时用 workflow_dispatch。
push:
branches:
- master
paths:
- 'CMakeLists.txt'
- 'cmake/**'
Expand All @@ -17,6 +22,7 @@ on:
- 'scripts/**'
- 'sdk/**'
- '.github/workflows/sdk-coverage.yml'
workflow_dispatch:

jobs:
coverage:
Expand Down
153 changes: 153 additions & 0 deletions .github/workflows/sdk-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: sdk-windows

# Windows 侧回归。项目主力是 Windows + MinGW,但此前只有 Linux 覆盖:
# 2026-11-06 那个 seek 差异是 Linux 先撞出来的,反方向同样成立。
#
# 成本说明:3rdparty(Boost / FFmpeg / SDL2 / FLAC / Ogg / Vorbis / zlib / g3log / g3sinks)
# 在 Windows 上只能源码构建,冷缓存一轮约 24 分钟;把 dist 目录整体缓存后,
# 热缓存一轮约 7~8 分钟(2026-11-07 实测:run 42341337832 冷 24m05s,
# run 42346594655 热 7m33s)。缓存键只包含第三方构建输入,业务代码改动不会让缓存失效。
on:
push:
branches:
- master
paths:
- 'CMakeLists.txt'
- 'cmake/**'
- 'app/**'
- 'scripts/**'
- 'sdk/**'
- '.github/workflows/sdk-windows.yml'
pull_request:
paths:
- 'CMakeLists.txt'
- 'cmake/**'
- 'app/**'
- 'scripts/**'
- 'sdk/**'
- '.github/workflows/sdk-windows.yml'
workflow_dispatch:

concurrency:
group: sdk-windows-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
windows:
runs-on: windows-latest
# 冷缓存(构建全部第三方库)留足余量;热缓存约 10~15 分钟。
timeout-minutes: 150
env:
# 无头 runner 没有音频设备,走 SDL 的 dummy 驱动(与 Linux 侧一致)。
SDL_AUDIODRIVER: dummy
QT_ROOT: C:\Qt
TOOLS_ROOT: C:\tools
# runner 自带 CMake 可能是 4.x,对 cmake_minimum_required(3.5) 直接报错。
# 全程使用 setup 脚本装好的 3.31.6,不依赖 PATH 顺序。
CMAKE_BIN: C:\tools\cmake-3.31.6-windows-x86_64\bin

steps:
- uses: actions/checkout@v7

# 第三方库是纯源码构建、与业务代码无关,按构建输入做键。
# 业务代码改动不会命中不同的 key,所以日常 PR 都走热缓存。
- name: Restore 3rdparty dist cache
id: dist
uses: actions/cache/restore@v4
with:
path: sdk/3rdparty/dist/windows_x86_64_gcc_debug
key: 3rdparty-win-${{ hashFiles('sdk/3rdparty/CMakeLists.txt', 'sdk/3rdparty/build_option.cmake', 'cmake/toolchain/toolchain.windows_x86_64_mingw.cmake', 'sdk/3rdparty/ffmpeg-4.4.4/build.sh', 'sdk/3rdparty/boost_1_81_0/windows_build.bat', 'sdk/3rdparty/boost_1_81_0/bootstrap.bat') }}
restore-keys: |
3rdparty-win-

# 脚本本身是仓库里已验证的 Windows 一键环境(CMake 3.31.6 + Qt 5.14.2 + MinGW 7.3.0
# + 可选的三方库构建)。这里不另写一套安装逻辑,避免 CI 与本地环境漂移。
# 缓存命中时跳过三方库构建;根工程配置脚本总会做。
#
# 必须用**哈希表** splat:脚本的参数块里 $QtRoot / $ToolsRoot / $CmakeVersion ...
# 都是位置参数,数组 splat($extra = @(); $extra += "-SkipThirdParty")会把那个字符串
# 按位置塞进第一个未绑定参数,实测 $CmakeVersion 变成 "-SkipThirdParty",
# 下载 URL 拼成 v-SkipThirdParty/cmake--SkipThirdParty-windows-x86_64.zip 然后 404。
- name: Setup toolchain (Qt / MinGW / CMake) and 3rdparty
id: setup
shell: pwsh
run: |
$params = @{
QtRoot = $env:QT_ROOT
ToolsRoot = $env:TOOLS_ROOT
Jobs = 4
QtMirror = 'https://download.qt.io'
PyPIIndex = 'https://pypi.org/simple'
}
if ("${{ steps.dist.outputs.cache-hit }}" -eq "true") {
Write-Host "3rdparty dist cache hit: skip building dependencies"
$params.SkipThirdParty = $true
} else {
Write-Host "3rdparty dist cache miss: building dependencies from source"
}
./scripts/setup_windows_env.ps1 @params

# 每个 step 都是新进程,脚本内改的 $env:Path 不会传给后续步骤,必须显式导出。
# 注意顺序:这些 PATH 必须在配置根工程之前就位——sdk/test/CMakeLists.txt 会把
# $ENV{PATH} 烘焙进 add_test 的 PATH,改晚了测试会找不到 DLL(0xc0000135)。
# 脚本在最后一节已经用这份 PATH 配置过根工程,这里导出是为了后续 build / ctest 步骤。
- name: Export PATH for later steps
shell: pwsh
run: |
@(
"$env:QT_ROOT\Tools\mingw730_64\bin",
"$env:QT_ROOT\5.14.2\mingw73_64\bin",
"$env:TOOLS_ROOT\cmake-3.31.6-windows-x86_64\bin"
) | ForEach-Object { $_ | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 }
"QT_ROOT_PATH=$env:QT_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8

# Boost 的中间产物体量很大,构建完就删,避免把 runner 磁盘吃满。
- name: Drop 3rdparty build tree
if: steps.dist.outputs.cache-hit != 'true'
shell: pwsh
run: Remove-Item -LiteralPath sdk/3rdparty/build -Recurse -Force -ErrorAction SilentlyContinue

- name: Build
shell: pwsh
run: |
& "$env:CMAKE_BIN\cmake.exe" --build build --parallel
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Run SDK tests
shell: pwsh
run: |
& "$env:CMAKE_BIN\ctest.exe" --test-dir build -R sdk_ --output-on-failure
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# 覆盖度矩阵的 declared / measured 只在断言失败时才由 doctest 打印,通过时数值不可见。
# 用 -s 打开成功断言输出,把 Windows 侧实测毫秒数留在日志里,供与 Linux 侧逐项核对
# (两侧必须落在同一容差内,见 doc/format-support.md)。与 Linux workflow 的同名步骤对齐。
# 这里直接起 exe,不经过 add_test 烘焙的 PATH,所以要自己补齐运行时 DLL 目录:
# 只补 3rdparty 时本地实测 UnitTests.exe 以 127 秒退且零输出(缺 Qt / MinGW 运行时),
# 而 pipefail 下 grep 无匹配同样返回 1,两者在日志里分不开。故先把 exe 的 rc 与输出
# 完整落盘再筛,失败也不阻塞 PR(与 Linux 侧同名步骤的观测定位一致)。
- name: Report coverage matrix numbers
continue-on-error: true
shell: bash
run: |
set -o pipefail
QT="$(cygpath -u "$QT_ROOT_PATH")"
export PATH="$(cygpath -u 'sdk/3rdparty/dist/windows_x86_64_gcc_debug/bin'):$QT/5.14.2/mingw73_64/bin:$QT/Tools/mingw730_64/bin:$PATH"
./build/sdk/test/UnitTests.exe -ts=MediaCoverage -s > coverage.log 2>&1
echo "UnitTests rc=$? lines=$(wc -l < coverage.log)"
head -20 coverage.log
grep -E "declared |coverage matrix covered" coverage.log

# 冷缓存才需要回写;actions/cache 的 post 步骤只在 job 成功时保存,
# 这里显式保存,避免「构建成功但后续测试红」时整轮 90 分钟白跑。
# 条件挂在 setup 的 outcome 上:只有第三方库真的构建成功才值得缓存。
#
# !cancelled() 不能省:if 里不出现任何状态检查函数时,Actions 会自动套一层
# 隐式 success(),于是「测试红」会让本步骤连带被跳过——正好是它要救的场景
# (2026-11-07 run 42341337832 就是这样,构建成功但缓存没写回,白等一轮冷构建)。
- name: Save 3rdparty dist cache
if: (!cancelled()) && steps.setup.outcome == 'success' && steps.dist.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: sdk/3rdparty/dist/windows_x86_64_gcc_debug
key: 3rdparty-win-${{ hashFiles('sdk/3rdparty/CMakeLists.txt', 'sdk/3rdparty/build_option.cmake', 'cmake/toolchain/toolchain.windows_x86_64_mingw.cmake', 'sdk/3rdparty/ffmpeg-4.4.4/build.sh', 'sdk/3rdparty/boost_1_81_0/windows_build.bat', 'sdk/3rdparty/boost_1_81_0/bootstrap.bat') }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ build-coverage/
__pycache__/
*.pyc
ui_shot_*.png
# setup_windows_env.ps1 里的 aqt 会把安装日志落在仓库根,CI 上会污染 git status
aqtinstall.log
# Windows 工作流「Report coverage matrix numbers」步骤的输出落盘
coverage.log
/log/
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ cmake --build build --target package_portable

## NOTES
- `doc/` 与 `README.md` 提供背景,但真实构建/测试入口以当前 `CMakeLists.txt` 为准。
- **CI 有两个工作流**:`sdk-coverage`(Linux x86_64,含覆盖率门禁)与 `sdk-windows`(windows-latest + MinGW)。
两者的 `push` 都只对 `master` 生效,分支验证走 `pull_request`;需要单独验证某个分支用
`gh workflow run <name> --ref <branch>`。Windows 侧的 3rdparty 走 `sdk/3rdparty/dist` 缓存,
缓存命中约 10~15 分钟,冷缓存 40~90 分钟;改动 `sdk/3rdparty` 的构建输入(CMakeLists / build_option /
toolchain / ffmpeg 与 boost 构建脚本)会让缓存失效。Windows 任务用脚本装好的 CMake 3.31.6 绝对路径,
因为 runner 自带的 CMake 4.x 对 `cmake_minimum_required(3.5)` 直接报错。
- **`if:` 不含状态检查函数时,Actions 会自动套一层隐式 `success()`**。所以「构建成功就回写缓存」这类
步骤必须显式写 `(!cancelled()) && ...`,否则前面的用例一红它就跟着被跳过(2026-11-07 踩过:
冷构建 24 分钟做完、缓存没写回,下一轮又白等一次冷构建)。
- **Windows 上非 ASCII 文件名必须按宽字符打开**:`sdk/extractor/FileSource.cpp` 用 `_wfopen`
+ `sdk_utils::toWideString`;`toNativeString` 是 UTF-8 → ANSI(`CP_ACP`) 的有损转换,
在 en-US runner 上中文变 `?`,只保留给确实需要 ANSI 的老 API。不要再用它打开文件。
- `sdk/cosmos/` 是 header-only 工具区;规则不够特殊,暂不单独挂子 AGENTS。
- `music/` 里包含真实媒体样本;涉及 extractor / decode 的改动,优先补 `sdk_media_smoke` 或 `sdk_extractor_spec_tests`。
- 用户口头说“提交一版”时,默认含义是:`git add` + `git commit` + `git push`;除非用户显式缩小范围,否则按这三个步骤执行。
Expand Down
126 changes: 126 additions & 0 deletions doc/parallel-implementation-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# 并行实现的环境与协议(坏输入 effort,历史归档)

> **历史文档(2026-11-07 归档)**
>
> 本文件原为 `.worktrees/_shared/ENV.md`,是 2026-11-03 坏输入地图(#22)并行实现期间,
> 给所有 implementer 共用的环境协议。相关 worktree 目录即将清理,原文若不归档会随目录一起丢失。
>
> **分支名与目录已经过时**:`feat/bad-input-spec`、`bad-input/<票号>` 分支和
> `.worktrees/t34`、`t35`、`t37` 等目录均已合并或废弃,不要照第三节的命令直接执行。
> **第三节「五个坑」仍然有效**,跨平台并行开发时最容易踩。
>
> 当前构建与测试命令以根 `README.md` / `AGENTS.md` 为准;测试分组见
> `sdk/test/CMakeLists.txt`(2026-11-06 起按耗时拆为四组 ctest)。

## 哪些还有效(2026-11-07 复核)

| 条目 | 状态 |
|---|---|
| `dist` 需要目录联接(worktree 里没有 3rdparty 构建产物,缺了 config 直接 `FATAL_ERROR`) | 仍有效。**反向风险已确认**:`t34/t35/t37` 的 `sdk/3rdparty/dist` 是指回主仓库的 junction,用资源管理器或 `rm -rf` 删目录会跟进删掉主仓库的 `dist` |
| 配置时的 `PATH` 会被烘焙进 `add_test`(`sdk/test/CMakeLists.txt` 的 `SDK_TEST_PATH`) | **仍有效**。改了 PATH 必须重新跑一次 `cmake -S . -B build`,否则测试报 `0xc0000135`,而手动跑同一个 exe 却是好的 |
| `ctest` 不在 PATH 里,要用绝对路径 | 仍有效 |
| Git Bash 里 `./UnitTests.exe` 偶发 `exit=127` 且 stdout 全空 | **仍有效**,那不是通过,是没跑起来 |
| 用例名用英文(GBK 控制台下中文 `-tc=` 会静默 0 匹配) | 仍有效 |
| 建联接用 PowerShell,不要 `cmd //c` / `mklink` | 仍有效 |
| `-j4` 而不是 `-j8`(多个 implementer 并行编译) | 视并行度而定,单人开发可以 `-j8` |

已过时:集成分支名、`ctest -R sdk_unit_tests` 跑全量(现在是 `sdk_unit_tests` / `sdk_badinput_tests` /
`sdk_streaming_tests` / `sdk_interaction_tests` 四组)、`.worktrees/_shared/` 共享笔记机制。

---

以下为原文,除本节外未改动。

# SoundBridge 坏输入实现:环境与协议

所有 implementer 共用这一份。先读完再动手,这里的每一步都已经实跑验证过。

## 绝对路径

| 用途 | 路径 |
|---|---|
| 主仓库 | `D:\workspace\SoundBridge` |
| **集成分支** | `feat/bad-input-spec` |
| 规格(唯一真源) | `D:\workspace\SoundBridge\doc\bad-input-spec.md` |
| 共享笔记(可读可写) | `D:\workspace\SoundBridge\.worktrees\_shared\` |
| 你的 worktree | `D:\workspace\SoundBridge\.worktrees\t<票号>` |

## 一、建立你的 worktree

```bash
cd /d/workspace/SoundBridge
git worktree add .worktrees/t<票号> -b bad-input/<票号> feat/bad-input-spec
```

`dist` 被 `.gitignore` 排除,worktree 里没有第三方库,而根 `CMakeLists.txt` 找不到它会直接 `FATAL_ERROR`。
用目录联接指回主仓库(**必须**,否则配置就挂):

```powershell
New-Item -ItemType Junction -Path "D:\workspace\SoundBridge\.worktrees\t<票号>\sdk\3rdparty\dist" -Target "D:\workspace\SoundBridge\sdk\3rdparty\dist"
```

## 二、配置与构建

**配置命令必须带 `PATH` 和 `QT_ROOT_PATH`**(原因见第三节第 2 条):

```bash
cd /d/workspace/SoundBridge/.worktrees/t<票号>
export PATH="/d/Qt/Tools/mingw730_64/bin:/d/Qt/Qt5.14.2/5.14.2/mingw73_64/bin:$PATH"
export QT_ROOT_PATH=D:/Qt
D:/tools/cmake-3.31.6-windows-x86_64/bin/cmake.exe -S . -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_TOOLCHAIN_FILE=D:/workspace/SoundBridge/.worktrees/t<票号>/cmake/toolchain/toolchain.windows_x86_64_mingw.cmake \
-DCMAKE_MAKE_PROGRAM=D:/Qt/Tools/mingw730_64/bin/mingw32-make.exe \
-G "MinGW Makefiles"
```

构建(全量约 2 分钟,增量约 3 秒):

```bash
D:/tools/cmake-3.31.6-windows-x86_64/bin/cmake.exe --build build -j4
```

`-j4` 是**故意的**:机器 8 核,同时会有两三个 implementer 在编译,别用 `-j8` 互相抢。

## 三、测试

```bash
D:/tools/cmake-3.31.6-windows-x86_64/bin/ctest.exe --test-dir build -R sdk_unit_tests --output-on-failure
```

单跑一个套件(快):

```bash
cd build/sdk/test && ./UnitTests.exe -ts=BadInput
```

**五个坑,踩了会浪费半小时:**

1. **`dist` 联接**:见第一节。缺了报 `3rdparty/dist/... not exist, please build 3rdparty`。
2. **配置时的 `PATH` 会被烘焙进测试命令**。`sdk/test/CMakeLists.txt:62-65` 把 `$ENV{PATH}` 拼进 `SDK_TEST_PATH`,`add_test` 又用 `cmake -E env "PATH=..."` **替换**整个 PATH。所以配置时 PATH 里没有 Qt 的两个 bin,测试就会以 `0xc0000135`(找不到 DLL)失败——而**手动跑同一个 exe 却是好的**,极易误判成代码问题。改了 PATH 后要重新跑一次 `cmake -S . -B build` 让它重新烘焙。
3. **`ctest` 不在 PATH 里**,用上面的全路径。
4. **`cmd //c`、`mklink` 在 Git Bash 里不可靠**,建联接用 PowerShell 工具,不要 `cmd //c`。
5. **Git Bash 里 `./UnitTests.exe` 偶发 `exit=127` 且 stdout 全空**——那不是测试通过,是没跑起来。遇到空输出换 PowerShell 工具重跑。

## 四、改代码前必须知道的项目约定

- **不改 `sdk/3rdparty/`**,一行业务问题都不要通过改供应商代码解决。
- **C++ 用例名用英文**:中文名在 GBK 控制台下 `-tc=` 过滤会静默 0 匹配。
- 错误分两层:内部 `sdk_utils::status_t`,对外 `soundbridge::ErrorCode` / `sdk::ErrorCode`。
- 编码/路径转换集中在 `sdk/utils/Utf8Path.h`,不要往业务代码里散 `#ifdef _WIN32`。
- 仓库用 CRLF 行尾,改写已有文件时不要改行尾风格。

## 五、TDD 怎么做

1. 先写一个**当前会红**的用例,红的原因必须是「缺陷还在」,不是「还没实现」。
2. 素材用**字节手术在临时目录合成**,不入库、跑完删;读源走 `FileSource`(`music/` 里有中文名文件)。
3. 跑到绿,再跑整套 `ctest -R sdk_`,确认没打翻别的。
4. 判据看规格第 2 节总表与各节「判据」小节,**不要自己发明容差**。

## 六、完成协议

1. 只改你这张票覆盖的东西。碰到票外的缺陷,记进 `_shared/findings-<票号>.md`,不要顺手改。
2. 提交到自己的分支 `bad-input/<票号>`,信息用 `fix:` / `feat:` 前缀 + 中文分点。
3. **报告完成前先 `git merge feat/bad-input-spec`**,把集成分支的最新进展并进来,冲突自己解。
4. **不要 push 远端**,也不要碰 `feat/bad-input-spec` 本身——合并由编排方做。
5. 报告里写清楚:改了什么文件、新用例叫什么、跑过什么命令、结果如何。不要复述规格。
13 changes: 9 additions & 4 deletions scripts/setup_windows_env.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,13 @@ if (-not $SkipThirdParty) {
Push-Location $tpBuild
try {
# -DVERSION 用于跳过 g3log 中依赖 git 的版本号推导(该处嵌套引号在 Windows 上不可靠)
# -DVAR=值 必须整体加引号:PowerShell 对未加引号的 -Name=Value 令牌不做变量展开,
# 会把 -DCMAKE_BUILD_TYPE=$BuildType 原样传给 cmake,导致 CMAKE_BUILD_TYPE 字面量是
# "$BuildType"、dist 目录名变成 windows_x86_64_gcc_$buildtype,而 '$' 在 mingw32-make
# 里是变量前缀,最终 include 路径被吃成空目录(2026-11-07 Windows CI 变红的根因)。
cmake .. -G "MinGW Makefiles" `
-DCMAKE_BUILD_TYPE=$BuildType `
-DCMAKE_TOOLCHAIN_FILE="$RepoRoot\cmake\toolchain\toolchain.windows_x86_64_mingw.cmake" `
"-DCMAKE_BUILD_TYPE=$BuildType" `
"-DCMAKE_TOOLCHAIN_FILE=$RepoRoot\cmake\toolchain\toolchain.windows_x86_64_mingw.cmake" `
-DVERSION=2.1.0 -UBUILD_OPTION_G3LOG -UBoost_USE_STATIC_LIBS
if ($LASTEXITCODE -ne 0) { throw "第三方库配置失败" }

Expand Down Expand Up @@ -192,9 +196,10 @@ $buildDir = Join-Path $RepoRoot "build"
New-Item -ItemType Directory -Force -Path $buildDir | Out-Null
Push-Location $buildDir
try {
# 同样必须整体加引号,原因见上面第三方库那处的注释。
cmake .. -G "MinGW Makefiles" `
-DCMAKE_BUILD_TYPE=$BuildType `
-DCMAKE_TOOLCHAIN_FILE="$RepoRoot\cmake\toolchain\toolchain.windows_x86_64_mingw.cmake"
"-DCMAKE_BUILD_TYPE=$BuildType" `
"-DCMAKE_TOOLCHAIN_FILE=$RepoRoot\cmake\toolchain\toolchain.windows_x86_64_mingw.cmake"
if ($LASTEXITCODE -ne 0) { throw "根工程配置失败" }
} finally {
Pop-Location
Expand Down
Loading
Loading