diff --git a/README.md b/README.md index 87a3fc5..06914ad 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,104 @@ # NeoECU Tooling Unified CLI tooling for building and flashing NeoECU firmware + +## Configuration + +Run commands from anywhere inside a project that contains `neoecu.toml`. The tool walks up the directory tree until it finds that file. + +`neoecu.toml` uses a `[build]` table. A core is considered configured only when both its directory and type are present: + +```toml +[build] +m7_dir = "path/to/cm7/project" +m7_type = "ST" +m4_dir = "path/to/cm4/project" +m4_type = "Zephyr" +``` + +Supported core types are `ST` for CM7 and `Zephyr` for CM4. If either the CM7 or CM4 keys are missing or incomplete, that core is skipped and the tool runs as a single-core project for the core that is fully configured. + +Examples: + +```toml +[build] +m7_dir = "STM32" +m7_type = "ST" +``` + +```toml +[build] +m4_dir = "Zephyr" +m4_type = "Zephyr" +``` + +## Commands + +### `neoecu build` + +Builds the configured firmware cores. With both cores configured, this builds CM7 first with CMake and CM4 second with West/Zephyr. With only one fully configured core, only that core is built. + +Options: + +- `--debug`: build CM7 with the `Debug` CMake preset. If omitted, CM7 uses `Release`. +- `--release`: build CM7 with the `Release` CMake preset. +- `--clean`: clean before building. CM7 uses CMake `--clean-first`; CM4 uses West pristine rebuild. +- `--CM7`: build only the CM7 core. +- `--CM4`: build only the CM4 core. + +Examples: + +```sh +neoecu build +neoecu build --debug +neoecu build --release --clean +neoecu build --CM7 +neoecu build --CM4 --clean +``` + +### `neoecu flash` + +Flashes the configured firmware cores with J-Link. With both cores configured, CM4 is flashed first and CM7 second. With only one fully configured core, only that core is flashed. + +Options: + +- `--CM7`: flash only the CM7 core. +- `--CM4`: flash only the CM4 core. + +Examples: + +```sh +neoecu flash +neoecu flash --CM7 +neoecu flash --CM4 +``` + +### `neoecu init` + +Initializes the configured project cores. For CM7, it runs the CMake `Debug` and `Release` presets and performs an initial debug build. For CM4, it installs West and Zephyr Python requirements, then runs an initial West build. + +Example: + +```sh +neoecu init +``` + +### `neoecu clean` + +Removes generated build directories for the configured cores while protecting the project root and configured source directories. + +Example: + +```sh +neoecu clean +``` + +### `neoecu envcheck` + +Checks whether expected build tools, Zephyr environment variables, and Arm toolchain dependencies are available. + +Example: + +```sh +neoecu envcheck +``` diff --git a/pyproject.toml b/pyproject.toml index 2a0967a..2a6ec63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "neoecu" -version = "2.0.0" +version = "2.1.0" description = "NeoECU build and flash tooling" authors = [{ name = "Hector van der Aa", email = "hector@h3cx.dev" }] readme = "README.md" diff --git a/src/neoecu/cli.py b/src/neoecu/cli.py index f7c2e64..1fc8992 100644 --- a/src/neoecu/cli.py +++ b/src/neoecu/cli.py @@ -12,7 +12,7 @@ def main(): subparsers = parser.add_subparsers(dest="command") build_parser = subparsers.add_parser("build") - subparsers.add_parser("flash") + flash_parser = subparsers.add_parser("flash") subparsers.add_parser("envcheck") subparsers.add_parser("init") subparsers.add_parser("clean") @@ -20,6 +20,10 @@ def main(): build_parser.add_argument("--debug", action="store_true") build_parser.add_argument("--release", action="store_true") build_parser.add_argument("--clean", action="store_true") + build_parser.add_argument("--CM7", action="store_true", help="Build only the CM7 core") + build_parser.add_argument("--CM4", action="store_true", help="Build only the CM4 core") + flash_parser.add_argument("--CM7", action="store_true", help="Flash only the CM7 core") + flash_parser.add_argument("--CM4", action="store_true", help="Flash only the CM4 core") args = parser.parse_args() @@ -33,7 +37,7 @@ def main(): pair = neoecu.common.check_pair(config) if not pair: - print("neoecu currently only support building ST for M7 and Zephyr for M4, update your neoecu.toml") + print("neoecu supports ST for CM7 and Zephyr for CM4. Configure at least one supported core in neoecu.toml") sys.exit(1) if args.command == "build": @@ -41,7 +45,7 @@ def main(): run(project_root, args) elif args.command == "flash": from .commands.flash import run - run(project_root) + run(project_root, args) elif args.command == "envcheck": from .commands.envcheck import run run() diff --git a/src/neoecu/commands/build.py b/src/neoecu/commands/build.py index e4031de..9a29262 100644 --- a/src/neoecu/commands/build.py +++ b/src/neoecu/commands/build.py @@ -48,9 +48,11 @@ def run(root_path: Path, args): print("Building NeoECU...") config = neoecu.common.load_config(root_path) - - m7_root = root_path / config["build"]["m7_dir"] - m4_root = root_path / config["build"]["m4_dir"] + try: + cores = neoecu.common.select_cores(config, args) + except ValueError as exc: + print(exc) + sys.exit(1) # Validate mode if args.debug and args.release: @@ -59,12 +61,13 @@ def run(root_path: Path, args): mode = "Debug" if args.debug else "Release" - # Ensure M7 initialized - m7_build_dir = m7_root / "build" - if not m7_build_dir.exists(): - if not init_m7(m7_root): - print("Failed to init STM32 project") - sys.exit(1) + if "CM7" in cores: + m7_root = neoecu.common.core_root(root_path, config, "CM7") + m7_build_dir = m7_root / "build" + if not m7_build_dir.exists(): + if not init_m7(m7_root): + print("Failed to init STM32 project") + sys.exit(1) table = Table() table.add_column("Step") @@ -75,15 +78,17 @@ def run(root_path: Path, args): results: list[bool] = [] - # M7 build - m7_ok = build_m7(m7_root, mode, args.clean) - table.add_row(f"M7 {mode}", ok_str if m7_ok else fail_str) - results.append(m7_ok) + if "CM7" in cores: + m7_root = neoecu.common.core_root(root_path, config, "CM7") + m7_ok = build_m7(m7_root, mode, args.clean) + table.add_row(f"CM7 {mode}", ok_str if m7_ok else fail_str) + results.append(m7_ok) - # M4 build - m4_ok = build_m4(m4_root, args.clean) - table.add_row("M4 build", ok_str if m4_ok else fail_str) - results.append(m4_ok) + if "CM4" in cores: + m4_root = neoecu.common.core_root(root_path, config, "CM4") + m4_ok = build_m4(m4_root, args.clean) + table.add_row("CM4 build", ok_str if m4_ok else fail_str) + results.append(m4_ok) console.print(table) diff --git a/src/neoecu/commands/clean.py b/src/neoecu/commands/clean.py index aee1f68..65240de 100644 --- a/src/neoecu/commands/clean.py +++ b/src/neoecu/commands/clean.py @@ -52,28 +52,26 @@ def run(root_path: Path): console = Console() config = neoecu.common.load_config(root_path) - - m7_root = root_path / config["build"]["m7_dir"] - m4_root = root_path / config["build"]["m4_dir"] + cores = neoecu.common.configured_cores(config) # Build directories - m7_root_build_dir = m7_root / "build" - m7_cm7_build_dir = m7_root / "CM7" / "build" - m7_cm4_build_dir = m7_root / "CM4" / "build" - m4_root_build_dir = m4_root / "build" + build_dirs = [] - build_dirs = [ - ("M7 root build", m7_root_build_dir), - ("M7 CM7 build", m7_cm7_build_dir), - ("M7 CM4 build", m7_cm4_build_dir), - ("M4 root build", m4_root_build_dir), - ] + protected = [root_path] - protected = [ - root_path, - m7_root, - m4_root, - ] + if "CM7" in cores: + m7_root = neoecu.common.core_root(root_path, config, "CM7") + protected.append(m7_root) + build_dirs.extend([ + ("M7 root build", m7_root / "build"), + ("M7 CM7 build", m7_root / "CM7" / "build"), + ("M7 CM4 build", m7_root / "CM4" / "build"), + ]) + + if "CM4" in cores: + m4_root = neoecu.common.core_root(root_path, config, "CM4") + protected.append(m4_root) + build_dirs.append(("M4 root build", m4_root / "build")) table = Table() table.add_column("Directory") diff --git a/src/neoecu/commands/flash.py b/src/neoecu/commands/flash.py index 8c57538..fef705b 100644 --- a/src/neoecu/commands/flash.py +++ b/src/neoecu/commands/flash.py @@ -5,6 +5,8 @@ from pathlib import Path import neoecu.common import subprocess +import sys + def gen_flash_script(jlink_path: Path, core: str, bin_path: Path) -> Path: jlink_path.mkdir(parents=True, exist_ok=True) @@ -29,51 +31,42 @@ def gen_flash_script(jlink_path: Path, core: str, bin_path: Path) -> Path: new_script += "g\n" new_script += "exit" - script_path.write_text(new_script); + script_path.write_text(new_script) return script_path +def find_single_elf(build_path: Path, pattern: str, core: str) -> Path: + matches = list(build_path.glob(pattern)) + + if not matches: + raise FileNotFoundError(f"No {pattern} found in {core} build directory") + + if len(matches) > 1: + raise RuntimeError(f"Multiple {core} ELF files found: {matches}") + + return matches[0] -def run(root_path: Path): - +def run(root_path: Path, args): config = neoecu.common.load_config(root_path) + try: + cores = neoecu.common.select_cores(config, args) + except ValueError as exc: + print(exc) + sys.exit(1) - m7_root = root_path / config["build"]["m7_dir"] - m4_root = root_path / config["build"]["m4_dir"] - - m7_jlink_scripts = m7_root / ".jlink_scripts" - m4_jlink_scripts = m4_root / ".jlink_scripts" - - m7_build = m7_root / "CM7" / "build" - - matches = list(m7_build.glob("*_CM7.elf")) - - if not matches: - raise FileNotFoundError("No *_CM7.elf found in build directory") - - if len(matches) > 1: - raise RuntimeError(f"Multiple CM7 ELF files found: {matches}") - - m7_bin = matches[0] - - m4_build = m4_root / "build" / "zephyr" - - matches = list(m4_build.glob("zephyr.elf")) - - if not matches: - raise FileNotFoundError("No *_CM4.elf found in build directory") - - if len(matches) > 1: - raise RuntimeError(f"Multiple CM4 ELF files found: {matches}") - - m4_bin = matches[0] - - m7_flash_script = gen_flash_script(m7_jlink_scripts, "CM7", m7_bin) - m4_flash_script = gen_flash_script(m4_jlink_scripts, "CM4", m4_bin) - - subprocess.run(["JLinkExe", "-CommanderScript", f"{m4_flash_script}"], cwd=m4_root) - subprocess.run(["JLinkExe", "-CommanderScript", f"{m7_flash_script}"], cwd=m7_root) - - + if "CM4" in cores: + m4_root = neoecu.common.core_root(root_path, config, "CM4") + m4_jlink_scripts = m4_root / ".jlink_scripts" + m4_build = m4_root / "build" / "zephyr" + m4_bin = find_single_elf(m4_build, "zephyr.elf", "CM4") + m4_flash_script = gen_flash_script(m4_jlink_scripts, "CM4", m4_bin) + subprocess.run(["JLinkExe", "-CommanderScript", f"{m4_flash_script}"], cwd=m4_root) + if "CM7" in cores: + m7_root = neoecu.common.core_root(root_path, config, "CM7") + m7_jlink_scripts = m7_root / ".jlink_scripts" + m7_build = m7_root / "CM7" / "build" + m7_bin = find_single_elf(m7_build, "*_CM7.elf", "CM7") + m7_flash_script = gen_flash_script(m7_jlink_scripts, "CM7", m7_bin) + subprocess.run(["JLinkExe", "-CommanderScript", f"{m7_flash_script}"], cwd=m7_root) diff --git a/src/neoecu/commands/init.py b/src/neoecu/commands/init.py index d3564cc..92f600a 100644 --- a/src/neoecu/commands/init.py +++ b/src/neoecu/commands/init.py @@ -10,8 +10,7 @@ from pathlib import Path from rich.table import Table from rich.console import Console -from neoecu.commands import build -from neoecu.common import load_config +import neoecu.common def get_env(var: str) -> str | None: @@ -36,49 +35,53 @@ def run(root_path: Path): table.add_column("Status") results: list[bool] = [] + config = neoecu.common.load_config(root_path) + cores = neoecu.common.configured_cores(config) - # Step 1 — Install west - west_ok = run_cmd(["uv", "pip", "install", "west"]) - table.add_row("Install west", ok_str if west_ok else fail_str) - results.append(west_ok) + if "CM4" in cores: + # Step 1 — Install west + west_ok = run_cmd(["uv", "pip", "install", "west"]) + table.add_row("Install west", ok_str if west_ok else fail_str) + results.append(west_ok) - # Step 2 — Zephyr requirements - requirements = get_env("ZEPHYR_BASE") - if requirements is None: - table.add_row("Zephyr requirements", "[red]ZEPHYR_BASE not set[/red]") - console.print(table) - sys.exit(1) + # Step 2 — Zephyr requirements + requirements = get_env("ZEPHYR_BASE") + if requirements is None: + table.add_row("Zephyr requirements", "[red]ZEPHYR_BASE not set[/red]") + console.print(table) + sys.exit(1) - requirements_path = Path(requirements) / "scripts" / "requirements.txt" + requirements_path = Path(requirements) / "scripts" / "requirements.txt" - deps_ok = run_cmd(["uv", "pip", "install", "-r", str(requirements_path)]) - table.add_row("Install Zephyr deps", ok_str if deps_ok else fail_str) - results.append(deps_ok) + deps_ok = run_cmd(["uv", "pip", "install", "-r", str(requirements_path)]) + table.add_row("Install Zephyr deps", ok_str if deps_ok else fail_str) + results.append(deps_ok) - # Step 3 — Load config - config = load_config(root_path) - m7_root = root_path / config["build"]["m7_dir"] - m4_root = root_path / config["build"]["m4_dir"] + if "CM7" in cores: + m7_root = neoecu.common.core_root(root_path, config, "CM7") - # Step 4 — CMake Debug Init - debug_ok = run_cmd(["cmake", "--preset", "Debug"], cwd=m7_root) - table.add_row("CMake Debug Init", ok_str if debug_ok else fail_str) - results.append(debug_ok) + # Step 4 — CMake Debug Init + debug_ok = run_cmd(["cmake", "--preset", "Debug"], cwd=m7_root) + table.add_row("CMake Debug Init", ok_str if debug_ok else fail_str) + results.append(debug_ok) - # Step 5 — CMake Release Init - release_ok = run_cmd(["cmake", "--preset", "Release"], cwd=m7_root) - table.add_row("CMake Release Init", ok_str if release_ok else fail_str) - results.append(release_ok) + # Step 5 — CMake Release Init + release_ok = run_cmd(["cmake", "--preset", "Release"], cwd=m7_root) + table.add_row("CMake Release Init", ok_str if release_ok else fail_str) + results.append(release_ok) - # Step 6 - CMake Debug Build - build_ok = run_cmd(["cmake", "--build", "--preset", "Debug", "--clean-first", "--verbose"], cwd=m7_root) - table.add_row("CMake Debug Build", ok_str if build_ok else fail_str) - results.append(build_ok) + # Step 6 - CMake Debug Build + build_ok = run_cmd(["cmake", "--build", "--preset", "Debug", "--clean-first", "--verbose"], cwd=m7_root) + table.add_row("CMake Debug Build", ok_str if build_ok else fail_str) + results.append(build_ok) - # Step 7 - Zephyr Debug - zephyr_ok = run_cmd(["west", "build","-p", "always", "-b", "arduino_giga_r1/stm32h747xx/m4"], cwd=m4_root) - table.add_row("Zephyr Build", ok_str if zephyr_ok else fail_str) - results.append(zephyr_ok) + if "CM4" in cores: + m4_root = neoecu.common.core_root(root_path, config, "CM4") + + # Step 7 - Zephyr Debug + zephyr_ok = run_cmd(["west", "build", "-p", "always", "-b", "arduino_giga_r1/stm32h747xx/m4"], cwd=m4_root) + table.add_row("Zephyr Build", ok_str if zephyr_ok else fail_str) + results.append(zephyr_ok) # Print results table console.print(table) diff --git a/src/neoecu/common.py b/src/neoecu/common.py index 03fe129..f07fd54 100644 --- a/src/neoecu/common.py +++ b/src/neoecu/common.py @@ -4,6 +4,20 @@ from pathlib import Path import tomllib +CORE_CONFIG = { + "CM7": { + "dir_key": "m7_dir", + "type_key": "m7_type", + "supported_type": "ST", + }, + "CM4": { + "dir_key": "m4_dir", + "type_key": "m4_type", + "supported_type": "Zephyr", + }, +} + + def find_project_root( start: Path | None = None, filename: str = "neoecu.toml", @@ -27,7 +41,52 @@ def load_config(root: Path) -> dict: with open(config_path, "rb") as f: return tomllib.load(f) + +def configured_cores(config: dict) -> list[str]: + build_config = config.get("build", {}) + cores: list[str] = [] + + for core, core_config in CORE_CONFIG.items(): + if all(build_config.get(key) for key in (core_config["dir_key"], core_config["type_key"])): + cores.append(core) + + return cores + + +def core_root(root: Path, config: dict, core: str) -> Path: + return root / config["build"][CORE_CONFIG[core]["dir_key"]] + + +def select_cores(config: dict, args) -> list[str]: + selected = [] + + if getattr(args, "CM7", False): + selected.append("CM7") + if getattr(args, "CM4", False): + selected.append("CM4") + + if not selected: + selected = configured_cores(config) + + configured = configured_cores(config) + missing = [core for core in selected if core not in configured] + if missing: + missing_cores = ", ".join(missing) + raise ValueError(f"{missing_cores} is not fully configured in neoecu.toml") + + return selected + + def check_pair(config: dict) -> bool: - if (config["build"]["m7_type"] == "ST") & (config["build"]["m4_type"] == "Zephyr"): - return True - return False + build_config = config.get("build", {}) + cores = configured_cores(config) + + if not cores: + return False + + for core in cores: + core_config = CORE_CONFIG[core] + if build_config[core_config["type_key"]] != core_config["supported_type"]: + return False + + return True diff --git a/uv.lock b/uv.lock index ae4b64f..ecb38eb 100644 --- a/uv.lock +++ b/uv.lock @@ -25,7 +25,7 @@ wheels = [ [[package]] name = "neoecu" -version = "0.1.0" +version = "2.1.0" source = { editable = "." } dependencies = [ { name = "rich" },