ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
scaffold.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Generate policy-compliant repository project skeletons."""
5
6from __future__ import annotations
7
8import argparse
9import contextlib
10import io
11import os
12import re
13import shutil
14import subprocess
15import sys
16import tempfile
17from pathlib import Path
18
19REPO_ROOT = Path(__file__).resolve().parents[2]
20NAME_RE = re.compile(r"[a-z][a-z0-9_]*\Z")
21GENERATED_FILE_COUNT = 18
22SCAFFOLD_TYPES = ("app", "host", "lib", "example", "shared_lib")
23SCAFFOLD_RECIPES = (
24 "apps::board::new",
25 "apps::host::new",
26 "libs::new",
27 "apps::example::new",
28 "apps::shared::new",
29)
30
31
32def project_name(value: str) -> str:
33 """Return a safe C/path identifier or raise an argparse error."""
34 if NAME_RE.fullmatch(value) is None:
35 message = (
36 "name must start with a lowercase letter and contain only lowercase "
37 "letters, digits, and underscores"
38 )
39 raise argparse.ArgumentTypeError(message)
40 return value
41
42
43def _format_content(path: Path, content: str) -> str:
44 """Format generated C-family source before it reaches the filesystem."""
45 if Path(path).suffix not in {".c", ".h"}:
46 return content
47 formatter = shutil.which("clang-format")
48 if formatter is None:
49 message = "clang-format is required to scaffold C-family source"
50 raise RuntimeError(message)
51 formatted = subprocess.run( # noqa: S603 -- trusted formatter and generated input
52 [
53 formatter,
54 f"--style=file:{REPO_ROOT / '.clang-format'}",
55 f"--assume-filename={path}",
56 ],
57 input=content,
58 capture_output=True,
59 text=True,
60 check=True,
61 )
62 return formatted.stdout
63
64
65def create_file(path: Path, content: str) -> None:
66 """Create one formatted scaffold file without overwriting existing work."""
67 path.parent.mkdir(parents=True, exist_ok=True)
68 if not path.exists():
69 formatted = _format_content(path, content.lstrip())
70 path.write_text(formatted, encoding="utf-8")
71 print(f"Created: {path}")
72 else:
73 print(f"Skipped: {path} (already exists)")
74
75
76_HEADER_TMPL = """/**
77 * @file inc/{name}.h
78 * @brief {desc}
79 *
80 * @copyright Copyright (c) 2026 Brighton Sikarskie
81 * SPDX-License-Identifier: MIT
82 */
83
84#pragma once
85
86/**
87 * @brief Initialize {name} state.
88 * @pre Call once before using this library.
89 * @post The library is ready for use.
90 * @since 0.1.0
91 */
92void {name}_init(void);
93"""
94
95_LIB_SRC_TMPL = """/**
96 * @file src/{name}.c
97 * @brief {desc}
98 *
99 * @copyright Copyright (c) 2026 Brighton Sikarskie
100 * SPDX-License-Identifier: MIT
101 */
102
103#include "{name}.h"
104
105void {name}_init(void)
106{{
107 /* The initial scaffold owns no process-wide state. */
108}}
109"""
110
111_SHARED_LIB_CMAKE_TMPL = """# SPDX-License-Identifier: MIT
112# Copyright (c) 2026 Brighton Sikarskie
113cmake_minimum_required(VERSION 3.20)
114project({name} C)
115
116set(CMAKE_C_STANDARD 23)
117set(CMAKE_C_STANDARD_REQUIRED ON)
118
119add_library({name} STATIC src/{name}.c)
120target_include_directories({name} PUBLIC inc PRIVATE src)
121target_compile_options({name} PRIVATE -Wall -Wextra -Werror)
122"""
123
124_HOST_CMAKE_TMPL = """# SPDX-License-Identifier: MIT
125# Copyright (c) 2026 Brighton Sikarskie
126cmake_minimum_required(VERSION 3.20)
127project({name} C)
128
129set(CMAKE_C_STANDARD 23)
130set(CMAKE_C_STANDARD_REQUIRED ON)
131
132add_executable(
133 {name}
134 src/main.c
135 src/{name}.c
136)
137
138target_include_directories(
139 {name} PRIVATE inc
140)
141target_compile_options({name} PRIVATE -Wall -Wextra -Werror)
142
143# Unit Tests
144enable_testing()
145add_executable(test_{name} tests/src/test_{name}.c src/{name}.c)
146target_include_directories(test_{name} PRIVATE inc)
147target_compile_definitions(test_{name} PRIVATE RA8_OFF_TARGET TEST_MODE)
148target_compile_options(test_{name} PRIVATE -Wall -Wextra -Werror)
149add_test(NAME test_{name} COMMAND test_{name})
150"""
151
152_HOST_MAIN_TMPL = """/**
153 * @file src/main.c
154 * @brief {desc}
155 *
156 * @copyright Copyright (c) 2026 Brighton Sikarskie
157 * SPDX-License-Identifier: MIT
158 */
159
160#include <stdio.h>
161
162#include "{name}.h"
163
164int main(int argc, char** argv)
165{{
166 (void)argc;
167 (void)argv;
168 {name}_init();
169 printf("Hello from {name}!\\n");
170 return 0;
171}}
172"""
173
174_HOST_TEST_TMPL = """/**
175 * @file tests/src/test_{name}.c
176 * @brief Unit tests for {name}
177 *
178 * @copyright Copyright (c) 2026 Brighton Sikarskie
179 * SPDX-License-Identifier: MIT
180 */
181
182#include <stdio.h>
183
184#include "{name}.h"
185
186int main(void)
187{{
188 {name}_init();
189 printf("Running tests for {name}...\\n");
190 printf("All tests passed!\\n");
191 return 0;
192}}
193"""
194
195_FW_CMAKE_TMPL = """# SPDX-License-Identifier: MIT
196# Copyright (c) 2026 Brighton Sikarskie
197cmake_minimum_required(VERSION 3.20)
198
199get_directory_property(_ra8_has_parent PARENT_DIRECTORY)
200if(NOT _ra8_has_parent)
201 project({name} LANGUAGES C ASM)
202endif()
203
204set(_d "${{CMAKE_CURRENT_SOURCE_DIR}}")
205while(NOT EXISTS "${{_d}}/cmake/ra8_add_app.cmake" AND NOT "${{_d}}" STREQUAL "/")
206 get_filename_component(_d "${{_d}}" DIRECTORY)
207endwhile()
208include("${{_d}}/cmake/ra8_add_app.cmake")
209
210ra8_add_app(
211 NAME {name}
212 STACK_BYTES 2200
213 DESCRIPTION "{desc}"
214 # LIBS my_lib
215)
216"""
217
218_FW_MAIN_TMPL = """/**
219 * @file src/main.c
220 * @brief {desc}
221 *
222 * @copyright Copyright (c) 2026 Brighton Sikarskie
223 * SPDX-License-Identifier: MIT
224 */
225
226#include <stdint.h>
227
228#include "{name}.h"
229
230#include "ra8_boot_entry.h"
231#include "ra8_log.h"
232
233void main(void)
234{{
235 {name}_init();
236 ra8_log_info("APP", "Hello from {name}!");
237
238 while (1) {{
239 /* Application work belongs here. */
240 }}
241}}
242"""
243
244
245def scaffold_lib(repo_root: Path, name: str) -> None:
246 """Create a reusable first-party library skeleton."""
247 lib_dir = repo_root / "libs" / name
248 desc = f"Library: {name}"
249 create_file(lib_dir / "inc" / f"{name}.h", _HEADER_TMPL.format(name=name, desc=desc))
250 create_file(lib_dir / "src" / f"{name}.c", _LIB_SRC_TMPL.format(name=name, desc=desc))
251 print(
252 f"\nLibrary {name} scaffolded! You can now depend on it using "
253 f"LIBS {name} in an app's CMakeLists.txt."
254 )
255
256
257def scaffold_shared_lib(repo_root: Path, name: str) -> None:
258 """Create an application-scoped shared-library skeleton."""
259 app_dir = repo_root / "apps" / "shared_libs" / name
260 desc = f"Shared Library: {name}"
261 create_file(
262 app_dir / "CMakeLists.txt",
263 _SHARED_LIB_CMAKE_TMPL.format(name=name, desc=desc),
264 )
265 create_file(
266 app_dir / "src" / f"{name}.c",
267 _LIB_SRC_TMPL.format(name=name, desc=desc),
268 )
269 create_file(
270 app_dir / "inc" / f"{name}.h",
271 _HEADER_TMPL.format(name=name, desc=desc),
272 )
273 print(f"\nProject {name} scaffolded at {app_dir}!")
274
275
276def scaffold_host_app(repo_root: Path, name: str) -> None:
277 """Create a host-application skeleton and its unit test."""
278 app_dir = repo_root / "apps" / "host" / name
279 desc = f"macOS Host App: {name}"
280 create_file(app_dir / "CMakeLists.txt", _HOST_CMAKE_TMPL.format(name=name, desc=desc))
281 create_file(app_dir / "src" / "main.c", _HOST_MAIN_TMPL.format(name=name, desc=desc))
282 create_file(app_dir / "src" / f"{name}.c", _LIB_SRC_TMPL.format(name=name, desc=desc))
283 create_file(
284 app_dir / "tests" / "src" / f"test_{name}.c",
285 _HOST_TEST_TMPL.format(name=name, desc=desc),
286 )
287 create_file(
288 app_dir / "inc" / f"{name}.h",
289 _HEADER_TMPL.format(name=name, desc=desc),
290 )
291 print(f"\nProject {name} scaffolded at {app_dir}!")
292
293
294def scaffold_firmware_app(repo_root: Path, app_type: str, name: str) -> None:
295 """Create a board application or firmware-example skeleton."""
296 if app_type == "example":
297 app_dir = repo_root / "examples" / "ek_ra8d2" / "hw_pending" / name
298 desc = f"Example: {name}"
299 else:
300 app_dir = repo_root / "apps" / "board" / "stand_alone" / name
301 desc = f"Standalone App: {name}"
302
303 create_file(app_dir / "CMakeLists.txt", _FW_CMAKE_TMPL.format(name=name, desc=desc))
304 create_file(
305 app_dir / "src" / "main.c",
306 _FW_MAIN_TMPL.format(name=name, desc=desc),
307 )
308 create_file(app_dir / "src" / f"{name}.c", _LIB_SRC_TMPL.format(name=name, desc=desc))
309 create_file(
310 app_dir / "inc" / f"{name}.h",
311 _HEADER_TMPL.format(name=name, desc=desc),
312 )
313 print(f"\nProject {name} scaffolded at {app_dir}!")
314
315
316def scaffold_project(repo_root: Path, project_type: str, name: str) -> None:
317 """Dispatch one validated scaffold kind."""
318 if project_type == "lib":
319 scaffold_lib(repo_root, name)
320 elif project_type == "shared_lib":
321 scaffold_shared_lib(repo_root, name)
322 elif project_type == "host":
323 scaffold_host_app(repo_root, name)
324 else:
325 scaffold_firmware_app(repo_root, project_type, name)
326
327
328def _name_template_selftest() -> list[str]:
329 """Check identifier containment and render every template."""
330 good = ("reader", "ra8_demo", "app2")
331 bad = ("", "../escape", "two-words", "Upper", "2fast", "name/path", "space name")
332 failures = [f"valid name rejected: {name}" for name in good if NAME_RE.fullmatch(name) is None]
333 failures.extend(
334 f"unsafe name accepted: {name!r}" for name in bad if NAME_RE.fullmatch(name) is not None
335 )
336 templates = (
337 _HEADER_TMPL,
338 _LIB_SRC_TMPL,
339 _SHARED_LIB_CMAKE_TMPL,
340 _HOST_CMAKE_TMPL,
341 _HOST_MAIN_TMPL,
342 _HOST_TEST_TMPL,
343 _FW_CMAKE_TMPL,
344 _FW_MAIN_TMPL,
345 )
346 for index, template in enumerate(templates):
347 rendered = template.format(name="ra8_demo", desc="Self-test scaffold")
348 if "{name}" in rendered or "{desc}" in rendered:
349 failures.append(f"template {index} left an unsubstituted field")
350 if re.search(r"\bTODO\b", rendered):
351 failures.append(f"template {index} emitted a bare TODO placeholder")
352
353 return failures
354
355
356def _tree_selftest() -> list[str]:
357 """Generate all five project kinds into an isolated tree."""
358 failures: list[str] = []
359 with tempfile.TemporaryDirectory(prefix="ra8-scaffold-selftest-") as temp_root:
360 with contextlib.redirect_stdout(io.StringIO()):
361 for project_type in SCAFFOLD_TYPES:
362 scaffold_project(Path(temp_root), project_type, f"probe_{project_type}")
363 generated = []
364 for directory, _, filenames in os.walk(temp_root):
365 generated.extend(Path(directory) / filename for filename in filenames)
366 if len(generated) != GENERATED_FILE_COUNT:
367 failures.append(
368 f"generated-tree census changed: {len(generated)} != {GENERATED_FILE_COUNT} files"
369 )
370 for path in generated:
371 content = Path(path).read_text(encoding="utf-8")
372 rel = path.relative_to(temp_root).as_posix()
373 if not content.strip():
374 failures.append(f"generated an empty file: {rel}")
375 if re.search(r"\bTODO\b", content):
376 failures.append(f"generated a TODO placeholder: {rel}")
377 if (
378 rel.endswith("main.c")
379 and rel.startswith(("examples/", "apps/board/stand_alone/"))
380 and '#include "ra8_boot_entry.h"' not in content
381 ):
382 failures.append(f"firmware scaffold omits ra8_boot_entry.h: {rel}")
383 formatter = shutil.which("clang-format")
384 c_files = [path for path in generated if Path(path).suffix in {".c", ".h"}]
385 if formatter is None:
386 failures.append("clang-format is unavailable for generated-tree validation")
387 else:
388 formatted = subprocess.run( # noqa: S603 -- fixed generated files
389 [
390 formatter,
391 f"--style=file:{REPO_ROOT / '.clang-format'}",
392 "--dry-run",
393 "--Werror",
394 *c_files,
395 ],
396 cwd=REPO_ROOT,
397 capture_output=True,
398 text=True,
399 check=False,
400 )
401 if formatted.returncode != 0:
402 failures.append("generated C/header templates are not clang-formatted")
403 return failures
404
405
406def _recipe_selftest() -> list[str]:
407 """Check that every Just entry point resolves and rejects a missing name."""
408 failures: list[str] = []
409 just_bin = shutil.which("just")
410 if just_bin is None:
411 return ["just executable is unavailable"]
412 for recipe in SCAFFOLD_RECIPES:
413 resolved = subprocess.run( # noqa: S603 -- fixed project recipe surface
414 [just_bin, "--dry-run", recipe, "ra8_scaffold_probe"],
415 cwd=REPO_ROOT,
416 capture_output=True,
417 text=True,
418 check=False,
419 )
420 if resolved.returncode != 0:
421 failures.append(f"Just scaffold recipe does not resolve: {recipe}")
422 missing = subprocess.run( # noqa: S603 -- argv is [resolved just, fixed recipe], no shell, no caller input
423 [just_bin, recipe],
424 cwd=REPO_ROOT,
425 capture_output=True,
426 text=True,
427 check=False,
428 )
429 if missing.returncode == 0:
430 failures.append(f"Just scaffold recipe accepts a missing name: {recipe}")
431 return failures
432
433
434def selftest() -> int:
435 """Prove names, templates, trees, and all five Just entry points."""
436 failures = _name_template_selftest() + _tree_selftest() + _recipe_selftest()
437 if failures:
438 for failure in failures:
439 print(f"selftest: scaffold.py FAIL: {failure}", file=sys.stderr)
440 return 1
441 print(
442 "selftest: scaffold.py OK (3 valid names, 7 unsafe names, "
443 f"8 templates, {len(SCAFFOLD_RECIPES)} Just recipes)"
444 )
445 return 0
446
447
448def main() -> int:
449 """Parse the CLI and generate one requested project skeleton."""
450 parser = argparse.ArgumentParser(description="Scaffold boilerplate for the RA8 firmware repo")
451 parser.add_argument("--selftest", action="store_true", help="run internal detector checks")
452 parser.add_argument(
453 "type",
454 nargs="?",
455 choices=SCAFFOLD_TYPES,
456 help="Type of project to scaffold",
457 )
458 parser.add_argument(
459 "name", nargs="?", type=project_name, help="Name of the project (e.g. my_cool_app)"
460 )
461 args = parser.parse_args()
462
463 if args.selftest:
464 return selftest()
465 if args.type is None or args.name is None:
466 parser.error("type and name are required unless --selftest is used")
467
468 scaffold_project(REPO_ROOT, args.type, args.name)
469 return 0
470
471
472if __name__ == "__main__":
473 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298