Files
esphome/script/helpers_zephyr.py

117 lines
4.6 KiB
Python

"""Load clang-tidy idedata for the nrf52/Zephyr environment.
The compile commands come from a configure-only build of a minimal Zephyr
project using the native sdk-nrf toolchain (see
``esphome.components.nrf52.clang_tidy``); this module extracts the include
paths, defines and compiler flags clang-tidy needs from them.
"""
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
def load_idedata(environment, temp_folder, platformio_ini):
if explicit := os.environ.get("ESPHOME_ZEPHYR_COMPILE_COMMANDS"):
compile_commands_path = Path(explicit)
else:
from esphome.components.nrf52.clang_tidy import generate_compile_commands
work_dir = (Path(temp_folder) / f"zephyr-{environment}").resolve()
compile_commands_path = generate_compile_commands(
work_dir, Path(platformio_ini)
)
if not compile_commands_path.is_file():
raise RuntimeError(f"compile_commands.json not found: {compile_commands_path}")
def extract_include_paths(command):
include_paths = []
include_pattern = re.compile(r'("-I\s*[^"]+)|(-isystem\s*[^\s]+)|(-I\s*[^\s]+)')
for match in include_pattern.findall(command):
split_strings = re.split(
r"\s*-\s*(?:I|isystem)", list(filter(lambda x: x, match))[0]
)
include_paths.append(split_strings[1].strip())
return include_paths
def extract_defines(command):
define_pattern = re.compile(r"-D\s*([^\s]+)")
ignore_prefixes = ("_ASMLANGUAGE", "NRF_802154_ECB_PRIORITY=")
return [
match.replace("\\", "")
for match in define_pattern.findall(command)
if not any(match.startswith(prefix) for prefix in ignore_prefixes)
]
def get_builtin_include_paths(compiler):
result = subprocess.run(
[compiler, "-E", "-x", "c++", "-", "-v"],
input="",
text=True,
stderr=subprocess.PIPE,
stdout=subprocess.DEVNULL,
check=True,
)
include_paths = []
start_collecting = False
for line in result.stderr.splitlines():
if start_collecting:
if line.startswith(" "):
include_paths.append(line.strip())
else:
break
if "#include <...> search starts here:" in line:
start_collecting = True
return include_paths
def extract_cxx_flags(command):
# Extracts CXXFLAGS from the command string, excluding includes and
# defines. Anchored per token: a substring match would extract a bogus
# "-format-zero-length" from -Wno-format-zero-length.
flag_pattern = re.compile(
r"^(-O[0-3s]|-g|-std=.+|-Wall|-Wextra|-Werror|--.+|-f.+|-m.+|-imacros.+)$"
)
flags = []
tokens = shlex.split(command)
for i, token in enumerate(tokens):
if token == "-imacros" and i + 1 < len(tokens):
flags.append(f"-imacros{tokens[i + 1]}")
elif flag_pattern.match(token):
flags.append(token)
return flags
def transform_to_idedata_format(compile_commands):
# Use only the tidy app TU (main.cpp): as the app target, its compile
# command already carries the full Zephyr include set. Unioning every
# TU instead would drag in per-library internal include dirs (e.g. the
# Zephyr POSIX shim, whose signal.h redefines newlib's sigset_t) that
# no ESPHome source compiles against.
entry = next(
(e for e in compile_commands if e["file"].endswith("main.cpp")), None
)
if entry is None:
raise RuntimeError("tidy main.cpp not found in compile_commands.json")
command = entry["command"]
# Find the compiler by name: the command may be prefixed with a
# launcher (Zephyr auto-enables ccache when present).
cxx_path = next((t for t in shlex.split(command) if t.endswith("++")), None)
if cxx_path is None:
raise RuntimeError(f"no C++ compiler in compile command: {command}")
return {
"includes": {
"toolchain": get_builtin_include_paths(cxx_path),
"build": extract_include_paths(command),
},
"defines": extract_defines(command),
"cxx_path": cxx_path,
"cxx_flags": extract_cxx_flags(command),
}
compile_commands = json.loads(compile_commands_path.read_text(encoding="utf-8"))
return transform_to_idedata_format(compile_commands)