Generic component scaffolding tool for firmware projects. Generates files from Jinja2 templates and supports fully interactive or pre-filled CLI usage. Primarily focused on Zephyr RTOS but not exclusive to it.
pip install -e .This enables both python -m scaffold and the scaffold CLI entry point.
# Fully interactive — prompts for everything
python -m scaffold
# Component type selected, rest interactive
python -m scaffold driver
# Pre-fill fields, prompt for the rest
python -m scaffold driver --name my_drv --path drivers/
# Pre-fill everything, accept all defaults silently
python -m scaffold driver --name my_drv --path drivers/ --skip-defaults
# Load external component definitions from a directory
python -m scaffold --components-dir my-components/widget| Option | Description |
|---|---|
[component] |
Component type to scaffold. Omit to select interactively. |
--name, -n |
Component name in snake_case. |
--path, -p |
Output path relative to the current directory. |
--skip-defaults, -S |
Accept all defaults without prompting. |
--components-dir, -C |
Directory to load extra component modules from. |
--hide-defaults |
Suppress built-in components; show only external ones. |
Scaffold loads configuration from up to two files before applying CLI flags. This lets you persist settings like components-dir per-user or per-project without passing them on every invocation.
| Priority | Source |
|---|---|
| 1 | $HOME/.config/scaffold/config.yml — user-level |
| 2 | <cwd>/scaffold.yml — project-level (git-trackable) |
| 3 | CLI flags |
Keys from lower-priority sources are preserved when a higher-priority source does not set them.
# scaffold.yml
components-dir: path/to/components
hide_defaults: true # suppress built-in components; show only external onescomponents-dir is always resolved relative to the file that declares it: scaffold.yml paths resolve against <cwd>, user-config paths resolve against ~/.config/scaffold/, and --components-dir on the CLI resolves against the current working directory.
Valid keys are defined in scaffold/config_schema.json. Any config file that contains an unknown key, a wrong value type, or malformed YAML is rejected with a clear error message — scaffold will not start.
# scaffold.yml (committed to your repo)
components-dir: tools/scaffold-componentsAll team members get the same components-dir automatically after cloning, with no manual setup required.
| Component | Default path | Generated files |
|---|---|---|
driver |
drivers/ |
CMakeLists.txt, Kconfig, README.md, {name}.h, {name}.c |
service |
services/ |
CMakeLists.txt, Kconfig, README.md, include/service/{name}_access.h, src/{name}.c, src/{name}_internal.h |
lib |
lib/ |
CMakeLists.txt, Kconfig, README.md, {name}.h, {name}.c |
sample |
samples/ |
CMakeLists.txt, README.md, sample.yaml, prj.conf, src/main.c |
test |
tests/ |
CMakeLists.txt, README.md, testcase.yaml, prj.conf, src/main.c |
Component definitions can live outside this repository. Point --components-dir at any directory containing .py files that define a COMPONENT constant. Each component declares its own template directory, making external component sets fully self-contained.
my-components/
├── widget/
│ ├── __init__.py
│ ├── widget.py # COMPONENT definition
│ └── templates/ # Jinja2 templates for widget
│ └── widget.c.j2
└── sensor/
├── __init__.py
├── sensor.py
└── templates/
└── sensor.c.j2
A minimal external component:
from pathlib import Path
from scaffold.descriptors import Component, Field, TemplateFile
from scaffold.validators import check_path_conflict, validate_snake_case
_HERE = Path(__file__).parent
COMPONENT = Component(
name="my_type",
default_path="src/",
fields=[
Field(name="name", prompt="Name (snake_case)", validator=validate_snake_case),
Field(name="path", prompt="Output path", default="src/"),
],
validators=[check_path_conflict],
templates=[
TemplateFile("main.c.j2", "{name}.c"),
],
templates_dir=_HERE / "templates",
)Load the component set at runtime:
scaffold --components-dir my-components/widget widget --name my_widgetExternal components with the same name as a built-in will override it. Use --hide-defaults (or hide_defaults: true in scaffold.yml) to suppress built-ins entirely and show only external components.
- Create a subdirectory
example_components/<type>/. - Add
__init__.py(empty) and<type>.pywith aCOMPONENTconstant. Settemplates_dir=Path(__file__).parent / "templates". - Add Jinja2 templates under
example_components/<type>/templates/. - Auto-discovery picks it up — nothing else needs to change.
See example_components/driver/driver.py as a reference.
Add scaffold as a module in your west manifest:
# west.yml
manifest:
projects:
- name: scaffold
url: https://github.com/edgebr/scaffold
revision: main
path: tools/scaffoldOnce added, run west update and use west scaffold with the same arguments as the CLI:
west scaffold driver --name my_drv --path drivers/ --skip-defaults
west scaffold --components-dir my-components/ --hide-defaults widget --name my_widget