构建生命周期
7 个工具管理一次固件构建,从“这个构建会做什么?“到“开始“再到 “立即停止”。
| 工具 | 做什么 |
|---|---|
build(别名 build.start) | 启动构建。立即返回 task_id。 |
build.status | 检查某个 task_id 的状态:pending、running、succeeded、failed、canceled。 |
build.cancel | 停止一个正在跑或排队的构建。 |
build.rust_elf | 从 Rust no_std ELF 构建 flash bundle(Tier-S 固件路径)。 |
set_target.run | 在构建机器上运行 idf.py set-target。 |
generate_build_plan | 告诉你一次构建会做什么,但不真的跑。 |
get_clean_plan | 告诉你 idf.py clean 或 fullclean 会删什么。 |
build / build.start
在构建服务器上启动一次固件构建,立即返回。构建本身在沙箱里后台跑;
你用 build.status 或读 build://log/{task_id} 跟踪进度。
典型输入:
| 字段 | 类型 | 说明 |
|---|---|---|
target | string | ESP 芯片 —— esp32、esp32s3、esp32c6 等。 |
profile | string | debug(默认)或 release。 |
idf_version | string(可选) | pin 一个特定的 IDF 版本。默认用项目的 .idf-version 或构建服务器的默认值。 |
clean | bool(可选) | true 则做一次干净构建,而不是增量构建。 |
params | object(可选) | 配方特有的覆盖项。 |
返回:
{
"task_id": "0abf...e2",
"status": "pending"
}
task_id 是你跟踪构建用的东西。把它存起来。
示例对话:
把固件编译成 esp32s3 release 版。
你的助手用 {"target": "esp32s3", "profile": "release"} 调用
build,然后看 build.status 直到结束。
仅计划模式: CLI 下 build 默认走远程构建。传 --local 只生成
构建计划而不编译。MCP 服务器下,如果 CONTROL_BASE_URL 或
MCP_AUTH_SECRET 缺失,build 返回 "status": "planning" 的计划。
用 generate_build_plan 可以在任何模式下显式获取无副作用的构建计划。
CLI: espctl build
不想走 MCP,想自己用命令行驱动构建。还是同一个构建服务器、同一个沙箱 —— 只是前端从 AI 助手换成了 CLI。
espctl build [path] [--target <chip>] [--clean] \
[--remote <url> | --local] \
[--git-url <url> [--git-ref <ref>]] \
[--idf-version <ver>] [--sbom] [--elf]
默认就是远程构建。详见 仅计划模式 vs 远程构建。
标志矩阵
| 标志 | 默认 | 说明 |
|---|---|---|
path(位置参数) | . | 项目目录。.espctl.toml 和 .idf-version 都从这个路径读。 |
--target | .espctl.toml 里的 default_target | 芯片 —— esp32、esp32s3、esp32c3、esp32c6 等。 |
--clean | false | 先清掉构建目录。只在 local 下生效,远程模式忽略。 |
--remote <url> | 从 ~/.config/espctl/credentials.json 读,否则 https://esphome.cloud | 覆盖构建服务器 URL。和 --local 互斥。 |
--local | false | 只生成构建计划,不编译。和 --remote 互斥。 |
--git-url <url> | — | 让 agent 直接 clone 这个仓库,不上传本地项目包。仅远程模式。 |
--git-ref <ref> | (默认分支) | 要 checkout 的分支、tag 或 commit。和 --git-url 一起用。 |
--idf-version <ver> | .idf-version → .espctl.toml 的 [idf_version] → 服务器默认值 | pin 一个特定 IDF 版本。如果 .idf-version 文件不存在,会写一份。 |
--sbom | false | 在 build/sbom.spdx 生成 SPDX SBOM。仅远程模式可用。 |
--elf | false | 让 agent 把不剥离符号的应用 ELF 留在 workspace,以便后面用 espctl elf 拉回来做 JTAG 调试。仅远程模式生效(C 项目流程);默认不保留 —— 多 MB 的 ELF 不是每次构建都需要,加上 --elf 才显式 opt-in。 |
模式选择顺序
CLI 按下面顺序决定模式:
--local→ 仅计划,不编译。--remote <url>→ 远程构建到这个 URL。- 否则:
espctl login保存的服务器。 - 否则:
https://esphome.cloud(内置默认值)。
常见用法
# 默认:用已保存的凭据走远程构建
espctl build . --target esp32s3
# 远程构建并生成 SPDX SBOM
espctl build . --target esp32s3 --sbom
# 一次性覆盖服务器(不持久化登录)
espctl build . --target esp32 --remote https://staging.example.com
# 直接从一个 git ref 构建(agent 自己 clone,不上传项目)
espctl build --remote https://esphome.cloud \
--git-url https://github.com/espressif/esp-idf \
--git-ref v5.3.1 --target esp32c3
# 显式 pin IDF 版本
espctl build --target esp32s3 --idf-version v5.3.1
# 远程构建 + 让 agent 保留 ELF(JTAG 调试用,后面 `espctl elf` 取回)
espctl build . --target esp32s3 --elf
# 仅计划模式(离线 / 预检)
espctl build --local --target esp32s3
# 本地完整重建
espctl build --local --target esp32s3 --clean
输出与退出码
Human 模式打印分阶段进度(clone、configure、compile、link)和最终
的 manifest 摘要。--json 模式按行输出一串 PipelineEvent JSON
对象,最后一条是 manifest。
成功:退出 0,在项目目录写出 build/flash_bundle.tar.gz。带
--sbom 还会写出 build/sbom.spdx。
编译或运行时失败:退出 1。
配置或目标无效错误:退出 2。
相关 MCP 工具
build/build.start—— 同一个构建,通过编程方式触发。generate_build_plan—— 这就是--local内部干的事。sbom.create—— 仅针对已有task_id生成 SBOM,适合事后补 SBOM。
Rust no_std bundle —— build.rust_elf / espctl build --rust-elf
ESP-IDF C 项目走上面那个 build 工具,自动产出 flash bundle。
Rust no_std 构建(例如 aegis-v3/firmware/tier-s-bench-m7m8/)
只产出 ELF —— 必须把 bootloader + partition table + app 合并成一张
镜像之后,结果才能被 flash 消费。这一对接口包装了 espflash save-image --merge,把结果按
flash.run 工具和 espctl flash
CLI 期待的 flash-bundle 格式打包。
MCP:build.rust_elf
输入:
| 字段 | 必填 | 说明 |
|---|---|---|
elf_path | 是 | Rust no_std ELF 在 agent 文件系统上的绝对路径。 |
target | 否(默认 esp32s3) | 芯片 —— esp32、esp32s2、esp32s3、esp32c2、esp32c3、esp32c6、esp32h2 之一(build.rust_elf 不支持 esp32p4 / esp32c5 / esp32c61,因为 Rust no_std Xtensa/RISC-V 工具链当前不覆盖它们;C ESP-IDF 路径下的 build 工具支持完整 10 芯片列表)。 |
out_path | 否 | 输出 bundle 路径。默认:<elf_basename>-flash-bundle.tar.gz,放在 ELF 旁边。 |
返回:
{
"bundle_path": "/.../handshake-full-flash-bundle.tar.gz",
"bundle_size_bytes": 119675,
"manifest": {
"schema_version": 1,
"build": {
"job_id": "20260505T025256Z-handshake-full",
"project": "handshake-full",
"ref": "unknown",
"idf_version": "n/a-rust-no_std",
"target": "esp32s3",
"created_at": "2026-05-05T02:52:56Z"
},
"flash": {
"segments": [
{ "offset": "0x0", "file": "files/firmware.bin", "sha256": "e1d36f..." }
],
"flash_mode": "dio",
"flash_freq": "80mhz",
"flash_size": "16mb"
}
}
}
返回的 bundle 可以直接交给 flash.run 或 espctl flash,无需再转换。
CLI:espctl build --rust-elf
espctl build --rust-elf <ELF> [--target <chip>] [--out <bundle>]
| 参数 | 默认值 | 说明 |
|---|---|---|
--rust-elf <ELF> | — | Rust ELF 路径。与 --remote、--local、--git-url、--git-ref、--clean、--sbom、--idf-version 互斥。 |
--target <chip> | esp32s3 | 芯片 —— 与 MCP 工具同集合。 |
--out <PATH> | ELF 旁边的 <elf_basename>-flash-bundle.tar.gz | 输出 bundle 路径。需要配合 --rust-elf。 |
典型流程(Tier-S 固件):
cargo +esp build --bin handshake-full \
--target xtensa-esp32s3-none-elf --release
espctl build --rust-elf .../release/handshake-full --target esp32s3
espctl flash .../release/handshake-full-flash-bundle.tar.gz \
--port /dev/ttyACM0
espctl monitor --port /dev/ttyACM0
默认值与假设
| 设置项 | 值 | 出处 |
|---|---|---|
| Flash 模式 | dio | 与 aegis-v3/bench/build-flash-bundle.sh 一致。 |
| Flash 频率 | 80mhz | 与 bench 脚本一致。 |
| Flash 容量 | 16mb | 与 bench 脚本一致。 |
| 合并偏移量 | 0x0 | espflash save-image --merge 产出单个合并镜像(bootloader + partition + app),整体烧到 0x0。 |
idf_version 字段 | n/a-rust-no_std | manifest 字段必填,但 ESP-IDF 概念不适用于 Rust no_std 构建。 |
ref 字段 | 在 ELF 父目录跑 git rev-parse --short HEAD,失败则 unknown | 尽力而为 —— 当 ELF 是在某个 git 检出里构建时有效。 |
必备工具
- PATH 上的
espflash4.x(作为子进程被调用,执行save-image --merge)。 安装方法:cargo install espflash --locked。
替代
aegis-v3/bench/build-flash-bundle.sh —— 标准实现现在搬到了 espctl
里;bash 脚本变成了一个保留兼容性的薄壳,只是为了让 bench 树原有的
默认路径继续可用。
build.status
检查一个之前启动的构建的状态。
输入:
{ "task_id": "0abf...e2" }
返回:
{
"task_id": "0abf...e2",
"status": "running",
"progress": 0.42,
"started_at": 1712340000,
"updated_at": 1712340060,
"phase": "compiling"
}
status 取值之一:pending、running、succeeded、failed、
canceled。一些助手还会显示 progress(0.0–1.0)和一个自由形式
的 phase(例如 cmake-configure、compiling、linking、
flashing)。
常见模式: 大多数助手每 1–3 秒查一次,带超时。不要硬刷服务 ——
有一个 build://log/{task_id} 资源会随新行到达推送,比反复问更高效。
build.cancel
停止一个 pending 或 running 的构建。如果构建已经结束,这是 no-op
(不会报错)。
输入:
{ "task_id": "0abf...e2" }
返回:
{ "task_id": "0abf...e2", "status": "canceled" }
取消是尽力而为的 —— 服务器先请求构建停下来,短暂等待后强制停止。 已经在跑的编译步骤可能再继续几秒才停。
generate_build_plan
告诉你一次构建会做什么,但不真的跑。适合:
- 在按“开始“之前先看看接下来会发生什么。
- 仅计划模式(没设构建服务器)。
- 为 CI 或审计捕获一份可重现的构建描述。
输入: 和 build 一样(target、profile 等)。
返回: 一个结构化的计划。具体字段取决于配方,但通常包括:
recipe_ididf_version_resolvedtargetprofilecommand_pipeline—— 有序的构建步骤列表expected_artifacts—— 构建会产出哪些文件estimated_duration_secs—— 基于历史的尽力估计
无副作用。 可以反复调。
get_clean_plan
告诉你 idf.py clean(增量清理)或 idf.py fullclean(完全清理)
会从构建目录删什么,但实际不删任何东西。
输入:
{ "scope": "clean" } // 或 "fullclean"
返回: 会被删的文件和目录列表,加上合计。
{
"scope": "clean",
"would_delete": [
"build/esp-idf/main/...",
"build/esp-idf/CMakeFiles/...",
"build/.../*.o"
],
"total_files": 1342,
"total_bytes": 187654321
}
适合在做破坏性清理之前确认,尤其是在 CI 里。