Kconfig & Installer Architecture¶
How a menuconfig choice becomes a real .cfg file on the printer - the
pipeline behind Getting Started with Box Turtle
and doc_tools' screenshot pipeline, from a contributor's
side rather than a user's.
The three stages¶
installer/Kconfig* ──[menuconfig / olddefconfig]──▶ .mmu_config
(the menu tree) (one flat key=value file)
│
│ [installer/build.py + Jinja]
▼
config/*.cfg templates
│
│ [rendered]
▼
Real .cfg files on the printer
(what extras/mmu/ actually loads)
- The menu tree (
installer/Kconfigand everything it pulls in) defines every questionmenuconfigcan ask and every symbol that can end up in.mmu_config. .mmu_configis kconfiglib's own flatCONFIG_X=y/CONFIG_Y="value"file - the single source of truth for one printer's choices. Nothing downstream reads the Kconfig tree directly; everything reads this file.installer/build.pyrenders the Jinja templates inconfig/against.mmu_config, producing the realmmu.cfg,mmu_hardware.cfg,mmu_parameters.cfgetc. thatextras/mmu/actually loads on the printer- the object graph described in Code Layout.
The menu tree itself¶
installer/
├── Kconfig mainmenu + top-level structure
├── Kconfig.<feature> one file per feature area - espooler, encoder,
│ sync_feedback_buffer, purging, tip_shaping, ...
├── mmu_types/Kconfig.<name> one file per supported MMU (box_turtle, ercf, ...)
│ └── starters/ Kconfig.everything / Kconfig.custom - starting points
├── boards/Kconfig.<name> one file per supported controller board
│ ├── per_gate/ per-gate board variants (ebb, slb)
│ └── custom/ fully custom board starting points
├── toolheads/Kconfig.<name> one file per supported toolhead combination
├── servos/Kconfig.<name> one file per supported servo model
├── sensors/Kconfig.* entry/exit/gear-touch/shared-exit sensor options
├── connection/Kconfig.* mmu_mcu / buffer_mcu connection options
├── macro_vars/Kconfig.* per-macro tunable variable groups (cut_tip, purge, ...)
└── lib/kconfiglib/ a vendored, patched copy of kconfiglib itself
Adding support for a new board, toolhead, servo, or MMU type is - almost
always - adding exactly one new Kconfig.<name> file in the matching
directory, not editing an existing one. That one-file-per-variant convention
is why there are over 130 Kconfig* files in installer/ for what is,
conceptually, one menu.
Happy Hare's Kconfig dialect¶
installer/Kconfig's own header comment is the authoritative list, since
these are local extensions on top of standard kconfiglib syntax - nothing
here is documented anywhere else:
generated_default "<template>" "<args>" [start] [stop]- a default value computed from other symbols by string formatting, optionally repeated ({iter}) across a range. This is how, for example, a per-gate LED pixel range default gets built fromPARAM_NUM_GATESwithout a hand-written default for every possible gate count.- A
floattype, beyond kconfiglib's usualbool/int/string/hex. force_show- display a symbol even when its normal dependency isn't met.- The
#~DEFAULT~#marker - records whether a value in.mmu_configstill matches its computed default, which is what lets menuconfig show(NOT DEFAULT)next to anything you've actually changed, and what theRkey resets against. @repeat var=i min=0 max=3@ ... @endrepeat@- textual macro expansion for near-identical repeated blocks (e.g. one config stanza per gate), nestable.- Font manipulators in prompts/help text -
[[B]]bold[[/B]],[[COLOR:n]], etc. array_editor <separator> [size]- a dedicated editor for comma/semicolon-separated list values, rather than editing the raw delimited string.- Plus menuconfig-side changes: the
Rreset-to-default key, an array editor UI, and general formatting/style work.
installer/lib/kconfiglib/kconfiglib.py is where most of this actually
lives - a patched copy of the upstream kconfiglib
project (the same library behind the Linux kernel's own menuconfig), not a
from-scratch parser.
From .mmu_config to real .cfg files¶
installer/build.py is the renderer. Given a .mmu_config, it:
- Loads it with kconfiglib and evaluates every symbol.
- Renders the Jinja templates in
config/base/,config/macros/andconfig/optional/against that symbol set. - On an upgrade (an existing install, not a fresh one), reconciles the
freshly-rendered output against whatever's actually on disk using
installer/parser.py- a layout-preserving.cfgparser (tokenizer → AST → writer) built specifically so a user's hand-edits survive a regeneration. This is the Refresh / Replace / Merge choice./install.sh -iasks about when it detects a conflict - see the note on it in Getting Started with Box Turtle. - Runs any applicable
installer/upgrades.pystep - a per-version-pairupgrade_<from>_to_<to>(self, cfg)method that migrates old option names/ values forward. Every function currently in that file is commented out (they're v3-era examples, kept as a template) - the mechanism is real and wired up, just unused since the v4 baseline; the next breaking config change is what would add a live one.
Running ./install.sh without touching your printer¶
./install.sh is the end-user entry point wrapping everything above - git
branch handling, menuconfig, rendering, Moonraker's update-manager, and
service restarts. Two flags matter enough for day-to-day development that
they're worth knowing before the first run, not discovering by accident:
-z- skip the git self-update / branch-switch step (installer/self_update.sh) that./install.shotherwise runs automatically on every invocation. Without it, a plain./install.shwhile you have local uncommitted changes can pull and re-exec itself before you expect --zis how you stop it from touching your working tree at all.-t- test mode. SetsTESTDIR=/tmp/mmu_testand redirects everything the run would normally touch to live under it instead:CONFIG_KLIPPER_HOME,CONFIG_KLIPPER_CONFIG_HOME(/tmp/mmu_test/printer_data/config),CONFIG_MOONRAKER_HOME, andKCONFIG_CONFIGitself (/tmp/mmu_test/.mmu_config) - plus service restarts are disabled. Your real.mmu_configand printer config are never opened. When it finishes, the rendered result is sitting at/tmp/mmu_test/printer_data/config/mmu/for inspection, exactly as if it had been installed for real.
Combined - ./install.sh -z -t - is the safe way to run the entire
installer, menuconfig included, against a disposable sandbox: nothing
about your own machine or repo checkout is at risk, and the result is real
enough to point other tooling at afterward, e.g.
make console ARGS='--profile /tmp/mmu_test/printer_data/config' (see
The Simulator).
Where to go next¶
- Code Layout - what the rendered
.cfgfiles actually get loaded into. - Testing -
test/hh/cfg.pyrenders the same templates this page describes, from Python profiles rather than an interactive menu, and caches the Kconfig parse per profile for speed. - Documentation tooling - drives real
menuconfigsessions to produce the screenshots on this site.