Compare commits
7 Commits
master
...
ci/remote-ci
| Author | SHA1 | Date | |
|---|---|---|---|
| 529c58da39 | |||
| dc5d6a0680 | |||
| ae80bb8e34 | |||
| c1e7e8c42c | |||
| 51ecd36192 | |||
| 2d18193d38 | |||
| 5869c0307b |
@@ -46,7 +46,7 @@ RemoDispBus 推流回归单独跑:`.venv\Scripts\python.exe simulator/tests/ch
|
||||
|
||||
| 现象 | 根因 |
|
||||
|---|---|
|
||||
| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,头文件须有 `include/SDL2/` 副本(仓库已内置) |
|
||||
| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,须有 `include/SDL2/` 副本。⚠ **这份 SDK 不入库**(`.gitignore` 排除 `simulator/SDL2/`,实测 `git ls-files` 在该路径命中 0 个文件)——"下载才有"不等于"仓库已内置";CI/runner 的干净检出里没有它,2026-10-27 run 45 就在这里红。CI 侧用机器级 `DTU_SDL2_DIR` 指一份 SDK,见 `docs/CI/远程CI完整指南.md` §4.5 |
|
||||
| 找不到 `lv_conf.h` | `LV_CONF_DIR`/`SDL2_INCLUDE_DIR` 必须在 `add_subdirectory(lvgl)` **之前**定义 |
|
||||
| 窗口全黑 | mono 主题未生效 / 缺 `lv_refr_now(NULL)` / 重复喂 tick(SDL 驱动内部已 `lv_tick_set_cb`) |
|
||||
| 画面正常但按键全无效 | 主循环**不得**调 `SDL_PollEvent`——LVGL SDL 驱动内部 5ms 定时器统一分发,先轮询会抢光事件 |
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
---
|
||||
name: remote-ci-ops
|
||||
description: 远程 CI(Gitea Actions + 本机 Windows 自建 runner)运维闭环:runner 保活与抢锁清理、"没有 run / 任务消失"的四因定位、红因三分法(job 没跑起来 / runner 缺环境 / 真门禁失败)、无令牌时用 Gitea 的 MySQL 直查 runner 在线与历次 run/job 时长与红绿史(含 status 枚举定死办法)、用 gitea-runner.exe generate-config 枚举 runner 真实配置键而不猜键名、本地对照日志落 ci-artifacts/、门禁数字口径、gitlint 在 CI 的两种空转(含 runner 无 git 身份致 exit 254 假失败)、PS 5.1 原生 stderr 与编码陷阱、改 workflow / 门禁脚本的红线与改后验收清单。背景与全部实测细节在 docs/CI/远程CI完整指南.md,本 skill 只给操作顺序与判据。
|
||||
whenToUse: CI 没跑、run 被取消或零日志、页面报的失败时长/红绿史跟本地或日志对不上、"这 job 到底跑没跑起来"、门禁假绿/假红、runner 显示 online 却每 10 秒重启、自建 runner 有哪些可用配置项(cache 之类,不确定键名)、要改 .gitea/workflows/ci.yml 或 scripts/ci_*.ps1|py、或没有 API 令牌却要判断 CI 状态时。
|
||||
---
|
||||
|
||||
# remote-ci-ops — 远程 CI 运维闭环
|
||||
|
||||
**权威细节**:`docs/CI/远程CI完整指南.md`(部署、全部实测坑、章节导航),唯一入口 `docs/CI/README.md`。
|
||||
本 skill 只回答两件事:**出问题时按什么顺序查**、**动手前后各验什么**。同一事实不在两处详述。
|
||||
|
||||
> **TODO(待同步回指南)**:`docs/CI/远程CI完整指南.md` §6.1 目前只写 `action_runner.last_online` 的在线核验与 `mysql.exe` CLI;
|
||||
> 本轮把同一通道扩到 `action_run` / `action_run_job` / `action_task` 的历史与时长,且实际驱动是
|
||||
> **pymysql**(不是 mysql.exe)。下面 §4.3 / §7 是本轮实测口径,落定后应并回`docs/CI/远程CI完整指南.md` §6.1 与 §2.3。
|
||||
|
||||
**本机事实**(变了就改指南,别改这里):runner `win-stm32-ci`(`gitea-runner` v4.0.1,labels `[windows]`,与 `runs-on: windows` 匹配);目录 `D:\Docker\act_runner\`;Gitea 1.25.4 监听 `127.0.0.1:3001`,数据库 MySQL `127.0.0.1:3306`/gitea(凭据在 `D:\Docker\Gitea\custom\conf\app.ini` 的 `[database]`,**口令不进脚本、不进报告**);本仓库 `repository.id = 10`(`wandering1/stm32f4-base`,另一仓 stm32f103 = 13);**CI 只跑这一台机器**,与其他机器无关。
|
||||
|
||||
## 0. 先分层,别一上来读 workflow
|
||||
|
||||
**分层之前先做 §1**:一次红很可能压根不是"门禁内容"问题,直接读 workflow 会白绕一圈。
|
||||
|
||||
| 层 | 就位判据 | 不在位的症状 |
|
||||
|---|---|---|
|
||||
| ① 服务端 Actions 开关 | `docs/CI/远程CI完整指南.md` §1.1 | push 后 Actions 页**完全空白** |
|
||||
| ② runner 在线且工具链对 SYSTEM 可见 | `docs/CI/远程CI完整指南.md` §4.1 + 下面 §4 | run 建了但一直排队;或 L0 死在"环境自检"、一个钩子都没跑 |
|
||||
| ③ workflow 与门禁 | `scripts/ci_local.py` 本地全绿 | 具体 job 红/绿与本地不一致 |
|
||||
|
||||
**本地永远先跑这个**(runner 掉线也不影响它,是 L0 的唯一保障;日志落法见 §5):
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --keep-going # L0 + L0b + L0.5
|
||||
```
|
||||
|
||||
## 1. 第一步:这个 job 到底跑没跑起来(红因三分法)
|
||||
|
||||
**只看一条判据**:runner 侧那份 job 日志(`D:\Docker\act_runner\log\job\*-task-NN.log`)里
|
||||
**有没有出现任何一条钩子名(`cppcheck.......Passed`)或 `pre-commit hooks -- passed=A failed=B skipped=C` 汇总行**。
|
||||
|
||||
| 类 | 判据(日志里的证据行) | 结论与动作 |
|
||||
|---|---|---|
|
||||
| **A. job 没跑起来** | checkout 步骤报拉不到 `actions/checkout` / 连 `github.com` 超时,**通篇零钩子输出** | 门禁内容无罪。内网无公网时的三种处置在 `docs/CI/远程CI完整指南.md` §5.3(推荐零依赖纯 git checkout)。**先修它再谈别的**,否则后面所有"红"都不可读 |
|
||||
| **B. runner 机器缺环境** | `::warning::runner 未安装 MSVC`、`no Python with a SYSTEM-visible pre-commit`、`runner missing cppcheck`、`test-coverage` 因缺 **mscoverage** 组件而 FAIL(按用户裁定缺件即 FAIL,不 skip)、`ui-golden` 因缺 **SDL2 SDK** 而 FAIL(该 SDK 不入库,见指南 §4.5) | 也是"非代码问题",但**别改成 skip 蒙过去**:按`docs/CI/远程CI完整指南.md` §4.1/§4.2 补机器级工具链或 `DTU_*`,改完重启 runner |
|
||||
| **C. 真门禁失败** | 钩子清单完整且 `failed>0` 或某条 `Failed`,**且 §5 的本地同命令复现同样的红** | 这才轮到读 workflow / 门禁脚本,按失败钩子逐条定位 |
|
||||
|
||||
**顺序不能反**:A/B 两类在本地都是"全绿",用"本地绿 + CI 红"直接推"环境问题"也推不出是 A 还是 B——只有钩子输出能把它们分开。本轮实测:页面报的红看着像门禁失败,实际是 A(checkout 联网超时),而 A 修好之后 L0 还会再红两次——一条 C、两条 B。
|
||||
|
||||
## 2. 症状 → 一条判据 → 动作
|
||||
|
||||
| 症状 | 判据(跑这一条就够) | 结论 / 动作 |
|
||||
|---|---|---|
|
||||
| Actions 页空白、无 run | 看改了啥 + 怎么推的 | 四因:①服务端开关未开 ②workflow 没推到远程 ③**纯文档**(`paths-ignore: docs/**`、`**/*.md`)④**新建分支的首次 push**——`before` 是全零 SHA,变更文件集算不出来被 `paths-ignore` 滤空。④ 的实测:建分支那次 push 无 run,紧接着一次普通 push 立刻出 task;绕法 `workflow_dispatch` 或再推一个非文档提交 |
|
||||
| **页面报的时长/红数与本地或日志对不上** | §4.3 查 `action_run`/`action_run_job` 的 created/started/stopped | **页面时长不可照抄**。本轮实测页面写 `Failing after 6m49s`,库里 41 次 run **最长 ≈4m33s**,run 41 总时长 95 s(L0 45 s / L0.5 43 s)——**没有任何一条记录对得上 6m49s**(多半掺了排队/重试/页面自身口径)。以库里的 `total=stopped-created`、`run=stopped-started`、`queue=started-created` 三个数为准 |
|
||||
| run 全 **cancelled**、零日志 | `& '.\scripts\ci_runner_bootstrap.ps1' -Status` | runner 掉线;且 `ci.yml` 的 `concurrency.cancel-in-progress: true` 会把排队 run 被下次 push 取消 → **症状是"任务消失"而不是"任务失败"**(历史上 16–21 连续 12 个 job 零日志即此) |
|
||||
| `ui-golden` 红、日志是 `C1083: SDL2/SDL.h` 或 `MSB1009: 项目文件不存在`,本地全绿 | 前者 = 检出里根本没有 SDL2 SDK(`.gitignore` 排除它,`git ls-files` 在该 SDK 目录下命中 0 个文件);后者 = 上一轮配置在硬校验处中断,**CMake 缓存文件已写下、工程文件却没生成**,于是下一轮跳过配置 | 设机器级 `DTU_SDL2_DIR`(指南 §4.5);模拟器构建脚本已能自愈,也可手删那份构建目录。**别把构建产物路径当文档引用写**——它不入库,CI 的干净检出里不存在,`doc-links` 会「本地过、CI 挂」(指南 §4.1.2 同族) |
|
||||
| runner 显示 **online** 却每 10 秒重启、日志暴涨(实测 ~20 MB/天) | daemon 日志出现 `another gitea-runner process is already using ".runner"` | **双看门狗抢锁**:`Unregister-ScheduledTask` **不会**停掉旧实例起的进程 → 孤儿看门狗养着旧 daemon → 它持 `.runner.lock` → 新任务里的 daemon 起不来,而**服务端仍显示 online**(旧 daemon 在服务)。修:管理员 `-StartNow`(它**先杀看门狗、再杀 daemon**,顺序反了会被 10 秒内复活) |
|
||||
| L0 失败但**本地同一条命令全绿**,日志 `exit code: 254` + `Missing git configuration: please set user.name` | 看是不是 254 | **不是提交信息问题**:`actions/checkout` 把 HOME 指向每次任务的临时目录,SYSTEM 无全局 git 身份 → gitlint 在读规则**之前**就配置错误退出。已由 `scripts/ci_gitlint_range.ps1` 用 `GIT_CONFIG_*` 注入一次性身份修掉;**254 = 门禁没跑起来(exit 2),1 = 信息真违规(exit 1)**,别把两者混成一句 |
|
||||
| 钩子条数对不上 | `grep -c 'id:' .pre-commit-config.yaml` | **报告数 = 定义数 − 1**(gitlint 属 `commit-msg` stage,`--all-files` 跑不到且表现为全绿 = 静默空转)。增删钩子后重算这个差值 |
|
||||
| `test-coverage` 恒 `[ENV] exit 2`(缺 `Microsoft.CodeCoverage.Console.exe`),或换 VS 世代后大面积假红 | `& '.venv\Scripts\python.exe' scripts/check_test_coverage.py --list` 看它探到几个根;再看机器级 `DTU_MSVC_CL` 指的是哪套 cl | 该 exe **只随 VS 分发**(NuGet 实测拿不到);装在非默认盘还要显式指 `DTU_MSVC_CL`。安装入口 + 组件 ID + 三条管理员命令在 `docs/CI/远程CI完整指南.md` §4.4 |
|
||||
| `mem-numbers` 报"文档与 .map 不一致",但**没改过代码也没改过那篇文档** | `.map` 的 mtime 是否早于最近一次切分支 | **跨分支假象**:`.map` 是 gitignored 的 Keil 产物,切分支后它仍属于另一分支的代码。重新 `@build` 即一致,不要去改文档数字 |
|
||||
| 明明设了 `DTU_KEIL_UV4` 却报 `UV4.exe not found` | `[Environment]::GetEnvironmentVariable('DTU_KEIL_UV4','User')` | 会话没继承用户级变量。**回填时别按"进程里已有的变量名"枚举**——Machine 轮会把只在 User 定义的变量置空(PS 赋空值 = 删除),User 轮就再也看不到它。正确:名字取 Machine/User **并集**,且只在取到值时写入 |
|
||||
| 日志里出现 `gitea-runner.exe : …` + 4 行 `CategoryInfo`/`FullyQualifiedErrorId` | 实测 2 行真日志写成 8 行 | PS 5.1 的 `2>&1` 把每条原生 stderr 包成 ErrorRecord。修:`2>&1 \| ForEach-Object { "$_" }`(等价于原始行,且 `$LASTEXITCODE` 不受影响)。详见`docs/CI/远程CI完整指南.md` §7.3 |
|
||||
| 日志文件半 UTF-8 半乱码 | 文件头是否 `FF FE` | `>` / `*>>` 在 PS 5.1 = `Out-File` = **默认 UTF-16LE**;与 `Add-Content -Encoding ascii` 混用会一个文件两种编码 |
|
||||
|
||||
## 3. runner 保活与注册(唯一入口脚本)
|
||||
|
||||
```powershell
|
||||
# 免管理员预演,先看它会做什么
|
||||
& '.\scripts\ci_runner_bootstrap.ps1' -DryRun
|
||||
# 正式:写机器级 DTU_* + 装看门狗任务 + 立即起(需管理员)
|
||||
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv -StartNow
|
||||
# 体检(非管理员会明说哪些项读不到,而不是谎报)
|
||||
& '.\scripts\ci_runner_bootstrap.ps1' -Status
|
||||
```
|
||||
|
||||
要点(细节与理由见 `docs/CI/远程CI完整指南.md` §2.4):
|
||||
|
||||
- **不能只起 daemon**:v4 无服务支持,且**首次 Declare 失败就退出** → 一次 Gitea 重启就永久静默掉线。
|
||||
- **不能用 Windows 服务**(服务化方案已被实测推翻,见 `docs/CI/远程CI完整指南.md` §3.0);任务计划程序的"失败后重启"**永不触发**,因为 daemon 的退出是**正常退出**。
|
||||
- 任务对象:`-AtStartup` + `Delay=PT1M`、`ExecutionTimeLimit` 为零、`RestartCount 999`、`MultipleInstances IgnoreNew`、`$settings.StartWhenAvailable=$true`——⚠ `StartWhenAvailable` 是**任务设置**属性,挂到 trigger 上会抛异常(曾因此注册崩溃)。
|
||||
- `0x41301` = `SCHED_S_TASK_RUNNING`(不是失败);`-AtStartup` 的任务"下次运行时间"为空是正常的。
|
||||
- 改完 bootstrap 脚本里的启动器模板,**磁盘上的看门狗要下次 `-StartNow` 或开机才刷新**——不必为此专门重启 runner。
|
||||
|
||||
## 4. 没有 API 令牌时怎么判断状态
|
||||
|
||||
匿名调 `/api/v1/repos/.../actions/runs` 是 **401**。三条不依赖令牌的路,按"在线 → 落没落地 → 历史与时长"排。
|
||||
|
||||
### 4.1 在线核验(查 `action_runner`)
|
||||
|
||||
`action_runner.last_online` 与当前 Unix 秒相差 **< 30 秒**即在正常心跳;`agent_labels` 应为 `["windows"]`(不含 windows 就是标签配错)。口径见 `docs/CI/远程CI完整指南.md` §6.1。
|
||||
|
||||
### 4.2 看 runner 侧(最直接,能证明"job 真落到本机了")
|
||||
|
||||
```powershell
|
||||
$d = 'D:\Docker\act_runner\log'
|
||||
# 日志被 daemon 占着,必须共享读打开(ReadAllBytes 会 IOException)
|
||||
$fs = [System.IO.File]::Open("$d\runner-daemon.log",'Open','Read','ReadWrite')
|
||||
# 关键行:declare successfully / writing the log of task N to … / Running job with …
|
||||
Get-ChildItem "$d\job" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 5
|
||||
```
|
||||
|
||||
判读:`declare successfully` 后长时间零输出 = **在线且空闲**;`log\job\*.log` 出现新文件 = 有 task 落地;每个 job 结束时 `[runner] task N finished: success|failure`。**这份 `finished:` 行也是 §4.3 里校验 status 枚举的裁判**(§1 的三分法同样从这些文件起手)。
|
||||
|
||||
⚠ 非管理员**读不到 SYSTEM 进程的 `CommandLine`**(返回 `$null` → 过滤器静默匹配 0 个)→ 绝不能据此说"没有看门狗"。`Process.StartTime` 对 SYSTEM 也不可读,但 CIM 的 `CreationDate` 可读 → 用**进程存活时长**对比 10 秒重启间隔来判断重启循环,**不要 grep 不轮转的日志尾部**(旧失败一直在尾部,会对健康 runner 报假警)。
|
||||
⚠ 查询进程自己的命令行含关键字会**自匹配**:把模式拆开写,如 `'run_runner_watch' + 'dog\.ps1'`。
|
||||
|
||||
### 4.3 查 run / job 历史与时长(推翻页面上的说法)
|
||||
|
||||
**驱动就认 pymysql**:本机 `C:/ProgramData/Anaconda3/python.exe` 实测 `pymysql=True`、`MySQLdb=False`、`mysql.connector` **缺失**(import 直接 `ModuleNotFoundError`)→ `docs/CI/远程CI完整指南.md` §6.1 的 `mysql.exe` CLI 路线本机走不通,改 pymysql 连 `127.0.0.1:3306`/gitea。
|
||||
|
||||
**口令只从 app.ini 运行时读**:`[database]` 段里 `HOST` 是 `host:port` 一体要拆开,`NAME`/`USER`/`PASSWD` 直接用;**绝不要把口令写进脚本字面量、报告、截图或提交**(本轮就是这么漏的一次)。同段还有 `PATH = ...gitea.db`,但 `DB_TYPE = mysql` → **别去读那个 sqlite 文件**。`token`/`PASSWD` 一类行在打印任何配置文件时统一 redact。
|
||||
|
||||
```python
|
||||
# 写成文件跑(ci-artifacts/_q.py),别内联,见 §6
|
||||
import re, pymysql
|
||||
ini = open(r'D:/Docker/Gitea/custom/conf/app.ini', encoding='utf-8').read()
|
||||
sec = ini.split('[database]')[1].split('\n[', 1)[0]
|
||||
kv = dict(re.findall(r'(?m)^(\w+)\s*=\s*(.*)$', sec))
|
||||
host, port = kv['HOST'].split(':')
|
||||
db = pymysql.connect(host=host, port=int(port), user=kv['USER'], password=kv['PASSWD'],
|
||||
database=kv['NAME'], charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor)
|
||||
cur = db.cursor()
|
||||
cur.execute("SHOW TABLES LIKE 'action%'") # → action_run / action_run_job / action_task /
|
||||
# action_run_index / action_runner / action_task_step …
|
||||
cur.execute("SHOW COLUMNS FROM action_run_job") # 🔴 列名以这里为准,别凭记忆 SELECT
|
||||
```
|
||||
|
||||
**取数(本仓库务必带 `repo_id=10`,否则混进 stm32f103 的 run)**:
|
||||
|
||||
- `action_run`:`id, repo_id, status, title, ref, sha, event, created, started, stopped`,`ORDER BY id DESC`;
|
||||
- `action_run_job`:按 run 关联到 `id, status, attempt, task_id, created, started, stopped`(本轮 run 41 → job 85/86)。
|
||||
**`task_id` 就是 job 日志文件名的 `task-NN`** → `action_run_job.task_id = 66` ↔ `log/job/20261025-133227-task-66.log`,这是 DB 与 runner 日志互校的唯一把手;
|
||||
- 时长三个数:`total = stopped-created`、`run = stopped-started`、`queue = started-created`(本轮实测 queue 普遍 1 s,所以页面 6m49s 不可能是排队)。时间戳是 **Unix 秒**,`datetime.datetime.fromtimestamp(x)` 再打印;
|
||||
- **连续红次数按 job 数、不按 run**:把同一 job 历次 `status` 从新到旧排,数连续非"成功"的长度(本轮据此得出 L0 自上线 **13 次全红、从未绿过**,run 28~41)。
|
||||
|
||||
**status 枚举:先定死再下结论**(本轮最大的误判源)。
|
||||
|
||||
| 码 | 含义 | 依据 |
|
||||
|---|---|---|
|
||||
| 0 | Unknown / 未开始 | 本轮映射试错后才定 |
|
||||
| 1 | Success | 同上 |
|
||||
| 2 | **Failure** | run 41 = 2 → Failure,与页面红、与 runner 日志 `task finished: failure` 三方一致 |
|
||||
| ≥3 | **本轮未核实** | 试错时猜过 `3=Failure`/`4=Cancelled`/`5=Skipped`,**都没有交叉验证** → 用到之前先拿一条已知结局的 run 反推(TODO:从 Gitea 源码定死,别再靠猜) |
|
||||
| 5 / 6 | **非终态**(排队中 / 运行中) | 观测于 run 45:推送后先看到 job=5(未开始)与 6(进行中),数分钟后转 1 / 2;**别把它当成库里坏了或卡死** |
|
||||
|
||||
- **写查询时同时打印原始数字和映射名**;`action_run` 与 `action_run_job` **默认按同一套码处理是危险假设**,跨到别的表要重验。
|
||||
- 判据:同一个 run 换一套映射后红绿翻转(本轮 step 53 判 `Success`、step 56 判 `Failure`)→ 说明你还没定死枚举,**这时任何"红绿史"都不许出口**。
|
||||
- 中文标题在 bash 终端回显成乱码(本轮 `'docs(log): ???'`)只是**显示**问题,库里数据没坏 → **认 run 用 `id`/`sha`/`ref`,不要用标题文本匹配**;要看中文就把结果写文件再按 UTF-8 读。
|
||||
|
||||
## 5. 本地对照:日志一律落 `ci-artifacts/`
|
||||
|
||||
判"本地绿 CI 红"必须有同一份可引用的本地日志。落点在仓库内 gitignored 的 `ci-artifacts/`(`.gitignore`:159),**不要写仓库外绝对路径**:
|
||||
|
||||
```bash
|
||||
mkdir -p ci-artifacts && .venv/Scripts/python.exe scripts/ci_local.py --keep-going \
|
||||
> ci-artifacts/local_ci_$(date +%Y%m%d).log 2>&1; echo EXIT=$? >> ci-artifacts/local_ci_$(date +%Y%m%d).log
|
||||
```
|
||||
|
||||
**本轮实测的替换**:先写到 `D:/Code/_sync_backup_20261026/ci_local_*.log`(仓库外)在后台 bash job 里直接 `No such file or directory` → 改落 `ci-artifacts/` 才跑通。跑门禁这类分钟级任务用后台 job 起,再 `job_output` 收,别占着前台。Keil 构建对照同理落 `ci-artifacts/local_build_*.log`。
|
||||
|
||||
## 6. 脚本层:写查询与批量探测的规矩(本轮踩点集中在这里)
|
||||
|
||||
- **一段 Python 查询写成 JS 模板字符串会炸**(本轮两次:`Expected unicode escape`、`Expected ';', '}' or <eof>`)。正确做法:按行存字符串数组 → `join('\n')` → 落盘 `ci-artifacts/_qN.py` → 用 `C:/ProgramData/Anaconda3/python.exe` 跑 → **收尾统一 `fs.rmSync` 删掉**(临时查询脚本不入库,也别留在仓库里)。
|
||||
- **`run_code` 里不能用 `import x from '…'`**(`'import', and 'export' cannot be used outside of module code`)→ `const fs = await import('node:fs')`。
|
||||
- **批量探测拆小批**:一次调用塞 4 条 Windows 探测(worktree / 目录 / 服务枚举 / app.ini)撞过 120 s deadline,只拿到部分输出。一次 1~2 条,慢源(`Get-ChildItem -Recurse`、`Get-CimInstance Win32_Service` 全量、`generate-config`)单独跑并显式 `timeoutMs`。
|
||||
- **别对全部 runner job 日志一把梭 grep**:日志混合编码(UTF-16LE / CP936),逐文件返回一堆 `[ERR …]`。要么单文件 `head`/`sed -n 'A,Bp'`/`grep`(本轮有效路径),要么解析时逐文件 try/catch 容错。
|
||||
- **`pwsh`/`read`/`subagent` 这类工具只能在 `run_code` 程序里 `await tools.xxx()` 调**,直接当工具名调会 `unknown tool`;每次调用**都要带 `description`**(漏了会 `missing required property "description"`)。
|
||||
|
||||
## 7. runner 有哪些配置项:先 `generate-config`,别猜键名
|
||||
|
||||
**判断"这个键存不存在"的唯一凭据是 runner 自己 dump 出来的默认配置文本**,全程只读——不写 `config.yaml`、不删 `.runner`(那是注册凭据,删了要重新 register)、不重启/注销 runner(只有改 `runner.labels` 才必须重启,`docs/CI/远程CI完整指南.md` §2.3.2)。
|
||||
|
||||
```powershell
|
||||
& 'D:\Docker\act_runner\gitea-runner.exe' generate-config 2>&1 | Out-String -Width 160
|
||||
```
|
||||
|
||||
- 从输出里 **grep 目标键名**(本轮实测清单含 `cache` / `capacity` / `container` / `default_image` / `action_shallow_clone` / `access_key_file` / `addr` / `allocate_pty` / `bind_workdir` / `bucket` / `docker_host` / `docker_timeout` / `enabled` / `endpoint` / `env_file` / `envs` / `external_secret` / `external_secret_file` …,以及 `#set_act_env: true`)。
|
||||
- **dump 里绝大多数键是带 `#` 的注释行** → "在清单里" ≠ "本机已启用";是否被显式覆盖要跟 `config.yaml` 逐行比:**注释=默认值生效,非注释=本机写死了**。
|
||||
- **别拿段名当 grep 锚点**:本轮找 `[actions]` 段和 `DEFAULT_ACTIONS_URL` 时段名锚点落空、上下文打出来的是 `log_report_*` 那几行 → 直接按**键名** grep。另外服务端的 `DEFAULT_ACTIONS_URL` 是 app.ini 的键(`docs/CI/远程CI完整指南.md` §1.2.1),不要当成 runner 的键去 `config set`。
|
||||
- 老教训仍然成立:v4 的 `log` 段只有 `level` 和 `job`,**没有 `file`**,`config set log.file …` 会 `unknown config key`(`docs/CI/远程CI完整指南.md` §2.3.2)→ 改键前以 `generate-config` 输出为准;写完用 `gitea-runner.exe config get <键>` 自证。
|
||||
|
||||
## 8. 门禁口径:讲规则,不讲数字
|
||||
|
||||
数字随分支和提交漂移(实测同一句"18 个钩子"在另一分支是 26,master 后来涨到 33;单测从 3 套件/288 断言涨到 10 套件/647)。**只有规则稳定**:
|
||||
|
||||
- L0:`pre-commit hooks -- passed=A failed=B skipped=C`,其中 **A+B+C = 定义数 − 1**;`skipped` 本机唯一常见项是 `mem-numbers`(无 `.map` 即跳过,并以 `::warning::` 明示"CI 不守体积门禁",见`docs/CI/远程CI完整指南.md` §0.0)。
|
||||
- L0b:`gitlint: N commit message(s) checked, 0 failed`。
|
||||
- L0.5:`<N> runner(s), A/A assertions passed, fail=0`,**N = `test/host/` 下 `run_unit_test.py` 的个数**。
|
||||
- **判定只看 `failed=0`、`fail=0`、A==A**;抄历史数字当基准必然误判。
|
||||
|
||||
## 9. 改 workflow / 门禁脚本的红线
|
||||
|
||||
`.gitea/workflows/ci.yml`:
|
||||
|
||||
- `shell: powershell`,**不是 `pwsh`**(Windows 自带只有 5.1,`pwsh` 会 `Cannot find: pwsh in PATH`)。
|
||||
- **`run:` 块内必须纯 ASCII**:runner 把每块写成无 BOM 临时 `.ps1` 交给 5.1 → 按 CP936 解码 → 中文导致"字符串缺少终止符"(报错行号正好在含中文那行,极易误判为逻辑错)。中文只写在 YAML 的 `name:`/注释里。
|
||||
- 写 `GITHUB_ENV` / `GITHUB_PATH` / `GITHUB_OUTPUT` 必须显式 `-Encoding ascii`(`Out-File -Append` 默认 UTF-16LE)。
|
||||
- 零依赖存档:`Tee-Object` 到 `ci-artifacts/*.log` + `$GITHUB_STEP_SUMMARY` + `::notice::`;**不用 `actions/upload-artifact`**(内网可能拉不到外层 action,`docs/CI/远程CI完整指南.md` §5.3)。
|
||||
- 工具链探测收在 `scripts/ci_find_python.ps1`(可离线自测 + 机器级安装兜底);`DTU_*` 最高优先级,且**显式设置就必须有效、不静默回退**。
|
||||
|
||||
`scripts/ci_*.ps1`:纯 ASCII(含中文的 `.ps1` 才需要 BOM,见`docs/CI/远程CI完整指南.md` §7.1);`-Encoding utf8NoBOM` 是 **PS 7 才有的**,5.1 要写文件用 `[System.IO.File]::WriteAllText($p, $s, (New-Object System.Text.UTF8Encoding($false)))`。
|
||||
|
||||
gitlint 专属:消息文件必须写在**仓库内**(跨盘符被 pre-commit 拒 `path is on mount 'C:', start on mount 'D:'`)且 **UTF-8 无 BOM**(BOM 占标题 1 字符,扭曲 100 上限)。
|
||||
|
||||
## 10. 改完必须验(顺序别省)
|
||||
|
||||
```powershell
|
||||
# .ps1:纯 ASCII + 语法
|
||||
$b=[System.IO.File]::ReadAllBytes($p); @($b|?{$_ -gt 127}).Count # 必须 0
|
||||
$e=$null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $p),[ref]$null,[ref]$e)|Out-Null; @($e).Count # 必须 0
|
||||
|
||||
# run: 块:必须按 runner 的语义测——点源(10-27 用 run 44 换来的两条教训)
|
||||
# runner 执行方式: powershell -command . '<块体>' ← 只有这个是真语义
|
||||
$tmp = '.gitea/_blk_probe.ps1' # 用 python 从 ci.yml 抽出某步 run 正文写到这里,别另抄一份
|
||||
& powershell.exe -NoProfile -Command "`$ErrorActionPreference='Stop'; . (Resolve-Path $tmp)"
|
||||
# 教训一:块被点源进会话,$ErrorActionPreference 继承为 Stop 时,原生程序往 stderr
|
||||
# 写一行就变成**终止性** NativeCommandError,`2>` 重定向挡不住(`2>` 加 `$null` 也不行)。
|
||||
# 所以块首显式 `$ErrorActionPreference='Continue'`,原生调用一律摊平成字符串。
|
||||
# 教训二:`exit 1` 的退出码语义随调用方式变——`-File` 直跑=1;`-File` 包一层点源=**0 且继续执行**;
|
||||
# `-Command` 点源=1。用 `-File` 测会把失败全测成 rc=0(我第一版就栽在这,四条失败路径全绿)。
|
||||
|
||||
# .yml:可解析 + 全部 run: 块纯 ASCII + 无 BOM + LF
|
||||
& '.venv\Scripts\python.exe' -c "import yaml;d=yaml.safe_load(open(r'.gitea/workflows/ci.yml',encoding='utf-8'));print(list(d))"
|
||||
|
||||
# 文档:链接与章节引用
|
||||
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all
|
||||
|
||||
# 全量门禁(等价 CI 的两个 job)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --keep-going
|
||||
```
|
||||
|
||||
提交信息:标题 ≤100、**正文每行 ≤80**、UTF-8 无 BOM、`git commit -F <文件>`;**先程序化数一遍长度再提交**(中文标题最容易超,`-m` 还会被终端代码页转码)。
|
||||
|
||||
推送后到 runner 侧确认(§4.2):两个 job 各自的 `SUMMARY` 行 + `finished: success`。**"推送成功"不等于"CI 跑过"**。
|
||||
|
||||
**汇报口径**:说"红了几次 / 跑了多久"必须写明**来源**(库里的 `action_run_job` 时长,还是 runner 日志,还是页面)与**所用的 status 映射**;三者不一致时以库 + 日志为准并说清页面为什么不可信。只读排查就全程只读——不改文件、不 commit/push、不动 runner 注册状态。
|
||||
|
||||
## 11. 分支与发布(走通的做法)
|
||||
|
||||
- 预演冲突面,不动工作区:`git merge-tree --write-tree --name-only <a> <b>`(exit 1 = 有冲突)。
|
||||
- 数字口径类冲突的正确解法:**改成规则 + 分分支快照**,而不是选一边留数字——否则同一篇文档在另一分支上必错,下次合并再撞一次。
|
||||
- 变更记录是**历史,永不改写**,只在最上方按日期加行。
|
||||
- 想验证 CI 又不想动主干:复制一条分支推上去(`git branch <copy> <src>` → `git push -u origin <copy>`),跑完再删。注意**新建分支的首次 push 不触发 run**(§2 症状表第 1 行)。
|
||||
- 主干落后很多时先同步再排障:备份脏文件 → 只 stash 被跟踪的改动 → `checkout` → `merge --ff-only` → `stash pop`,同步完再跑 §1(本轮本地落后 248 个提交,先同步才看得出真实红因)。
|
||||
- 删分支前先算独有提交:`git rev-list --count origin/master..<branch>`,**> 0 就别删**(删了只能靠 SHA 找回);本地一律 `git branch -d`(未合并它会自己拒绝)。
|
||||
- `git commit` / `git push` 都是"先问"动作(AGENTS.md 边界节),不是任何任务的隐含步骤。
|
||||
+244
-7
@@ -125,11 +125,128 @@ jobs:
|
||||
name: L0 静态门禁 (pre-commit)
|
||||
runs-on: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# 钩子要对比基线与历史(doc-links 等读工作区即可,但 gitlint/cubemx 需完整历史)
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 检出源码(原生 git;不依赖 github.com 上的 action)
|
||||
shell: powershell
|
||||
env:
|
||||
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||
DTU_REPO_REF: ${{ github.ref }}
|
||||
DTU_REPO_SHA: ${{ github.sha }}
|
||||
DTU_REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Pure ASCII only (see file header).
|
||||
#
|
||||
# WHY NOT a "uses:" action: the runner resolves every uses: by contacting the
|
||||
# actions URL during "Prepare all required actions" -- BEFORE the first step
|
||||
# runs. On this intranet runner that call intermittently dies with a connect
|
||||
# timeout: runs 36 (10-21) / 41 (10-25) / 42 (10-26) each burned ~45 s and
|
||||
# produced ZERO hook output, while a manual curl to the same URL minutes later
|
||||
# returned 200. Manual reachability is not CI health: the resolver runs as
|
||||
# SYSTEM and the runner caches nothing across jobs. Gitea 1.25.4 also removed
|
||||
# the custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to
|
||||
# stop depending on the public resolver at all.
|
||||
#
|
||||
# KEEP IN SYNC with the same step in the other job. This logic deliberately
|
||||
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file is
|
||||
# not checked out yet at the moment it would be needed -- chicken and egg.
|
||||
#
|
||||
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
|
||||
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
|
||||
# raw native stderr line into a TERMINATING NativeCommandError. Measured
|
||||
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
|
||||
# a probe killed the whole job 3 seconds in, before any gate could run.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
function Invoke-DtuGit {
|
||||
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
|
||||
# 1) native stderr has to be flattened into plain strings (see above), and
|
||||
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
|
||||
# Returns Code + lines and lets the caller decide what to print.
|
||||
param([string[]]$G)
|
||||
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
|
||||
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
|
||||
}
|
||||
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
|
||||
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
|
||||
# Never let a fetch wait for a human: the runner has no console, and a
|
||||
# credential prompt would burn the whole job timeout while looking like a hung
|
||||
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
|
||||
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
|
||||
# store it cannot answer.
|
||||
$env:GIT_TERMINAL_PROMPT = '0'
|
||||
$ws = (Get-Location).Path
|
||||
Write-Host "workspace: $ws"
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
|
||||
$r = Invoke-DtuGit -G @('init', '-q', '.')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
}
|
||||
# "git remote get-url" writes to stderr when the remote is absent -- that is
|
||||
# the exact line that killed run 44. Ask the config instead: an absent key
|
||||
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
|
||||
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
|
||||
$url = ($probe.Lines -join '').Trim()
|
||||
if ($probe.Code -eq 0 -and $url) {
|
||||
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
|
||||
} else {
|
||||
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
|
||||
}
|
||||
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
|
||||
# ranges and several gates diff against history, so a shallow clone turns them
|
||||
# into "unknown revision" failures. Tags are skipped on purpose: this repository
|
||||
# has zero tags and no script uses "git describe --tags" (measured 2026-10-27).
|
||||
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
|
||||
# The token, when the instance provides one, travels only as a per-invocation
|
||||
# -c override: it never lands in .git/config and is never echoed in logs.
|
||||
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
|
||||
if ($env:DTU_REPO_TOKEN) {
|
||||
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
|
||||
# placeholder user and the token as the password. (Bearer is the GitHub API
|
||||
# style, not the git-http style.) Honest caveat: this branch CANNOT be
|
||||
# verified here -- the repository is public, so an anonymous fetch succeeds
|
||||
# and any token we fabricate locally only proves that a bad one is
|
||||
# rejected. If the repository ever goes private this is the first line to
|
||||
# check; the failure mode is a loud rc=1, never a silent fallback.
|
||||
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
|
||||
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
|
||||
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
|
||||
}
|
||||
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
|
||||
if ($f.Code -ne 0) {
|
||||
$f.Lines | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
|
||||
exit 1
|
||||
}
|
||||
# Some event refs (PR merge commits, refs outside refs/heads) are not covered
|
||||
# by the branch refspec -- fall back to an explicit ref fetch, then INSIST the
|
||||
# wanted SHA is really present rather than silently building the wrong tree.
|
||||
$want = $env:DTU_REPO_SHA + '^{commit}'
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
|
||||
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
|
||||
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
}
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
|
||||
Write-Host ' (refusing to run the gates against a tree we did not verify)'
|
||||
exit 1
|
||||
}
|
||||
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
|
||||
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
|
||||
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
|
||||
if ($rr.Code -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
|
||||
# -ffd, deliberately not -ffdx: -x would delete IGNORED files. Within ONE job several
|
||||
# hooks share this directory (pip / the simulator build tree), and local reruns reuse
|
||||
# it. NOTE measured 2026-10-27: CI gives every job a FRESH workspace (the L0 dirs of
|
||||
# run 44/45/46 all differ, each paying a full 137-161 s fetch), so -ffd is a safe
|
||||
# default, not a speed trick.
|
||||
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
|
||||
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
|
||||
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
|
||||
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
|
||||
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
|
||||
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
|
||||
- name: 环境自检(Python / cppcheck / pre-commit)
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -399,8 +516,128 @@ jobs:
|
||||
name: L0.5 主机侧单测 (MSVC)
|
||||
runs-on: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 检出源码(原生 git;与 l0-gates 同一实现口径)
|
||||
shell: powershell
|
||||
env:
|
||||
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||
DTU_REPO_REF: ${{ github.ref }}
|
||||
DTU_REPO_SHA: ${{ github.sha }}
|
||||
DTU_REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Pure ASCII only (see file header).
|
||||
#
|
||||
# WHY NOT a "uses:" action: the runner resolves every uses: by contacting the
|
||||
# actions URL during "Prepare all required actions" -- BEFORE the first step
|
||||
# runs. On this intranet runner that call intermittently dies with a connect
|
||||
# timeout: runs 36 (10-21) / 41 (10-25) / 42 (10-26) each burned ~45 s and
|
||||
# produced ZERO hook output, while a manual curl to the same URL minutes later
|
||||
# returned 200. Manual reachability is not CI health: the resolver runs as
|
||||
# SYSTEM and the runner caches nothing across jobs. Gitea 1.25.4 also removed
|
||||
# the custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to
|
||||
# stop depending on the public resolver at all.
|
||||
#
|
||||
# KEEP IN SYNC with the same step in the other job. This logic deliberately
|
||||
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file is
|
||||
# not checked out yet at the moment it would be needed -- chicken and egg.
|
||||
#
|
||||
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
|
||||
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
|
||||
# raw native stderr line into a TERMINATING NativeCommandError. Measured
|
||||
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
|
||||
# a probe killed the whole job 3 seconds in, before any gate could run.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
function Invoke-DtuGit {
|
||||
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
|
||||
# 1) native stderr has to be flattened into plain strings (see above), and
|
||||
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
|
||||
# Returns Code + lines and lets the caller decide what to print.
|
||||
param([string[]]$G)
|
||||
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
|
||||
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
|
||||
}
|
||||
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
|
||||
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
|
||||
# Never let a fetch wait for a human: the runner has no console, and a
|
||||
# credential prompt would burn the whole job timeout while looking like a hung
|
||||
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
|
||||
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
|
||||
# store it cannot answer.
|
||||
$env:GIT_TERMINAL_PROMPT = '0'
|
||||
$ws = (Get-Location).Path
|
||||
Write-Host "workspace: $ws"
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
|
||||
$r = Invoke-DtuGit -G @('init', '-q', '.')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
}
|
||||
# "git remote get-url" writes to stderr when the remote is absent -- that is
|
||||
# the exact line that killed run 44. Ask the config instead: an absent key
|
||||
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
|
||||
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
|
||||
$url = ($probe.Lines -join '').Trim()
|
||||
if ($probe.Code -eq 0 -and $url) {
|
||||
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
|
||||
} else {
|
||||
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
|
||||
}
|
||||
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
|
||||
# ranges and several gates diff against history, so a shallow clone turns them
|
||||
# into "unknown revision" failures. Tags are skipped on purpose: this repository
|
||||
# has zero tags and no script uses "git describe --tags" (measured 2026-10-27).
|
||||
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
|
||||
# The token, when the instance provides one, travels only as a per-invocation
|
||||
# -c override: it never lands in .git/config and is never echoed in logs.
|
||||
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
|
||||
if ($env:DTU_REPO_TOKEN) {
|
||||
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
|
||||
# placeholder user and the token as the password. (Bearer is the GitHub API
|
||||
# style, not the git-http style.) Honest caveat: this branch CANNOT be
|
||||
# verified here -- the repository is public, so an anonymous fetch succeeds
|
||||
# and any token we fabricate locally only proves that a bad one is
|
||||
# rejected. If the repository ever goes private this is the first line to
|
||||
# check; the failure mode is a loud rc=1, never a silent fallback.
|
||||
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
|
||||
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
|
||||
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
|
||||
}
|
||||
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
|
||||
if ($f.Code -ne 0) {
|
||||
$f.Lines | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
|
||||
exit 1
|
||||
}
|
||||
# Some event refs (PR merge commits, refs outside refs/heads) are not covered
|
||||
# by the branch refspec -- fall back to an explicit ref fetch, then INSIST the
|
||||
# wanted SHA is really present rather than silently building the wrong tree.
|
||||
$want = $env:DTU_REPO_SHA + '^{commit}'
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
|
||||
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
|
||||
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
}
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
|
||||
Write-Host ' (refusing to run the gates against a tree we did not verify)'
|
||||
exit 1
|
||||
}
|
||||
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
|
||||
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
|
||||
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
|
||||
if ($rr.Code -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
|
||||
# -ffd, deliberately not -ffdx: -x would delete IGNORED files. Within ONE job several
|
||||
# hooks share this directory (pip / the simulator build tree), and local reruns reuse
|
||||
# it. NOTE measured 2026-10-27: CI gives every job a FRESH workspace (the L0 dirs of
|
||||
# run 44/45/46 all differ, each paying a full 137-161 s fetch), so -ffd is a safe
|
||||
# default, not a speed trick.
|
||||
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
|
||||
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
|
||||
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
|
||||
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
|
||||
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
|
||||
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
|
||||
- name: 探测 MSVC(VS BuildTools / Community 等)
|
||||
id: msvc
|
||||
shell: powershell
|
||||
|
||||
@@ -185,4 +185,4 @@
|
||||
| 待办与审计发现登记簿 | `docs/任务规划/README.md` |
|
||||
| 多任务并行开发(开空间 / 合并回收 / 台架与构建排队) | **`task-worktree` skill**(`@task`,用到自动加载)、`docs/环境与部署/并行任务工作树方案.md` |
|
||||
| 换机 / 工具路径环境变量 | `docs/环境与部署/` |
|
||||
| 远程 CI(Gitea Actions,**只跑本机一台 runner**) | [`docs/CI/README.md`](docs/CI/README.md) |
|
||||
| 远程 CI(Gitea Actions,**只跑本机一台 runner**) | **`remote-ci-ops` skill**(排障顺序与判据,用到自动加载)、[`docs/CI/README.md`](docs/CI/README.md) |
|
||||
|
||||
@@ -77,7 +77,11 @@ int iec104_comm_init(void) {
|
||||
uint16_t iec104_comm_ca(void) {
|
||||
uint16_t v = s_comm.ca;
|
||||
|
||||
if ((v < IEC104_COMM_CA_MIN) || (v > IEC104_COMM_CA_MAX)) {
|
||||
/* 只判下界。上界 IEC104_COMM_CA_MAX 就是 uint16_t 的最大值,写成
|
||||
* 「v > 65535」对 uint16_t 恒假(cppcheck compareValueOutOfTypeRangeError),
|
||||
* 留着只会让下一个读代码的人以为上界真被守过。合法带口径不变,
|
||||
* 仍照 iec104_comm.h 的常量注释(ca 1..65535 / port 1024..65535)。 */
|
||||
if (v < IEC104_COMM_CA_MIN) {
|
||||
return IEC104_COMM_DEF_CA;
|
||||
}
|
||||
return v;
|
||||
@@ -91,7 +95,7 @@ uint16_t iec104_comm_ca(void) {
|
||||
uint16_t iec104_comm_port(void) {
|
||||
uint16_t v = s_comm.port;
|
||||
|
||||
if ((v < IEC104_COMM_PORT_MIN) || (v > IEC104_COMM_PORT_MAX)) {
|
||||
if (v < IEC104_COMM_PORT_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
|
||||
return IEC104_COMM_DEF_PORT;
|
||||
}
|
||||
return v;
|
||||
@@ -104,7 +108,7 @@ uint16_t iec104_comm_port(void) {
|
||||
* 限定条件:UI 保存交接路(task-9);落盘走 SYS_EVT_CFG_SAVE 整档外科 save
|
||||
*/
|
||||
int iec104_comm_ca_set(uint16_t ca) {
|
||||
if ((ca < IEC104_COMM_CA_MIN) || (ca > IEC104_COMM_CA_MAX)) {
|
||||
if (ca < IEC104_COMM_CA_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
|
||||
return -1;
|
||||
}
|
||||
s_comm.ca = ca;
|
||||
@@ -118,7 +122,7 @@ int iec104_comm_ca_set(uint16_t ca) {
|
||||
* 限定条件:同 iec104_comm_ca_set
|
||||
*/
|
||||
int iec104_comm_port_set(uint16_t port) {
|
||||
if ((port < IEC104_COMM_PORT_MIN) || (port > IEC104_COMM_PORT_MAX)) {
|
||||
if (port < IEC104_COMM_PORT_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
|
||||
return -1;
|
||||
}
|
||||
s_comm.port = port;
|
||||
|
||||
@@ -161,18 +161,23 @@ static int s_send_asdu(iec104_link_t *lk, uint8_t type, uint8_t vsq,
|
||||
*/
|
||||
static void s_cmd_con(iec104_link_t *lk, uint8_t type, const uint8_t *obj,
|
||||
uint16_t obj_len, uint8_t cot, uint8_t pn) {
|
||||
uint8_t body[BODY_MAX];
|
||||
uint16_t n = obj_len;
|
||||
|
||||
if (n > BODY_MAX) {
|
||||
n = 0u;
|
||||
n = 0u; /* 超长回显退化为空信息体 */
|
||||
}
|
||||
if (n > 0u) {
|
||||
uint8_t body[BODY_MAX]; /* 作用域收进真要回显的分支,不把未初始化缓冲传下去 */
|
||||
|
||||
(void)memcpy(body, obj, n);
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
} else {
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, NULL, 0u, 0u);
|
||||
}
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
}
|
||||
|
||||
/* ---------------- 数据上送 ---------------- */
|
||||
@@ -280,12 +285,13 @@ static void s_spont_step(iec104_link_t *lk, uint8_t *budget) {
|
||||
uint8_t body[8]; /* 最大 = SIQ + CP56(7)(M_SP_TB_1) */
|
||||
uint8_t i;
|
||||
uint8_t d;
|
||||
uint8_t blen;
|
||||
uint8_t type;
|
||||
int rc;
|
||||
|
||||
while ((*budget > 0u) && ((lk->pend_yx != 0u) || (lk->pend_yc != 0u))) {
|
||||
if (lk->pend_yx != 0u) {
|
||||
uint8_t blen;
|
||||
|
||||
i = 0u;
|
||||
while (i < 33u) {
|
||||
if ((lk->pend_yx & (1uLL << i)) != 0u) {
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
目录下的两篇指南 `CI配置指南.md` 与 `Gitea_Runner部署指南.md` 合并而来,**两篇均已删除**,
|
||||
去重后为单一来源)。
|
||||
|
||||
> **agent 排障时的操作顺序**在 **`remote-ci-ops` skill**(`.dsh/skills/remote-ci-ops/SKILL.md`,
|
||||
> 用到自动加载):症状 → 一条判据 → 动作,加上改 workflow / 门禁脚本的红线与改后验收清单。
|
||||
> 分工是——**本目录讲"为什么"和"怎么装",skill 讲"出问题时按什么顺序查"**,同一事实不在两处详述。
|
||||
|
||||
| 想干什么 | 去看 |
|
||||
|---|---|
|
||||
| 先搞清 CI 需要哪三样东西就位、本机拓扑、部署前置清单 | §0 / §0.1 / §0.2 |
|
||||
@@ -29,6 +33,7 @@
|
||||
| `config.yaml` 各字段语义(capacity / timeout / labels / 日志目录…) | §2.3 |
|
||||
| **常驻:为什么必须看门狗、不能只起 daemon、也不能做成 Windows 服务** | §2.4 + §3.0 |
|
||||
| runner 装什么软件、为什么变量必须对 **SYSTEM** 可见、机器级 `DTU_*` | §4(§4.1.1~§4.1.3 是三个真坑) |
|
||||
| **覆盖率工具链(mscoverage)怎么装、机器级 MSVC 怎么指**(含三条可直接粘贴的管理员命令) | §4.4 |
|
||||
| workflow 位置 / 触发 / 内网拉不到 `actions/checkout` 的三种处置 | §5(§5.3 零依赖写法) |
|
||||
| 两个 job 各自跑哪些步骤、两条 Python 探测链要同序同步 | §5.5 |
|
||||
| 端到端验证清单、无网页登录时直查 MySQL 核验 | §6 / §6.1 |
|
||||
@@ -80,7 +85,12 @@
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
| 2026-10-27 | run 46:`ui-golden` 转绿(`DTU_SDL2_DIR` 生效)。新的 1 根红是我自己写的 `doc-links` 违规(把 gitignored 构建产物当引用),§4.1.2 同族,已修;并更正「CI 工作区跨 run 复用」的错话——实测每 job 全新目录、每次 137~161 s 全量拉取,毒缓存只在同一 job 内与本机流传(指南 §4.5/§5.3/§7 与 `ci.yml` 注释同步) |
|
||||
|---|---|
|
||||
| 2026-10-27 | run 44(推送到已存在分支会正常建 run)暴露的教训写进指南 §5.3:run: 块是被**点源**执行的,`$ErrorActionPreference` 继承 Stop 时原生 stderr 会终止整步;且测 run: 块只能用 `-Command ". 文件"`(`-File` 包点源会把失败测成 rc=0)。§7 补「几秒就亡」一行,skill §10 补测法 |
|
||||
| 2026-10-27 | run 45 首次真跑门禁(L0 46 根绿、L0.5 3188/3188);唯一红 `ui-golden` 定性为 **runner 缺环境**:SDL2 SDK 不入库。新增指南 §4.5:机器级 `DTU_SDL2_DIR` + CMake 配置期硬校验 + `Find-Sdl2Sdk` + `build_sim.ps1` 毒缓存自愈;§4.1.3 变量表补到 5 个、§5.5 与 §7 各补一处;顺带更正 lvgl-simulator skill 的「SDL2 已内置」说法 |
|
||||
| 2026-10-27 | 指南 §5.3 **整节重写并落地**:workflow 两个 job 的 `uses:` 归零(原生 git 全量检出,六项实测 A/B/C/D/E/F + 两条自查命令见 §5.3);§1.2.1 第三行按 1.25.4 实际行为改正(自定义 `DEFAULT_ACTIONS_URL` 已无效,旧写法会把人引向一个不存在的开关);§7 的 checkout 行改为“看见这症状=有人把 `uses:` 加回来了”。另:`scripts/ci_runner_bootstrap.ps1` 的 MSVC 探测改为 `vswhere` + 工具集钉世代(默认 14.51,与覆盖率基线同版本),`-SetEnv` 开始成对写 `DTU_MS_COVERAGE` |
|
||||
| 2026-10-27 | 新增指南 §4.4「覆盖率工具链(mscoverage)与 VS2026 BuildTools」并加进上面的目录表。当天另修掉 `cppcheck` 两根真红(`ci/remote-ci` 的 `5869c03`)——此前 L0 的 28 次红里混着**三类**:runner 缺覆盖率组件、外网拉 `actions/checkout` 抖动、真代码问题。**别再一律当成环境噪声**,按 §7 逐条定性 |
|
||||
| 2026-10-12 | 🐛 **首次真跑出 run**:`L0.5` **成功**(`10 runner(s), 647/647, fail=0`,与本地逐字一致);`L0` **假失败**——gitlint 因 runner 无 git 身份而 `exit 254`(`Missing git configuration: please set user.name`),旧文案把它说成"提交信息违规",且该步 `exit 1` 连带让 26 个钩子全部没跑。已在 `ci_gitlint_range.ps1` 用 `GIT_CONFIG_*` 注入一次性身份(不写文件)并区分 254/1 退出码。另记:**新建分支的首次 push 不触发 run**,需 `workflow_dispatch` 或再推一个非文档提交。详见指南 §7 |
|
||||
| 2026-10-12 | 🔀 本轮 CI 修复随 `master` **合入 `v2-on-ui`**(该分支另有 21 个功能提交;冲突仅 `AGENTS.md` 与 `docs/README.md` 两处,解法是"保留其新版结构 + 把 CI 指针搬进对应位置")。因该分支门禁更多(**26 钩子 / 10 单测套件** vs master 的 18 / 3),指南里写死的数字已改为**规则 + 分分支快照**,见指南 §6 / §7 / §10 |
|
||||
| 2026-10-12 | ✅ 双看门狗缺陷**实测收敛**(管理员 `-StartNow` 清掉 3 个残留 → 单实例、链路恢复、runner online)。另修两处:daemon 日志走 stderr 被 PS 5.1 `2>&1` 包成 ErrorRecord(2 行真日志写成 8 行,每 4 行 3 行噪声)→ 启动器改 `ForEach-Object { "$_" }`;`-Status` 诚实化(非管理员读不到 SYSTEM 进程命令行 → 明说需提权而非谎报"无看门狗";重启循环判据改看进程存活时长,不再靠不轮转日志的尾部字符串报假警)。详见指南 §7.3 |
|
||||
|
||||
+242
-33
@@ -156,7 +156,7 @@ ENABLED = true
|
||||
|---|---|
|
||||
| **不配(默认)** | 从 **[github.com](https://github.com)** 拉取 action |
|
||||
| `self` | 从**你自己的 Gitea 实例**拉取(要求 action 已镜像进你的实例) |
|
||||
| 具体 URL(如 `https://gitea.com`) | 从该地址拉取 |
|
||||
| 具体 URL | ⚠ **本实例的 1.25.4 上这一档没有可用取值**:该版本只保留 `github`(默认)与 `self` 两个关键字,写具体 URL **不再改变下载源**(2026-10 核对时踩到的第一现场就是这里,本表旧版写的是老行为)。别把希望放在这个键上——**修法见 §5.3:不依赖 `uses:`** |
|
||||
|
||||
> ⚠️ **不要把你的实例域名写进去当普通 URL**——要"从自己实例拉"就用关键字 **`self`**;
|
||||
> 且**写了 `self` 还得先把 action 镜像进自己实例**,否则 `actions/checkout` 依然拉不到。
|
||||
@@ -166,7 +166,7 @@ ENABLED = true
|
||||
| 场景 | 配置 |
|
||||
|---|---|
|
||||
| 服务器**能**访问 github.com | **什么都不配**(默认走 github.com,最省事) |
|
||||
| 内网,**不能**访问 github.com | 建议**改用零依赖写法**(见本文 §5.3 方案 3),`DEFAULT_ACTIONS_URL` 配不配都行;<br>若坚持用 `actions/checkout@v4`,则需 `DEFAULT_ACTIONS_URL = self` **且**把该 action 镜像进实例 |
|
||||
| 内网,**不能**访问 github.com | **本仓库现状(2026-10-27)**:workflow 里 `uses:` 已归零,`DEFAULT_ACTIONS_URL` 配不配都不影响(见 §5.3)。若哪天有人把 `uses:` 加回来,这条路就会重新变成外网依赖——`self` 需先镜像 action,而自定义 URL 在本版本无效 |
|
||||
|
||||
> Gitea 官方 FAQ 原文:*"From `https://github.com` by default, or from your own instance
|
||||
> when `[actions].DEFAULT_ACTIONS_URL` is `self`."* 使用别处 action 也可在 workflow 里直接写全路径:
|
||||
@@ -832,13 +832,15 @@ mv .dsh/skills/serial-assistant/scripts/state.bak_ci .dsh/skills/serial-assistan
|
||||
#### 4.1.3 机器级 `DTU_*` 环境变量(runner 的正式覆盖通道,2026-10-02 起)
|
||||
|
||||
workflow 已改为统一消费 `DTU_*` 接口(见 [本地工具路径环境变量方案.md §3.3](../环境与部署/本地工具路径环境变量方案.md)):
|
||||
**显式变量优先 → 自动发现回退**,三个 job 步骤相关的变量:
|
||||
**显式变量优先 → 自动发现回退**,与 job 步骤相关的变量(2026-10-27 实测 `-Status` 列 5 个):
|
||||
|
||||
| 变量 | 作用 | 未设置时的回退 |
|
||||
|---|---|---|
|
||||
| `DTU_PRECOMMIT_PYTHON` | 指向装有 pre-commit 的 python.exe(L0/L0.5 用) | PATH 的 `python` → 工作区 `.venv`(干净克隆里通常没有) |
|
||||
| `DTU_CPPCHECK` | 指向 cppcheck.exe(L0 钩子用) | SYSTEM 的 PATH(用户级 PATH 不可见) |
|
||||
| `DTU_MSVC_CL` | 指向 x64 `cl.exe`(L0.5 用) | `%ProgramFiles%`/`%ProgramFiles(x86)%` 推导的 VS 标准根自动探测 |
|
||||
| `DTU_MS_COVERAGE` | 指向 `Microsoft.CodeCoverage.Console.exe`(`test-coverage` 钩子用) | 顺 `DTU_MSVC_CL` 所在的那套 VS 根自动发现(§4.4) |
|
||||
| `DTU_SDL2_DIR` | 指向一份 SDL2 SDK 根(须含 `include\SDL2\SDL.h`;`ui-golden` 要编模拟器) | `simulator\SDL2\SDL2-*`——**干净检出里根本没有这份 SDK,CI 必红**(§4.5) |
|
||||
|
||||
设置(**必须管理员**;scope 用 `Machine` 才能被 SYSTEM 任务看到):
|
||||
|
||||
@@ -887,6 +889,139 @@ Start-ScheduledTask -TaskName GiteaActionsRunner
|
||||
|
||||
这条命令跑的就是 CI 的同一套判定。**本地能过,runner 上基本就能过**(前提是 PATH 一致)。
|
||||
|
||||
### 4.4 覆盖率工具链(mscoverage)与 VS2026 BuildTools——2026-10-27 实测落地
|
||||
|
||||
`test-coverage` 总闸依赖 `Microsoft.CodeCoverage.Console.exe`;缺它按用户裁定 **FAIL、不 skip**
|
||||
(2026-10-24;口径见 `docs/测试说明/主机单测覆盖率口径.md` §7 第 8 条)。这台 runner 在 2026-10-27 之前
|
||||
**恒** `[ENV] exit 2`:机器上只有 VS2019 生成工具(MSVC 14.29),没有该 exe。
|
||||
|
||||
**为什么只能走安装**:该 exe **没有免安装的独立发行版**——2026-10-27 实测 NuGet 两个候选包:
|
||||
`Microsoft.Testing.Extensions.CodeCoverage` 18.12.0(109 个条目、**.exe 数为 0**,全是 managed dll,那是
|
||||
给 MSTest / Testing-Platform 用的扩展库)与 `Microsoft.CodeCoverage` 18.11.0(只有老版
|
||||
`build/netstandard2.0/CodeCoverage/CodeCoverage.exe`,**零个 Console**)→ `Microsoft.CodeCoverage.Console.exe`
|
||||
只随 Visual Studio 本体分发(17.9+ / 18.x 的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\`)。
|
||||
|
||||
**安装入口与组件 ID**(`aka.ms/vs/18/release/…` 实测**不存在**(落到 Bing 的短链未找到页),`stable` 与
|
||||
`insiders` 才有;组件 ID 由 `aka.ms/vs/18/stable/channel` → `VisualStudio.vsman`(18.11.12324.101,17 MB)
|
||||
解析得到:`Microsoft.VisualStudio.PackageGroup.TestTools.CodeCoverage` 是 18.11 清单里**唯一**引用
|
||||
`Microsoft.VisualStudio.TestTools.DynamicCodeCoverage` 与 `Microsoft.CodeCoverage.Console.Targeted`
|
||||
(X86/X64/ARM64 三份 vsix)的组):
|
||||
|
||||
```powershell
|
||||
# 管理员 PowerShell。本机实装在 E 盘——换机若装回 C 盘默认路径,下面的「非默认盘」警告即不适用
|
||||
curl.exe -L -o "$env:TEMP\vs_buildtools18.exe" https://aka.ms/vs/18/stable/vs_buildtools.exe
|
||||
& "$env:TEMP\vs_buildtools18.exe" --quiet --wait --norestart --nocache `
|
||||
--installPath "E:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools" `
|
||||
--add Microsoft.VisualStudio.Workload.VCTools `
|
||||
--add Microsoft.VisualStudio.PackageGroup.TestTools.CodeCoverage `
|
||||
--includeRecommended
|
||||
```
|
||||
|
||||
装完实测内容:`CodeCoverage.Console\Microsoft.CodeCoverage.Console.exe`(mscoverage 18.11.0-preview.26421.2,
|
||||
**能脱离 vcvars 独立跑**,`collect -f cobertura` 就是门禁所用的那条命令)+ `Team Tools\Dynamic Code Coverage
|
||||
Tools\CodeCoverage.exe`(老通道,脚本不用它)+ MSVC **14.44.35207 / 14.51.36231 / 14.52.36815**;Windows SDK
|
||||
复用 C 盘现成的 `Windows Kits\10\10.0.26100`。**选 14.51 是为了与量 `scripts/test_coverage_baseline.json` 那台
|
||||
同版本**(口径 §2 记 VS 2026 / MSVC 14.51)——覆盖率分母随编译器变(口径 §2.1 实测 `sha256.c`:gcc 105 行 vs
|
||||
MSVC 114 行),**换编译器世代 = 换分母**;指错版本只会把「ENV 红」变成「假红」。
|
||||
|
||||
⚠ **装在非默认盘 = 门禁与 CI 都看不见,必须显式指机器级变量**:`scripts/ci_runner_bootstrap.ps1` 的
|
||||
`Find-MsvcCl`(§2.4)与 `test/host/common/unit_test_harness.py` 的 `_default_vs_roots()` 都只由
|
||||
`%ProgramFiles%` / `%ProgramFiles(x86)%` 派生根(本机 = C 盘),且 `Find-MsvcCl` **取首个命中**(候选表里
|
||||
2019 排在最前)。好消息是 **`DTU_MS_COVERAGE` 不用设**:`scripts/check_test_coverage.py` 的 `_vs_roots()`
|
||||
第一个候选是「`cl.exe` 上溯 8 级的 VS 根」——机器级 `DTU_MSVC_CL` 一指过去,同根下的标准相对路径就自动命中
|
||||
该 exe(本机实测:40/40 套跑通、相对基线零回退)。。`-SetEnv` 现在也会把
|
||||
`DTU_MS_COVERAGE` **显式写上**(探测顺序:先按选中的 `cl.exe` 反推同一套 VS 根,再遍历 vswhere 实例)——
|
||||
**这两个变量要成对改**:它们指向同一套 VS 才是自洽状态;写死它的代价是将来卸掉 VS 时门禁会
|
||||
硬失败并打印 `[ENV]`,而那正是我们想要的(宁可响亮地红,不要静默地绿)。
|
||||
|
||||
**本机三条命令(管理员 PowerShell,可整段粘贴)**:
|
||||
|
||||
```powershell
|
||||
# ① 机器级 MSVC 指到 E 盘 VS2026 的 14.51(覆盖率分母与基线同版本)
|
||||
[Environment]::SetEnvironmentVariable('DTU_MSVC_CL','E:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Tools\MSVC\14.51.36231\bin\Hostx64\x64\cl.exe','Machine')
|
||||
|
||||
# ② 重启 runner 守护进程(非 -Status 路径会重写看门狗、并先 Unregister 再 Register 计划任务,离线十几秒)
|
||||
& 'D:\Code\DTU Code\STM32F4-Base\scripts\ci_runner_bootstrap.ps1' -StartNow
|
||||
|
||||
# ③ 只读体检;关键行 = MACHINE DTU_MSVC_CL = E:\...\14.51.36231\bin\Hostx64\x64\cl.exe
|
||||
& 'D:\Code\DTU Code\STM32F4-Base\scripts\ci_runner_bootstrap.ps1' -Status
|
||||
```
|
||||
|
||||
- ✅ **`-SetEnv` 的 MSVC 探测已重写**(同日)。旧版 `Find-MsvcCl` 只扫 `%ProgramFiles%` 派生的 C 盘根、
|
||||
且**取首个命中**(候选表 2019 在最前)。本机 C 盘的 VS2019 已于 **2026-10-27 01:00** 被卸(`…\2019`
|
||||
只剩空目录、C 盘 x64 `cl.exe` 实测 **0 个**),所以旧版在本机的真实行为**不是**“覆回 14.29”,而是
|
||||
**找不到 → 只 Warn → 什么都不写**,机器级变量于是长期指着那条已失效的 C 盘路径(后果:CI 的 L0.5
|
||||
走 `DTU_MSVC_CL` 显式通道 → 路径不存在 = 硬失败,不做自动回退,见 `unit_test_harness.py`)。
|
||||
现在改成 `vswhere -all -products *` 取实例(**能看见非默认盘**)+ 工具集版本降序,并按 `-MsvcToolset`
|
||||
**前缀钉住世代**(默认 `14.51.` = 与覆盖率基线同版本;钉不上会连 WARN 再回落最新,绝不静默),
|
||||
`-MsvcLatest` 显式要最新。实测本机:候选 3 个(14.44 / 14.51 / 14.52,全在 E 盘)→ 默认返回
|
||||
**14.51.36231**;`-Latest` 返回 14.52;`-MsvcToolset 99.` 走告警回落路径。
|
||||
- ⚠ 但 `Set-MachineEnv` **没有 `-Force` 就不覆盖已有值**(保留 + 告警)——所以探测修好后,**首次仍要带
|
||||
`-Force`** 才能把机器级变量从旧值换成新探到的路径;带 `-Force` 前**先看清它探到了哪套**(`-SetEnv`
|
||||
会打印候选与命中),别盲刷。
|
||||
- ⚠ 若 ③ 显示变量已对、而 CI 日志仍打印旧 `cl.exe`:计划任务服务的环境块没刷新,`Restart-Service Schedule`
|
||||
后重跑 ②。**唯一算数的证据是 CI 日志**——L0.5 步骤会打印它实际使用的 `cl.exe` 来源。
|
||||
- 装完 18 世代后 `vswhere` 会挑到最高版 → 模拟器与 `ui-golden` 的 CMake 生成器变成 `Visual Studio 18 2026`
|
||||
(`simulator/build_sim.ps1` 已支持 16/17/18 三档),UI 相关改动按 `ui-verify` skill 重跑像素回归。
|
||||
- 残留待核:口径 §7 第 8 条记的「Community 18.10 与 BuildTools 18.9 两份安装」**不在本机**——2026-10-27
|
||||
`vswhere -all -products *` 实测本机 18 世代实例只有刚装的这套 E 盘 BuildTools(另一账户下是否有 per-user
|
||||
安装,非提权读不到该账户 HKCU,**未证**)。→ 「CI runner 与本机同一台」这句要按**量基线那台可能是另一台
|
||||
机器**来理解;换机或换账户一律先 `check_test_coverage.py --list` 再跑全量。
|
||||
|
||||
### 4.5 UI 像素回归的 SDL2 SDK:CI 工作区**天然没有**(2026-10-27 run 45 实测)
|
||||
|
||||
`ui-golden` 要先把 LVGL 模拟器编出来,而模拟器依赖 **SDL2 SDK**。这份 SDK 是**下载来的第三方件**:
|
||||
`.gitignore` 里明写 `simulator/SDL2/`,`git ls-files` 在该路径下命中 **0 个文件**。于是任何一份干净
|
||||
检出里都没有它,而开发机上有——所以本地一直绿,**这个缺失在本地永远照不出来**。
|
||||
|
||||
run 45 第一次在 CI 跑到 `ui-golden` 时报的是:
|
||||
|
||||
```
|
||||
error C1083: 无法打开包括文件: "SDL2/SDL.h"
|
||||
(Lib/lvgl/src/drivers/sdl/lv_sdl_private.h:20)
|
||||
```
|
||||
|
||||
⚠ **别把它读成「UI 像素回归红了」**:这是红因三分法里的**第二类(runner 缺环境)**,与像素、与
|
||||
被检的提交内容都无关。配置阶段那句 `-- SDL2: <路径>` 也**不能**当证据——`simulator/CMakeLists.txt`
|
||||
过去只是 `set()` 拼路径、不校验存在,缺件时照样打印,直到编译 LVGL 的 SDL 驱动才炸。
|
||||
|
||||
**已落地的修法(四件)**:
|
||||
|
||||
1. `simulator/CMakeLists.txt` 支持机器级 **`DTU_SDL2_DIR`** 覆盖 SDK 位置;**不设该变量时行为与
|
||||
过去完全一致**(实测:不设 → 取 `simulator/CMakeLists.txt` 里写死的仓内默认路径 → `ui-golden Passed`)。
|
||||
2. 同文件加**配置期硬校验**:缺 SDK 时一行 `FATAL_ERROR` 讲清三条出路(本机装 / CI 设变量 /
|
||||
删旧 build 缓存),而不是几百行 C1083 冒充代码错误。实测护栏生效:改前那种工作区里
|
||||
`C1083` 行数从一整屏降到 **0**,只剩护栏自己那一条。
|
||||
3. `scripts/ci_runner_bootstrap.ps1` 新增 `Find-Sdl2Sdk`,顺序:已有 `DTU_SDL2_DIR`(Machine →
|
||||
User → Process,逐个校验含 `include\SDL2\SDL.h`)→ 机器级 `D:\DTU_SDK\SDL2-*` → 本仓
|
||||
`simulator/SDL2/SDL2-*`;**命中第 3 档会 WARN**「runner 工作区里不会有这个路径」。`-SetEnv`
|
||||
写入该变量,`-Status` 一并列出。
|
||||
4. **SDK 放机器级而不是仓内**:本机已把那份 26 MB 的 SDK 整目录拷到 `D:\DTU_SDK\` 下(子目录名
|
||||
就是 SDK 自带的版本号目录名)。`Find-Sdl2Sdk` 按 `SDL2-*` 通配发现、**不写死版本**,所以文档
|
||||
里不钉那个目录名(它形如编号,会撞进 `id-check` 校验 8 的噪声桶,而那条上限只许减不许增)。
|
||||
同理也别把变量指回某人的开发检出——CI 的工作区是 runner 自己的目录,不是你这份仓。
|
||||
|
||||
⚠ **第二个坑(毒缓存):设好变量仍然红,红因已经不是 SDK**。上一次配置在 `FATAL_ERROR` 处中断
|
||||
时,CMake **已经把缓存文件写下**、工程文件却没生成;`build_sim.ps1` 的判断是「有缓存就不再配置」,
|
||||
于是下一轮**跳过配置**直接进 MSBuild,报 `MSB1009: 项目文件不存在`——离根因十万八千里。
|
||||
|
||||
⚠ **更正(我自己写错的句子)**:这里原本写「这份毒缓存会**跨 run 存活**」,**对 CI 是错的**。实测 CI
|
||||
每个 job 都用一个**全新**工作区——run 44 / 45 / 46 的 L0 目录哈希分别是 `b4a9…`、`e3ad…`、`74ce…`,
|
||||
同一个 L0 job 三次都不同,且每次都付 **137~161 s 全量拉取**。所以跨 run 不会残留。真正的暴露面在
|
||||
**同一个 job 内**:`ui-golden` 与板端导航自检都要编模拟器、共用同一份构建目录,前者中断就会毒到
|
||||
后者;本机日常开发同理(我就是先在本地那份等价工作区里撞到的)。自愈检查因此仍然必要:
|
||||
`build_sim.ps1` 第三节原本就有「生成器不符 → 删构建目录重来」的先例,照同一形状补一条——有缓存
|
||||
但没有工程文件 → 删重。
|
||||
|
||||
实测闭环(在一份**没有**仓内 SDK、且带着 run 45 毒缓存的检出里):
|
||||
|
||||
```
|
||||
ws cache present=True ALL_BUILD present=False <- 毒化状态,正是 CI 现在的处境
|
||||
[RECOVERY] rc=0 UI 像素回归(菜单 8 状态 + 内容页 40 页 黄金参考)....Passed
|
||||
```
|
||||
|
||||
即:只需管理员跑一次 `-SetEnv`(与 §4.4 的三条命令同批),CI 的 `ui-golden` 就会转绿,
|
||||
且**自愈** run 45 已经污染的那份工作区。
|
||||
---
|
||||
|
||||
## 5. 仓库端:workflow 文件
|
||||
@@ -923,38 +1058,100 @@ on:
|
||||
- 纯文档改动**不触发**(省 runner);
|
||||
- 想手动跑一次:仓库 → Actions → 选 `CI` → **Run workflow**。
|
||||
|
||||
### 5.3 内网无公网时的 action 拉取(重要)
|
||||
### 5.3 内网无公网时的 action 拉取:本仓库已彻底移除 `uses:`(2026-10-27 落地)
|
||||
|
||||
workflow 用了 `actions/checkout@v4`。**默认从 `github.com` 拉取**(见 §1.2.1;
|
||||
不是从你自己的实例,也不是从 gitea.com —— 这点很反直觉);实例不通
|
||||
github.com 会在 checkout 步骤卡住/失败。三种处置:
|
||||
**为什么非自己动不可**:`uses:` 的解析发生在 **`Prepare all required actions`** 阶段——**先于任何步骤**、
|
||||
无条件联网(默认 github.com,见 §1.2.1),而且**每个 job 各自解析进自己的 `.cache/act/<hash>`,跨 job 不
|
||||
复用**。这条链在本机实测不稳:run 36(10-21)/ 41(10-25)/ 42(10-26,分支 `IEC60870`)三次都是 **~45 s
|
||||
后整 job 死亡、零钩子输出**,而同一分钟手工 `curl` 同一地址返回 200——**手工可达 ≠ CI 可用**(解析器跑在
|
||||
SYSTEM 下)。Gitea 1.25.4 又去掉了自定义 `DEFAULT_ACTIONS_URL`(§1.2.1 表第三行),服务端没有可拧的键,
|
||||
**唯一归本仓库管的修法就是不再依赖它**。
|
||||
|
||||
1. **镜像到自有实例**:`DEFAULT_ACTIONS_URL = self`,**并先把 `actions/checkout` 导入你自己的 Gitea**;
|
||||
2. **指向可达镜像**:`DEFAULT_ACTIONS_URL` 指向某个能访问的 action 站(或在 workflow 里
|
||||
直接写全路径 `uses: https://<host>/owner/repo@ref`);
|
||||
3. **零依赖**(内网最稳,推荐):把 checkout 换成纯 git 命令,不依赖任何 action:
|
||||
**现状**:两个 job 的第 1 步都是原生 git 检出,workflow 里 `uses:` **归零**。检出正文**内联**在 workflow
|
||||
里,**不能**抽成 `scripts/*.ps1`——它执行时脚本自己还没被检出(鸡生蛋),这与 `ci_find_python.ps1` /
|
||||
`ci_gitlint_range.ps1` 的抽取方向**相反**;代价是两个 job 各持一份、**必须逐字一致**(自查见本节末)。
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
shell: pwsh
|
||||
run: |
|
||||
git init .
|
||||
git remote add origin $env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY.git
|
||||
git fetch --depth 1 origin $env:GITHUB_SHA
|
||||
git checkout $env:GITHUB_SHA
|
||||
**这份检出做了什么**(等价 `checkout@v4` + `fetch-depth: 0`):`init` 或复用现有 `.git` → `remote
|
||||
set-url origin <server_url>/<repository>.git` → 完整 refspec `+refs/heads/*:refs/remotes/origin/*` 全量
|
||||
`fetch --prune --no-tags` → 若事件 sha 不在分支 refspec 覆盖内(PR 合并提交等)再显式 fetch 事件 ref →
|
||||
**验证 sha 真在历史里**(不在就 `::error::` 硬失败,**绝不默默构建一棵错的树**)→ `checkout --detach` →
|
||||
`reset --hard` → `clean -ffd`。
|
||||
|
||||
四个刻意的选择(每条都有实测依据):
|
||||
|
||||
| 选择 | 为什么 |
|
||||
|---|---|
|
||||
| `clean -ffd`,**不是** `-ffdx` | `-x` 会删**被忽略**的文件:同一个 job 内多个钩子共用这份工作区(pip、模拟器构建目录等),本机重复跑也靠它。⚠ **更正(2026-10-27 实测):CI 并不跨 run 复用工作区**——run 44/45/46 的 L0 目录哈希 `b4a9…`/`e3ad…`/`74ce…` 各不相同,每个 job 都付一次 137~161 s 全量拉取;所以选 `-ffd` 是**保守**(将来若真复用,被忽略的缓存不会被误删),不是靠它省时间。实测:放一个 gitignored 文件 + 一个未跟踪垃圾 + 改一个跟踪文件 → 重跑后 `dirty_files=0`、垃圾被清、**被忽略文件原样保留** |
|
||||
| `GIT_TERMINAL_PROMPT=0` + `-c credential.helper=` | 否则一次认证挑战会让 git **等人类输入**,而 runner 没有控制台 → 白占满 30 分钟 job 超时、表现得像“CI 挂了”。实测灌一个坏令牌:`fatal: unable to get password from user` → **rc=1 立即失败**,不挂起 |
|
||||
| 令牌只走 per-invocation `-c http.extraheader` | 不写进 `.git/config`、不回显。匿名也能拉(仓库公开:`/info/refs?service=git-upload-pack` 实测 200)→ CI 不因此新增“令牌必须可用”的依赖 |
|
||||
| 不抓 tag | 本仓库 tag 数 = 0,且无任何脚本用 `git describe --tags`(2026-10-27 实测);要用时再加 |
|
||||
|
||||
> 🔴 **旧版本节给过一段“可以直接抄”的示例,里面两条都是坑,别再照抄**:① `shell: pwsh` —— runner 的
|
||||
> PATH 里没有 `pwsh`,会直接 `Cannot find: pwsh in PATH`,job 在跑任何业务逻辑之前就失败(本文件头部
|
||||
> 已写死只能 `shell: powershell`);② `git fetch --depth 1` —— 浅克隆让 `scripts/ci_gitlint_range.ps1`
|
||||
> 的 `base..head` 和任何需要历史的门禁报 unknown revision。至于方案 1/2(`self` 需先把 action 镜像进
|
||||
> 实例、自定义 URL 在 1.25.4 无效),都不如**直接不依赖**。
|
||||
|
||||
**实测六项**(临时工作区、内层一律 `powershell.exe` **5.1** 忠实复现 runner“写成无 BOM .ps1 再执行”的方式;
|
||||
关键一步是 **E:把正文从 `ci.yml` 里抽出来跑**,测的就是文件里那份字节):
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| A | 全新工作区、匿名无令牌 | rc=0:`commits=665`、refspec 完整、分离 HEAD、远端分支 4 / tag 0、`dirty_files=0` |
|
||||
| B | 复用工作区(改跟踪文件 + 未跟踪垃圾 + gitignored 文件) | rc=0:`dirty_files=0`、垃圾清掉、**ignored 保留**、`17ece86..HEAD` 解出 **18 个提交**(恰等于上游那 18 笔 → gitlint 的 range 可用) |
|
||||
| C | 目标 sha 不在远端(未推的本地提交) | **rc=1** `::error::<sha> is not present in the fetched history`,且 HEAD 纹丝不动 |
|
||||
| D | 灌一个坏令牌 | rc=1,`fatal: unable to get password from user`(**快速失败**,没挂住) |
|
||||
| E | 执行**从 `ci.yml` 抽出的**正文 | rc=0、与 A 同数;**零 ErrorRecord 噪声**;首次全量拉取 172 s(18.65 MiB / loopback) |
|
||||
| F | 同一工作区再跑一次 | rc=0、秒级——**稳态 CI 不重复付全量代价**,只有工作区全新时才慢 |
|
||||
| G | **点源 + `$ErrorActionPreference` 为 Stop(= runner 语义)**,全新工作区 | 修复后 rc=0、`checkout ok: c1e7e8c / commits=669`、首拉 168 s;**修复前同一模式 3 s 亡**(就是 run 44)——这一行才算真正的门 |
|
||||
| H | 点源模式下的四条失败路径 | `DTU_REPO_SHA` 空 / `DTU_REPO_URL` 空 / sha 不在远端 / 坏令牌 → **ExitCode 全 = 1**(成功与复用 = 0)。A–F 那批是用 `-File` 测的,G/H 起才换成语义正确的点源测法 |
|
||||
> ⚠ **一条未证的边界**:`pull_request` 也是本 workflow 的触发器,而 PR 事件的 `github.ref` 是
|
||||
> `refs/pull/<n>/merge`,**不在**我用的分支 refspec 内 → 会走「再显式 fetch 事件 ref」那条兜底。
|
||||
> 这台实例**至今 0 个 PR**(库里 `pull_request` 计数 = 0),`git ls-remote 'refs/pull/*'` 也返回空,
|
||||
> 所以「Gitea 1.25.4 到底提不提供 merge ref」**在这里无法证实**。真撞上的话**症状是响亮的**:
|
||||
> `::error::<sha> is not present in the fetched history` + rc=1,**不会**默默测一棵错的树;届时的
|
||||
> 兜底是改 fetch `refs/pull/<n>/head`(gitlint 的 base 本来就是 `github.event.pull_request.base.sha`,
|
||||
> 不受影响)。同一条备注也适用于 `${{ github.server_url }}`:原生检出与 `checkout@v4` 用的是**同一个**
|
||||
> server_url 拼法,**可达性假设没有变化**——变的只是不再联网 github.com。
|
||||
|
||||
> 🔴 **run 44 真机首验栽了一次,两条教训都属于「本地照不出来」那一类**(10-27 10:38 推上
|
||||
> `ci/remote-ci` 之后两个 job 分别 **3 s / 2 s 亡**,日志原文如下):
|
||||
>
|
||||
> ```
|
||||
> git : error: No such remote 'origin'
|
||||
> + CategoryInfo : NotSpecified: (...) [], RemoteException
|
||||
> + FullyQualifiedErrorId : NativeCommandError
|
||||
> ##[error]Process completed with exit code 1.
|
||||
> ```
|
||||
>
|
||||
> ① **`2>$null` 不算摊平**。runner 把 `run:` 块**点源**进会话(`shell: powershell -command
|
||||
> . 那个文件`),会话的 `$ErrorActionPreference` 是 Stop 时,原生程序**任何写到 stderr 的一行**
|
||||
> 都会被 PS 5.1 包成**终止性** `NativeCommandError`——`2>$null` 不但没吞掉它,反而让它先把整
|
||||
> 个步骤炸掉(`git remote get-url` 在远端不存在时正是往 stderr 写)。修法两条一起做:块首
|
||||
> **显式** `$ErrorActionPreference = Continue`;所有原生 git 调用统一走一个把 stderr 摊平成
|
||||
> 字符串的漏斗(现在这份就是这么写的)。
|
||||
> ② **测法必须复现「点源」,否则连失败都测不出来**。把 `exit 1` 写在被点源的文件里时,
|
||||
> 三种调用方式的实测语义如下:
|
||||
>
|
||||
> | 调用方式 | `exit 1` 之后 | 进程退出码 |
|
||||
> |---|---|---|
|
||||
> | `powershell -File a.ps1` | 进程结束 | 1 |
|
||||
> | `powershell -File b.ps1`,而 b 里点源 a | **继续执行 b 的后续语句** | **0** |
|
||||
> | `powershell -Command ". a.ps1"` ← **runner 的用法** | 进程结束 | **1** |
|
||||
>
|
||||
> 我第一版复现器恰好用了中间那种,于是把四条失败路径全测成 `rc=0`(**假警报**,同时也
|
||||
> 照不出教训 ①)。**要验 workflow 里的 `run:` 块,只能用 `-Command` 点源**,并且最好显式
|
||||
> 把 `$ErrorActionPreference` 前置成 Stop——那才是 runner 的真实语义。
|
||||
|
||||
**自查(改 workflow 前后都跑,第二条是内联换来的同步义务)**:
|
||||
|
||||
```powershell
|
||||
# 1) uses: 必须为 0(注释里的不算)
|
||||
Select-String -Path '.gitea/workflows/ci.yml' -Pattern '^\s*-?\s*uses:'
|
||||
# 2) 两个 job 的第 1 步正文必须逐字一致
|
||||
& '.venv\Scripts\python.exe' -c "import yaml;d=yaml.safe_load(open('.gitea/workflows/ci.yml',encoding='utf-8'));a=[j['steps'][0]['run'] for j in d['jobs'].values()];print('JOBS=%d CHARS=%d' % (len(a),len(a[0])));print('OK identical' if len(set(a))==1 else 'DIFFERS -- keep in sync (see docs/CI guide 5.3)')"
|
||||
```
|
||||
|
||||
> 方案 3 无需任何外部 action 与任何 `DEFAULT_ACTIONS_URL` 配置;代价是失去 checkout 的缓存优化
|
||||
> (对本仓库影响很小)。
|
||||
> 参考:Gitea FAQ「Where will the runner download scripts when using actions such as `actions/checkout@v4`?」
|
||||
|
||||
**本机环境判断(2026-10-02)**:Gitea 装了 `OFFLINE_MODE = true`,说明这台机器的外网访问
|
||||
**可能受限**。如果 runner 拉不到 github.com,checkout 步骤会卡住。
|
||||
|
||||
> ⚠️ 若走方案 1:**`self` 是关键字,不是写你的域名**(见 §1.2.1)。
|
||||
|
||||
**建议**:**先按原样试一次**。若 checkout 步骤报网络错误,再换成上面的零依赖写法(方案 3)。
|
||||
|
||||
### 5.4 产物存档与结果汇总(2026-10-02 起)
|
||||
|
||||
**目标**:CI 失败时**不查 MySQL / 不翻 runner 磁盘**,在 Gitea 页面即可看到完整输出与摘要。
|
||||
@@ -988,8 +1185,8 @@ github.com 会在 checkout 步骤卡住/失败。三种处置:
|
||||
|
||||
| job | `runs-on` | 步骤要点 |
|
||||
|---|---|---|
|
||||
| `l0-gates` | `windows` | checkout(`fetch-depth: 0`,gitlint/cubemx 需完整历史)→ 环境自检(**Python 探测走 `scripts/ci_find_python.ps1`**、cppcheck 按 `DTU_CPPCHECK` → PATH → 标准安装目录,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **提交信息门禁 `scripts/ci_gitlint_range.ps1`**(见 §7 排障表"钩子条数对不上"行)→ **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/l0-pre-commit.log`)→ 打印 `::notice::L0: passed/failed/skipped` 汇总行 → 归档日志说明 |
|
||||
| `l05-host-unit-tests` | `windows` | checkout → **探测 MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/l05-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::L0.5: K runner(s), A/B assertions passed`;失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") |
|
||||
| `l0-gates` | `windows` | **原生 git 检出**(全量 refspec,不再用 `actions/checkout`;见 §5.3)→ 环境自检检(**Python 探测走 `scripts/ci_find_python.ps1`**、cppcheck 按 `DTU_CPPCHECK` → PATH → 标准安装目录,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **提交信息门禁 `scripts/ci_gitlint_range.ps1`**(见 §7 排障表"钩子条数对不上"行)→ **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/l0-pre-commit.log`)→ 打印 `::notice::L0: passed/failed/skipped` 汇总行 → 归档日志说明;**`ui-golden` 需要 SDL2 SDK,而该 SDK 不入库** → 依赖机器级 `DTU_SDL2_DIR`(缺件即 FAIL,护栏与落地见 §4.5) |
|
||||
| `l05-host-unit-tests` | `windows` | **原生 git 检出**(与 `l0-gates` 第 1 步逐字一致,内联带来的同步义务,自查见 §5.3 末)→ **探测 MSVC**MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/l05-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::L0.5: K runner(s), A/B assertions passed`;失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") |
|
||||
|
||||
> 🔴 **两条 Python 探测链必须同序同步**:本地 `scripts/ci_local.py` 与 CI 侧
|
||||
> `scripts/ci_find_python.ps1` 的候选顺序要一致。2026-10-11 那次 L0 全灭,就是 workflow
|
||||
@@ -1064,7 +1261,9 @@ date +%s # 对比 last_online
|
||||
| push 后 Actions 页**空白/无任务** | ① 服务端开关未开 ② workflow 没 push 到远程 ③ 只改了 `docs/**`(被 `paths-ignore` 滤掉) ④ **新建分支的首次 push**(实测 2026-10-12:创建 `v2-on-ui-ci` 那次 push 没产生 run——runner 在线空闲、job 日志零新增;紧接着一次普通 push 立刻出 task。原因:`before` 是全零 SHA,变更文件集算不出来,`paths-ignore` 求值时被滤空) | ①②③ 逐条核对 §1 / §5.1 / §5.2;④ 用 `workflow_dispatch` 手动触发,或再推一个非文档路径的提交 |
|
||||
| 任务一直 **Waiting / 排队** | 没有 label 匹配 `windows` 的 runner | 检查 `--labels "windows:host"`;Settings→Runners 是否 Idle |
|
||||
| `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) |
|
||||
| `checkout` 步骤卡住/超时 | 拉不到 `actions/checkout@v4`(内网) | 见 §5.3 |
|
||||
| `Prepare all required actions` 或 `checkout` 卡住/超时(~45 s 后整 job 亡、零钩子输出) | 有人把 `uses:` 加回了 workflow —— 该阶段**先于任何步骤**无条件联网,手工 curl 可达不代表 runner 可达 | 本仓库 2026-10-27 起已无 `uses:`(§5.3 有六项实测与自查命令);自查:`Select-String .gitea/workflows/ci.yml -Pattern '^\s*-?\s*uses:'` 应为空 |
|
||||
| job **3~5 秒就亡**、日志里是 `CategoryInfo` + `FullyQualifiedErrorId : NativeCommandError`,而同一条命令在本地全绿 | `run:` 块被**点源**进会话且 `$ErrorActionPreference` 继承为 Stop → 原生程序的 stderr 行变成**终止性**错误(`2>$null` 挡不住) | 块首显式设 `Continue`,原生调用一律摊平(`2>&1` 配 `ForEach-Object` 那类漏斗);复现方法见 §5.3 的 run 44 实录 |
|
||||
| `ui-golden` 红、日志里是 `C1083: SDL2/SDL.h` 或 `MSB1009: 项目文件不存在`,而本地全绿 | 前者 = 检出里根本没有 SDL2 SDK(`.gitignore` 排除、不入库);后者 = **同一个 job 内**前一个钩子配置中断留下的半截构建缓存(CI 不跨 run 残留,见 §4.5 的更正) | 设机器级 `DTU_SDL2_DIR`(§4.5);构建脚本已自愈,也可手删那份构建目录 |
|
||||
| `l05` 日志出现 `::warning::runner 未安装 MSVC` | VS 装在非默认路径或未装 | 见 §4.2(默认路径 / 同步改两处根目录) |
|
||||
| `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;但注意 **CI 未守体积门禁**(见 §0.0),改代码后本地 `@build` + `check_mem_numbers.py --scan` 兜底 |
|
||||
| L0.5 显示 `2 runner(s), X/Y ...` 但 X<Y | 有单测用例未通过 | 看该模块的 `::error::unit test failed` 行与其 `ci-artifacts/l05-<模块>.log` |
|
||||
@@ -1072,6 +1271,7 @@ date +%s # 对比 last_online
|
||||
| `Tee-Object` 步骤报 `TerminatorExpectedAtEndOfString` / 字符串缺少终止符 | `run:` 块混入了非 ASCII(GBK 解码错位) | 见文件头 🔴🔴;`run:` 内必须纯 ASCII |
|
||||
| `l05` 报 `fatal error C1083: 无法打开包括文件 "stdio.h"` | Windows SDK 缺失 | 装 VS 时勾"使用 C++ 的桌面开发"+ SDK |
|
||||
| 单测退出码 2 | 环境错误(多为无 MSVC) | 同上 |
|
||||
| `test-coverage` 恒 Failed、日志写 `[ENV] 找不到 Microsoft.CodeCoverage.Console.exe`;或换了 VS 世代后覆盖率大面积「假红」 | 该机没有 VS 17.9+/18.x(**该 exe 只随 VS 分发**,NuGet 拿不到);或 VS 装在非默认盘而机器级 `DTU_MSVC_CL` 仍指旧世代 | §4.4(安装入口 + 组件 ID + 三条可直接粘贴的命令 + `-Force` 陷阱) |
|
||||
| `pre-commit` 首次运行久 | 首次要下载各钩子的环境 | 正常,runner 缓存后会快 |
|
||||
| `daemon` 启动报 **connection refused / dial tcp** | `--instance` 填了不可达地址 | 删 `.runner` → 重新 `register`,`--instance http://127.0.0.1:3001` |
|
||||
| 启动报 **token** 相关错误 | Registration Token 过期(约 1 小时)或已用过 | 回网页重新创建 runner 拿新 token,重新 `register` |
|
||||
@@ -1310,6 +1510,10 @@ powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
|
||||
# 兜底通道(只有变量没设时才会走到):把 pre-commit 装进【全局】site-packages,见 §4.1.1
|
||||
# ⚠️ 改过任何环境变量都必须 Stop 再 Start 任务——环境块是任务启动时的快照
|
||||
|
||||
# 5b) 覆盖率工具链(mscoverage)/ 把机器级 MSVC 指到 VS2026 —— 见 §4.4
|
||||
# 安装入口、组件 ID、三条可直接粘贴的管理员命令都在 §4.4;本机 VS 装在 E 盘,
|
||||
# 非默认盘必须显式设 DTU_MSVC_CL(否则门禁与 CI 都看不见它)。别加 -Force
|
||||
|
||||
# 6) 复验(-s = 模拟 SYSTEM 视角,必须过)
|
||||
& 'C:\ProgramData\Anaconda3\python.exe' -s -m pre_commit --version # pre-commit 4.6.2
|
||||
|
||||
@@ -1351,7 +1555,12 @@ powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
| 2026-10-27 | ✅ **run 46:`ui-golden` 转绿(A 方案生效,`DTU_SDL2_DIR` 由 SYSTEM 命中机器级 `D:\DTU_SDK`),L0 = 47 Passed / 1 Failed / 2 Skipped**。那 1 根红是**我自己**:新加的症状行里把 gitignored 的构建产物路径当文档引用写了,本机存在、干净检出不存在 → `doc-links`「本地过、CI 挂」,**正是本文 §4.1.2 记的那一坑**,我一边写它一边踩了它;已在干净克隆(无构建目录)里先复现再修(复现命令与判据同 §4.1.2)。顺带更正我先前两句关于工作区复用的错话:实测 CI **每个 job 一个全新工作区**(run 44/45/46 的 L0 目录哈希 `b4a9…`/`e3ad…`/`74ce…` 互不相同,每次付 137~161 s 全量拉取),所以「毒缓存跨 run 存活」只对**本机与同一 job 内**成立(`ui-golden` 与板端导航自检共用构建目录,前者中断会毒到后者),对跨 run 不成立——§4.5、§5.3 的 `-ffd` 依据、§7 行与 `ci.yml` 注释四处同步改为实测口径(历史行不动,只在上方加本行) |
|
||||
|---|---|
|
||||
| 2026-10-27 | 🟡 **run 45 真把门禁跑起来了**:L0 46 根钩子 Passed / L0.5 = 40 runner 3188/3188 fail=0(`checkout ok: ae80bb8 / commits=670`,SYSTEM 读到机器级 `DTU_MSVC_CL` = E 盘 14.51)。唯一红 = `ui-golden`,定性为**第二类 runner 缺环境**而非像素回归:SDL2 SDK 被 `.gitignore` 排除、不入库,干净检出天然没有(本地有 → 一直绿)。新增 §4.5:`CMakeLists.txt` 支持机器级 `DTU_SDL2_DIR`(不设=行为不变,实测 Passed)+ 配置期硬校验(护栏把一整屏 C1083 降到 0 行)+ `Find-Sdl2Sdk` 三级发现(机器级 `D:\DTU_SDK` 优先,命中仓内路径会 WARN)+ **毒缓存坑**:配置中断留下 `CMakeCache.txt` 却没有工程文件,而 `build_sim.ps1` 是「有缓存就不再配置」→ 下一轮跳过配置、报离根因极远的 `MSB1009`,且 `simulator/build/` 被 `git clean -ffd` 刻意保留 → **跨 run 存活**;已按该文件既有先例补「有缓存无工程 → 删重」。实测闭环:在一份没有仓内 SDK 且带毒缓存的等价工作区里,仅设变量即 `Passed`(8+40 全过、自愈)。§4.1.3 变量表由 3 个补到 5 个;§5.5 的 `l0-gates` 步骤要点补上 `ui-golden` 的 SDK 依赖;skill 侧补症状行、三分法 B 类例、`run.status` 5/6 为非终态;顺带更正 lvgl-simulator skill 中「SDL2 仓库已内置」的说法 |
|
||||
| 2026-10-27 | 🔴 **run 44 真机首验失败一次 → 两条「本地照不出来」的教训写进 §5.3**:把原生检出差上去推到 `ci/remote-ci`(10:38)后两个 job 分别 3 s / 2 s 亡,日志是 `error: No such remote 'origin'` 被包成 `NativeCommandError` 终止整步。教训①:runner 用 `powershell -command . '<文件>'` **点源**执行 run: 块,会话继承 `$ErrorActionPreference=Stop` 时原生程序的 stderr 一行就是**终止性错误**,`2>` 重定向挡不住——修法:块首显式 `Continue` + 所有 git 调用走摊平漏斗(另把 `git remote get-url` 换成 `git config --get`,缺 key 时不写 stderr)。教训②:**测法必须复现点源**——实测 `exit 1` 语义矩阵:`-File` 直跑=1 / `-File` 包点源=**0 且继续执行** / `-Command` 点源=1;我第一版复现器用了中间那种,把四条失败路径全测成 rc=0(假警报,也照不出教训①)。换点源测法后:全新工作区 150 s `checkout ok: c1e7e8c / commits=669`、四条失败路径 ExitCode 全 1、复用工作区 1.6 s 且 gitignored 文件保留。§5.3 实测表补 G/H 两行、§7 补「几秒就亡」一行、`remote-ci-ops` skill §10 补测法。另记:本仓 push 到**已存在**的远端分支会正常建 run(run 44),而新建分支首推不建(已三次复现) |
|
||||
| 2026-10-27 | ✂️ **CI 侧两处根治 + 更正我上一条的一处推断**:① **workflow 去掉 `uses:`**——两个 job 的第 1 步换成原生 git 全量检出(完整 refspec + 分离 HEAD + `reset --hard` + `clean -ffd`、令牌只走 per-invocation `-c http.extraheader`、`GIT_TERMINAL_PROMPT=0` 防挂死、目标 sha 不在历史里就 `::error::` 硬失败而不是默默构建错树)。动因是 run 36/41/42 三次 **`Prepare all required actions` 阶段 ~45 s 外网超时把整个 job 打死、零钩子输出**,而同一分钟手工 `curl` 返回 200。§5.3 整节重写,含六项实测(A/B/C/D/E/F——关键是 E:**把正文从 `ci.yml` 里抽出来跑**,测的就是文件里那份字节)与两条自查命令;§7 的 checkout 行同步改为“看见此症状=有人把 `uses:` 加回来了”。② **`scripts/ci_runner_bootstrap.ps1` 的 `Find-MsvcCl` 重写**:`vswhere -all -products *` 取实例(**能看见非默认盘**)+ 工具集版本降序 + `-MsvcToolset` 前缀**钉住世代**(默认 `14.51.`,与覆盖率基线同版本;钉不上连 WARN 再回落最新,绝不静默)+ 新增 `-MsvcLatest`;`-SetEnv` 增写 `DTU_MS_COVERAGE`(须与 `DTU_MSVC_CL` 成对改)。③ **更正**:先前写“旧探测会把机器级变量覆回 C 盘 14.29”——本机实情是 C 盘 VS2019 已于 **01:00 卸载**(`…\2019` 只剩空目录、C 盘 x64 `cl.exe` 实测 0 个),旧 `-SetEnv` 在本机是**找不到 → 只 Warn → 不写**,于是变量一直指着失效路径(CI 的 L0.5 走显式通道即硬失败,不做静默回退)。实测新逻辑:候选 3 个(14.44/14.51/14.52,全在 E 盘)→ 默认命中 **14.51.36231**,`-Latest` 给 14.52,`-MsvcToolset 99.` 走告警回落。④ §1.2.1 表第三行按 1.25.4 实际行为改正(自定义 `DEFAULT_ACTIONS_URL` 取值已无效)——旧写法会把下一个排障的人引向一个不存在的开关 |
|
||||
| 2026-10-27 | 🧯 **拆穿「L0 恒红 = 环境噪声」这个想当然 + 覆盖率工具链补齐**:① `cppcheck` 恒红**不是**版本漂移——本机与 runner 同机、同 exe(2.21.0)、同参数复现即红,两根 `uninitializedVariable`(`s_cmd_con` 在空信息体分支把未初始化 `body` 传下去、`s_spont_step` 的 `blen` 作用域过宽)是真代码问题,已在 `ci/remote-ci` 的 `5869c03` 修掉(`@build` 0/0,`Code 231712→231732`)。② 新增 §4.4:`Microsoft.CodeCoverage.Console.exe` **只随 VS 分发**(NuGet 两候选包实测 .exe 数为 0 / 只有老版 `CodeCoverage.exe`),本机补装 VS2026 BuildTools 18.11 于 **E 盘**后仍因非默认盘不被看见,须显式机器级 `DTU_MSVC_CL`(指 14.51 = 与 `scripts/test_coverage_baseline.json` 同版本);三条管理员命令、`-Force` 陷阱、`Restart-Service Schedule` 判据一并写入。③ 排障教训:**页面时长与本地对不上时以 `commit_status` + runner 日志为准**——页面那条「L0 Failing after 6m49s」在库里是 run 26 的 409 s(cppcheck 真红),同一 sha 另有一次 5 s 的 checkout 超时,**两者不是一回事**,别拿一个解释另一个 |
|
||||
| 2026-10-25 | 🔒 **A-5 残余销项联动(§0.0 重写)**:体积门禁由"CI 不守"改为**同码凭证强制**——本地 `@build` 0/0 后 `ram_baseline.py --update` 盖 `scripts/size_attestation.json`,L0 作业末尾 `check_size_gate.py --ci` 复核同码性 + 红线复算(runner 无需 Keil);主干 push 拦红、其余事件 warn-only;`mem-budget` 补进 SKIP 名单诚实显示。指纹取 git 索引 blob(跨 checkout 稳定,修 7c4628b);三项仍守不住的口径唯一正文 = 脚本 docstring |
|
||||
| 2026-10-12 | 🐛 **第一次真跑出 run,当场暴露 gitlint 门禁空转**:把本轮修复推到新分支 `v2-on-ui-ci` 后拿到 task 31/32——**L0.5 成功**(`10 runner(s), 647/647 assertions passed, fail=0`,与本地逐字一致),**L0 失败**却与提交信息毫无关系:`exit code: 254` + `Missing git configuration: please set user.name`。根因是 `actions/checkout` 把 HOME 指向每次任务的临时目录、SYSTEM 账号没有 git 身份,gitlint 在读到规则之前就配置错误退出;旧脚本还把失败原因**写死**成"标题超 100 字符",加上该步 `exit 1` 让后面 26 个钩子**一个都没跑**。修法:脚本先探测身份,缺失就用 `GIT_CONFIG_COUNT/KEY/VALUE` 注入一次性身份(不写文件、不碰开发者机器),并区分 254(门禁没跑起来 → `exit 2`)与真实违规(→ `exit 1`)。另记一条新坑:**新建分支的首次 push 不产生 run**(§7 排障表 ④) |
|
||||
| 2026-10-12 | 🔀 **随 `master` 合入 `v2-on-ui`:数字口径改为分支中立**。本指南原先写死 master 的数字(18 钩子 / 17 报告、3 runner / 288 断言),而 `v2-on-ui` 是 **26 钩子 / 10 套件**——同一篇文档在两个分支上必然有一个是错的。§6 验收第 8 行、§7 排障表、§10 验收标准三处改为讲规则(**报告数 = 定义数 − 1**;**runner 数 = `test/host/` 下 `run_unit_test.py` 个数**)+ 分分支实测快照。本次合并只带文档与脚本,**零 C 代码改动**;合并后本分支 `@build` 0 错 0 警、`ci_local.py` 全绿(L0 25 钩子 + L0.5 10 runner / 647 断言) |
|
||||
|
||||
@@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1`
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 257,272 B (251.24 KB) | 1,048,576 B (1024 KB) | 24.5% |
|
||||
| ER_IROM1 | 257,280 B (251.25 KB) | 1,048,576 B (1024 KB) | 24.5% |
|
||||
| RW_IRAM1 | 80,312 B (78.43 KB) | 131,072 B (128 KB) | 61.3% |
|
||||
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
|
||||
| **Flash (ROM 合计)** | **258,792 B (252.7 KB)** | 1,048,576 B (1024 KB) | **24.7%** |
|
||||
| **Flash (ROM 合计)** | **258,800 B (252.7 KB)** | 1,048,576 B (1024 KB) | **24.7%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|
||||
|---|---|---|---|---|
|
||||
| ER_IROM1 (Flash) | 257,272 B | 1,048,576 B | 24.5% | 参考 |
|
||||
| ER_IROM1 (Flash) | 257,280 B | 1,048,576 B | 24.5% | 参考 |
|
||||
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
|
||||
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
|
||||
| RW_IRAM1 | 80,312 B | 131,072 B | 61.3% | PASS |
|
||||
|
||||
@@ -163,6 +163,11 @@
|
||||
16:17,都是各自安装当日,不是后来补勾)→ VS 18 的 BuildTools 默认就带它。真实风险面只剩:没有
|
||||
VS 只有裸 MSVC 的机器、`DTU_MS_COVERAGE` 指错、以及靠 PATH 兜底的环境。CI runner 与本机同一台
|
||||
(`docs/CI/README.md`);**换机仍先跑 `--list` 再跑全量**。
|
||||
⚠ **2026-10-27 实测补注(换机场景真发生了)**:这台 runner 机当日 `vswhere -all -products *` 只有
|
||||
VS2019 生成工具(MSVC 14.29)→ 上面「CI runner 与本机同一台」这句对**量基线那台**不成立。补装 VS2026
|
||||
BuildTools 18.11 后仍因装在 E 盘(非默认盘)被门禁与 CI 同时看不见,最终靠显式机器级 `DTU_MSVC_CL` 指
|
||||
14.51(与量基线同版本)跑通 40/40 套、零基线回退。安装入口、组件 ID、三条命令与 `-Force` 陷阱统一记在
|
||||
`docs/CI/远程CI完整指南.md` §4.4——**本处不重复,免得两处漂移**。
|
||||
9. **并集口径下"单套百分比"没有意义。** 同一文件被多套触及很常见(`sha256.c` 被 `sha256` 与
|
||||
`ftp_auth` 都编译),所以对外只报**并集**,单套只报"触及几个文件 / 多少行"。
|
||||
|
||||
|
||||
+191
-21
@@ -49,7 +49,11 @@
|
||||
|
||||
.PARAMETER SetEnv
|
||||
Discover and write MACHINE-level DTU_PRECOMMIT_PYTHON / DTU_CPPCHECK /
|
||||
DTU_MSVC_CL. Existing values are preserved unless -Force.
|
||||
DTU_MSVC_CL / DTU_MS_COVERAGE / DTU_SDL2_DIR. Existing values are preserved unless
|
||||
-Force. MSVC discovery goes through vswhere (so VS on a non-default
|
||||
drive is found) and pins the toolset with -MsvcToolset, because the
|
||||
coverage baseline was recorded with one specific MSVC version -- see
|
||||
the remote-CI guide (docs/CI/) section 4.4.
|
||||
|
||||
.PARAMETER StartNow
|
||||
Start the task after registering it.
|
||||
@@ -65,6 +69,16 @@
|
||||
.PARAMETER Force
|
||||
With -SetEnv, overwrite existing machine-level values.
|
||||
|
||||
.PARAMETER MsvcToolset
|
||||
Toolset version PREFIX to prefer when writing DTU_MSVC_CL (default
|
||||
'14.51.'). 'newest available' is deliberately NOT the rule: the host
|
||||
unit-test coverage baseline was measured with MSVC 14.51 and the
|
||||
denominator changes with the compiler generation. When no toolset
|
||||
matches the prefix the script warns and falls back to the newest.
|
||||
|
||||
.PARAMETER MsvcLatest
|
||||
Ignore -MsvcToolset and take the newest MSVC toolset found.
|
||||
|
||||
.OUTPUTS
|
||||
Progress lines. Exit codes: 0 = ok, 1 = precondition failed, 2 = not elevated.
|
||||
|
||||
@@ -80,7 +94,9 @@ param(
|
||||
[switch]$StartNow,
|
||||
[switch]$Status,
|
||||
[switch]$DryRun,
|
||||
[switch]$Force
|
||||
[switch]$Force,
|
||||
[string]$MsvcToolset = '14.51.',
|
||||
[switch]$MsvcLatest
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -138,24 +154,149 @@ function Find-Cppcheck {
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-MsvcCl {
|
||||
$pf = $env:ProgramFiles
|
||||
$pf86 = ${env:ProgramFiles(x86)}
|
||||
if ([string]::IsNullOrWhiteSpace($pf)) { $pf = 'C:\Program Files' }
|
||||
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
|
||||
$roots = @()
|
||||
foreach ($year in @('2019', '2022', '2026', '18')) {
|
||||
foreach ($edition in @('BuildTools', 'Community', 'Professional', 'Enterprise')) {
|
||||
$roots += (Join-Path $pf86 "Microsoft Visual Studio\$year\$edition")
|
||||
$roots += (Join-Path $pf "Microsoft Visual Studio\$year\$edition")
|
||||
function Find-VsInstances {
|
||||
# VS install roots, newest first, via vswhere. This matters because
|
||||
# vswhere knows about NON-DEFAULT INSTALL DRIVES: this machine keeps
|
||||
# VS 18 BuildTools under E:, which a %ProgramFiles%-derived probe can
|
||||
# never see. Returns an empty array when vswhere is unusable so the
|
||||
# caller can fall back to the classic (incomplete) root scan.
|
||||
$vswhere = $env:DTU_VSWHERE
|
||||
if ([string]::IsNullOrWhiteSpace($vswhere)) {
|
||||
$pf86 = [Environment]::GetFolderPath('ProgramFilesX86')
|
||||
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
|
||||
$vswhere = Join-Path $pf86 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $vswhere)) { return @() }
|
||||
$raw = & $vswhere -all -products * -format json 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or -not $raw) { return @() }
|
||||
$list = @()
|
||||
foreach ($i in @($raw | ConvertFrom-Json)) {
|
||||
if (-not $i -or -not $i.installationPath) { continue }
|
||||
if (-not (Test-Path -LiteralPath $i.installationPath)) { continue }
|
||||
$v = [version]'0.0'
|
||||
if ($i.installationVersion) { [void][version]::TryParse([string]$i.installationVersion, [ref]$v) }
|
||||
$list += [pscustomobject]@{ Root = [string]$i.installationPath; Version = $v }
|
||||
}
|
||||
return @($list | Sort-Object -Property Version -Descending)
|
||||
}
|
||||
|
||||
function Find-MsvcClCandidates {
|
||||
# Every x64-host cl.exe on this machine, tagged with its toolset version.
|
||||
# Comparison operators in PowerShell are case-insensitive by default, so
|
||||
# -match / -notmatch cover HostX64 / Hostx64 spelling differences.
|
||||
$cands = @()
|
||||
$roots = @(Find-VsInstances | ForEach-Object { $_.Root })
|
||||
if ($roots.Count -eq 0) {
|
||||
Warn 'vswhere unusable -- falling back to %ProgramFiles% roots; VS installs on other drives are INVISIBLE to that probe'
|
||||
$pf = [Environment]::GetFolderPath('ProgramFiles')
|
||||
$pf86 = [Environment]::GetFolderPath('ProgramFilesX86')
|
||||
if ([string]::IsNullOrWhiteSpace($pf)) { $pf = 'C:\Program Files' }
|
||||
if ([string]::IsNullOrWhiteSpace($pf86)) { $pf86 = 'C:\Program Files (x86)' }
|
||||
foreach ($year in @('18', '2026', '2022', '2019')) {
|
||||
foreach ($edition in @('BuildTools', 'Community', 'Professional', 'Enterprise')) {
|
||||
foreach ($base in @($pf, $pf86)) {
|
||||
$r = Join-Path $base ('Microsoft Visual Studio\' + $year + '\' + $edition)
|
||||
if (Test-Path -LiteralPath $r) { $roots += $r }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($r in $roots) {
|
||||
if (-not (Test-Path -LiteralPath $r)) { continue }
|
||||
$cl = Get-ChildItem -LiteralPath $r -Recurse -Filter 'cl.exe' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -like '*Hostx64\x64*' } |
|
||||
Select-Object -First 1
|
||||
if ($cl) { return $cl.FullName }
|
||||
$msvc = Join-Path $r 'VC\Tools\MSVC'
|
||||
if (-not (Test-Path -LiteralPath $msvc)) { continue }
|
||||
foreach ($f in (Get-ChildItem -LiteralPath $msvc -Recurse -Filter 'cl.exe' -ErrorAction SilentlyContinue)) {
|
||||
$full = $f.FullName
|
||||
if ($full -notmatch '\\bin\\Hostx64\\x64\\cl\.exe$') { continue }
|
||||
$toolset = ''
|
||||
if ($full -match '\\VC\\Tools\\MSVC\\([^\\]+)\\bin\\') { $toolset = $Matches[1] }
|
||||
$ver = [version]'0.0'
|
||||
[void][version]::TryParse($toolset, [ref]$ver)
|
||||
$cands += [pscustomobject]@{ Cl = $full; Toolset = $toolset; Version = $ver; VsRoot = $r }
|
||||
}
|
||||
}
|
||||
return @($cands)
|
||||
}
|
||||
|
||||
function Find-MsvcCl {
|
||||
# Rewritten 2026-10-27. The previous version walked a hard-coded root list
|
||||
# derived from %ProgramFiles% and returned the FIRST hit: with VS2019
|
||||
# (MSVC 14.29) and a newer VS both present it handed back 14.29, and it
|
||||
# could not see the E: install at all. "First hit" is never right for a
|
||||
# compiler: the coverage baseline (scripts/test_coverage_baseline.json)
|
||||
# was recorded with MSVC 14.51 and the instrumented denominator moves when
|
||||
# the compiler generation moves (measured: App/util/sha256.c is 105 lines
|
||||
# under gcc vs 114 under MSVC -- see the host coverage policy doc, 2.1).
|
||||
# Hence the default is a PINNED toolset prefix, not "newest".
|
||||
param([string]$ToolsetPrefix = '', [switch]$Latest)
|
||||
$cands = @(Find-MsvcClCandidates)
|
||||
if ($cands.Count -eq 0) { return $null }
|
||||
$sorted = @($cands | Sort-Object -Property Version -Descending)
|
||||
$seen = (@($sorted | ForEach-Object { $_.Toolset } | Select-Object -Unique)) -join ', '
|
||||
if (-not $Latest -and $ToolsetPrefix) {
|
||||
$hit = @($sorted | Where-Object { $_.Toolset -like ($ToolsetPrefix + '*') })
|
||||
if ($hit.Count -gt 0) {
|
||||
Info ('MSVC pinned to toolset prefix ' + $ToolsetPrefix + '* -> ' + $hit[0].Toolset + ' (' + $hit.Count + ' of ' + $cands.Count + ' candidate(s); toolsets found: ' + $seen + ')')
|
||||
return $hit[0].Cl
|
||||
}
|
||||
Warn ('no MSVC toolset matching prefix ' + $ToolsetPrefix + ' on this machine (found: ' + $seen + ')')
|
||||
Warn 'falling back to the NEWEST toolset -- that changes the coverage denominator; read the remote-CI guide (docs/CI/) section 4.4 before accepting it'
|
||||
}
|
||||
Info ('using newest MSVC toolset ' + $sorted[0].Toolset + ' of ' + $cands.Count + ' candidate(s); toolsets found: ' + $seen)
|
||||
return $sorted[0].Cl
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Locate an SDL2 SDK root usable by the UI simulator (ui-golden gate).
|
||||
# The SDK is DOWNLOADED, not tracked: .gitignore excludes simulator/SDL2/, so a
|
||||
# CI workspace never contains it. Measured 2026-10-27 run 45: ui-golden died with
|
||||
# C1083 "SDL2/SDL.h" on the runner while every local run was green -- a missing
|
||||
# third-party SDK, not a pixel regression. Discovery order:
|
||||
# 1) an already-valid DTU_SDL2_DIR (Machine, then Process)
|
||||
# 2) a machine-wide copy under -MachineSdkRoot (default D:\DTU_SDK)
|
||||
# 3) this repository checkout's simulator/SDL2/SDL2-* -- works for local runs,
|
||||
# but a runner workspace will not contain it, so on the CI box prefer 1 or 2
|
||||
# Returns the root that contains include\SDL2\SDL.h, else $null.
|
||||
# ---------------------------------------------------------------------------
|
||||
function Find-Sdl2Sdk {
|
||||
param([string]$MachineSdkRoot = 'D:\DTU_SDK')
|
||||
$rel = 'include\SDL2\SDL.h'
|
||||
$cand = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($sc in @('Machine', 'User', 'Process')) {
|
||||
$v = [Environment]::GetEnvironmentVariable('DTU_SDL2_DIR', $sc)
|
||||
if ($v) { $cand.Add($v) }
|
||||
}
|
||||
if ($MachineSdkRoot -and (Test-Path -LiteralPath $MachineSdkRoot)) {
|
||||
Get-ChildItem -LiteralPath $MachineSdkRoot -Directory -Filter 'SDL2-*' -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending | ForEach-Object { $cand.Add($_.FullName) }
|
||||
}
|
||||
$repoSdk = Join-Path (Split-Path -Parent $PSScriptRoot) 'simulator\SDL2'
|
||||
if (Test-Path -LiteralPath $repoSdk) {
|
||||
Get-ChildItem -LiteralPath $repoSdk -Directory -Filter 'SDL2-*' -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending | ForEach-Object { $cand.Add($_.FullName) }
|
||||
}
|
||||
foreach ($c in $cand) {
|
||||
if ($c -and (Test-Path -LiteralPath (Join-Path $c $rel))) { return $c }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Find-CoverageConsole {
|
||||
# Microsoft.CodeCoverage.Console.exe ships ONLY inside Visual Studio
|
||||
# (measured 2026-10-27: NuGet candidates contain no such .exe, or only the
|
||||
# legacy Team Tools CodeCoverage.exe whose CLI differs). Its location
|
||||
# inside a VS install is a fixed relative path:
|
||||
param([string]$ClPath = '')
|
||||
$rel = 'Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\Microsoft.CodeCoverage.Console.exe'
|
||||
$roots = @()
|
||||
if ($ClPath) {
|
||||
$vsRoot = ''
|
||||
# cl.exe = <root>\VC\Tools\MSVC\<ver>\bin\Hostx64\x64\cl.exe
|
||||
if ($ClPath -match '^(.*)\\VC\\Tools\\MSVC\\') { $vsRoot = $Matches[1] }
|
||||
if ($vsRoot) { $roots += $vsRoot }
|
||||
}
|
||||
foreach ($i in @(Find-VsInstances)) { $roots += $i.Root }
|
||||
foreach ($r in ($roots | Where-Object { $_ } | Select-Object -Unique)) {
|
||||
$cand = Join-Path $r $rel
|
||||
if (Test-Path -LiteralPath $cand) { return $cand }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
@@ -273,10 +414,10 @@ if ($Status) {
|
||||
if (Test-Gitea $GiteaUrl) { Ok "Gitea reachable at $GiteaUrl" }
|
||||
else { Fail "Gitea NOT reachable at $GiteaUrl" }
|
||||
|
||||
foreach ($n in @('DTU_PRECOMMIT_PYTHON', 'DTU_CPPCHECK', 'DTU_MSVC_CL')) {
|
||||
foreach ($n in @('DTU_PRECOMMIT_PYTHON', 'DTU_CPPCHECK', 'DTU_MSVC_CL', 'DTU_MS_COVERAGE', 'DTU_SDL2_DIR')) {
|
||||
$v = Get-MachineEnv $n
|
||||
if ($v) { Ok ("MACHINE {0} = {1}" -f $n, $v) }
|
||||
else { Warn "MACHINE $n not set (CI auto-discovery still applies; see scripts/ci_find_python.ps1)" }
|
||||
else { Warn "MACHINE $n not set (CI auto-discovery still applies; -SetEnv writes it, docs/CI/ guide sections 4.4 / 4.5 explain the pairing)" }
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
@@ -359,8 +500,37 @@ if ($SetEnv) {
|
||||
if ($cpp) { Set-MachineEnv 'DTU_CPPCHECK' $cpp 'DTU_CPPCHECK' }
|
||||
else { Warn 'cppcheck not found (install Cppcheck.Cppcheck, or set DTU_CPPCHECK manually)' }
|
||||
|
||||
$cl = Find-MsvcCl
|
||||
if ($cl) { Set-MachineEnv 'DTU_MSVC_CL' $cl 'DTU_MSVC_CL' }
|
||||
# ui-golden needs the SDL2 SDK and a checked-out tree never has it (see
|
||||
# Find-Sdl2Sdk). Pin it machine-wide so the runner resolves it under SYSTEM too.
|
||||
$sdl = Find-Sdl2Sdk
|
||||
if ($sdl) {
|
||||
Set-MachineEnv 'DTU_SDL2_DIR' $sdl 'DTU_SDL2_DIR'
|
||||
$repoRoot = (Split-Path -Parent $PSScriptRoot) + [IO.Path]::DirectorySeparatorChar
|
||||
if ($sdl.StartsWith($repoRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
Warn 'DTU_SDL2_DIR points INSIDE this repo checkout -- a runner workspace will not contain that path.'
|
||||
Warn 'copy an SDK to D:\DTU_SDK (or point DTU_SDL2_DIR at a machine-wide path), then re-run -SetEnv'
|
||||
}
|
||||
}
|
||||
else {
|
||||
Warn 'no SDL2 SDK found: the ui-golden gate fails with C1083 SDL2/SDL.h (measured run 45).'
|
||||
Warn 'download it per the simulator deployment guide (docs/CI/ guide, section 4.5) or copy one to D:\DTU_SDK, then re-run -SetEnv'
|
||||
}
|
||||
|
||||
$cl = Find-MsvcCl -ToolsetPrefix $MsvcToolset -Latest:$MsvcLatest
|
||||
if ($cl) {
|
||||
Set-MachineEnv 'DTU_MSVC_CL' $cl 'DTU_MSVC_CL'
|
||||
# Pin the coverage collector too, and pin it INSIDE the same VS as the
|
||||
# chosen cl.exe: the gate derives it from the compiler ancestor anyway,
|
||||
# but an explicit machine value also covers the case where somebody
|
||||
# later points DTU_MSVC_CL at another install. Both vars must be
|
||||
# changed together (docs/CI/ guide, section 4.4).
|
||||
$cov = Find-CoverageConsole -ClPath $cl
|
||||
if ($cov) { Set-MachineEnv 'DTU_MS_COVERAGE' $cov 'DTU_MS_COVERAGE' }
|
||||
else {
|
||||
Warn 'Microsoft.CodeCoverage.Console.exe NOT found -- the test-coverage gate FAILS with [ENV] exit 2 (by the 2026-10-24 ruling it does not skip).'
|
||||
Warn 'install VS BuildTools 17.9+/18.x including Microsoft.VisualStudio.PackageGroup.TestTools.CodeCoverage, then re-run -SetEnv'
|
||||
}
|
||||
}
|
||||
else { Warn 'MSVC cl.exe (Hostx64\x64) not found -- L0.5 will degrade to "skipped" without it' }
|
||||
|
||||
Info 'machine env vars are read when a PROCESS STARTS: the daemon must be restarted below'
|
||||
|
||||
@@ -9,3 +9,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
|
||||
2026-10-24 213780 18860 1204 131852 232640 22.2 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
|
||||
2026-10-25 224860 24640 1332 135100 249500 23.8 79008 60.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
|
||||
2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736
|
||||
2026-10-27 231720 25560 1520 136216 257280 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736
|
||||
|
||||
|
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"built_at": "2026-10-26T23:32:22+08:00",
|
||||
"fingerprint": "06808c35e47b1113c385277d666806349a09249384347b3ecfd6f27ee79948b0",
|
||||
"built_at": "2026-10-27T10:17:30+08:00",
|
||||
"fingerprint": "1204cb60a4a9a44c3a3435166e07d7d0a54f32c98768951e79c85d6de422a447",
|
||||
"fingerprint_files": 1430,
|
||||
"fingerprint_source": "git-index",
|
||||
"gated_regions": [
|
||||
"RW_IRAM1",
|
||||
"RW_CCM"
|
||||
],
|
||||
"git_head": "b623a406ad58c2d281d8e54f118999a0016da328",
|
||||
"git_head": "51ecd36192982fec4bfd1f50fbfba3555c571c4c",
|
||||
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
|
||||
"map_mtime": 1793026332,
|
||||
"map_mtime": 1793067428,
|
||||
"pct": {
|
||||
"ER_IROM1": 24.54,
|
||||
"RW_CCM": 87.89,
|
||||
@@ -19,7 +19,7 @@
|
||||
"regions": {
|
||||
"ER_IROM1": {
|
||||
"max": 1048576,
|
||||
"size": 257272
|
||||
"size": 257280
|
||||
},
|
||||
"RW_CCM": {
|
||||
"max": 65280,
|
||||
@@ -37,7 +37,7 @@
|
||||
"schema": 2,
|
||||
"scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring",
|
||||
"sizes": {
|
||||
"code": 231712,
|
||||
"code": 231720,
|
||||
"ro_data": 25560,
|
||||
"rw_data": 1520,
|
||||
"zi_data": 136216
|
||||
|
||||
@@ -9,7 +9,15 @@ set(LVGL_DIR "${PROJECT_ROOT}/Lib/lvgl")
|
||||
set(UI_DIR "${PROJECT_ROOT}/Drivers/BSP/UI")
|
||||
set(NET_DIR "${PROJECT_ROOT}/Drivers/BSP/NET")
|
||||
set(NETCFG_DIR "${PROJECT_ROOT}/Drivers/BSP/NETCFG")
|
||||
set(SDL2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/SDL2-2.32.10")
|
||||
# SDL2 是**下载来的第三方 SDK**,simulator/SDL2/ 被 .gitignore 排除、**不入库**(§3.1 部署文档)。
|
||||
# 于是 CI/runner 的工作区里天然没有它:2026-10-27 远程 CI(run 45)第一次跑到 ui-golden 就
|
||||
# 报 C1083 找不到 SDL2/SDL.h——本地有 SDK 所以本地一直是绿的,照不出来。允许用机器级
|
||||
# DTU_SDL2_DIR 指向任意一份 SDK;**不设该变量时行为与今天完全一致**。
|
||||
if(DEFINED ENV{DTU_SDL2_DIR} AND NOT "$ENV{DTU_SDL2_DIR}" STREQUAL "")
|
||||
set(SDL2_DIR "$ENV{DTU_SDL2_DIR}")
|
||||
else()
|
||||
set(SDL2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/SDL2-2.32.10")
|
||||
endif()
|
||||
|
||||
#=============================================================================
|
||||
# 编译标准
|
||||
@@ -33,7 +41,21 @@ endif()
|
||||
# LVGL SDL 驱动内部 #include "SDL2/SDL.h",include 目录下已有 SDL2/ 子目录
|
||||
set(SDL2_INCLUDE_DIR "${SDL2_DIR}/include" CACHE PATH "SDL2 include directory for LVGL")
|
||||
|
||||
#=============================================================================
|
||||
# 缺 SDK 就在**配置期**一行说清。过去这里只打印拼出来的路径、不校验存在,于是配置阶段
|
||||
# 照样输出 "SDL2: <路径>",直到编译 LVGL 的 SDL 驱动才炸出几百行 C1083,看着像代码错了
|
||||
# 缺 SDK 就在**配置期**一行说清。过去这里只打印拼出来的路径、不校验存在,于是配置阶段
|
||||
# 照样输出 "SDL2: <路径>",直到编译 LVGL 的 SDL 驱动才炸出几百行 C1083,看着像代码错了
|
||||
# (CI run 45 的第一反应就是"像素回归红了吗")。查的是 SDL2_INCLUDE_DIR 而非 SDL2_DIR:
|
||||
# 它是 CACHE 变量,复用旧 build 目录时缓存值可能比 SDL2_DIR 更旧,两处都得被这一条盖住。
|
||||
if(NOT EXISTS "${SDL2_INCLUDE_DIR}/SDL2/SDL.h")
|
||||
message(FATAL_ERROR
|
||||
"SDL2 SDK 不可用:找不到 ${SDL2_INCLUDE_DIR}/SDL2/SDL.h"
|
||||
" - 本机首次部署:照 docs/UI设计/LVGL模拟器部署与使用说明.md §3.1 下载 SDK 到 simulator/SDL2/"
|
||||
" - CI / runner:工作区里没有、也不该有这份 SDK;请设机器级 DTU_SDL2_DIR 指向一份 SDK"
|
||||
" (scripts/ci_runner_bootstrap.ps1 -SetEnv 会自动发现并写入该变量)"
|
||||
" - 若刚换过 SDK 位置:build 目录里的旧缓存可能仍指向旧路径,删掉 simulator/build 再配置"
|
||||
)
|
||||
endif()
|
||||
# LVGL 子目录(引用项目自带的 LVGL v9.3.0 源码)
|
||||
#=============================================================================
|
||||
# LVGL 内部会自动检测 lv_conf.h(通过 LV_CONF_INCLUDE_SIMPLE 宏)
|
||||
|
||||
@@ -117,6 +117,16 @@ if (Test-Path $cache) {
|
||||
}
|
||||
}
|
||||
|
||||
# 半截缓存检查:上一次配置如果在 CMakeLists 里 FATAL_ERROR 中断(例如缺 SDL2 SDK),
|
||||
# CMakeCache.txt 已经写下、工程文件却根本没生成,于是下面「有缓存就不配置」会跳过配置,
|
||||
# 失败点跑到离根因很远的 MSBuild(MSB1009: 项目文件不存在)——2026-10-27 在 CI 等价工作区
|
||||
# 实测:设好 DTU_SDL2_DIR 之后仍然红,红因就是这个残留缓存。与上面的生成器不符同理,
|
||||
# 删掉 build/ 重来,让报错回到配置期那一行。
|
||||
if ((Test-Path $cache) -and -not (Test-Path (Join-Path $buildDir 'ALL_BUILD.vcxproj'))) {
|
||||
Write-Host '[build_sim] build/ 有缓存但没有工程文件(上次配置中断)-> 删除重建' -ForegroundColor Yellow
|
||||
if (-not (Remove-BuildDir $buildDir)) { exit 3 }
|
||||
}
|
||||
|
||||
# --- 4. 配置 + 构建 ---------------------------------------------------------
|
||||
if (-not (Test-Path (Join-Path $buildDir 'CMakeCache.txt'))) {
|
||||
Write-Host '[build_sim] CMake 配置 ...'
|
||||
|
||||
Reference in New Issue
Block a user