Tooling V1

This commit is contained in:
2026-03-21 22:23:08 +01:00
parent 6ea8f7ddbd
commit 0e8a6f7e9f
8 changed files with 508 additions and 4 deletions

View File

@@ -3,22 +3,53 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import argparse
import sys
import neoecu.common
def main():
parser = argparse.ArgumentParser(prog="neoecu")
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser("build")
build_parser = subparsers.add_parser("build")
subparsers.add_parser("flash")
subparsers.add_parser("envcheck")
subparsers.add_parser("init")
subparsers.add_parser("clean")
build_parser.add_argument("--debug", action="store_true")
build_parser.add_argument("--release", action="store_true")
build_parser.add_argument("--clean", action="store_true")
args = parser.parse_args()
project_root = neoecu.common.find_project_root()
if project_root is None:
print("neoecu could not find a neoecu.toml file, perhaps you are not running the command from inside the project path")
sys.exit(1)
config = neoecu.common.load_config(project_root)
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")
sys.exit(1)
if args.command == "build":
from .commands.build import run
run()
run(project_root, args)
elif args.command == "flash":
from .commands.flash import run
run()
elif args.command == "envcheck":
from .commands.envcheck import run
run()
elif args.command == "init":
from .commands.init import run
run(project_root)
elif args.command == "clean":
from .commands.clean import run
run(project_root)
else:
parser.print_help()

View File

@@ -1,5 +1,94 @@
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
def run():
import sys
import subprocess
from pathlib import Path
from rich.table import Table
from rich.console import Console
import neoecu.common
def run_cmd(cmd: list[str], cwd: Path) -> bool:
result = subprocess.run(cmd, cwd=cwd)
return result.returncode == 0
def init_m7(path: Path) -> bool:
debug_ok = run_cmd(["cmake", "--preset", "Debug"], path)
release_ok = run_cmd(["cmake", "--preset", "Release"], path)
return debug_ok and release_ok
def build_m7(path: Path, mode: str, clean: bool) -> bool:
cmd = ["cmake", "--build", "--preset", mode]
if clean:
cmd.append("--clean-first")
return run_cmd(cmd, path)
def build_m4(path: Path, clean: bool) -> bool:
cmd = ["west", "build", "-b", "arduino_giga_r1/stm32h747xx/m4"]
if clean:
cmd.insert(2, "-p")
cmd.insert(3, "always")
return run_cmd(cmd, path)
def run(root_path: Path, args):
console = Console()
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"]
# Validate mode
if args.debug and args.release:
print("Cannot build Debug and Release at once")
sys.exit(1)
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)
table = Table()
table.add_column("Step")
table.add_column("Status")
ok_str = "[green]OK[/green]"
fail_str = "[red]FAILED[/red]"
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)
# 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)
if all(results):
print("Build success")
else:
print("Build failed")
sys.exit(1)

View File

@@ -0,0 +1,98 @@
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
import shutil
import sys
from pathlib import Path
from rich.table import Table
from rich.console import Console
import neoecu.common
def safe_remove_dir(path: Path, root: Path, protected: list[Path]) -> bool:
"""
Safely remove a directory with strict protections:
- Must be inside project root
- Must not match protected paths
- Must contain "build" in its path
"""
try:
resolved = path.resolve()
root_resolved = root.resolve()
# Ensure inside project root
if not resolved.is_relative_to(root_resolved):
raise ValueError("outside project root")
# Ensure not protected
for p in protected:
if resolved == p.resolve():
raise ValueError("protected path")
# Ensure this is actually a build directory
if "build" not in resolved.parts:
raise ValueError("not a build directory")
# Remove if exists
if resolved.exists():
shutil.rmtree(resolved)
return True
return True # treat non-existent as success
except Exception:
return False
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"]
# 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 = [
("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,
m7_root,
m4_root,
]
table = Table()
table.add_column("Directory")
table.add_column("Status")
ok_str = "[green]OK[/green]"
fail_str = "[red]FAILED[/red]"
results: list[bool] = []
for name, path in build_dirs:
result = safe_remove_dir(path, root_path, protected)
table.add_row(name, ok_str if result else fail_str)
results.append(result)
console.print(table)
if all(results):
print("Clean success")
else:
print("Clean completed with errors")
sys.exit(1)

View File

@@ -0,0 +1,103 @@
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
import subprocess
from rich.table import Table
from rich.console import Console
import shutil
import os
def which_check(tool: str) -> bool:
return shutil.which(tool) is not None
def has_newlib() -> bool:
try:
result = subprocess.run(
["arm-none-eabi-gcc", "-print-file-name=libc.a"],
capture_output=True,
text=True
)
path = result.stdout.strip()
# If gcc returns just "libc.a", it means NOT found
return path != "libc.a"
except FileNotFoundError:
return False
def get_env(var: str) -> str | None:
return os.environ.get(var)
def run():
missing_str = "[red]MISSING[/red]"
ok_str = "[green]OK[/green]"
console = Console()
print("Checking build tools")
build_tools_table = Table()
build_tools_table.add_column("Tool")
build_tools_table.add_column("Status")
tools = ["ninja", "cmake", "west", "STM32CubeMX", "JLinkExe"]
for tool in tools:
result = which_check(tool)
if result:
build_tools_table.add_row(tool, ok_str)
else:
build_tools_table.add_row(tool, missing_str)
zephyr_base = get_env("ZEPHYR_BASE")
if zephyr_base is None:
build_tools_table.add_row("Zephyr Base", missing_str)
else:
build_tools_table.add_row("Zephyr Base", ok_str)
zephyr_sdk = get_env("ZEPHYR_SDK_INSTALL_DIR")
if zephyr_sdk is None:
build_tools_table.add_row("Zephyr SDK", missing_str)
else:
build_tools_table.add_row("Zephyr SDK", ok_str)
console.print(build_tools_table)
build_deps_table = Table()
build_deps_table.add_column("Dependency")
build_deps_table.add_column("Status")
deps = [
# Compiler
"arm-none-eabi-gcc",
# Binutils (core)
"arm-none-eabi-ld", # linker
"arm-none-eabi-as", # assembler
"arm-none-eabi-ar", # archive tool
"arm-none-eabi-nm", # symbol table
"arm-none-eabi-objcopy", # binary conversion
"arm-none-eabi-objdump", # disassembler
"arm-none-eabi-strip", # strip symbols
"arm-none-eabi-size", # size info
"arm-none-eabi-strings", # extract strings
"arm-none-eabi-readelf", # ELF inspection
]
for dep in deps:
result = which_check(dep)
if result:
build_deps_table.add_row(dep, "[green]OK[/green]")
else:
build_deps_table.add_row(dep, "[red]MISSING[/red]")
if has_newlib():
build_deps_table.add_row("arm-none-eabi-newlib", "[green]OK[/green]")
else:
build_deps_table.add_row("arm-none-eabi-newlib", "[red]MISSING[/red]")
console.print(build_deps_table)

View File

@@ -0,0 +1,91 @@
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
import subprocess
import os
import sys
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
def get_env(var: str) -> str | None:
return os.environ.get(var)
def run_cmd(cmd: list[str], cwd: Path | None = None) -> bool:
result = subprocess.run(cmd, cwd=cwd)
return result.returncode == 0
def run(root_path: Path):
console = Console()
ok_str = "[green]OK[/green]"
fail_str = "[red]FAILED[/red]"
print("Initializing project")
table = Table()
table.add_column("Step")
table.add_column("Status")
results: list[bool] = []
# 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)
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)
# 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 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 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)
# Final result
if all(results):
print("Init success")
else:
print("Init failed")
sys.exit(1)

33
src/neoecu/common.py Normal file
View File

@@ -0,0 +1,33 @@
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path
import tomllib
def find_project_root(
start: Path | None = None,
filename: str = "neoecu.toml",
max_depth: int = 10
) -> Path | None:
current = start or Path.cwd()
for _ in range(max_depth + 1):
if (current / filename).exists():
return current # return the directory (project root)
if current.parent == current:
break
current = current.parent
return None
def load_config(root: Path) -> dict:
config_path = root / "neoecu.toml"
with open(config_path, "rb") as f:
return tomllib.load(f)
def check_pair(config: dict) -> bool:
if (config["build"]["m7_type"] == "ST") & (config["build"]["m4_type"] == "Zephyr"):
return True
return False