4a4ad6bc5f
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名, 不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体 run_boot_test.py 的真实调用。 - AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改 「编译(/keil-build)」「串口回归(/serial-boot-test)」 - keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree 方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围) - 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入 等 41 行历史证据保留旧名 - gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑 - 顺手补并行方案同行漏网计数 51→52 钩子 验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52 钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
480 lines
20 KiB
Python
480 lines
20 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]
|
||
[--lock-task ID] [--lock-wait N] [--no-lock]
|
||
|
||
解析顺序(命令行 > 项目档案 > 环境 > 探测):
|
||
工程 --project > 档案 "uvprojx" > 自动搜 cwd 下唯一 *.uvprojx
|
||
UV4 --uv4 > 档案 "uv4" > $DSH_KEIL_UV4 > $DTU_KEIL_UV4 > PATH
|
||
(环境变量**显式设了但路径无效 = 立即报错,不静默回退**)
|
||
日志 --log > 档案 "build_log" > <工程目录>\\build_log.txt
|
||
|
||
判定口径(实测教训沉淀;原仓库 MDK-ARM/build.bat 已删除,本脚本即仓库编译入口):
|
||
1. 先删旧日志——UV4 -o 只在真跑了构建才写日志,残留旧日志会被当成本次结果
|
||
2. UV4 单实例:IDE 开着时 -b 可能被转发给 IDE(退出码丢失)→ 只 WARN 不杀进程
|
||
3. PASS = 日志存在 + 无 "error:"(大小写不敏感)+ 含 "0 Error(s), 0 Warning(s)"
|
||
(0E0W 精确匹配同时把"只有警告"的构建判 FAIL)
|
||
|
||
**build 锁:本脚本自持自释(2026-10-12)**
|
||
UV4 不能两个实例同时 `-b`,所以构建锁不该由调用方"记得释放"——那正是泄漏源
|
||
(Agent 手工 `lock acquire` 后忘了 `release`,锁要挂到 TTL 才被接管)。
|
||
改为:本脚本进入时 acquire、退出时**无条件** release(try/finally,构建失败/异常/
|
||
SystemExit 都会释放)。
|
||
- 锁工具 = 同级 skill `task-worktree/scripts/task_worktree.py`(**找不到则降级为
|
||
不加锁并告警**,保持本 skill 可移植)
|
||
- 持有者名 `--lock-task`:默认 `$DTU_TASK_ID` → 否则从 cwd 的 `_wt/<ID>-<slug>/`
|
||
反推 `<ID>`(与工作树命名约定一致,故"手工锁了同名任务"仍幂等)→ 否则 `build-cli`
|
||
- 被占用:默认**立即失败**(退出码 4,不静默等待);`--lock-wait N` 可等待 N 秒
|
||
- 调用方**已持有** build 锁时(如 `task-worktree done` 的门禁),设
|
||
`DTU_LOCK_HELD=build` 让本脚本跳过自持自释,避免自我死锁
|
||
- **TTL 自报**:acquire 时按 `--timeout` 声明陈旧窗口(`timeout/3600 + 15min`,
|
||
下限 0.5h)。故崩溃/强杀留下的锁,其自动接管窗口与本次构建上限同量级,
|
||
而不再是全局 6h(编译很快,兜底窗口也该短)。
|
||
|
||
退出码(与 task-worktree 同一约定;2026-10-12 修正——此前实现与文档不符):
|
||
0 = PASS(0 错 0 警)
|
||
1 = 构建失败(看打印的报错行 / 日志)
|
||
2 = **环境/前置错误**(UV4 找不到或路径无效、工程找不到、档案解析失败)
|
||
3 = 用法错误
|
||
4 = build 锁被占用(并发 -b 不可行)
|
||
|
||
⚠ 修正说明:旧实现把环境错误写成 `raise SystemExit("...")` → **实际退出码是 1**,
|
||
而用法错误由 argparse 抛 SystemExit(2) → 恰好占用了文档里"环境错误"的位置。
|
||
故 AGENTS.md / `new-machine-setup` SKILL.md 里写的"报 exit 2 = 环境问题"当时**不成立**。
|
||
现已按文档口径改正:环境错误 `BuildEnvError` → 2,argparse 用法错误 → 3。
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
LOCK_NAME = "build"
|
||
|
||
|
||
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 跑编译(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。
|
||
|
||
argparse 默认 `error()` 走 `SystemExit(2)`,与"环境错误=2"冲突,故覆盖之。
|
||
"""
|
||
|
||
def error(self, message):
|
||
self.print_usage(sys.stderr)
|
||
self.exit(3, "{}: error: {}\n".format(self.prog, message))
|
||
|
||
|
||
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 BuildEnvError(("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 BuildEnvError("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 BuildEnvError("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 BuildEnvError("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 BuildEnvError("BUILD_ENV_ERROR: no *.uvprojx under cwd "
|
||
"(--project or config 'uvprojx')")
|
||
raise BuildEnvError("BUILD_ENV_ERROR: multiple .uvprojx found, pick one "
|
||
"(--project or config 'uvprojx'):\n " +
|
||
"\n ".join(str(h) for h in hits))
|
||
|
||
|
||
# --------------------------------------------------------------- build 锁(自持自释)
|
||
|
||
|
||
def find_lock_tool():
|
||
"""定位 task-worktree 的 lock CLI(同级 skill)。找不到 → None(降级为不加锁)。
|
||
|
||
keil-build 与 task-worktree 是兄弟目录:.dsh/skills/{keil-build,task-worktree}/
|
||
故从本文件上溯 2 级即到 skills/。找不到不报错——保持本 skill 可独立搬运。
|
||
"""
|
||
try:
|
||
skills = Path(__file__).resolve().parents[2]
|
||
except IndexError:
|
||
return None
|
||
cand = skills / "task-worktree" / "scripts" / "task_worktree.py"
|
||
return cand if cand.is_file() else None
|
||
|
||
|
||
def plugin_tree_owner(cwd: Path):
|
||
"""DSH worktree 插件的树(仓库内 .dsh-worktrees/worktree/<name>)→ 持有者名 pw:<name>。
|
||
|
||
2026-10-29 与 task-worktree skill 的 `plugin` 子命令对齐:插件建的树不在 _wt/ 约定里,
|
||
以前在这里跑构建,持有者名一律落成 "build-cli"——`lock status` 看不出是谁在占 UV4,
|
||
多个插件树 / 主树并发时就只剩猜。名字里的多段斜杠压平成 -,与
|
||
task_worktree.py 的 plugin_lock_owner() 完全一致(同一个可 grep 的标识)。
|
||
"""
|
||
parts = cwd.parts
|
||
if ".dsh-worktrees" not in parts:
|
||
return None
|
||
tail = parts[parts.index(".dsh-worktrees") + 1:]
|
||
if tail and tail[0] == "worktree":
|
||
tail = tail[1:]
|
||
return "pw:" + "-".join(tail) if tail else "pw:?"
|
||
|
||
|
||
def default_lock_task():
|
||
"""持有者名:$DTU_TASK_ID > cwd 反推 `_wt/<ID>-<slug>/` 的 `<ID>` > 插件树 `pw:<name>` > "build-cli"。"""
|
||
v = os.environ.get("DTU_TASK_ID")
|
||
if v:
|
||
return v
|
||
try:
|
||
cwd = Path.cwd().resolve()
|
||
except OSError:
|
||
return "build-cli"
|
||
for p in (cwd, *cwd.parents):
|
||
if p.parent.name == "_wt":
|
||
return p.name.split("-", 1)[0] or p.name
|
||
return plugin_tree_owner(cwd) or "build-cli"
|
||
|
||
|
||
def build_lock_ttl_hours(timeout_s):
|
||
"""build 锁自报的陈旧窗口(小时)≈ UV4 超时上限 + 15min 余量,下限 0.5h。
|
||
|
||
这样「崩溃/被强杀」留下的锁,其自动接管窗口与本次构建的真实上限同量级,
|
||
而不是全局 6h —— 编译很快,锁的兜底窗口也该短。
|
||
"""
|
||
try:
|
||
t = max(0.0, float(timeout_s))
|
||
except (TypeError, ValueError):
|
||
t = 600.0
|
||
return max(0.5, t / 3600.0 + 0.25)
|
||
|
||
|
||
def lock_cli(tool, action, task, wait=0, force=False, ttl=None):
|
||
"""调 task-worktree 的 `lock <action> build --task <task>`。返回 (ok, 输出)。"""
|
||
cmd = [sys.executable, str(tool), "lock", action, LOCK_NAME, "--task", task]
|
||
if action == "acquire" and wait > 0:
|
||
cmd += ["--wait", str(wait)]
|
||
if action == "acquire" and ttl:
|
||
cmd += ["--ttl", "{:.4f}".format(ttl)]
|
||
if force:
|
||
cmd += ["--force"]
|
||
try:
|
||
r = subprocess.run(cmd, capture_output=True, text=True,
|
||
encoding="utf-8", errors="replace",
|
||
stdin=subprocess.DEVNULL)
|
||
except OSError as e:
|
||
return False, "lock CLI 调用失败:{}".format(e)
|
||
return r.returncode == 0, (r.stdout + r.stderr).strip()
|
||
|
||
|
||
def acquire_build_lock(args):
|
||
"""返回 (state, tool, task, note)。state ∈ ok / caller / skipped / busy。"""
|
||
if args.no_lock:
|
||
return "skipped", None, None, "--no-lock"
|
||
held = [x.strip() for x in (os.environ.get("DTU_LOCK_HELD") or "").split(",")
|
||
if x.strip()]
|
||
if LOCK_NAME in held:
|
||
return "caller", None, None, "调用方已持有(DTU_LOCK_HELD)"
|
||
tool = find_lock_tool()
|
||
if tool is None:
|
||
return "skipped", None, None, "未找到 task-worktree skill"
|
||
task = args.lock_task or default_lock_task()
|
||
ok, out = lock_cli(tool, "acquire", task, wait=args.lock_wait,
|
||
ttl=build_lock_ttl_hours(args.timeout))
|
||
if not ok:
|
||
return "busy", tool, task, out
|
||
return "ok", tool, task, ""
|
||
|
||
|
||
def run_build(args):
|
||
"""真正的构建主体(锁由 main 负责)。返回退出码。"""
|
||
cfg, err = load_config()
|
||
if err:
|
||
raise BuildEnvError("BUILD_ENV_ERROR: {}".format(err))
|
||
|
||
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 会在仓库根按字面量建 %SystemDrive%/ 垃圾树
|
||
harden_windows_env()
|
||
|
||
# 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))
|
||
check_percent_literal_junk()
|
||
|
||
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
|
||
|
||
|
||
def main(argv=None):
|
||
for stream in (sys.stdout, sys.stderr):
|
||
try:
|
||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||
except Exception:
|
||
pass
|
||
|
||
ap = _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")
|
||
ap.add_argument("--lock-task", default=None,
|
||
help="build 锁持有者名 (default: $DTU_TASK_ID / _wt/<ID>-* / build-cli)")
|
||
ap.add_argument("--lock-wait", type=int, default=0,
|
||
help="build 锁等待秒数 (0=不等待,占用即退出码 4)")
|
||
ap.add_argument("--no-lock", action="store_true",
|
||
help="完全不加 build 锁(危险:并发 -b 会互相干扰)")
|
||
args = ap.parse_args(argv)
|
||
|
||
state, tool, task, note = acquire_build_lock(args)
|
||
if state == "busy":
|
||
print("BUILD_LOCK_BUSY: build 锁被占用(UV4 不能并发构建)")
|
||
if note:
|
||
for ln in note.splitlines():
|
||
print("BUILD_LOCK_BUSY: {}".format(ln))
|
||
print("BUILD_LOCK_BUSY: 用 --lock-wait N 等待,或等对方 release")
|
||
return 4
|
||
if state == "ok":
|
||
print("LOCK: 已获取 build 锁(task={})".format(task))
|
||
elif state == "caller":
|
||
print("LOCK: 由调用方持有,跳过自持自释")
|
||
else:
|
||
print("WARN: 本次构建**未加 build 锁**({})——并发 -b 会互相干扰".format(note))
|
||
|
||
try:
|
||
return run_build(args)
|
||
except BuildEnvError as e:
|
||
print(str(e))
|
||
return 2
|
||
finally:
|
||
if state == "ok":
|
||
ok, out = lock_cli(tool, "release", task)
|
||
print("LOCK: {}".format(out or ("已释放 build 锁" if ok else "释放 build 锁失败")))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|