Skip to content

Commit f00cb7d

Browse files
committed
scripts: west_commands: wip templating
blah Signed-off-by: Benjamin Cabé <[email protected]>
1 parent 8e6077e commit f00cb7d

File tree

11 files changed

+291
-0
lines changed

11 files changed

+291
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from copier_template_extensions import ContextHook
2+
3+
4+
class Categories(ContextHook):
5+
def hook(self, context):
6+
context["categories"] = {
7+
"Application Development": "application_development",
8+
"Architecture-dependent Samples": "arch",
9+
"Basic": "basic",
10+
"Bluetooth": "bluetooth",
11+
"Boards": "boards",
12+
"C++": "cpp",
13+
"Data Structures": "data_structures",
14+
"Drivers": "drivers",
15+
"Hello World": "hello_world",
16+
"Kernel and Scheduler": "kernel",
17+
"External modules": "modules",
18+
"Networking": "net",
19+
"Philosophers": "philosophers",
20+
"POSIX API": "posix",
21+
"PSA": "psa",
22+
"Sensors": "sensor",
23+
"Shields": "shields",
24+
"Subsystems": "subsys",
25+
"Synchronization": "synchronization",
26+
"Sysbuild": "sysbuild",
27+
"Test": "test",
28+
"TF-M Integration": "tfm_integration",
29+
"Userspace": "userspace",
30+
}

scripts/requirements-base.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ requests>=2.32.0
2424
semver
2525
tqdm>=4.67.1
2626
reuse
27+
copier
28+
copier-template-extensions
2729

2830
# for ram/rom reports
2931
anytree

scripts/west-commands.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,8 @@ west-commands:
9999
- name: gtags
100100
class: Gtags
101101
help: create a GNU global tags file for the current workspace
102+
- file: scripts/west_commands/template.py
103+
commands:
104+
- name: template
105+
class: Template
106+
help: use built-in templates to create new samples, drivers, tests, etc.

scripts/west_commands/template.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Copyright (c) 2025
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
import argparse
6+
import textwrap
7+
from pathlib import Path
8+
9+
from copier import Worker
10+
from west.commands import WestCommand
11+
12+
13+
class Template(WestCommand):
14+
"""Use built-in templates to create new samples, drivers, tests, etc.
15+
16+
Initial implementation supports creating a new sample using the Copier
17+
template engine. Additional template types can be added over time.
18+
"""
19+
20+
def __init__(self):
21+
super().__init__(
22+
"template",
23+
"use built-in templates to create new samples, drivers, tests, etc.",
24+
"Create Zephyr content from templates (initially: samples)",
25+
accepts_unknown_args=False,
26+
)
27+
28+
def do_add_parser(self, parser_adder):
29+
parser = parser_adder.add_parser(
30+
self.name,
31+
help=self.help,
32+
formatter_class=argparse.RawDescriptionHelpFormatter,
33+
description=self.description,
34+
epilog=textwrap.dedent("""
35+
Examples:
36+
37+
- Create a new sample and answer prompts interactively:
38+
west template --type sample
39+
"""),
40+
)
41+
42+
parser.add_argument(
43+
"-t",
44+
"--type",
45+
choices=["sample"],
46+
default="sample",
47+
help="Type of template to instantiate (default: sample)",
48+
)
49+
50+
# parser.add_argument(
51+
# "-o",
52+
# "--output",
53+
# dest="output",
54+
# metavar="DIR",
55+
# type=Path,
56+
# help=(
57+
# "Destination root directory. For samples, this should typically be the "
58+
# "'samples' directory. Defaults to <workspace>/zephyr/samples."
59+
# ),
60+
# )
61+
62+
return parser
63+
64+
def do_run(self, args, _):
65+
# Determine template kind (only 'sample' for now)
66+
template_kind = args.type
67+
68+
# Resolve workspace and zephyr repository paths
69+
workspace_topdir = Path(self.topdir)
70+
zephyr_repo_dir = workspace_topdir / "zephyr"
71+
72+
if not zephyr_repo_dir.is_dir():
73+
self.die(f"Could not locate Zephyr repository at {zephyr_repo_dir}")
74+
75+
templates_root = Path(__file__).parent / "templates"
76+
if template_kind == "sample":
77+
copier_template_dir = templates_root / "sample"
78+
default_output_dir = zephyr_repo_dir / "samples"
79+
else:
80+
self.die(f"Unsupported template type: {template_kind}")
81+
return
82+
83+
if not copier_template_dir.is_dir():
84+
self.die(f"Built-in template not found. Expected at: {copier_template_dir}")
85+
86+
output_dir = default_output_dir
87+
output_dir.mkdir(parents=True, exist_ok=True)
88+
89+
with Worker(
90+
src_path=str(copier_template_dir),
91+
dst_path=str(output_dir),
92+
unsafe=True,
93+
quiet=True,
94+
) as worker:
95+
worker.run_copy()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import subprocess
2+
import sys
3+
4+
from copier_template_extensions import ContextHook
5+
6+
# Cache for boards list to avoid repeated subprocess calls
7+
_cached_boards = None
8+
9+
10+
class ZephyrContextHook(ContextHook):
11+
def hook(self, context):
12+
context["categories"] = {
13+
"Application Development": "application_development",
14+
"Architecture-dependent Samples": "arch",
15+
"Basic": "basic",
16+
"Bluetooth": "bluetooth",
17+
"Boards": "boards",
18+
"C++": "cpp",
19+
"Data Structures": "data_structures",
20+
"Drivers": "drivers",
21+
"Hello World": "hello_world",
22+
"Kernel and Scheduler": "kernel",
23+
"External modules": "modules",
24+
"Networking": "net",
25+
"Philosophers": "philosophers",
26+
"POSIX API": "posix",
27+
"PSA": "psa",
28+
"Sensors": "sensor",
29+
"Shields": "shields",
30+
"Subsystems": "subsys",
31+
"Synchronization": "synchronization",
32+
"Sysbuild": "sysbuild",
33+
"Test": "test",
34+
"TF-M Integration": "tfm_integration",
35+
"Userspace": "userspace",
36+
}
37+
38+
global _cached_boards
39+
if _cached_boards is None:
40+
try:
41+
# a bit dirty but will do for now :)
42+
result = subprocess.run(
43+
["west", "boards", "--format={name}"],
44+
capture_output=True,
45+
text=True,
46+
check=True,
47+
)
48+
boards = [
49+
line.strip() for line in result.stdout.strip().split('\n') if line.strip()
50+
]
51+
_cached_boards = boards
52+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
53+
# Fallback to a default list if west command fails
54+
print(f"Warning: Failed to get board list from 'west boards': {e}", file=sys.stderr)
55+
_cached_boards = ["wio_terminal"]
56+
57+
context["valid_boards"] = _cached_boards
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
_jinja_extensions:
2+
- copier_templates_extensions.TemplateExtensionLoader
3+
- ../extensions/context_hook.py:ZephyrContextHook
4+
5+
_tasks:
6+
- "echo"
7+
- "echo 🎉 Code sample '{{ sample_name }}' is now available in {{ _copier_conf.dst_path }}/{{ sample_name }}"
8+
- "echo 'You can build it with the following command:'"
9+
- "echo 'west build -b qemu_x86 {{ _copier_conf.dst_path }}/{{ sample_name }}'"
10+
11+
sample_name:
12+
type: str
13+
help: "Name of the code sample? (directory name under samples/)"
14+
15+
reference_board:
16+
type: str
17+
help: |
18+
Which board should be used as reference for the code sample?
19+
It will be mentioned in the README.rst file and used as the default integration
20+
platform for the tests.
21+
choices: ["native_sim", "qemu_x86", "other"]
22+
23+
reference_board_other:
24+
type: str
25+
when: "{{ reference_board == 'other' }}"
26+
help: "Board name"
27+
validator: >-
28+
{% if reference_board_other not in valid_boards %}
29+
Invalid board name
30+
{% endif %}
31+
32+
category:
33+
type: str
34+
help: "Category of the code sample" # this is not used at the moment, just here for testing purposes
35+
choices: "{{ categories }}"
36+
37+
options:
38+
type: str
39+
help: "Selection options you want to enable for the sample"
40+
multiselect: true
41+
default: '["CONFIG_LOG","CONFIG_CONSOLE"]'
42+
choices:
43+
Logging: CONFIG_LOG
44+
Console: CONFIG_CONSOLE
45+
Shell: CONFIG_SHELL
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cmake_minimum_required(VERSION 3.20.0)
2+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
3+
project({{ sample_name }})
4+
5+
target_sources(app PRIVATE src/main.c)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.. zephyr:code-sample:: {{ sample_name }}
2+
:name: A descriptive short name for the sample
3+
:relevant-api: space-separated list of Doxygen groups of APIs this sample is a good showcase of
4+
5+
Short text description of the sample. It is recommended to word this as if you were completing
6+
the sentence "This code sample shows how to ...").
7+
8+
Overview
9+
********
10+
11+
[A longer description about the sample and what it does]
12+
13+
Requirements
14+
************
15+
16+
[List of required software and hardware components. Provide pointers to
17+
hardware components such as sensors and shields]
18+
19+
Building and Running
20+
********************
21+
22+
.. zephyr-app-commands::
23+
:zephyr-app: samples/{{ sample_name }}
24+
:host-os: unix
25+
:board: {{ reference_board if reference_board != 'other' else reference_board_other }}
26+
:goals: run
27+
:compact:
28+
29+
References
30+
**********
31+
32+
.. target-notes::
33+
34+
[ Links to external references such as datasheets or additional documentation]
35+
36+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Kconfig options for sample '{{ sample_name }}'
2+
{%- for option in options %}
3+
{{ option }}=y
4+
{%- endfor %}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# todo :)

0 commit comments

Comments
 (0)