Files
STM32F4-Base/.dsh/skills/keil-build/scripts/keil_build.py
T
wandering1 c77c5824f9 build: 删除 MDK-ARM/build.bat,@build 完全收敛到 keil-build skill
- 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 文档通过
2026-10-10 16:23:54 +08:00

189 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())