Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Build Lifecycle

Six tools manage a firmware build, from “what would this do?” to “go” to “stop now”.

ToolWhat it does
build (alias build.start)Start a build. Returns a task_id right away.
build.statusCheck on a task_id: pending, running, succeeded, failed, canceled.
build.cancelStop a running or queued build.
set_target.runRun idf.py set-target on the build machine.
generate_build_planTell you what a build would do, without running it.
get_clean_planTell you what idf.py clean or fullclean would delete.

build / build.start

Starts a firmware build on the build server and returns right away. The build itself runs in the background inside a sandbox; you follow along with build.status or by reading build://log/{task_id}.

Typical inputs:

FieldTypeNotes
targetstringESP chip — esp32, esp32s3, esp32c6, etc.
profilestringdebug (default) or release.
idf_versionstring (optional)Pin a specific IDF version. Defaults to the project’s .idf-version or the build server’s default.
cleanbool (optional)If true, do a clean build instead of incremental.
paramsobject (optional)Recipe-specific overrides.

Returns:

{
  "task_id": "0abf...e2",
  "status": "pending"
}

The task_id is what you’ll use to follow the build. Save it.

Example dialogue:

Build the firmware for esp32s3 in release mode.

Your assistant calls build with {"target": "esp32s3", "profile": "release"}, then watches build.status until it finishes.

Plan-only mode: In the CLI, build goes remote by default. Pass --local to get a build plan without compilation. In the MCP server, if CONTROL_BASE_URL or MCP_AUTH_SECRET is missing, build returns a plan with "status": "planning". Use generate_build_plan to explicitly get a plan without side effects in either mode.


CLI: espctl build

When you’d rather drive a build by hand instead of through MCP. Same build server, same sandbox — just a CLI in front instead of your AI assistant.

espctl build [path] [--target <chip>] [--clean] \
             [--remote <url> | --local] \
             [--git-url <url> [--git-ref <ref>]] \
             [--idf-version <ver>] [--sbom]

Remote build is the default. See Plan-only vs Remote Build for the long form.

Flag matrix

FlagDefaultNotes
path (positional).Project directory. .espctl.toml and .idf-version are read from this path.
--targetdefault_target from .espctl.tomlChip — esp32, esp32s3, esp32c3, esp32c6, etc.
--cleanfalseClean build directory first. Local-only; ignored in remote mode.
--remote <url>from ~/.config/espctl/credentials.json, then https://esphome.cloudOverride the build server URL. Conflicts with --local.
--localfalseGenerate a build plan without compiling. Conflicts with --remote.
--git-url <url>Have the agent clone this repo instead of receiving a project bundle. Remote mode only.
--git-ref <ref>(default branch)Branch, tag, or commit to check out. Used with --git-url.
--idf-version <ver>.idf-version[idf_version] in .espctl.toml → server defaultPin a specific IDF version. Written to .idf-version if the file does not exist.
--sbomfalseGenerate an SPDX SBOM at build/sbom.spdx. Remote only.

Mode resolution

The CLI picks a mode in this order:

  1. --local → plan-only, no compilation.
  2. --remote <url> → remote build to that URL.
  3. Otherwise: the server saved by espctl login.
  4. Otherwise: https://esphome.cloud (the built-in default).

Common invocations

# Default: remote build using saved credentials
espctl build . --target esp32s3

# Remote build with SPDX SBOM
espctl build . --target esp32s3 --sbom

# One-shot server override (no login persisted)
espctl build . --target esp32 --remote https://staging.example.com

# Build directly from a git ref (agent clones — no project upload)
espctl build --remote https://esphome.cloud \
  --git-url https://github.com/ff4415/aegis-examples \
  --git-ref v0.4.2 --target esp32c3

# Pin an IDF version explicitly
espctl build --target esp32s3 --idf-version v5.3.1

# Plan-only (offline / pre-flight)
espctl build --local --target esp32s3

# Local clean rebuild
espctl build --local --target esp32s3 --clean

Output and exit codes

Human mode prints staged progress (clone, configure, compile, link) and a manifest summary. --json emits a stream of PipelineEvent JSON objects, one per line, ending with the manifest.

On success: exit 0 and build/flash_bundle.tar.gz in the project directory. With --sbom, also writes build/sbom.spdx. On compile or runtime failure: exit 1. On config or invalid-target error: exit 2.


build.status

Checks the state of a previously-started build.

Input:

{ "task_id": "0abf...e2" }

Returns:

{
  "task_id": "0abf...e2",
  "status": "running",
  "progress": 0.42,
  "started_at": 1712340000,
  "updated_at": 1712340060,
  "phase": "compiling"
}

status is one of pending, running, succeeded, failed, or canceled. Some assistants also show progress (0.0–1.0) and a free-form phase (e.g. cmake-configure, compiling, linking, flashing).

Common pattern: Most assistants check every 1–3 seconds with a timeout. Don’t hammer the server — there’s a build://log/{task_id} resource that pushes new lines as they happen, which is more efficient than asking over and over.


build.cancel

Stops a pending or running build. Doesn’t error if the build has already finished — it’s a no-op in that case.

Input:

{ "task_id": "0abf...e2" }

Returns:

{ "task_id": "0abf...e2", "status": "canceled" }

The cancel is best-effort — the server asks the build to stop, then forces it after a short wait. Compile steps already in progress may take a few seconds to wind down.


set_target.run

Runs idf.py set-target on the build machine for a project. Creates a pending task that the build agent picks up and executes.

Unlike set_target (which updates local config only), this tool actually runs the set-target command on the remote build machine.

Input:

{ "target": "esp32c3" }
FieldRequiredNotes
targetYesChip — esp32, esp32s3, esp32c3, esp32c6, etc.

Returns:

{
  "task_id": "d1e2...f3",
  "target": "esp32c3",
  "recipe_id": "idf_set_target"
}

generate_build_plan

Tells you what a build would do, without running it. Useful for:

  • Reviewing what’s about to happen before you click “go”.
  • Plan-only mode (no build server set).
  • Capturing a reproducible build description for CI or audit.

Input: Same as build (target, profile, etc.).

Returns: A structured plan. Exact fields depend on the recipe, but typically include:

  • recipe_id
  • idf_version_resolved
  • target
  • profile
  • command_pipeline — the ordered list of build steps
  • expected_artifacts — what files the build will produce
  • estimated_duration_secs — best-effort guess from past runs

No side effects. Safe to call as many times as you want.


get_clean_plan

Tells you what idf.py clean (incremental clean) or idf.py fullclean (full wipe) would delete from the build directory, without actually deleting anything.

Input:

{ "scope": "clean" }   // or "fullclean"

Returns: A list of files and directories that would be removed, plus totals.

{
  "scope": "clean",
  "would_delete": [
    "build/esp-idf/main/...",
    "build/esp-idf/CMakeFiles/...",
    "build/.../*.o"
  ],
  "total_files": 1342,
  "total_bytes": 187654321
}

Useful before doing a destructive cleanup, especially in CI.


See also