Tooling V1
This commit is contained in:
@@ -5,7 +5,9 @@ description = "NeoECU build and flash tooling"
|
||||
authors = [{ name = "Hector van der Aa", email = "hector@h3cx.dev" }]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"rich>=14.3.3",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
neoecu = "neoecu.cli:main"
|
||||
|
||||
@@ -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
|
||||
57
uv.lock
generated
Normal file
57
uv.lock
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neoecu"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "rich" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "rich", specifier = ">=14.3.3" }]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user