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:
@@ -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