Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.9
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

- repo: https://github.com/adrienverge/yamllint
rev: v1.37.0
hooks:
- id: yamllint
36 changes: 36 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Contributing

## Development setup

```sh
git clone https://github.com/edgebr/scaffold
cd scaffold
pip install -e ".[dev]"
pre-commit install
```

`pre-commit install` sets up a Git hook that runs automatically on every `git commit`. It enforces the same checks as CI so formatting and lint issues are caught locally before they ever reach a pull request.

## Checks

| Hook | What it checks |
| ---- | -------------- |
| `ruff` | Python lint (auto-fixes where possible) |
| `ruff-format` | Python formatting |
| `yamllint` | YAML style (configured in `.yamllint`) |

Run all checks manually at any time:

```sh
pre-commit run --all-files
```

## Running tests

```sh
pytest tests/
```

## Project layout

See [`CLAUDE.md`](CLAUDE.md) for a full description of the architecture, data model, and conventions for adding new component types.
94 changes: 48 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,31 @@ python -m scaffold driver --name my_drv --path drivers/
python -m scaffold driver --name my_drv --path drivers/ --skip-defaults

# Load external component definitions from a directory
python -m scaffold --components-dir external_templates/
python -m scaffold --components-dir my-components/widget
```

### Options

| 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. |
| 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. |

## Configuration

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.

### Resolution order (lowest → highest priority)

| Priority | Source |
|---|---|
| 1 | `$HOME/.config/scaffold/config.yml` — user-level |
| 2 | `<cwd>/scaffold.yml` — project-level (git-trackable) |
| 3 | CLI flags |
| 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.

Expand All @@ -58,6 +59,7 @@ Keys from lower-priority sources are preserved when a higher-priority source doe
```yaml
# scaffold.yml
components-dir: path/to/components
hide_defaults: true # suppress built-in components; show only external ones
```

`components-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.
Expand All @@ -77,29 +79,32 @@ All team members get the same `components-dir` automatically after cloning, with

## Built-in components

| 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 | 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` |

## External components

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.

```
external_templates/
├── module.py # COMPONENT definition
├── shell_cmd.py
└── templates/
├── module/ # Jinja2 templates for module
└── shell_cmd/ # Jinja2 templates for shell_cmd
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
```

See [`external_templates/`](external_templates/) for working examples.

A minimal external component:

```python
Expand All @@ -120,17 +125,26 @@ COMPONENT = Component(
templates=[
TemplateFile("main.c.j2", "{name}.c"),
],
templates_dir=_HERE / "templates" / "my_type",
templates_dir=_HERE / "templates",
)
```

Load the component set at runtime:

```sh
scaffold --components-dir my-components/widget widget --name my_widget
```

External 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.

## Adding a built-in component type

1. Create `scaffold/components/<type>.py` with a `COMPONENT` constant.
2. Add templates under `scaffold/templates/<type>/`.
3. Auto-discovery picks it up — nothing else needs to change.
1. Create a subdirectory `example_components/<type>/`.
2. Add `__init__.py` (empty) and `<type>.py` with a `COMPONENT` constant. Set `templates_dir=Path(__file__).parent / "templates"`.
3. Add Jinja2 templates under `example_components/<type>/templates/`.
4. Auto-discovery picks it up — nothing else needs to change.

See `scaffold/components/driver.py` as a reference.
See `example_components/driver/driver.py` as a reference.

## West integration

Expand All @@ -150,17 +164,5 @@ Once added, run `west update` and use `west scaffold` with the same arguments as

```sh
west scaffold driver --name my_drv --path drivers/ --skip-defaults
```

## Development

**Lint:**
```sh
ruff check scaffold/
ruff format scaffold/
```

**Test:**
```sh
pytest tests/
west scaffold --components-dir my-components/ --hide-defaults widget --name my_widget
```
Empty file added example_components/__init__.py
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from scaffold.descriptors import Component, Field, TemplateFile
from pathlib import Path

from scaffold.descriptors import Component, Field, FieldDir, TemplateFile
from scaffold.validators import check_path_conflict, validate_snake_case

COMPONENT = Component(
Expand All @@ -10,7 +12,7 @@
prompt="Driver name (snake_case)",
validator=validate_snake_case,
),
Field(name="path", prompt="Output path", default="drivers/"),
FieldDir(name="path", prompt="Output path", default="drivers/"),
],
validators=[check_path_conflict],
templates=[
Expand All @@ -20,4 +22,5 @@
TemplateFile("name.h.j2", "{name}.h"),
TemplateFile("name.c.j2", "{name}.c"),
],
templates_dir=Path(__file__).parent / "templates",
)
Empty file.
7 changes: 5 additions & 2 deletions scaffold/components/lib.py → example_components/lib/lib.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from scaffold.descriptors import Component, Field, TemplateFile
from pathlib import Path

from scaffold.descriptors import Component, Field, FieldDir, TemplateFile
from scaffold.validators import check_path_conflict, validate_snake_case

COMPONENT = Component(
Expand All @@ -10,7 +12,7 @@
prompt="Library name (snake_case)",
validator=validate_snake_case,
),
Field(name="path", prompt="Output path", default="lib/"),
FieldDir(name="path", prompt="Output path", default="lib/"),
],
validators=[check_path_conflict],
templates=[
Expand All @@ -20,4 +22,5 @@
TemplateFile("name.h.j2", "{name}.h"),
TemplateFile("name.c.j2", "{name}.c"),
],
templates_dir=Path(__file__).parent / "templates",
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% set sym = name | upper %}
{% set sym = "LIB_" ~ name | upper %}
config {{ sym }}
bool "{{ name | replace('_', ' ') | title }} library support"
help
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{% set sym = name | upper %}
# `lib/{{ name }}`
# `{{ path }}{{ name }}`

TODO: Describe the {{ name | replace('_', ' ') }} library.

## Structure

```text
lib/{{ name }}/
{{ path }}{{ name }}/
|-- CMakeLists.txt
|-- Kconfig
|-- README.md
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from scaffold.descriptors import Component, Field, TemplateFile
from pathlib import Path

from scaffold.descriptors import Component, Field, FieldDir, TemplateFile
from scaffold.validators import check_path_conflict, validate_snake_case

# Samples are standalone cmake projects — no parent patching needed.
Expand All @@ -12,7 +14,7 @@
prompt="Sample name (snake_case)",
validator=validate_snake_case,
),
Field(
FieldDir(
name="path",
prompt="Output path",
default="samples/",
Expand All @@ -28,4 +30,5 @@
TemplateFile("prj.conf.j2", "prj.conf"),
TemplateFile("main.c.j2", "src/main.c"),
],
templates_dir=Path(__file__).parent / "templates",
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# `samples/{{ name }}`
# `{{ path }}{{ name }}`

TODO: Describe the {{ name | replace('_', ' ') }} sample.

## Building

```sh
west build -b <board> samples/{{ name }}
west build -b <board> {{ path }}{{ name }}
```

## Flashing
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from scaffold.descriptors import Component, Field, TemplateFile
from pathlib import Path

from scaffold.descriptors import Component, Field, FieldDir, TemplateFile
from scaffold.validators import check_path_conflict, validate_snake_case

COMPONENT = Component(
Expand All @@ -10,7 +12,7 @@
prompt="Service name (snake_case)",
validator=validate_snake_case,
),
Field(name="path", prompt="Output path", default="services/"),
FieldDir(name="path", prompt="Output path", default="services/"),
],
validators=[check_path_conflict],
templates=[
Expand All @@ -21,4 +23,5 @@
TemplateFile("impl.c.j2", "src/{name}.c"),
TemplateFile("internal.h.j2", "src/{name}_internal.h"),
],
templates_dir=Path(__file__).parent / "templates",
)
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
cmake_minimum_required(VERSION 3.20.0)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project({{ name }})
project({{ name }}_test)

target_sources(app PRIVATE src/main.c)
target_sources(app PRIVATE src/{{ name }}_test.c)
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# `tests/{{ name }}`
# `{{ path }}{{ name }}`

TODO: Describe what this test suite covers.

## Building and Running

```sh
west build -p -b native_sim tests/{{ name }}
west build -p -b native_sim {{ path }}{{ name }}
west build -t run
```
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* @file {{ name }}_test.c
*
*/

#include <zephyr/ztest.h>

ZTEST_SUITE({{ name }}_suite, NULL, NULL, NULL, NULL, NULL);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from scaffold.descriptors import Component, Field, TemplateFile
from pathlib import Path

from scaffold.descriptors import Component, Field, FieldDir, TemplateFile
from scaffold.validators import check_path_conflict, validate_snake_case

# Tests are standalone cmake projects — no parent patching needed.
Expand All @@ -12,7 +14,7 @@
prompt="Test name (snake_case)",
validator=validate_snake_case,
),
Field(
FieldDir(
name="path",
prompt="Output path",
default="tests/",
Expand All @@ -26,6 +28,7 @@
TemplateFile("README.md.j2", "README.md"),
TemplateFile("testcase.yaml.j2", "testcase.yaml"),
TemplateFile("prj.conf.j2", "prj.conf"),
TemplateFile("main.c.j2", "src/main.c"),
TemplateFile("main.c.j2", "src/{name}_test.c"),
],
templates_dir=Path(__file__).parent / "templates",
)
Loading
Loading