4"""Generate policy-compliant repository project skeletons."""
6from __future__
import annotations
17from pathlib
import Path
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")
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:
36 "name must start with a lowercase letter and contain only lowercase "
37 "letters, digits, and underscores"
39 raise argparse.ArgumentTypeError(message)
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"}:
47 formatter = shutil.which(
"clang-format")
49 message =
"clang-format is required to scaffold C-family source"
50 raise RuntimeError(message)
51 formatted = subprocess.run(
54 f
"--style=file:{REPO_ROOT / '.clang-format'}",
55 f
"--assume-filename={path}",
62 return formatted.stdout
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)
69 formatted = _format_content(path, content.lstrip())
70 path.write_text(formatted, encoding=
"utf-8")
71 print(f
"Created: {path}")
73 print(f
"Skipped: {path} (already exists)")
80 * @copyright Copyright (c) 2026 Brighton Sikarskie
81 * SPDX-License-Identifier: MIT
87 * @brief Initialize {name} state.
88 * @pre Call once before using this library.
89 * @post The library is ready for use.
92void {name}_init(void);
99 * @copyright Copyright (c) 2026 Brighton Sikarskie
100 * SPDX-License-Identifier: MIT
105void {name}_init(void)
107 /* The initial scaffold owns no process-wide state. */
111_SHARED_LIB_CMAKE_TMPL =
"""# SPDX-License-Identifier: MIT
112# Copyright (c) 2026 Brighton Sikarskie
113cmake_minimum_required(VERSION 3.20)
116set(CMAKE_C_STANDARD 23)
117set(CMAKE_C_STANDARD_REQUIRED ON)
119add_library({name} STATIC src/{name}.c)
120target_include_directories({name} PUBLIC inc PRIVATE src)
121target_compile_options({name} PRIVATE -Wall -Wextra -Werror)
124_HOST_CMAKE_TMPL =
"""# SPDX-License-Identifier: MIT
125# Copyright (c) 2026 Brighton Sikarskie
126cmake_minimum_required(VERSION 3.20)
129set(CMAKE_C_STANDARD 23)
130set(CMAKE_C_STANDARD_REQUIRED ON)
138target_include_directories(
141target_compile_options({name} PRIVATE -Wall -Wextra -Werror)
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})
152_HOST_MAIN_TMPL =
"""/**
156 * @copyright Copyright (c) 2026 Brighton Sikarskie
157 * SPDX-License-Identifier: MIT
164int main(int argc, char** argv)
169 printf("Hello from {name}!\\n");
174_HOST_TEST_TMPL =
"""/**
175 * @file tests/src/test_{name}.c
176 * @brief Unit tests for {name}
178 * @copyright Copyright (c) 2026 Brighton Sikarskie
179 * SPDX-License-Identifier: MIT
189 printf("Running tests for {name}...\\n");
190 printf("All tests passed!\\n");
195_FW_CMAKE_TMPL =
"""# SPDX-License-Identifier: MIT
196# Copyright (c) 2026 Brighton Sikarskie
197cmake_minimum_required(VERSION 3.20)
199get_directory_property(_ra8_has_parent PARENT_DIRECTORY)
200if(NOT _ra8_has_parent)
201 project({name} LANGUAGES C ASM)
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)
208include("${{_d}}/cmake/ra8_add_app.cmake")
218_FW_MAIN_TMPL =
"""/**
222 * @copyright Copyright (c) 2026 Brighton Sikarskie
223 * SPDX-License-Identifier: MIT
230#include "ra8_boot_entry.h"
236 ra8_log_info("APP", "Hello from {name}!");
239 /* Application work belongs here. */
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))
252 f
"\nLibrary {name} scaffolded! You can now depend on it using "
253 f
"LIBS {name} in an app's CMakeLists.txt."
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}"
262 app_dir /
"CMakeLists.txt",
263 _SHARED_LIB_CMAKE_TMPL.format(name=name, desc=desc),
266 app_dir /
"src" / f
"{name}.c",
267 _LIB_SRC_TMPL.format(name=name, desc=desc),
270 app_dir /
"inc" / f
"{name}.h",
271 _HEADER_TMPL.format(name=name, desc=desc),
273 print(f
"\nProject {name} scaffolded at {app_dir}!")
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))
284 app_dir /
"tests" /
"src" / f
"test_{name}.c",
285 _HOST_TEST_TMPL.format(name=name, desc=desc),
288 app_dir /
"inc" / f
"{name}.h",
289 _HEADER_TMPL.format(name=name, desc=desc),
291 print(f
"\nProject {name} scaffolded at {app_dir}!")
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}"
300 app_dir = repo_root /
"apps" /
"board" /
"stand_alone" / name
301 desc = f
"Standalone App: {name}"
303 create_file(app_dir /
"CMakeLists.txt", _FW_CMAKE_TMPL.format(name=name, desc=desc))
305 app_dir /
"src" /
"main.c",
306 _FW_MAIN_TMPL.format(name=name, desc=desc),
308 create_file(app_dir /
"src" / f
"{name}.c", _LIB_SRC_TMPL.format(name=name, desc=desc))
310 app_dir /
"inc" / f
"{name}.h",
311 _HEADER_TMPL.format(name=name, desc=desc),
313 print(f
"\nProject {name} scaffolded at {app_dir}!")
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)
325 scaffold_firmware_app(repo_root, project_type, name)
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]
334 f
"unsafe name accepted: {name!r}" for name
in bad
if NAME_RE.fullmatch(name)
is not None
339 _SHARED_LIB_CMAKE_TMPL,
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")
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}")
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:
368 f
"generated-tree census changed: {len(generated)} != {GENERATED_FILE_COUNT} files"
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}")
378 rel.endswith(
"main.c")
379 and rel.startswith((
"examples/",
"apps/board/stand_alone/"))
380 and '#include "ra8_boot_entry.h"' not in content
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")
388 formatted = subprocess.run(
391 f
"--style=file:{REPO_ROOT / '.clang-format'}",
401 if formatted.returncode != 0:
402 failures.append(
"generated C/header templates are not clang-formatted")
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")
411 return [
"just executable is unavailable"]
412 for recipe
in SCAFFOLD_RECIPES:
413 resolved = subprocess.run(
414 [just_bin,
"--dry-run", recipe,
"ra8_scaffold_probe"],
420 if resolved.returncode != 0:
421 failures.append(f
"Just scaffold recipe does not resolve: {recipe}")
422 missing = subprocess.run(
429 if missing.returncode == 0:
430 failures.append(f
"Just scaffold recipe accepts a missing name: {recipe}")
434def selftest() -> int:
435 """Prove names, templates, trees, and all five Just entry points."""
436 failures = _name_template_selftest() + _tree_selftest() + _recipe_selftest()
438 for failure
in failures:
439 print(f
"selftest: scaffold.py FAIL: {failure}", file=sys.stderr)
442 "selftest: scaffold.py OK (3 valid names, 7 unsafe names, "
443 f
"8 templates, {len(SCAFFOLD_RECIPES)} Just recipes)"
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")
455 choices=SCAFFOLD_TYPES,
456 help=
"Type of project to scaffold",
459 "name", nargs=
"?", type=project_name, help=
"Name of the project (e.g. my_cool_app)"
461 args = parser.parse_args()
465 if args.type
is None or args.name
is None:
466 parser.error(
"type and name are required unless --selftest is used")
468 scaffold_project(REPO_ROOT, args.type, args.name)
472if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.