feat(state_machine): generate the example's HFSM from its model at build time - #776
Conversation
…ild time The example shipped ~2400 lines of generated C++ and no model. The machine could not be regenerated, edited, or checked, and the code had drifted: regenerating Complex.json with the current generator produces roughly two thousand different lines, three years of generator fixes that the example never got. The model is the source now, and the C++ is a build product like an object file. main/Complex.json is checked in; the three generated files are not. CMake runs the generator at configure time and lists Complex.json in CMAKE_CONFIGURE_DEPENDS, so editing the model regenerates on the next build rather than the next clean. Only the model-specific files are taken from the generator's output. It also emits its own state_base.hpp, deep_history_state.hpp, shallow_history_state.hpp and magic_enum.hpp, and this component already provides those -- espp's are the adapted ones the rest of the codebase expects, and putting the generator's copies on the include path would shadow them. Needs node >= 18; npx fetches the generator on demand, pinned to a commit so builds are reproducible. -DHFSM_GEN_COMMAND points the build at a local checkout instead, which is also how to build offline. The esp-idf CI image has no node, so that matrix entry installs it. Verified on esp-idf v6.1 / esp32: clean configure and build, and an edit to Complex.json reaching the compiled source on the next build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4hRq1q5VGPjNn2dnkkqBy
The build was taking the generator's whole output and copying three files out of it. The generator can now be asked for those three directly, so the copy step goes away. It matters beyond tidiness: the four files being skipped are a second copy of the runtime this component already provides, and they would sit on the include path in front of espp's. espp's are the ones the rest of the codebase is built against, and its magic_enum (0.9.5) is newer than the generator's (0.8.0). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4hRq1q5VGPjNn2dnkkqBy
The pin was a git ref for two reasons, and the release settles both: `npx webgme-hfsm@1.7.0` could not run at all, and --no-support did not exist. 1.8.0 is the first release with either. ^1.8.0 rather than an exact version: reproducible enough for a build that regenerates from a checked-in model, and picks up generator fixes without a commit here. Verified against the published package: a clean esp-idf v6.1 / esp32 build, and the npm CLI's output is byte-identical to the repo's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4hRq1q5VGPjNn2dnkkqBy
|
Updated now that v1.8.0 is published — both blockers are gone.
Verified against the published package rather than assuming: a clean
|
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Moves the Complex HFSM example to treat the JSON model as the source of truth and generate the C++ state machine during the build, removing committed generated artifacts that had drifted from the generator output.
Changes:
- Add
Complex.jsonmodel to source control and stop committing generated C++/metadata for the example. - Generate the HFSM at CMake configure time via
npx(with--no-support) and compile the generated.cppfrom the build directory. - Update documentation and CI to reflect/install the new Node-based build-time dependency.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| components/state_machine/example/main/Complex_metadata.json | Removes committed generator metadata file. |
| components/state_machine/example/main/Complex_generated_states.hpp | Removes committed generated header (now build output). |
| components/state_machine/example/main/Complex_generated_states.cpp | Removes committed generated source (now build output). |
| components/state_machine/example/main/Complex_event_data.hpp | Removes committed generated event data header (now build output). |
| components/state_machine/example/main/Complex.json | Adds the HFSM model as the checked-in source of truth. |
| components/state_machine/example/main/CMakeLists.txt | Adds configure-time generation of the HFSM C++ via npx and compiles generated output. |
| components/state_machine/example/README.md | Documents generation workflow, Node requirement, and offline/local-generator option. |
| components/state_machine/README.md | Adds general guidance on generating state machines with webgme-hfsm / Playground. |
| .github/workflows/build.yml | Installs Node/NPM for this example build in CI and runs idf.py build. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
✅Static analysis result - no issues found! ✅ |
There was a problem hiding this comment.
🟡 Changes recommended
The configured npm version is unavailable, and obsolete generated files can survive incremental regeneration.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Balanced
…tive npx, stale-artifact cleanup - build.yml: the state_machine example's node guard only checked presence, so a base image / apt source with node <18 would still fail. Check node's major version and install Node 20 (>=18) from NodeSource when it is missing or too old. - README: pin the documented generator to `npx -y -p webgme-hfsm@^1.8.0` -- `-y` for non-interactive/CI use, and the version pin (now that 1.8.0 is released) matches the example's CMake spec for reproducible generation. - example CMake: the generator only overwrites the files it emits, so a machine renamed/removed in the model left an obsolete generated .cpp in the persistent binary dir for CMake to keep compiling. Wipe + recreate the output dir before generation so a regen cannot succeed against stale artifacts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The example carried ~50 lines of CMake to find npx, run the generator and wire up the output. Anyone generating a second machine would have copied it, and the copies would have drifted. espp_generate_hfsm() now lives in the component's project_include.cmake, which ESP-IDF includes before any component's CMakeLists, so every component can call it. The example is four lines. It finds the generated file names rather than making the caller name them: they come from the machine's name in the model, so hardcoding Complex_generated_states.cpp meant knowing what your own model was called and keeping that in step by hand. The call needs an `if(NOT CMAKE_BUILD_EARLY_EXPANSION)` guard, which is not obvious and is documented at both call site and README: ESP-IDF's early requirements pass includes component CMakeLists WITHOUT project_include.cmake, so the function does not exist yet and the build fails with "Unknown CMake command". That pass does not read SRCS either, so the empty variables are what it expects. The cache variables are ESPP_-prefixed now that they are the component's interface rather than one example's local detail. Verified on esp-idf v6.1 / esp32: clean build, a model edit reaching the compiled source without a clean, the documented ESPP_HFSM_GEN_COMMAND override, and both error paths (a model that is not there, a misspelled argument) reporting which call was wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4hRq1q5VGPjNn2dnkkqBy
|
Done — if(NOT CMAKE_BUILD_EARLY_EXPANSION)
espp_generate_hfsm(
MODEL "${CMAKE_CURRENT_LIST_DIR}/Complex.json"
OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/hfsm"
SOURCES_VAR hfsm_srcs
INCLUDE_DIR_VAR hfsm_inc)
endif()
idf_component_register(SRCS "hfsm_example.cpp" ${hfsm_srcs}
INCLUDE_DIRS "." ${hfsm_inc})It also finds the generated file names rather than making the caller name them — they come from the machine's name in the model, so the old hardcoded One wart worth knowing about, since it isn't obvious and I hit it: the call needs the The cache variables are Verified on esp-idf v6.1 / esp32 — clean build, a model edit reaching the compiled source without a clean, the documented |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
| # Clear stale artifacts first. The generator only overwrites what it | ||
| # emits, so a machine renamed or removed in the model would leave an | ||
| # obsolete .cpp behind in the (persistent) binary dir for CMake to go | ||
| # on compiling. | ||
| file(REMOVE_RECURSE "${HFSM_OUTPUT_DIR}") | ||
| file(MAKE_DIRECTORY "${HFSM_OUTPUT_DIR}") |
Co-authored-by: finger563 <213467+finger563@users.noreply.github.com>
|
@copilot Fix the code for all comments in this review thread. When a review comment includes a suggested change, apply the suggestion exactly. Do not make changes beyond what is described in the linked review thread. |
Co-authored-by: finger563 <213467+finger563@users.noreply.github.com>
Addressed the review thread in commit |
|
@copilot your latest changes broke the state machine ci build. Fix them. |
There was a problem hiding this comment.
🟡 Changes recommended
The CI command has broken nested shell quoting, and the documented consumer setup omits a required component dependency.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
components/state_machine/project_include.cmake:37
^1.8.0is not a reproducible pin: npx may select any later 1.x generator, changing the generated C++ (or breaking the build) without a repository change. Since generated files are no longer checked in or locked, pin the exact generator version and update it explicitly.
# Pinned to a minor range: reproducible enough for a build that
# regenerates from a checked-in model, and picks up generator fixes
# without a commit here. 1.8.0 is the first release the CLI can be
# installed from at all, and the first with --no-support.
set(ESPP_HFSM_GEN_SPEC "webgme-hfsm@^1.8.0"
components/state_machine/README.md:26
- The caret range can resolve to later 1.x releases, so the statement that this makes generation reproducible is incorrect. Use the same exact generator version as CMake (for example,
webgme-hfsm@1.8.0) and update the adjacent explanation accordingly.
npx -y -p webgme-hfsm@^1.8.0 hfsm-gen my_machine.json -o generated
(-y skips npx's install prompt for non-interactive/CI use; the @^1.8.0
pin matches the example's CMake so generation is reproducible.)
- **Files reviewed:** 10/10 changed files
- **Comments generated:** 3
- **Review effort level:** Balanced
</details>
| idf_component_register(SRCS "main.cpp" ${hfsm_srcs} | ||
| INCLUDE_DIRS "." ${hfsm_inc}) |
Co-authored-by: finger563 <213467+finger563@users.noreply.github.com>
Fixed in |
The example shipped ~2400 lines of generated C++ and no model. The machine couldn't be regenerated, edited, or checked against anything — and it had drifted: regenerating from the model with the current generator produces roughly two thousand different lines (after clang-format), three years of generator fixes the example never received.
So the model is the source now, and the C++ is a build product like an object file.
main/Complex.jsonis checked in; the three generated files are not.CMAKE_CONFIGURE_DEPENDS, so editing the model regenerates on the next build, not the next clean.--no-support, so only the machine is produced. The shared runtime —state_base.hpp, the history states,magic_enum.hpp— comes from this component. A second copy beside the generated machine would land on the include path in front of espp's, and espp's are the ones the rest of the codebase is built against.What it needs
node >= 18, and nothing else —
npxfetches the generator on demand. Two escape hatches:The esp-idf CI image has no node (IDF's own
tools.jsondoesn't ship it), so that matrix entry installs it first. Worth being explicit: this is a new build-time dependency for this one example, which was the deliberate trade for not versioning generated code.Editing the machine
The model opens in the HFSM Playground — browser-based, no install, no server — or straight to this example's machine. Save back over
main/Complex.jsonand rebuild.Verified
Actually built, not just configured, on esp-idf v6.1 / esp32:
set-target+buildfrom an empty tree, with freshly generated code three years newer than what was committedComplex.json, rebuilt, confirmed the change reached the compiled source without a cleanBlocking on two things
--no-supportis not released yet — it's Add --no-support, for projects that vendor the runtime already finger563/webgme-hfsm#241. The pin here is that PR's commit.HFSM_GEN_SPECshould becomewebgme-hfsm@^1.8.0. It's a git ref today becausenpx webgme-hfsm@1.7.0doesn't work at all — installing that package and running the CLI fails outright, which is the packaging bug fixed in Make the WebGME server optional so the CLI can be installed finger563/webgme-hfsm#238 and not yet released.A note on the vendored runtime
I checked whether this component's
state_base.hpp/ history headers had gone stale against the generator's, since the generated code had. They have not. All 19 shared method bodies are byte-identical once comments and whitespace are normalised; the differences are espp's own improvements (declaration/definition split intosrc/state_base.cpp,explicitconstructors, doxygen). Andmagic_enumruns the other way: espp vendors 0.9.5, the generator ships 0.8.0. Nothing to update here — if anything the generator is the one behind.