c77c5824f9
- test/run_all_suites.py 是唯一真正执行它的地方:BUILD_BAT 常量改为 KEIL_BUILD(.dsh/skills/keil-build/scripts/keil_build.py);两处构建点 改传 --timeout(脚本内超时先于 _run 超时触发,超时得 rc=1 + BUILD_FAIL 文案,而不是被 _run 杀掉) - AGENTS.md / keil-build SKILL.md / new-machine-setup SKILL.md:删除 "等价原生入口"口径,改为注明该文件已于 2026-10-10 删除 - 环境与部署 3 篇 / 项目架构总览 / README / MDK-ARM/README / download.bat 注释 / check_mem_numbers.py 提示串:移除 build.bat 引用 - 刻意未改:docs/问题记录/项目审查与优化建议报告.md 的 P2-14 / N-18 (历史审计条目;doc-links 不校验裸文件名,保留不影响门禁) - 验证:keil_build.py --timeout 300 -> BUILD_OK / rc=0; run_all_suites.py --dry-run 正确解析 skill 路径;doc-links 123 文档通过
189 lines
7.3 KiB
Python
189 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""keil_build.py - Keil MDK-ARM 命令行构建 + 判定(纯标准库,可移植 skill 版)。
|
||
|
||
用法(**项目根目录**下运行,任意 python3):
|
||
python <skill>/scripts/keil_build.py
|
||
[--project X.uvprojx] [--uv4 UV4.exe] [--target 名字]
|
||
[--log build_log.txt] [--timeout 600]
|
||
|
||
解析顺序(命令行 > 项目档案 > 环境 > 探测):
|
||
工程 --project > 档案 "uvprojx" > 自动搜 cwd 下唯一 *.uvprojx
|
||
UV4 --uv4 > 档案 "uv4" > $DSH_KEIL_UV4 > $DTU_KEIL_UV4 > PATH
|
||
(环境变量**显式设了但路径无效 = 立即报错,不静默回退**)
|
||
日志 --log > 档案 "build_log" > <工程目录>\\build_log.txt
|
||
|
||
判定口径(实测教训沉淀;原仓库 MDK-ARM/build.bat 已删除,本脚本即 @build):
|
||
1. 先删旧日志——UV4 -o 只在真跑了构建才写日志,残留旧日志会被当成本次结果
|
||
2. UV4 单实例:IDE 开着时 -b 可能被转发给 IDE(退出码丢失)→ 只 WARN 不杀进程
|
||
3. PASS = 日志存在 + 无 "error:"(大小写不敏感)+ 含 "0 Error(s), 0 Warning(s)"
|
||
(0E0W 精确匹配同时把"只有警告"的构建判 FAIL)
|
||
|
||
退出码: 0=PASS(0错0警) 1=构建失败(看打印的报错行/日志) 2=UV4 未找到/无效 3=用法错误
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
|
||
def find_config_file():
|
||
env = os.environ.get("DSH_SERIAL_CONFIG")
|
||
if env:
|
||
return Path(env)
|
||
cwd = Path.cwd()
|
||
candidates = [cwd / ".serialtest" / "config.json",
|
||
cwd / ".opencode" / "serial_config.json"]
|
||
for c in candidates:
|
||
if c.is_file():
|
||
return c
|
||
return candidates[0]
|
||
|
||
|
||
def load_config():
|
||
"""档案缺失不是错误;解析失败报错由调用方 exit 3。返回 (cfg, err)。"""
|
||
f = find_config_file()
|
||
if not f.is_file():
|
||
return {}, ""
|
||
try:
|
||
raw = json.loads(f.read_text(encoding="utf-8-sig"))
|
||
except (OSError, ValueError) as e:
|
||
return {}, "cannot parse {}: {}".format(f, e)
|
||
if not isinstance(raw, dict):
|
||
return {}, "config root is not an object: {}".format(f)
|
||
return raw, ""
|
||
|
||
|
||
def find_uv4(cli_arg, cfg):
|
||
"""UV4 解析。显式来源(--uv4/档案/环境变量)设了但无效 = 硬错误,不回退。"""
|
||
for label, val in (("--uv4", cli_arg),
|
||
("config 'uv4'", cfg.get("uv4")),
|
||
("$DSH_KEIL_UV4", os.environ.get("DSH_KEIL_UV4")),
|
||
("$DTU_KEIL_UV4", os.environ.get("DTU_KEIL_UV4"))):
|
||
if val:
|
||
if Path(val).is_file():
|
||
return Path(val), label
|
||
raise SystemExit(("BUILD_ENV_ERROR: {} is set but not a file: {} "
|
||
"(fix the value; no silent fallback)").format(label, val))
|
||
w = shutil.which("UV4.exe") or shutil.which("UV4")
|
||
if w:
|
||
return Path(w), "PATH"
|
||
raise SystemExit("BUILD_ENV_ERROR: UV4.exe not found "
|
||
"(--uv4 / config 'uv4' / $DSH_KEIL_UV4 / $DTU_KEIL_UV4 / PATH)")
|
||
|
||
|
||
def find_project(cli_arg, cfg):
|
||
if cli_arg:
|
||
p = Path(cli_arg)
|
||
if not p.is_file():
|
||
raise SystemExit("BUILD_ENV_ERROR: --project not found: {}".format(p))
|
||
return p
|
||
v = cfg.get("uvprojx")
|
||
if v:
|
||
p = Path(v)
|
||
if not p.is_file():
|
||
raise SystemExit("BUILD_ENV_ERROR: config 'uvprojx' not found: {}".format(p))
|
||
return p
|
||
hits = sorted(p for p in Path.cwd().rglob("*.uvprojx")
|
||
if not any(part.startswith(".") for part in p.parts))
|
||
if len(hits) == 1:
|
||
print("PROJ: auto-detected {}".format(hits[0]))
|
||
return hits[0]
|
||
if not hits:
|
||
raise SystemExit("BUILD_ENV_ERROR: no *.uvprojx under cwd "
|
||
"(--project or config 'uvprojx')")
|
||
raise SystemExit("BUILD_ENV_ERROR: multiple .uvprojx found, pick one "
|
||
"(--project or config 'uvprojx'):\n " +
|
||
"\n ".join(str(h) for h in hits))
|
||
|
||
|
||
def main(argv=None):
|
||
for stream in (sys.stdout, sys.stderr):
|
||
try:
|
||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||
except Exception:
|
||
pass
|
||
|
||
ap = argparse.ArgumentParser(description="Keil UV4 CLI build + verdict (pure stdlib)")
|
||
ap.add_argument("--project", default=None, help="*.uvprojx (default: config/auto-detect)")
|
||
ap.add_argument("--uv4", default=None, help="UV4.exe path")
|
||
ap.add_argument("--target", default=None, help="build target name (-t; default: project default)")
|
||
ap.add_argument("--log", default=None, help="build log path (default: <proj dir>/build_log.txt)")
|
||
ap.add_argument("--timeout", type=int, default=600, help="UV4 timeout seconds")
|
||
args = ap.parse_args(argv)
|
||
|
||
cfg, err = load_config()
|
||
if err:
|
||
print("BUILD_ENV_ERROR: {}".format(err))
|
||
return 3
|
||
|
||
proj = find_project(args.project, cfg)
|
||
uv4, src = find_uv4(args.uv4, cfg)
|
||
target = args.target or cfg.get("uv_target") or ""
|
||
log = Path(args.log or cfg.get("build_log") or (proj.parent / "build_log.txt"))
|
||
|
||
print("UV4: {} ({})".format(uv4, src))
|
||
print("PROJ: {} target={}".format(proj, target or "(default)"))
|
||
print("LOG: {}".format(log))
|
||
|
||
# UV4 单实例陷阱:IDE 开着时 -b 可能被转发(退出码丢失、GUI 弹出)——只 WARN
|
||
try:
|
||
tl = subprocess.run(["tasklist", "/fi", "imagename eq UV4.exe", "/nh"],
|
||
capture_output=True, timeout=10)
|
||
if b"UV4.exe" in (tl.stdout or b""):
|
||
print("WARN: UV4 IDE is running - this build may be forwarded to it. "
|
||
"Close the IDE for a deterministic CLI build.")
|
||
except (OSError, subprocess.TimeoutExpired):
|
||
pass
|
||
|
||
# 先删旧日志:UV4 -o 只在真跑构建时写日志,残留会被当成本次结果
|
||
try:
|
||
log.unlink()
|
||
except FileNotFoundError:
|
||
pass
|
||
|
||
cmd = [str(uv4), "-b", str(proj), "-j0", "-o", str(log)]
|
||
if target:
|
||
cmd.insert(3, "-t" + target)
|
||
try:
|
||
proc = subprocess.run(cmd, capture_output=True, timeout=args.timeout)
|
||
rc = proc.returncode
|
||
except subprocess.TimeoutExpired:
|
||
print("BUILD_FAIL: UV4 timed out after {}s (hung or forwarded to IDE?)".format(args.timeout))
|
||
return 1
|
||
print("UV4_RC: {}".format(rc))
|
||
|
||
if not log.is_file():
|
||
print("BUILD_FAIL: no build log produced - UV4 did not run or hung")
|
||
return 1
|
||
text = log.read_text(encoding="utf-8", errors="replace")
|
||
lines = text.splitlines()
|
||
|
||
errs = [l for l in lines if "error:" in l.lower()]
|
||
if errs:
|
||
print("BUILD_FAIL: {} error line(s) in log:".format(len(errs)))
|
||
for l in errs[:20]:
|
||
print(" | {}".format(l.strip()))
|
||
if len(errs) > 20:
|
||
print(" ... {} more".format(len(errs) - 20))
|
||
return 1
|
||
if "0 Error(s), 0 Warning(s)" not in text:
|
||
print("BUILD_FAIL: no '0 Error(s), 0 Warning(s)' marker in log "
|
||
"(warnings present or build incomplete). Tail:")
|
||
for l in lines[-15:]:
|
||
print(" | {}".format(l.strip()))
|
||
return 1
|
||
for l in lines:
|
||
if l.strip().startswith("Program Size:"):
|
||
print(l.strip())
|
||
print("BUILD_OK: 0 errors, 0 warnings")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|