1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-01 10:52:19 +01:00

[clang] clang tidy support with zephyr (#8352)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
tomaszduda23
2025-05-13 01:36:34 +02:00
committed by GitHub
parent f4eb75e4e0
commit 7c0546c9f0
8 changed files with 237 additions and 45 deletions

View File

@@ -558,6 +558,7 @@ def lint_relative_py_import(fname):
"esphome/components/rp2040/core.cpp",
"esphome/components/libretiny/core.cpp",
"esphome/components/host/core.cpp",
"esphome/components/zephyr/core.cpp",
"esphome/components/http_request/httplib.h",
],
)

View File

@@ -40,12 +40,37 @@ def clang_options(idedata):
else:
cmd.append(f"--target={triplet}")
omit_flags = (
"-free",
"-fipa-pta",
"-fstrict-volatile-bitfields",
"-mlongcalls",
"-mtext-section-literals",
"-mdisable-hardware-atomics",
"-mfix-esp32-psram-cache-issue",
"-mfix-esp32-psram-cache-strategy=memw",
"-fno-tree-switch-conversion",
)
if "zephyr" in triplet:
omit_flags += (
"-fno-reorder-functions",
"-mfp16-format=ieee",
"--param=min-pagesize=0",
)
else:
cmd.extend(
[
# disable built-in include directories from the host
"-nostdinc++",
]
)
# set flags
cmd.extend(
[
# disable built-in include directories from the host
"-nostdinc",
"-nostdinc++",
# replace pgmspace.h, as it uses GNU extensions clang doesn't support
# https://github.com/earlephilhower/newlib-xtensa/pull/18
"-D_PGMSPACE_H_",
@@ -70,22 +95,7 @@ def clang_options(idedata):
)
# copy compiler flags, except those clang doesn't understand.
cmd.extend(
flag
for flag in idedata["cxx_flags"]
if flag
not in (
"-free",
"-fipa-pta",
"-fstrict-volatile-bitfields",
"-mlongcalls",
"-mtext-section-literals",
"-mdisable-hardware-atomics",
"-mfix-esp32-psram-cache-issue",
"-mfix-esp32-psram-cache-strategy=memw",
"-fno-tree-switch-conversion",
)
)
cmd.extend(flag for flag in idedata["cxx_flags"] if flag not in omit_flags)
# defines
cmd.extend(f"-D{define}" for define in idedata["defines"])
@@ -100,13 +110,16 @@ def clang_options(idedata):
# add library include directories using -isystem to suppress their errors
for directory in list(idedata["includes"]["build"]):
# skip our own directories, we add those later
if not directory.startswith(f"{root_path}") or directory.startswith(
(
f"{root_path}/.pio",
f"{root_path}/.platformio",
f"{root_path}/.temp",
f"{root_path}/managed_components",
if (
not directory.startswith(f"{root_path}")
or directory.startswith(
(
f"{root_path}/.platformio",
f"{root_path}/.temp",
f"{root_path}/managed_components",
)
)
or (directory.startswith(f"{root_path}") and "/.pio/" in directory)
):
cmd.extend(["-isystem", directory])

View File

@@ -5,6 +5,7 @@ import re
import subprocess
import colorama
import helpers_zephyr
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", "..")))
basepath = os.path.join(root_path, "esphome")
@@ -147,10 +148,14 @@ def load_idedata(environment):
# ensure temp directory exists before running pio, as it writes sdkconfig to it
Path(temp_folder).mkdir(exist_ok=True)
stdout = subprocess.check_output(["pio", "run", "-t", "idedata", "-e", environment])
match = re.search(r'{\s*".*}', stdout.decode("utf-8"))
data = json.loads(match.group())
if "nrf" in environment:
data = helpers_zephyr.load_idedata(environment, temp_folder, platformio_ini)
else:
stdout = subprocess.check_output(
["pio", "run", "-t", "idedata", "-e", environment]
)
match = re.search(r'{\s*".*}', stdout.decode("utf-8"))
data = json.loads(match.group())
temp_idedata.write_text(json.dumps(data, indent=2) + "\n")
return data

124
script/helpers_zephyr.py Normal file
View File

@@ -0,0 +1,124 @@
import json
from pathlib import Path
import re
import subprocess
def load_idedata(environment, temp_folder, platformio_ini):
build_environment = environment.replace("-tidy", "")
build_dir = Path(temp_folder) / f"build-{build_environment}"
Path(build_dir).mkdir(exist_ok=True)
Path(build_dir / "platformio.ini").write_text(
Path(platformio_ini).read_text(encoding="utf-8"), encoding="utf-8"
)
esphome_dir = Path(build_dir / "esphome")
esphome_dir.mkdir(exist_ok=True)
Path(esphome_dir / "main.cpp").write_text(
"""
#include <zephyr/kernel.h>
int main() { return 0;}
""",
encoding="utf-8",
)
zephyr_dir = Path(build_dir / "zephyr")
zephyr_dir.mkdir(exist_ok=True)
Path(zephyr_dir / "prj.conf").write_text(
"""
CONFIG_NEWLIB_LIBC=y
""",
encoding="utf-8",
)
subprocess.run(["pio", "run", "-e", build_environment, "-d", build_dir], check=True)
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])
return include_paths
def extract_defines(command):
defines = []
define_pattern = re.compile(r"-D\s*([^\s]+)")
for match in define_pattern.findall(command):
if match not in ("_ASMLANGUAGE"):
defines.append(match)
return defines
def find_cxx_path(commands):
for entry in commands:
command = entry["command"]
cxx_path = command.split()[0]
if not cxx_path.endswith("++"):
continue
return cxx_path
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):
flags = []
# Extracts CXXFLAGS from the command string, excluding includes and defines.
flag_pattern = re.compile(
r"(-O[0-3s]|-g|-std=[^\s]+|-Wall|-Wextra|-Werror|--[^\s]+|-f[^\s]+|-m[^\s]+|-imacros\s*[^\s]+)"
)
for match in flag_pattern.findall(command):
flags.append(match.replace("-imacros ", "-imacros"))
return flags
def transform_to_idedata_format(compile_commands):
cxx_path = find_cxx_path(compile_commands)
idedata = {
"includes": {
"toolchain": get_builtin_include_paths(cxx_path),
"build": set(),
},
"defines": set(),
"cxx_path": cxx_path,
"cxx_flags": set(),
}
for entry in compile_commands:
command = entry["command"]
exec = command.split()[0]
if exec != cxx_path:
continue
idedata["includes"]["build"].update(extract_include_paths(command))
idedata["defines"].update(extract_defines(command))
idedata["cxx_flags"].update(extract_cxx_flags(command))
# Convert sets to lists for JSON serialization
idedata["includes"]["build"] = list(idedata["includes"]["build"])
idedata["defines"] = list(idedata["defines"])
idedata["cxx_flags"] = list(idedata["cxx_flags"])
return idedata
compile_commands = json.loads(
Path(
build_dir / ".pio" / "build" / build_environment / "compile_commands.json"
).read_text(encoding="utf-8")
)
return transform_to_idedata_format(compile_commands)