Compare commits

1 Commits

Author SHA1 Message Date
b8fb2a3494 Release v2.0.0: merge dev into main 2026-03-22 12:32:36 +01:00
9 changed files with 115 additions and 278 deletions

101
README.md
View File

@@ -1,104 +1,3 @@
# 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
```

View File

@@ -1,6 +1,6 @@
[project]
name = "neoecu"
version = "2.1.0"
version = "2.0.0"
description = "NeoECU build and flash tooling"
authors = [{ name = "Hector van der Aa", email = "hector@h3cx.dev" }]
readme = "README.md"

View File

@@ -12,7 +12,7 @@ def main():
subparsers = parser.add_subparsers(dest="command")
build_parser = subparsers.add_parser("build")
flash_parser = subparsers.add_parser("flash")
subparsers.add_parser("flash")
subparsers.add_parser("envcheck")
subparsers.add_parser("init")
subparsers.add_parser("clean")
@@ -20,10 +20,6 @@ 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()
@@ -37,7 +33,7 @@ def main():
pair = neoecu.common.check_pair(config)
if not pair:
print("neoecu supports ST for CM7 and Zephyr for CM4. Configure at least one supported core in neoecu.toml")
print("neoecu currently only support building ST for M7 and Zephyr for M4, update your neoecu.toml")
sys.exit(1)
if args.command == "build":
@@ -45,7 +41,7 @@ def main():
run(project_root, args)
elif args.command == "flash":
from .commands.flash import run
run(project_root, args)
run(project_root)
elif args.command == "envcheck":
from .commands.envcheck import run
run()

View File

@@ -48,11 +48,9 @@ def run(root_path: Path, args):
print("Building NeoECU...")
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"]
# Validate mode
if args.debug and args.release:
@@ -61,13 +59,12 @@ def run(root_path: Path, args):
mode = "Debug" if args.debug else "Release"
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)
# 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)
table = Table()
table.add_column("Step")
@@ -78,17 +75,15 @@ def run(root_path: Path, args):
results: list[bool] = []
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)
# 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 "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)
# 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)
console.print(table)

View File

@@ -52,26 +52,28 @@ def run(root_path: Path):
console = Console()
config = neoecu.common.load_config(root_path)
cores = neoecu.common.configured_cores(config)
m7_root = root_path / config["build"]["m7_dir"]
m4_root = root_path / config["build"]["m4_dir"]
# Build directories
build_dirs = []
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"
protected = [root_path]
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),
]
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"))
protected = [
root_path,
m7_root,
m4_root,
]
table = Table()
table.add_column("Directory")

View File

@@ -5,8 +5,6 @@
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)
@@ -31,42 +29,51 @@ 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))
def run(root_path: Path):
config = neoecu.common.load_config(root_path)
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(f"No {pattern} found in {core} build directory")
raise FileNotFoundError("No *_CM7.elf found in build directory")
if len(matches) > 1:
raise RuntimeError(f"Multiple {core} ELF files found: {matches}")
raise RuntimeError(f"Multiple CM7 ELF files found: {matches}")
return matches[0]
m7_bin = matches[0]
m4_build = m4_root / "build" / "zephyr"
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)
matches = list(m4_build.glob("zephyr.elf"))
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 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 "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)

View File

@@ -10,7 +10,8 @@ from pathlib import Path
from rich.table import Table
from rich.console import Console
import neoecu.common
from neoecu.commands import build
from neoecu.common import load_config
def get_env(var: str) -> str | None:
@@ -35,53 +36,49 @@ 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)
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 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)
if "CM7" in cores:
m7_root = neoecu.common.core_root(root_path, config, "CM7")
# 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"]
# 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)
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)
# 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)

View File

@@ -4,20 +4,6 @@
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",
@@ -41,52 +27,7 @@ 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:
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
if (config["build"]["m7_type"] == "ST") & (config["build"]["m4_type"] == "Zephyr"):
return True
return False

2
uv.lock generated
View File

@@ -25,7 +25,7 @@ wheels = [
[[package]]
name = "neoecu"
version = "2.1.0"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "rich" },