Skip to content

Build Management — Build Scripts

The tools_build_scripts_build_management module is the central orchestration layer for building, cleaning, validating, and reporting on all board-specific firmware variants in the Pibot CNC Pendant project. It provides both a command-line and an interactive terminal interface to manage multi-board, multi-variant ESP-IDF firmware builds, enforce CMake option consistency across all board build scripts, and collect post-build memory and flash size reports.

This module is part of the broader tools_build_scripts toolkit. For UI resource generation see tools_build_scripts_ui_resources. For other build utilities see tools_build_scripts_utilities.


The module is composed of four Python scripts that work together as a pipeline:

Validates before any build

Dispatches per variant

Post-build analysis

After all builds complete

Writes

Reads and merges

Writes

build_mgr.py

Orchestrator

validate_build_scripts.py

CMake Option Guard

boards/<board>/build_scripts/build_one.py

Per-Board Builder

size_report.py

Size Metrics Helper

aggregate_size_summary.py

Summary Aggregator

installer/<variant>/size_report.txt

installer/size_summary.txt

ScriptRole
build_mgr.pyTop-level orchestrator: CLI and interactive mode, variant discovery, build dispatch, summary generation
validate_build_scripts.pyPre-build guard: verifies ROOT_CMAKE_OPTIONS in every board’s variants.py is consistent with root CMakeLists.txt
size_report.pyPost-build helper: runs idf.py size, parses results (JSON with text fallback), writes per-variant reports
aggregate_size_summary.pyStandalone aggregator: re-reads all size_report.txt files from disk and writes a unified size_summary.txt

Board variants are not hardcoded in build_mgr.py. The manager discovers them dynamically by scanning the boards/ directory for each board’s build_scripts/variants.py. This keeps the orchestrator decoupled from board-specific details.

No

Yes

No

Yes

collect_variants()

List boards/ directory

For each board directory

variants.py and

build_one.py exist?

Skip board

Load variants.py module

Merge FACTORY_VARIANTS + VARIANTS dicts

(factory entries first)

Register 'board/variant' key

in targets dict

Short alias already

in aliases?

Add short alias → entry

Mark alias → None

(ambiguous)

Return (targets, aliases)

Each discovered target is stored under two keys:

  • Full key: "board_name/variant_name" — always unambiguous, usable directly on the CLI.
  • Alias key: "variant_name" — a short-form alias. Set to None (ambiguous) if the same variant name appears on multiple boards; the user must use the full board/variant form in that case.

Board variants.py files define two dicts consumed during discovery:

DictPurpose
FACTORY_VARIANTSFactory firmware variants (bootloader, partitions, OTA initial image)
VARIANTSNormal application firmware variants

See the Board Support Packages documentation for the structure of per-board variants.py, common.py, and build_one.py.


aggregate_size_summary.pysize_report.pyidf.py (ESP-IDF)build_one.py (per board)validate_build_scripts.pybuild_mgr.pyUseraggregate_size_summary.pysize_report.pyidf.py (ESP-IDF)build_one.py (per board)validate_build_scripts.pybuild_mgr.pyUserFor normal variants only: re-invoke copy_factory_artifacts()to restore files overwritten by idf.py size cmake re-runWrites FAILED marker so aggregator sees the failurealt[Build succeeded][Build failed]loop[For each selected variant]--build_all / --build_board / --build_variantrun_validation()Parse ROOT_CMAKE_OPTIONS from all variants.py filesParse OPTION() declarations from CMakeLists.txtOK (return 0) or exit(1) on mismatchcollect_variants() then resolve selected targetsSort: FACTORY_VARIANTS first, then by board/variant namesubprocess python build_one.py variant [--dev] [--jobs=N]idf.py build (CMake configure + compile + link)ELF and binary artifactsreturncode 0 = successrun_idf_size_json(build_dir, cwd)idf.py -B build_dir size --format json --output-file tmp.jsonJSON metrics (or text fallback)Dict[str, Optional[int]] metricswrite_variant_report(installer_dir, label, metrics)installer/variant/size_report.txt written_write_failed_variant_report(installer_dir, label)aggregate_size_summary.main()Walk installer/ and parse all size_report.txt fileswrite_summary(repo_root, reports)installer/size_summary.txt pathBuild summary table and process exit code

run_all() sorts selected targets so that factory variants are always built before normal variants. This ensures bootloader, partition table, and OTA initial-image artifacts exist before any normal installer attempts to bundle them.

# Sort key in run_all(): factory variants → 0, others → 1; then board name, then variant name
selected = sorted(selected, key=lambda e: (0 if "factory" in e[2] else 1, e[1], e[2]))

Before any build or check, build_mgr.py unconditionally runs validate_build_scripts.py (skipped only for --list). This prevents wasted compile time when a build script references a CMake option that does not exist in CMakeLists.txt, which would silently produce a misconfigured firmware.

Missing options

in CMake but not in list

Stale options

in list but not in CMake

Sets match

Invalid options found

All valid

validate_build_scripts.main()

collect_root_option_table()

Parse ROOT_CMAKE_OPTIONS = [...]

from every boards/*/build_scripts/variants.py

collect_cmake_options()

Parse OPTION(NAME ...) lines

from root CMakeLists.txt

ROOT_CMAKE_OPTIONS

matches CMakeLists.txt?

Report: Missing from ROOT_CMAKE_OPTIONS

Report: Stale entries

return 1 → build_mgr exits before any build

collect_build_script_paths()

All *.py in boards/*/build_scripts/

excluding common.py

extract_used_options(path)

Regex: -DOPTION= patterns

in each build script

All -D options

in ROOT_CMAKE_OPTIONS?

Report unsupported CMake options

per script file

Print: OK - consistent

return 0

Validation exclusions: PROD_BUILD is excluded from the check. It is managed directly by build_mgr.py itself (injected at dispatch time, not via board ROOT_CMAKE_OPTIONS).

CheckSource ASource BError Condition
Option completenessCMakeLists.txt OPTION(...) namesROOT_CMAKE_OPTIONS lists in all variants.pyOption in CMake but not in any board list
Option stalenessROOT_CMAKE_OPTIONS in variants.pyCMakeLists.txt OPTION(...) namesOption in board list but removed from CMake
Script correctness-D<OPTION>= used in any *.py build scriptROOT_CMAKE_OPTIONS union setScript uses an option not declared in CMake

After each successful build, build_mgr.py collects memory and flash usage metrics via size_report.py and writes a human-readable report.

Yes

No / returncode != 0

run_idf_size_json(build_dir, cwd)

Read IDF_TARGET from

CMakeCache.txt in build_dir

Set IDF_TARGET in subprocess env

Overrides any stale shell variable

Run: idf.py -B build_dir size

--format json --output-file tmp.json

JSON output

successfully written?

_extract_from_json(data)

Parse flat JSON dict

Fallback: idf.py -B build_dir size

plain text output

_parse_text_size_output(stdout)

Regex on ASCII or Unicode table

Dict: dram_used, dram_total, dram_free

iram_used, iram_total, iram_free

flash_code, flash_data

write_variant_report(installer_dir, label, metrics)

Writes installer/variant/size_report.txt

The size reporter transparently handles two ESP32 RAM architectures:

ArchitectureRepresentative ChipsJSON KeyMapped To
Separate DRAM + IRAMESP32dram_total, iram_totaldram_* and iram_* separately
Combined DIRAMESP32-S3, ESP32-C3diram_totalMapped to dram_* (the constrained heap budget)

run_idf_size_json() reads the target chip directly from CMakeCache.txt and injects it as IDF_TARGET into the subprocess environment. This prevents cross-contamination when building multiple boards (which may target different chips) in a single session without clearing shell variables between boards.

Written to installer/<variant>/size_report.txt:

Size report for pibot_pendant_v1_0/8mb_wifi_fluidnc
======================================================================
DRAM used= 80,596 total= 180,736 free= 100,140 (44.59%)
IRAM used= 65,400 total= 131,072 free= 65,672 (49.90%)
Flash Code 648,880
Flash Data 72,400
======================================================================

When a build fails, a FAILED marker is written instead so the aggregator can detect it:

Size report for pibot_pendant_v1_0/8mb_wifi_fluidnc
======================================================================
FAILED
======================================================================

aggregate_size_summary.py operates entirely from on-disk size_report.txt files. It can be run standalone after a partial or interrupted batch build to regenerate installer/size_summary.txt without rebuilding.

Yes

No

aggregate_size_summary.main()

Walk installer/ directory

for all size_report.txt files

parse_size_report(path)

Extract label + DRAM/IRAM/Flash metrics

via regex on report text

Report contains

'FAILED'?

Store (label, None)

FAILED row in table

Store (label, metrics dict)

Data row in table

size_report.write_summary(repo_root, all_reports)

Sort: successful variants first,

failed last; both alphabetical

Format padded table:

Variant | DRAM used | DRAM free | DRAM % | IRAM free | Flash code | Flash data

Write installer/size_summary.txt

Return summary path

Example aggregated output:

Firmware size summary
================================================================
Variant | DRAM used | DRAM free | DRAM % | IRAM free | Flash code | Flash data
----------------------------------------------------------------
pibot_pendant.../8mb_wifi_fluidnc | 80,596 | 100,140 | 44.6% | 65,672 | 648,880 | 72,400
esp32_3248s035c/4mb_wifi_grbl | 75,200 | 105,536 | 41.6% | 70,144 | 601,200 | 68,800
================================================================
Generated: 2 ok, 0 failed

When invoked with no CLI arguments, build_mgr.py presents a numbered interactive prompt. Choices from the previous session are restored as defaults (indicated with <-- last next to the option and a bracketed default index in the prompt).

Yes

No

Yes

No

No CLI args

interactive_select()

Load .build_mgr_prefs.json

Defaults to empty dict if missing

Numbered prompt:

Select action

Build / Clean / Check

Last choice highlighted

Numbered prompt:

Select board

All boards or specific board

Last choice highlighted

All boards

selected?

selected = all discovered targets

Numbered prompt:

Select variant for that board

All variants or specific variant

Last choice highlighted

selected = matching subset of targets

action == build?

Yes/No prompt: Dev mode?

Keep logging and snapshot enabled

Yes/No prompt: Stop on first error?

Save prefs to .build_mgr_prefs.json

Keys: action, scope, variant, dev, stop_on_error

run_all(selected, do_clean, do_check,

dev_mode, stop_on_error)

Preferences are stored at tools/build_scripts/.build_mgr_prefs.json. The file is created automatically on first interactive run and is gitignored.

Wizard (a14): the prompts are now steps of _run_wizard(), like flash_mgr.py: every step after the first has a B) Back entry, and a summary is always shown last (Enter starts, a number changes that step only, B goes back to the last step, Q quits). Changing a step re-asks the later steps it made invalid (for example the variant after a board change). For a build, a Clean first step (yes / no, saved as clean_first) cleans before building, i.e. builds without cache.


Terminal window
python tools/build_scripts/build_mgr.py [MODE FLAG] [MODIFIERS]
FlagDescription
(no flag)Launch interactive mode
--list / --list-allList all discovered boards and variants
--build_allBuild all variants across all boards
--build_board BOARDBuild all variants for one board
--build_variant VARIANTBuild one variant; use board/variant if ambiguous
--clean_allRemove build + installer dirs for all variants (no rebuild)
--clean_board BOARDClean all variants for one board
--clean_variant VARIANTClean one variant
--check_allValidate scripts + idf.py reconfigure for all variants (no compilation)
--check_board BOARDCheck all variants for one board
--check_variant VARIANTCheck one variant
FlagDescription
--devDev mode: keeps dev tools enabled (logging, snapshot). Default: PROD_BUILD=ON disables all dev tools
--stop_on_errorExit immediately after first build failure
--jobs NPass -j N to idf.py/ninja. Reduce if GCC internal-compiler-error crashes occur under memory pressure
--clean_first (a14)With --build_*: clean all selected variants (build + installer dirs) before the first build, then build them (no cache). Everything is cleaned first: cleaning a variant after the factory build would wipe installer artifacts the factory build already copied
Terminal window
# List all available boards and variants
python tools/build_scripts/build_mgr.py --list
# Build all variants in production mode across all boards
python tools/build_scripts/build_mgr.py --build_all
# Build one board, stop immediately if any variant fails
python tools/build_scripts/build_mgr.py --build_board pibot_pendant_v1_0 --stop_on_error
# Build a variant by short name (only works if unique across all boards)
python tools/build_scripts/build_mgr.py --build_variant 8mb_wifi_fluidnc
# Build a variant on a specific board using the unambiguous full form
python tools/build_scripts/build_mgr.py --build_variant pibot_pendant_v1_0/8mb_wifi_fluidnc
# Build with dev tools enabled and 4 parallel compile jobs
python tools/build_scripts/build_mgr.py --build_all --dev --jobs 4
# Clean all boards (removes build + installer dirs; does NOT rebuild)
python tools/build_scripts/build_mgr.py --clean_all
# CMake-only check: validate scripts and reconfigure without compiling
python tools/build_scripts/build_mgr.py --check_all
# Re-aggregate size reports from disk without rebuilding
python tools/build_scripts/aggregate_size_summary.py

tools/build_scripts/
├── build_mgr.py # Orchestrator (this module)
├── validate_build_scripts.py # CMake option guard (runs before every build)
├── size_report.py # Size reporting helpers (idf.py size wrapper)
├── aggregate_size_summary.py # Standalone disk-based report aggregator
└── .build_mgr_prefs.json # Interactive mode preferences (auto-created, gitignored)
boards/
└── <board_name>/
└── build_scripts/
├── variants.py # Defines FACTORY_VARIANTS, VARIANTS, ROOT_CMAKE_OPTIONS
├── common.py # Board helpers: build_variant, check_variant, installer_dir_for
└── build_one.py # Entry point invoked by build_mgr per variant
installer/ # Generated artifacts (gitignored)
├── <variant_name>/
│ ├── size_report.txt # Per-variant memory/flash report (or FAILED marker)
│ └── *.bin, *.json, ... # Flashable firmware files produced by CMake
└── size_summary.txt # Aggregated table across all variants

validate_build_scripts.py is invoked unconditionally before dispatching any build or check operation (but not before --list). This catches ROOT_CMAKE_OPTIONS drift early — before any variant is compiled — rather than silently building with wrong feature flags after a CMakeLists.txt option rename.

run_all() sorts selected targets so all factory variants complete before any normal firmware variant. Factory builds produce the bootloader binary, partition table, and OTA initial image that firmware installers bundle. Without this ordering, a normal installer could package stale or missing factory components.

Factory Artifact Restoration After Size Reporting

Section titled “Factory Artifact Restoration After Size Reporting”

Running idf.py size internally re-triggers CMake, which may re-execute firmware post-build hooks and overwrite factory artifacts copied into the normal firmware’s installer directory. For normal variants only (those whose cwd is the repo root), build_mgr.py re-invokes copy_factory_artifacts() from the board’s common.py after size collection to restore them. Factory variants themselves must never call this — their own installer directory has no cross-board factory artifact relationship.

Multi-board sessions build different chips (ESP32, ESP32-S3, ESP32-C3) in sequence. If IDF_TARGET is set in the shell from a previous board, idf.py size would reject the mismatch and fail. size_report.py reads the actual target from CMakeCache.txt and forces it in the subprocess environment, making size collection immune to shell state between boards.

aggregate_size_summary.py reads size_report.txt files from disk rather than collecting live metrics. This enables re-running the aggregation after a partial or interrupted batch build without rebuilding, and allows partial builds where only a subset of variants are refreshed while others retain their last written report.


ModuleRelationship
Board Support PackagesEach board’s build_scripts/ directory is the discovery source for collect_variants(). The variants.py, common.py, and build_one.py files define the per-board side of this build system.
tools_build_scripts_ui_resourcesSibling module for generating UI resource partitions (images, fonts, themes). Operated independently from firmware builds but writes artifacts under the same installer/ convention.
tools_build_scripts_utilitiesSibling module containing supporting utilities: OTA initial image generation, sdkconfig splitting, and PNG-to-LVGL conversion.