diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 648b31f8f0..c007df6b9d 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -44db8a62d94c8fba83b95b73938db4377ebacc0adb504881387389f1cd8f2f3a +0550a8ea4182dbc007660de060dd023ce22c865c8e95040a36f3d07a5b354fc6 diff --git a/esphome/espidf/clang_tidy.py b/esphome/espidf/clang_tidy.py new file mode 100644 index 0000000000..2cfbe67a70 --- /dev/null +++ b/esphome/espidf/clang_tidy.py @@ -0,0 +1,440 @@ +"""Generate clang-tidy idedata via the native ESP-IDF toolchain. + +Produces idedata for clang-tidy **without an ESPHome YAML config**. Instead of +running codegen on a config, it generates a minimal ESP-IDF CMake project: + +* the managed-component dependencies come from ESPHome's own + ``idf_component.yml`` (arduinojson, lvgl, mdns, ...); +* the PlatformIO ``lib_deps`` (qr-code, mlx90393, ...) are converted to local + IDF components via the ESPHome PlatformIO->IDF converter; +* the ``main`` component ``REQUIRES`` every target-available builtin IDF + component, so their public include dirs land on the translation unit; +* the repo ``sdkconfig.defaults`` enables sdkconfig-gated components (bt, ...). + +then runs ``idf.py reconfigure`` (configure only, no compile) and reads the +resulting ``build/compile_commands.json``. The IDF version is the esp32 +component's recommended version. + +``ESPHOME_IDF_COMPILE_COMMANDS`` may point at an existing build's +``compile_commands.json`` to skip generation (fast iteration). +""" + +from dataclasses import dataclass +import os +from pathlib import Path + +TIDY_PROJECT_NAME = "esphome_tidy" + +# A do-nothing C++ app: just enough for IDF to configure a valid project. It's +# C++ (not C) so the compile command uses the C++ compiler and flags, matching +# how clang-tidy analyzes ESPHome's C++ sources. +_TIDY_MAIN_CPP = 'extern "C" void app_main() {}\n' + + +@dataclass(frozen=True) +class _Settings: + """Per-environment build settings derived from the tidy env name. + + The platform defines below are what a real ESPHome build adds via + cg.add_define; defines.h only *consumes* them, so without them + esphome/core/hal.h errors with "not implemented for this platform". + """ + + idf_target: str # esp32, esp32s3, ... + variant: str # ESP32, ESP32S3, ... + idf_version: str # ESP-IDF version to build with + target_framework: str # "espidf" or "arduino" + platform_defines: tuple[str, ...] + # Extra idf_component.yml deps the framework needs (e.g. arduino-esp32). + framework_deps: dict[str, dict] + + +def _settings_for(environment: str) -> _Settings: + """Derive build settings from a ``--tidy`` env name. + + Arduino on esp32 is itself a native ESP-IDF build with the + ``espressif/arduino-esp32`` component added, so both frameworks use this + path -- only the defines, IDF version, and that one component differ. + """ + from esphome.components.esp32 import ( + ARDUINO_FRAMEWORK_VERSION_LOOKUP, + ARDUINO_IDF_VERSION_LOOKUP, + ESP_IDF_FRAMEWORK_VERSION_LOOKUP, + ) + + parts = environment.split("-") + if len(parts) != 3 or parts[2] != "tidy" or parts[1] not in ("idf", "arduino"): + raise ValueError( + f"Unsupported clang-tidy environment {environment!r}: expected " + "--tidy with framework 'idf' or 'arduino' " + "(e.g. esp32-idf-tidy, esp32s3-arduino-tidy)" + ) + idf_target, framework, _ = parts + variant = idf_target.upper() + # Defines shared by both frameworks. ESPHOME_LOG_LEVEL must be set up front + # (as the PlatformIO tidy build_flags do) -- otherwise log.h's ``#ifndef`` + # sets it to NONE before defines.h redefines it, a macro-redefined warning + # across nearly every source. + common_defines = ( + "USE_ESP32", + f"USE_ESP32_VARIANT_{variant}", + "ESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE", + ) + + if framework == "arduino": + fw_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"] + return _Settings( + idf_target=idf_target, + variant=variant, + idf_version=str(ARDUINO_IDF_VERSION_LOOKUP[fw_version]), + target_framework="arduino", + platform_defines=( + *common_defines, + "USE_ARDUINO", + "USE_ESP32_FRAMEWORK_ARDUINO", + ), + framework_deps=_arduino_framework_deps(str(fw_version)), + ) + return _Settings( + idf_target=idf_target, + variant=variant, + idf_version=str(ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]), + target_framework="espidf", + platform_defines=( + *common_defines, + "USE_ESP_IDF", + "USE_ESP32_FRAMEWORK_ESP_IDF", + ), + framework_deps={}, + ) + + +def _arduino_framework_deps(version: str) -> dict[str, dict]: + """Arduino-only managed deps merged on top of esphome/idf_component.yml. + + arduino-esp32 provides Arduino.h and the arduino libraries; its version is + the recommended arduino framework version so the tidy build matches what + ESPHome ships. + """ + from esphome.components.esp32 import ARDUINO_ESP32_COMPONENT_NAME + + return {ARDUINO_ESP32_COMPONENT_NAME: {"version": version}} + + +_TOP_CMAKELISTS = """\ +# Auto-generated by ESPHome (clang-tidy idedata project) +cmake_minimum_required(VERSION 3.16) +set(IDF_TARGET {idf_target}) +include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) +{compile_options} +project({name}) +""" + +_MAIN_CMAKELISTS = """\ +# Auto-generated by ESPHome (clang-tidy idedata project) +idf_component_register( + SRCS "tidy.cpp" + REQUIRES {requires} +) +""" + + +def _setup_core(work_dir: Path, settings: _Settings) -> None: + """Point CORE at the tidy project + IDF version, without any YAML config.""" + from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION, KEY_VARIANT + import esphome.config_validation as cv + from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM + from esphome.core import CORE + + CORE.name = TIDY_PROJECT_NAME + # config_path's parent is the data dir root: the IDF install lives at + # ``/.esphome/idf`` -- keep it beside (not inside) the per-run + # project dir so clearing the project doesn't force an IDF re-download. + CORE.config_path = work_dir.parent / "tidy.yaml" + CORE.build_path = work_dir + esp32 = CORE.data.setdefault(KEY_ESP32, {}) + esp32[KEY_IDF_VERSION] = cv.Version.parse(settings.idf_version) + esp32[KEY_VARIANT] = settings.variant + # The target framework drives the PlatformIO-library -> IDF-component + # converter and ESPHome's CORE.using_arduino / using_esp_idf helpers. + CORE.data.setdefault(KEY_CORE, {})[KEY_TARGET_PLATFORM] = "esp32" + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = settings.target_framework + + +# Special IDF "components" that are tools/subprojects, not requirable by an app +# (they provide no public includes and break requirement resolution), plus our +# own ``main``. +_NON_REQUIRABLE_COMPONENTS = frozenset( + {"bootloader", "esptool_py", "partition_table", "main"} +) + + +def _parse_lib_deps(platformio_ini: Path, framework: str): + """Parse the framework's ``lib_deps`` from platformio.ini into Library specs. + + These are the PlatformIO libraries ESPHome components pull in via + ``cg.add_library``. The set is framework-specific: the arduino envs add + libs (FastLED, NeoPixelBus, MideaUART, ...) the idf envs don't. We read the + relevant ``[common*]`` sections directly (resolving the env's ``extends`` + chain) and skip the ``${...}`` cross-references and non-library entries. + """ + import configparser + + from esphome.core import Library + + parser = configparser.ConfigParser(interpolation=None, strict=False) + parser.read(platformio_ini) + + sections = [("common", "lib_deps_base"), ("common", "lib_deps")] + if framework == "arduino": + sections += [ + ("common:arduino", "lib_deps"), + ("common:esp32-arduino", "lib_deps"), + ] + else: + sections += [ + ("common:idf", "lib_deps"), + ("common:esp32-idf", "lib_deps"), + ] + + tokens: list[str] = [] + for section, key in sections: + if parser.has_option(section, key): + tokens += parser.get(section, key).splitlines() + + libs: list[Library] = [] + seen: set[str] = set() + for token in tokens: + token = token.split(";", 1)[0].strip() # drop trailing ; comment + # Skip blanks, ${...} cross-refs, and +<...> source filters. + if not token or token.startswith(("${", "+<")) or token in seen: + continue + seen.add(token) + if "://" in token or ".git" in token: + libs.append(Library(token, None, token)) # git repository (with #ref) + elif "@" in token: + name, _, version = token.partition("@") + libs.append(Library(name, version)) + # A bare name (SPI, Wire, WiFi, Networking, "ESP32 Async UDP", ...) is an + # Arduino framework built-in provided by arduino-esp32, not a convertible + # registry library (no owner/version), so skip it. + return libs + + +def _convert_pio_libs( + platformio_ini: Path, framework: str +) -> dict[str, dict[str, str]]: + """Convert the PlatformIO libs to IDF components; return manifest deps. + + Returns a mapping suitable for an ``idf_component.yml`` ``dependencies`` + block (``{name: {"override_path": }}``), reusing + ESPHome's own PlatformIO->IDF converter (registry/git resolution, no pio). + + The whole library set is resolved as a single batch so a shared transitive + dependency (e.g. esphome/libsodium pulled by both noise-c and esp_wireguard) + is deduplicated to one component instead of clashing override_path entries. + """ + from esphome.espidf.component import generate_idf_components + + libraries = _parse_lib_deps(platformio_ini, framework) + deps: dict[str, dict[str, str]] = {} + for component in generate_idf_components(libraries): + deps[component.get_sanitized_name()] = {"override_path": str(component.path)} + return deps + + +def _arduino_excluded_stubs(work_dir: Path) -> dict[str, dict]: + """Stub the arduino-bundled IDF components ESPHome doesn't use. + + arduino-esp32 declares deps (libsodium, RainMaker, modbus, ...) that ESPHome + replaces with its own library (noise-c) or doesn't use; point each at an + empty override_path component so the IDF manager doesn't resolve/download + them -- notably so ``espressif/libsodium`` doesn't clash with the converted + noise-c's ``libsodium``. Mirrors esp32's ``_write_idf_component_yml``. + + Components ESPHome's own idf_component.yml provides (e.g. lan867x for + ethernet) are NOT stubbed -- those are real deps we need, and arduino-esp32 + resolves to the same component rather than conflicting. + """ + import yaml + + from esphome.components.esp32 import ( + ARDUINO_EXCLUDED_IDF_COMPONENTS, + _idf_component_dep_name, + _idf_component_stub_name, + ) + + esphome_dir = Path(__file__).resolve().parent.parent + base_manifest = yaml.safe_load( + (esphome_dir / "idf_component.yml").read_text(encoding="utf-8") + ) + esphome_deps = set(base_manifest.get("dependencies") or {}) + + stubs_dir = work_dir / "component_stubs" + stubs_dir.mkdir(parents=True, exist_ok=True) + deps: dict[str, dict] = {} + for component in sorted(ARDUINO_EXCLUDED_IDF_COMPONENTS): + if _idf_component_dep_name(component) in esphome_deps: + continue # ESPHome needs this one for real (don't stub it away) + stub_path = stubs_dir / _idf_component_stub_name(component) + stub_path.mkdir(exist_ok=True) + (stub_path / "CMakeLists.txt").write_text( + "idf_component_register()\n", encoding="utf-8" + ) + deps[_idf_component_dep_name(component)] = { + "version": "*", + "override_path": str(stub_path), + } + return deps + + +def _write_tidy_project( + work_dir: Path, + requires: list[str], + extra_deps: dict[str, dict[str, str]], + settings: _Settings, +) -> None: + """Generate the minimal IDF CMake project (top + main + idf_component.yml).""" + main_dir = work_dir / "main" + main_dir.mkdir(parents=True, exist_ok=True) + + compile_options = "\n".join( + f'idf_build_set_property(COMPILE_OPTIONS "-D{define}" APPEND)' + for define in settings.platform_defines + ) + (work_dir / "CMakeLists.txt").write_text( + _TOP_CMAKELISTS.format( + name=TIDY_PROJECT_NAME, + compile_options=compile_options, + idf_target=settings.idf_target, + ), + encoding="utf-8", + ) + (main_dir / "CMakeLists.txt").write_text( + _MAIN_CMAKELISTS.format(requires=" ".join(requires)), encoding="utf-8" + ) + (main_dir / "tidy.cpp").write_text(_TIDY_MAIN_CPP, encoding="utf-8") + + # Managed components: ESPHome's own manifest (arduinojson, lvgl, mdns, ...), + # plus the converted PlatformIO libs as local (override_path) deps. Placing + # it in main/ makes every dep a requirement of the main component, so their + # public includes land on the tidy translation unit. + import yaml + + esphome_dir = Path(__file__).resolve().parent.parent # esphome/espidf -> esphome + manifest = yaml.safe_load( + (esphome_dir / "idf_component.yml").read_text(encoding="utf-8") + ) + manifest.setdefault("dependencies", {}).update(extra_deps) + (main_dir / "idf_component.yml").write_text( + yaml.safe_dump(manifest, sort_keys=False), encoding="utf-8" + ) + + # ESPHome's static-analysis sdkconfig (repo root): enables the flags any + # component sets (e.g. CONFIG_BT_ENABLED) so sdkconfig-gated IDF components + # register and expose their includes. IDF reads ``sdkconfig.defaults`` from + # the project root. + (work_dir / "sdkconfig.defaults").write_text( + (esphome_dir.parent / "sdkconfig.defaults").read_text(encoding="utf-8"), + encoding="utf-8", + ) + + +def _generate_compile_commands( + work_dir: Path, settings: _Settings, platformio_ini: Path +) -> Path: + """Generate the tidy project and run ``idf.py reconfigure`` (no build). + + Two-phase, like a real ESPHome build: a first configure with no builtin + requires discovers which components actually register for the target (e.g. + ``esp_tee`` only registers on c5/c6/h2), then a second configure requires + that discovered set so their public includes reach the tidy TU. + """ + import logging + + from esphome.build_gen.espidf import get_available_components + from esphome.espidf import toolchain + + # Surface ESPHome's INFO logs (ESP-IDF framework download/extract/install, + # git-library clones) -- they go through logging, which the clang-tidy + # script otherwise leaves at WARNING so the first-run downloads look silent. + logging.basicConfig(level=logging.INFO, format="%(message)s") + + _setup_core(work_dir, settings) + + # Framework deps (e.g. arduino-esp32) + PlatformIO libs converted to local + # IDF components, all added to the manifest as deps. + extra_deps = dict(settings.framework_deps) + extra_deps.update(_convert_pio_libs(platformio_ini, settings.target_framework)) + if settings.target_framework == "arduino": + # Stub the arduino-bundled components ESPHome doesn't use (avoids the + # libsodium clash with noise-c and ~26 unused heavy downloads). + extra_deps.update(_arduino_excluded_stubs(work_dir)) + + # Phase 1: discover the components available for this target. + _write_tidy_project(work_dir, [], extra_deps, settings) + if toolchain.run_reconfigure() != 0: + raise RuntimeError("idf.py reconfigure (discovery) failed") + + requires = sorted( + set(get_available_components() or []) - _NON_REQUIRABLE_COMPONENTS + ) + + # Phase 2: require every available builtin component. + _write_tidy_project(work_dir, requires, extra_deps, settings) + if toolchain.run_reconfigure() != 0: + raise RuntimeError("idf.py reconfigure failed") + + return work_dir / "build" / "compile_commands.json" + + +def _idedata_from_tidy_project(compile_commands: Path) -> dict: + """Assemble idedata from the single tidy translation unit. + + Unlike a real ESPHome build (many ``/src/esphome/`` TUs unioned), the tidy + project has one TU (``main/tidy.cpp``) that -- by requiring every component -- + already carries the full include set, so we parse it directly. + """ + import json + + from esphome.espidf.idedata import _get_toolchain_includes, _parse_entry + + entries = json.loads(Path(compile_commands).read_text(encoding="utf-8")) + entry = next((e for e in entries if e["file"].endswith("tidy.cpp")), None) + if entry is None: + raise RuntimeError(f"tidy.cpp not found in {compile_commands}") + cxx_path, defines, includes, cxx_flags = _parse_entry(entry) + + return { + "cxx_path": cxx_path, + "cxx_flags": cxx_flags, + "defines": defines, + "includes": { + "build": includes, + "toolchain": _get_toolchain_includes(cxx_path), + }, + } + + +def load_idedata(environment: str, temp_folder: str, platformio_ini: Path) -> dict: + if explicit := os.environ.get("ESPHOME_IDF_COMPILE_COMMANDS"): + compile_commands = Path(explicit) + else: + # The tidy env is ``--tidy`` (e.g. esp32-idf-tidy, + # esp32s3-arduino-tidy); derive the target, variant and framework. + settings = _settings_for(environment) + # Resolve to an absolute path: ``override_path`` entries in the generated + # component manifests are interpreted by the IDF component manager relative + # to the manifest's own directory, so a relative work dir would be + # mis-resolved (doubled under ``main/``). + work_dir = ( + Path(temp_folder) + / f"idf-tidy-{settings.idf_target}-{settings.target_framework}" + ).resolve() + compile_commands = _generate_compile_commands( + work_dir, settings, platformio_ini + ) + + if not compile_commands.is_file(): + raise RuntimeError(f"compile_commands.json not found: {compile_commands}") + return _idedata_from_tidy_project(compile_commands) diff --git a/script/clang-tidy b/script/clang-tidy index 56c0a9db71..ce266e2382 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -28,6 +28,12 @@ from helpers import ( temp_header_file, ) +# Limit the ESP-IDF tool install to esp32 for clang-tidy: the one xtensa-esp-elf +# toolchain bundles the s2/s3 compilers too, so all xtensa tidy envs still +# reconfigure while the large riscv32-esp-elf toolchain is skipped. Must be set +# before esphome.espidf.framework is imported (lazily, via load_idedata). +os.environ.setdefault("ESPHOME_IDF_DEFAULT_TARGETS", "esp32") + def clang_options(idedata): cmd = [] @@ -52,6 +58,10 @@ def clang_options(idedata): "-mfix-esp32-psram-cache-issue", "-mfix-esp32-psram-cache-strategy=memw", "-fno-tree-switch-conversion", + # GCC-only flags emitted by the native ESP-IDF toolchain build + "-freorder-blocks", + "-fno-jump-tables", + "-fno-shrink-wrap", ) if "zephyr" in triplet: @@ -97,8 +107,20 @@ 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 omit_flags) + # Copy compiler flags, dropping: ones clang doesn't understand; -Werror* + # (clang-tidy enforces .clang-tidy's WarningsAsErrors, and a build -Werror + # would bypass the -clang-diagnostic-* suppressions); and -std= (the native + # ESP-IDF build defaults to gnu++2b, but ESPHome compiles with gnu++20 per + # platformio.ini -- analyzing as C++23 flags code that doesn't build under + # gnu++20). Force gnu++20 to match the real build. + cmd.extend( + flag + for flag in idedata["cxx_flags"] + if flag not in omit_flags + and not flag.startswith("-Werror") + and not flag.startswith("-std=") + ) + cmd.append("-std=gnu++20") # defines cmd.extend(f"-D{define}" for define in idedata["defines"]) diff --git a/script/helpers.py b/script/helpers.py index 1ebfe405a7..8b6751c1d3 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -664,26 +664,22 @@ def load_idedata(environment: str) -> dict[str, Any]: start_time = time.time() print(f"Loading IDE data for environment '{environment}'...") - platformio_ini = Path(root_path) / "platformio.ini" + # Reuse the clang-tidy input hash as the cache key: it already covers every + # file baked into the generated idedata (platformio.ini, sdkconfig.defaults, + # esphome/idf_component.yml), so this can't drift from that file list. A + # content hash -- unlike an mtime comparison -- stays correct across git + # checkouts, which don't preserve mtimes. + from clang_tidy_hash import calculate_clang_tidy_hash + temp_idedata = Path(temp_folder) / f"idedata-{environment}.json" - changed = False - if ( - not platformio_ini.is_file() - or not temp_idedata.is_file() - or platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime - ): - changed = True + temp_hash = Path(temp_folder) / f"idedata-{environment}.hash" - if "idf" in environment: - # remove full sdkconfig when the defaults have changed so that it is regenerated - default_sdkconfig = Path(root_path) / "sdkconfig.defaults" - temp_sdkconfig = Path(temp_folder) / f"sdkconfig-{environment}" - - if not temp_sdkconfig.is_file(): - changed = True - elif default_sdkconfig.stat().st_mtime >= temp_sdkconfig.stat().st_mtime: - temp_sdkconfig.unlink() - changed = True + cache_key = calculate_clang_tidy_hash() + changed = ( + not temp_idedata.is_file() + or not temp_hash.is_file() + or temp_hash.read_text().strip() != cache_key + ) if not changed: data = json.loads(temp_idedata.read_text()) @@ -694,7 +690,12 @@ def load_idedata(environment: str) -> dict[str, Any]: # ensure temp directory exists before running pio, as it writes sdkconfig to it Path(temp_folder).mkdir(exist_ok=True) - if "nrf" in environment: + platformio_ini = Path(root_path) / "platformio.ini" + if "esp32" in environment: + from esphome.espidf.clang_tidy import load_idedata as idf_load_idedata + + data = idf_load_idedata(environment, temp_folder, platformio_ini) + elif "nrf" in environment: from helpers_zephyr import load_idedata as zephyr_load_idedata data = zephyr_load_idedata(environment, temp_folder, platformio_ini) @@ -705,6 +706,7 @@ def load_idedata(environment: str) -> dict[str, Any]: match = re.search(r'{\s*".*}', stdout.decode("utf-8")) data = json.loads(match.group()) temp_idedata.write_text(json.dumps(data, indent=2) + "\n") + temp_hash.write_text(cache_key + "\n") elapsed = time.time() - start_time print(f"IDE data generated and cached in {elapsed:.2f} seconds") diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 2996490295..b277ed18d0 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -1,15 +1,11 @@ -# ESP-IDF sdkconfig defaults used for development purposes only, not used during runtime. Used when PlatformIO is ran -# directly from the source directory, e.g. by IDEs or for static analysis (clang-tidy). This should enable all flags -# that are set by any component. +# ESP-IDF sdkconfig defaults used for development purposes only, not used during runtime. Used for static analysis +# (clang-tidy) -- by both the PlatformIO and the native ESP-IDF toolchain paths -- and when PlatformIO is run directly +# from the source directory (e.g. by IDEs). This should enable all flags that are set by any component. # esp32 -CONFIG_COMPILER_OPTIMIZATION_DEFAULT=n CONFIG_COMPILER_OPTIMIZATION_SIZE=y -CONFIG_PARTITION_TABLE_CUSTOM=y -#CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_SINGLE_APP=n CONFIG_FREERTOS_HZ=1000 -CONFIG_ESP_TASK_WDT=y +CONFIG_ESP_TASK_WDT_INIT=y CONFIG_ESP_TASK_WDT_PANIC=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n @@ -18,8 +14,7 @@ CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n CONFIG_BT_ENABLED=y # esp32_camera -CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC=y -CONFIG_ESP32_SPIRAM_SUPPORT=y +CONFIG_SPIRAM=y # zigbee CONFIG_ZB_ENABLED=y