Compare commits
2 Commits
v2.0.0
...
a85801a61b
| Author | SHA1 | Date | |
|---|---|---|---|
| a85801a61b | |||
| bd17fe9649 |
101
README.md
101
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
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,7 +61,8 @@ def run(root_path: Path, args):
|
||||
|
||||
mode = "Debug" if args.debug else "Release"
|
||||
|
||||
# Ensure M7 initialized
|
||||
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):
|
||||
@@ -75,14 +78,16 @@ def run(root_path: Path, args):
|
||||
|
||||
results: list[bool] = []
|
||||
|
||||
# M7 build
|
||||
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"M7 {mode}", ok_str if m7_ok else fail_str)
|
||||
table.add_row(f"CM7 {mode}", ok_str if m7_ok else fail_str)
|
||||
results.append(m7_ok)
|
||||
|
||||
# M4 build
|
||||
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("M4 build", ok_str if m4_ok else fail_str)
|
||||
table.add_row("CM4 build", ok_str if m4_ok else fail_str)
|
||||
results.append(m4_ok)
|
||||
|
||||
console.print(table)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
if "CM4" in cores:
|
||||
m4_root = neoecu.common.core_root(root_path, config, "CM4")
|
||||
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_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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,7 +35,10 @@ 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)
|
||||
@@ -55,10 +57,8 @@ def run(root_path: 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)
|
||||
@@ -75,8 +75,11 @@ def run(root_path: Path):
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user