Skip to content

Commit c5c3432

Browse files
committed
scripts: west_commands: Add 'new' command
Add a new west command as helper for Zephyr projects, drivers and modules. The project creation is the default state, generating a minimal configuration for a Zephyr app. The module creation consists of a simple template to be completed by the user. Driver creation follows the `example_application` format, producing or appending the created driver into the `drivers` and `dts/bindings` directories. This west command aims to ease developers load when starting new projects, or porting existing code into drivers/modules that operate with west and Zephyr. Signed-off-by: Paulo Santos <[email protected]>
1 parent 112871a commit c5c3432

File tree

2 files changed

+272
-0
lines changed

2 files changed

+272
-0
lines changed

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/new.py
103+
commands:
104+
- name: new
105+
class: New
106+
help: create new zephyr project, module or driver template

scripts/west_commands/new.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Copyright (c) 2025 Paulo Santos ([email protected]).
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
'''west "new" command'''
6+
7+
import argparse
8+
import textwrap
9+
10+
from pathlib import Path
11+
from west.commands import WestCommand
12+
13+
14+
def text_dedent(text: str) -> str:
15+
return textwrap.dedent(text.lstrip("\n"))
16+
17+
18+
class New(WestCommand):
19+
def __init__(self):
20+
super().__init__(
21+
"new",
22+
# Keep this in sync with the string in west-commands.yml.
23+
"create new zephyr project, module or driver template",
24+
"Create a new project, module or driver template at a given path.",
25+
accepts_unknown_args=False)
26+
27+
self.fname: str | None = None
28+
self.rname: str | None = None
29+
self.kconfig_var_name: str | None = None
30+
31+
def do_add_parser(self, parser_adder) -> argparse.ArgumentParser:
32+
parser = parser_adder.add_parser(
33+
self.name,
34+
help=self.help,
35+
description=self.description)
36+
37+
parser.add_argument("name",
38+
help="name of the desired new object.")
39+
parser.add_argument("-m", "--module",
40+
help="create a module template.",
41+
action="store_true")
42+
parser.add_argument("-d", "--driver",
43+
help="create a driver template.",
44+
action="store_true")
45+
46+
parser.add_argument("-p", "--path", help="creation parent directory.", default=Path.cwd())
47+
48+
return parser
49+
50+
def do_run(self, args: argparse.Namespace, unknown: list[str]):
51+
if args.module and args.driver:
52+
self.err("Options '--driver' and '--module' given simultaneously.")
53+
return
54+
55+
path = Path(args.path).absolute()
56+
57+
self.rname: str = args.name.lower().strip()
58+
self.fname = self.rname.replace("-", "_")
59+
self.kconfig_var_name = self.fname.upper()
60+
61+
self.inf(f"Creating new '{self.rname}' "
62+
f"{'module' if args.module else 'driver' if args.driver else 'project'}"
63+
f" @ {path} ...")
64+
65+
try:
66+
if args.module:
67+
self.create_new_module(path / self.fname)
68+
elif args.driver:
69+
self.create_new_driver(path)
70+
else:
71+
self.create_new_project(path)
72+
except FileExistsError:
73+
self.err(f"Could not create '{self.rname}', it already exists.")
74+
75+
def create_new_module(self, module_dir: Path):
76+
src_dir = module_dir / "src"
77+
inc_dir = module_dir / "includes"
78+
zephyr_dir = module_dir / "zephyr"
79+
80+
module_dir.mkdir()
81+
src_dir.mkdir()
82+
inc_dir.mkdir()
83+
zephyr_dir.mkdir()
84+
85+
with open(module_dir / "CMakeLists.txt", "w") as file:
86+
file.write(text_dedent(f"""
87+
if(CONFIG_{self.kconfig_var_name})
88+
zephyr_library()
89+
zephyr_library_sources(src/{self.fname}.c)
90+
zephyr_include_directories(includes)
91+
endif ()
92+
"""))
93+
94+
with open(module_dir / "Kconfig", "w") as file:
95+
file.write(text_dedent(f"""
96+
menuconfig {self.kconfig_var_name}
97+
bool "{self.name} module"
98+
99+
if {self.kconfig_var_name}
100+
# [...]
101+
endif
102+
"""))
103+
104+
with open(src_dir / f"{self.fname}.c", "w") as file:
105+
file.write(text_dedent(f"""
106+
#include "{self.fname}.h"
107+
108+
// [...]
109+
"""))
110+
111+
with open(inc_dir / f"{self.fname}.h", "w") as file:
112+
file.write(text_dedent(f"""
113+
#ifndef {self.kconfig_var_name}_H
114+
#define {self.kconfig_var_name}_H
115+
116+
// [...]
117+
118+
#endif /* {self.kconfig_var_name}_H */
119+
"""))
120+
121+
with open(zephyr_dir / "module.yml", "w") as file:
122+
file.write(text_dedent(f"""
123+
name: {self.name}
124+
build:
125+
cmake: .
126+
kconfig: Kconfig
127+
"""))
128+
129+
def create_new_driver(self, project_dir: Path):
130+
drivers_dir = project_dir / "drivers"
131+
driver_dir = drivers_dir / self.fname
132+
bindings_dir = project_dir / "dts" / "bindings" / self.fname
133+
134+
driver_dir.mkdir(parents=True)
135+
bindings_dir.mkdir(parents=True)
136+
137+
with open(drivers_dir / "CMakeLists.txt", "a") as file:
138+
file.write(f"\nadd_subdirectory_ifdef(CONFIG_{self.kconfig_var_name} {self.fname})")
139+
140+
with open(drivers_dir / "Kconfig", "a") as file:
141+
file.write(f'\nrsource "{self.fname}/Kconfig"')
142+
143+
with open(driver_dir / "CMakeLists.txt", "w") as file:
144+
file.write(text_dedent(f"""
145+
zephyr_library()
146+
zephyr_library_sources_ifdef(CONFIG_{self.kconfig_var_name}_DRV {self.fname}_drv.c)
147+
"""))
148+
149+
with open(driver_dir / "Kconfig", "w") as file:
150+
file.write(text_dedent(f"""
151+
menuconfig {self.kconfig_var_name}
152+
bool "{self.rname.capitalize()} device drivers"
153+
help
154+
This option enables the {self.rname} drivers class.
155+
156+
if {self.kconfig_var_name}
157+
158+
config {self.kconfig_var_name}_INIT_PRIORITY
159+
int "{self.rname.capitalize()} device drivers init priority"
160+
default KERNEL_INIT_PRIORITY_DEVICE
161+
help
162+
{self.rname.capitalize()} device drivers init priority.
163+
164+
module = {self.kconfig_var_name}
165+
module-str = {self.rname}
166+
167+
rsource "Kconfig.{self.fname}_drv"
168+
169+
endif # {self.kconfig_var_name}
170+
"""))
171+
172+
with open(driver_dir / f"Kconfig.{self.fname}_drv", "w") as file:
173+
file.write(text_dedent(f"""
174+
config {self.kconfig_var_name}_DRV
175+
bool "{self.rname.capitalize()} driver"
176+
default y
177+
depends on DT_HAS_{self.kconfig_var_name}_DRV_ENABLED
178+
"""))
179+
180+
with open(driver_dir / f"{self.fname}_drv.c", "w") as file:
181+
file.write(text_dedent(f"""
182+
#define DT_DRV_COMPAT {self.fname}_drv
183+
184+
#include <zephyr/device.h>
185+
186+
#include <zephyr/devicetree.h>
187+
#include <zephyr/kernel.h>
188+
189+
struct {self.fname}_drv_data {{
190+
// [...]
191+
}};
192+
193+
struct {self.fname}_drv_config {{
194+
// [...]
195+
}};
196+
197+
static DEVICE_API({self.fname}, {self.fname}_drv_api) = {{
198+
// [...]
199+
}};
200+
201+
static int {self.fname}_drv_init(const struct device *dev)
202+
{{
203+
const struct {self.fname}_drv_config *config = dev->config;
204+
struct {self.fname}_drv_data *data = dev->data;
205+
206+
// [...]
207+
208+
return 0;
209+
}}
210+
211+
#define {self.kconfig_var_name}_DRV_DEFINE(inst) \\
212+
static struct {self.fname}_drv_data data##inst; \\
213+
static const struct {self.fname}_drv_config config##inst = {{ \\
214+
/* [...] */ \\
215+
}}; \\
216+
\\
217+
DEVICE_DT_INST_DEFINE(inst, {self.fname}_drv_init, NULL, &data##inst, \\
218+
&config##inst, POST_KERNEL, \\
219+
CONFIG_{self.kconfig_var_name}_INIT_PRIORITY, \\
220+
&{self.fname}_drv_api);
221+
222+
DT_INST_FOREACH_STATUS_OKAY({self.kconfig_var_name}_DRV_DEFINE)
223+
"""))
224+
225+
with open(bindings_dir / f"{self.fname}_drv.yml", "w") as file:
226+
file.write(text_dedent(f"""
227+
description: |
228+
A {self.rname}-drv driver...
229+
230+
compatible: "{self.rname}-drv"
231+
232+
include: base.yaml
233+
234+
properties:
235+
# [...]
236+
"""))
237+
238+
def create_new_project(self, prj_root_dir: Path):
239+
project_dir = prj_root_dir / self.fname
240+
src_dir = project_dir / "src"
241+
242+
src_dir.mkdir(parents=True)
243+
244+
with open(project_dir / "CMakeLists.txt", "w") as file:
245+
file.write(text_dedent(f"""
246+
cmake_minimum_required(VERSION 3.20.0)
247+
248+
find_package(Zephyr REQUIRED HINTS $ENV{{ZEPHYR_BASE}})
249+
project({self.fname})
250+
251+
target_sources(app PRIVATE src/main.c)
252+
"""))
253+
254+
with open(project_dir / "prj.conf", "w") as file:
255+
file.write("\n")
256+
257+
with open(src_dir / "main.c", "w") as file:
258+
file.write(text_dedent("""
259+
#include <zephyr/kernel.h>
260+
261+
int main(void)
262+
{{
263+
printk("Hello World from west new!\\n");
264+
265+
return 0;
266+
}}
267+
"""))

0 commit comments

Comments
 (0)