Tooling V1
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
98
src/neoecu/commands/clean.py
Normal file
98
src/neoecu/commands/clean.py
Normal 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)
|
||||
103
src/neoecu/commands/envcheck.py
Normal file
103
src/neoecu/commands/envcheck.py
Normal 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)
|
||||
|
||||
91
src/neoecu/commands/init.py
Normal file
91
src/neoecu/commands/init.py
Normal 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
33
src/neoecu/common.py
Normal 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
|
||||
Reference in New Issue
Block a user