Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
07e931b46This file is pinned to the selected release. Download the original to open it in your design tool.
"""Compile/run the loader's portable ELF checks and the shipped Basics corpus.
python firmware/toolchain/check_module_elf.py [--sanitize]
Needs a C++17 host compiler (GCC/Clang, or MSVC Build Tools on Windows).
No board or PlatformIO required. --sanitize enables ASan/UBSan on GCC/Clang.
"""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import zipfile
ROOT = Path(__file__).resolve().parents[2]
def compiler_environment() -> tuple[str, dict[str, str]]:
env = dict(os.environ)
for name in (os.environ.get("CXX"), "c++", "g++", "clang++", "cl"):
if name and (compiler := shutil.which(name)):
return compiler, env
if os.name == "nt":
vswhere = Path(os.environ.get("ProgramFiles(x86)", "C:/Program Files (x86)")) / (
"Microsoft Visual Studio/Installer/vswhere.exe"
)
if vswhere.is_file():
install = subprocess.check_output([
str(vswhere), "-latest", "-products", "*", "-requires",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath",
], text=True).strip()
if install:
setup = Path(install) / "VC/Auxiliary/Build/vcvars64.bat"
# cmd does not understand subprocess's CRT-style escaping
# of embedded quotes in a list argument. Pass its command
# line directly; this path comes from the local VS installer.
output = subprocess.check_output(
f'cmd /d /u /s /c "call "{setup}" >nul && set"',
encoding="utf-16-le", timeout=60,
)
updates = dict(line.split("=", 1) for line in output.splitlines() if "=" in line)
# Some shells inherit both Path and PATH. vcvars writes PATH;
# do not let the stale mixed-case value hide the compiler.
env = {key.upper(): value for key, value in env.items()}
env.update({key.upper(): updates.get(key.upper(), value)
for key, value in updates.items()})
compiler = shutil.which("cl", path=env.get("PATH"))
if compiler:
return compiler, env
raise SystemExit("No C++17 compiler found; set CXX or install GCC/Clang/MSVC Build Tools.")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sanitize", action="store_true")
args = parser.parse_args()
compiler, env = compiler_environment()
with tempfile.TemporaryDirectory(prefix="patternflow-elf-") as directory:
work = Path(directory)
executable = work / ("module_elf_test.exe" if os.name == "nt" else "module_elf_test")
source = ROOT / "firmware/toolchain/tests/module_elf_test.cpp"
include = ROOT / "firmware/patternflow/src"
if Path(compiler).stem.lower() == "cl":
if args.sanitize:
raise SystemExit("--sanitize requires GCC or Clang (ASan + UBSan).")
command = [compiler, "/nologo", "/std:c++17", "/EHsc", "/W4", "/WX",
f"/I{include}", str(source), f"/Fe:{executable}"]
else:
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
f"-I{include}", str(source), "-o", str(executable)]
if args.sanitize:
command += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
subprocess.run(command, cwd=work, env=env, check=True)
fixtures = []
with zipfile.ZipFile(ROOT / "web/public/packs/basics.zip") as pack:
for index, member in enumerate(pack.namelist()):
if member.endswith(".pfm"):
# Archive paths never reach the filesystem: Path().name drops any
# directory component an archive might carry, and the substitution
# leaves nothing that could be one. The index keeps it unique even
# if two entries sanitise to the same string. The name is kept
# rather than generated because the test now reports which module
# is the largest, and "module_52.pfm" does not answer that.
safe = re.sub(r"[^A-Za-z0-9._-]", "_", Path(member).name)
fixture = work / f"{index:02d}-{safe}"
fixture.write_bytes(pack.read(member))
fixtures.append(str(fixture))
if not fixtures:
raise SystemExit("Basics pack has no .pfm fixtures")
subprocess.run([str(executable), *fixtures], cwd=work, env=env, check=True)
if __name__ == "__main__":
main()