fix(keil-build): 定性并挡住 %SystemDrive% 字面量垃圾目录(并发会话在飞工作)
@build 与 fontTools 类脚本在残缺环境里起子进程时(run_code 的 Node 进程 process.env 为空),Windows known-folder 的**可展开串**展开不了 → 变成相对路径,于是仓库根长出 %SystemDrive%/ProgramData/Microsoft/ /Windows/Caches/(约 1 MB 缓存的字面量复刻),它不是任何人在飞的改动。 - SKILL.md:新增「构建副产物」一节——症状 / 根因(三条现场证据)/ 清理(只用 bash 引号形态,明令禁止 rd /s /q %SystemDrive%,cmd 会 把它展开成 C: 等于对系统盘下令递归强删)/ 两层防复发(.gitignore 已入库 + 脚本守卫),并在坑表加一条速查 - keil_build.py +115 行:拉起 UV4 前 harden_windows_env() 回填缺失的 SystemDrive/SystemRoot/ProgramData/USERPROFILE/TMP 等并就地展开残留 的 %VAR% 字面量,命中打 WARN: ENV_SPARSE;构建后 check_percent_literal_junk() 见到字面量目录只打 WARN: JUNK_DIR 并 给出安全删除命令(守卫是兜底不是许可:@build 仍须走继承完整环境 的 shell,删目录也不由构建脚本代劳) py_compile 通过、--help 正常。本笔是并行会话在飞的改动,按用户 「把所有内容都 commit」的指示代为落盘;要退回:git reset --soft HEAD~1。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: keil-build
|
||||
description: Keil MDK-ARM 命令行编译闭环(自带独立脚本 keil_build.py,纯标准库,拷到任何 Keil 项目即用):UV4 -b 构建 → "0 错误 0 警告"判定 → build_log 定位报错。封装入口约定(PowerShell 与 bash/ops_bash 两种调用形态及各自的 exit 2 归属)、退出码判读、UV4 改写工程文件的入库陷阱。
|
||||
whenToUse: 需要编译 Keil MDK 工程、跑 `@build` 验证代码改动是否 0 错误 0 警告、解读 UV4 构建日志与退出码 2/4,或在 bash / `ops_bash` 里执行 `@build` 报语法错(syntax error near unexpected token)时。
|
||||
description: Keil MDK-ARM 命令行编译闭环(自带独立脚本 keil_build.py,纯标准库,拷到任何 Keil 项目即用):UV4 -b 构建 → "0 错误 0 警告"判定 → build_log 定位报错。封装入口约定(PowerShell 与 bash/ops_bash 两种调用形态及各自的 exit 2 归属)、退出码判读、UV4 改写工程文件的入库陷阱、构建副产物「%SystemDrive% 字面量垃圾目录」的定性与清理/入库排除。
|
||||
whenToUse: 需要编译 Keil MDK 工程、跑 `@build` 验证代码改动是否 0 错误 0 警告、解读 UV4 构建日志与退出码 2/4,或在 bash / `ops_bash` 里执行 `@build` 报语法错(syntax error near unexpected token),或构建后 `git status` 冒出 `%SystemDrive%` 目录时。
|
||||
---
|
||||
|
||||
# keil-build — Keil 命令行编译(独立可运行 skill)
|
||||
@@ -81,6 +81,49 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被
|
||||
- **只 build 锁自动;`bench` 锁仍须 Agent 手工持**(它是**会话作用域**,跨
|
||||
烧录→`@test`→探针多条命令,逐命令自动释放会让别的任务插进来扰动设备)。
|
||||
|
||||
## 构建副产物:`%SystemDrive%` 字面量垃圾目录(2026-10-26 两次复发 → 2026-10-28 定性并加守卫)
|
||||
|
||||
**症状**:`@build`(或 `scripts/check_font_charset.py` 这类 fontTools 字体工具)跑完,仓库根
|
||||
冒出未跟踪目录 `%SystemDrive%/ProgramData/Microsoft/Windows/Caches/`,里面是几个 `.db`
|
||||
(`cversions.2.db` + `{6AF0698E-…}` 图标缓存 + `{DDF571F2-…}` 字体缓存,合计约 1 MB)——
|
||||
就是本机 `C:\ProgramData\Microsoft\Windows\Caches` 那份缓存被**按字面量原地复刻**了一份。
|
||||
|
||||
**根因(2026-10-28 实测)**:Windows 的 known-folder 路径在注册表里是**可展开串**;展开不了,
|
||||
它就变成一个**相对路径**,于是按「当前目录」(= 仓库根)建出来。三条现场证据:
|
||||
|
||||
```bash
|
||||
reg.exe query HKCU\Environment # 本机 TMP/TEMP = REG_EXPAND_SZ %USERPROFILE%\AppData\Local\Temp
|
||||
env | wc -l # ops_bash 里 = 74(完整环境)
|
||||
node -e "console.log(Object.keys(process.env).length)" # run_code 里 = 0(空环境!)
|
||||
```
|
||||
|
||||
`run_code` 的 Node 进程 `process.env` 是**空的**,从它里面 `spawn` 出去的 UV4 / 字体脚本就拿不到
|
||||
`SystemDrive`/`ProgramData`/`ALLUSERSPROFILE`;实测 `os.path.expandvars("%SystemDrive%\ProgramData")`
|
||||
原样返回、且 `os.path.isabs()` = **False** → 目录就长在工作区里(同族还会冒出
|
||||
`%USERPROFILE%/AppData/Local/Temp`)。**正解不是「让用户去设环境变量」**,是别在残缺环境里跑构建(见第 2 条)。
|
||||
|
||||
**清理(有安全阀)**:
|
||||
|
||||
- ⚠ **绝不用 cmd / PowerShell 直接删**:`rd /s /q %SystemDrive%` 会被 cmd **展开成 `C:`**——
|
||||
等于对整块系统盘下令递归强删且不带确认。
|
||||
- ✅ 只用 bash 引号形态(`%` 在 bash 里没有特殊含义):先 `find '%SystemDrive%' -type l` 确认不是
|
||||
reparse point / 符号链接,再 `rm -rf -- '%SystemDrive%'`;或 Node
|
||||
`fs.rmSync("%SystemDrive%", { recursive: true, force: true })`。
|
||||
- 它是**工具副产物**,不是谁在飞的改动——直接删,别 stash、更别提交(提交它 = 把本机缓存塞进历史)。
|
||||
|
||||
**让它不再污染工作区(两层,均已落地、可核验)**:
|
||||
|
||||
1. **进不了版本库**:`.gitignore:167` 的 `%SystemDrive%/`(2026-10-26 入库,注释里就写着复发证据)。
|
||||
核验三步(2026-10-28 复跑全绿):造一份同名目录 + `.db` → `git status --porcelain` 零命中 →
|
||||
`git check-ignore -v <路径>` 落在 `.gitignore:167:%SystemDrive%/` → `git add -A --dry-run` 零暂存。
|
||||
2. **根本别再长出来**:本 skill 脚本 2026-10-28 起在拉起 UV4 前跑 `harden_windows_env()`——回填
|
||||
`SystemDrive`/`SystemRoot`/`windir`/`ComSpec`/`ProgramData`/`ALLUSERSPROFILE`/`ProgramFiles` 与
|
||||
`USERPROFILE`/`APPDATA`/`LOCALAPPDATA`/`TMP`/`TEMP`,就地展开环境里未展开的 `%VAR%` 字面量,
|
||||
并把 `TMP`/`TEMP` 指到真实目录;命中即打 `WARN: ENV_SPARSE:`。构建后再跑
|
||||
`check_percent_literal_junk()`,看到 `%SystemDrive%`/`%USERPROFILE%` 一类字面量目录就打
|
||||
`WARN: JUNK_DIR:`(**只告警不自动删**——删目录不该由构建脚本代劳)。守卫是兜底不是许可:
|
||||
**`@build` 仍要走 `ops_bash` / PowerShell(继承完整环境)**,非在 Node 里 spawn 就把 `env` 传全。
|
||||
|
||||
## 本仓库
|
||||
|
||||
**`@build` = 本 skill**(2026-10-10 起为仓库文档口径):在仓库根目录跑
|
||||
@@ -103,6 +146,10 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被
|
||||
判断:`git diff` 里只见 XML 格式/`PackID` 差异而**无源文件挂载增删** →
|
||||
`git checkout -- <uvprojx>` 丢弃;只有真的增删了源文件(grep 文件名计数变化)才提交。
|
||||
同目录的 `*.uvoptx` / `*.uvguix.*` 是 IDE 个人设置,已在 `.gitignore` 里,不会露出来。
|
||||
- **构建后仓库根冒出 `%SystemDrive%/`** = UV4 / 字体工具在残缺环境(缺 `SystemDrive`、`ProgramData`)
|
||||
里按**未展开的 known-folder 字面量**建出来的**相对路径**目录,不是任何人在飞的改动:
|
||||
`rm -rf -- '%SystemDrive%'`(bash 引号形态)删掉即可,**严禁 `rd /s /q %SystemDrive%`——cmd 会把它
|
||||
展开成 `C:`**;`.gitignore` 已兜住 + 脚本已加 `ENV_SPARSE`/`JUNK_DIR` 守卫 → 见「构建副产物」一节。
|
||||
- 报错定位读日志别只看终端回显:本仓库 = `MDK-ARM\build_log.txt`。
|
||||
- **exit 2 先分三种再动手**(2026-10-10 / 2026-10-26 实测):① **bash 语法错**——stderr 有
|
||||
`syntax error near unexpected token`,是把 PowerShell 形态丢进了 `ops_bash`,换写法即可;
|
||||
|
||||
@@ -64,6 +64,117 @@ class BuildEnvError(Exception):
|
||||
"""环境/前置错误(UV4 或工程找不到、档案解析失败)→ 退出码 2。"""
|
||||
|
||||
|
||||
def _expand_deep(v):
|
||||
"""展开未展开的 REG_EXPAND_SZ 字面量(百分号形态的变量引用),最多四轮,防自指死循环。"""
|
||||
for _ in range(4):
|
||||
nv = os.path.expandvars(v)
|
||||
if nv == v:
|
||||
break
|
||||
v = nv
|
||||
return v
|
||||
|
||||
|
||||
def harden_windows_env():
|
||||
"""拉起 UV4 前补齐/展开子进程环境——堵住仓库根的「%SystemDrive%/」字面量垃圾目录。
|
||||
|
||||
Windows 的 known-folder 路径在注册表里是**可展开串**:ProgramData 的真实取值来自
|
||||
「%SystemDrive%/ProgramData/…」,本机 HKCU Environment 里的 TMP/TEMP 实测就是未展开的
|
||||
「%USERPROFILE%/AppData/Local/Temp」。父进程环境残缺时(典型:Agent 在 Node
|
||||
child_process / run_code 里 spawn,子进程继承到一个**空环境**,SystemDrive、ProgramData、
|
||||
USERPROFILE 一个都没有)这些串展不开、留在原地就成了**相对路径**,于是 UV4 / fontTools 类
|
||||
字体工具按它在**当前目录**(= 仓库根)建出
|
||||
「%SystemDrive%/ProgramData/Microsoft/Windows/Caches/*.db」(图标+字体缓存库,约 1 MB),
|
||||
污染 git status(2026-10-26 同一会话两次复发;仓库 .gitignore 已入库兜住它,但正解是
|
||||
**根本别让它长出来**)。故此处回填 + 展开,并打可定位的 WARN:ENV_SPARSE。
|
||||
"""
|
||||
if os.name != "nt":
|
||||
return
|
||||
filled, expanded = [], []
|
||||
|
||||
for key, val in list(os.environ.items()):
|
||||
if isinstance(val, str) and "%" in val:
|
||||
new = _expand_deep(val)
|
||||
if new != val and "%" not in new:
|
||||
os.environ[key] = new
|
||||
expanded.append(key)
|
||||
|
||||
root = os.environ.get("SystemRoot") or os.environ.get("windir") or ""
|
||||
root = _expand_deep(root)
|
||||
if "%" in root or (root and not os.path.isdir(root)):
|
||||
root = os.path.join(os.path.abspath(os.sep), "Windows") # 兜底:当前盘根下的 Windows 目录
|
||||
drive = os.path.splitdrive(root)[0] or os.path.abspath(os.sep)[:2]
|
||||
drive = drive.rstrip(os.sep) or "C:"
|
||||
sysroot = drive + os.sep
|
||||
user = _expand_deep(os.environ.get("USERPROFILE") or "")
|
||||
if "%" in user:
|
||||
user = ""
|
||||
defaults = {
|
||||
"SystemDrive": drive,
|
||||
"SystemRoot": root,
|
||||
"windir": root,
|
||||
"ComSpec": os.path.join(root, "System32", "cmd.exe"),
|
||||
"ProgramData": os.path.join(sysroot, "ProgramData"),
|
||||
"ALLUSERSPROFILE": os.path.join(sysroot, "ProgramData"),
|
||||
"ProgramFiles": os.path.join(sysroot, "Program Files"),
|
||||
}
|
||||
if not user:
|
||||
guess = _expand_deep(os.path.expanduser("~"))
|
||||
if not guess.startswith("~") and "%" not in guess:
|
||||
user = guess
|
||||
if user:
|
||||
defaults["USERPROFILE"] = user
|
||||
defaults["APPDATA"] = os.path.join(user, "AppData", "Roaming")
|
||||
defaults["LOCALAPPDATA"] = os.path.join(user, "AppData", "Local")
|
||||
defaults["TMP"] = os.path.join(user, "AppData", "Local", "Temp")
|
||||
defaults["TEMP"] = defaults["TMP"]
|
||||
for key, val in defaults.items():
|
||||
if not os.environ.get(key):
|
||||
os.environ[key] = val
|
||||
filled.append(key)
|
||||
|
||||
# TMP/TEMP 必须落在真实目录:留未展开值 = 工具按字面量在当前目录建「%USERPROFILE%/…」垃圾树
|
||||
td = os.environ.get("TEMP") or os.environ.get("TMP") or ""
|
||||
if "%" in td or (td and not os.path.isdir(td)):
|
||||
cand = []
|
||||
lad = os.environ.get("LOCALAPPDATA") or ""
|
||||
if lad and "%" not in lad:
|
||||
cand.append(os.path.join(lad, "Temp"))
|
||||
cand.append(os.path.join(sysroot, "Temp"))
|
||||
picked = next((c for c in cand if os.path.isdir(c)), None)
|
||||
if not picked:
|
||||
for c in cand:
|
||||
try:
|
||||
os.makedirs(c, exist_ok=True)
|
||||
picked = c
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if picked:
|
||||
os.environ["TEMP"] = picked
|
||||
os.environ["TMP"] = picked
|
||||
filled.append("TEMP/TMP=" + picked)
|
||||
|
||||
if filled or expanded:
|
||||
print("WARN: ENV_SPARSE: 子进程环境残缺/含未展开字面量 —— 已回填[{}] 已展开[{}]".format(
|
||||
", ".join(sorted(filled)) or "-", ", ".join(sorted(expanded)) or "-"))
|
||||
print(" 正解:用继承**完整环境**的 shell 跑 @build(ops_bash / PowerShell),"
|
||||
"别从 Node spawn(空 process.env)直接起 UV4 或字体门禁脚本")
|
||||
|
||||
|
||||
def check_percent_literal_junk():
|
||||
"""构建后自检:环境残缺时 Windows 会在**当前目录**按字面量建 %SystemDrive%/ 之类的垃圾树。
|
||||
|
||||
只告警、不自动删——删除口径见本 skill「构建副产物」一节:必须用 bash 引号形态,
|
||||
绝不能用 cmd(cmd 会把 %SystemDrive% 展开成 C:,等于对整块系统盘下 rd /s /q)。
|
||||
"""
|
||||
for name in ("%SystemDrive%", "%USERPROFILE%", "%LOCALAPPDATA%", "%APPDATA%",
|
||||
"%ProgramData%", "%TEMP%", "%TMP%"):
|
||||
if os.path.isdir(name):
|
||||
print("WARN: JUNK_DIR: 工作区出现字面量目录 {}(未展开的 known-folder 路径;"
|
||||
".gitignore 已兜住,不得入库)".format(name))
|
||||
print(" 清理(只用 bash 引号形态,禁用 cmd 以免展开成 C:): rm -rf -- '{}'".format(name))
|
||||
|
||||
|
||||
class _ArgumentParser(argparse.ArgumentParser):
|
||||
"""用法错误 → 退出码 3。
|
||||
|
||||
@@ -242,6 +353,9 @@ def run_build(args):
|
||||
print("PROJ: {} target={}".format(proj, target or "(default)"))
|
||||
print("LOG: {}".format(log))
|
||||
|
||||
# 环境残缺守卫:先补齐/展开,否则 UV4 会在仓库根按字面量建 %SystemDrive%/ 垃圾树
|
||||
harden_windows_env()
|
||||
|
||||
# UV4 单实例陷阱:IDE 开着时 -b 可能被转发(退出码丢失、GUI 弹出)——只 WARN
|
||||
try:
|
||||
tl = subprocess.run(["tasklist", "/fi", "imagename eq UV4.exe", "/nh"],
|
||||
@@ -268,6 +382,7 @@ def run_build(args):
|
||||
print("BUILD_FAIL: UV4 timed out after {}s (hung or forwarded to IDE?)".format(args.timeout))
|
||||
return 1
|
||||
print("UV4_RC: {}".format(rc))
|
||||
check_percent_literal_junk()
|
||||
|
||||
if not log.is_file():
|
||||
print("BUILD_FAIL: no build log produced - UV4 did not run or hung")
|
||||
|
||||
Reference in New Issue
Block a user