diff --git a/.gitlab/ci/host-test.yml b/.gitlab/ci/host-test.yml index 6d32d7c82e4..6185496f178 100644 --- a/.gitlab/ci/host-test.yml +++ b/.gitlab/ci/host-test.yml @@ -482,22 +482,6 @@ pytest_buildv2_system: --work-dir ${CI_PROJECT_DIR}/test_build_system --junitxml ${CI_PROJECT_DIR}/XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} - -- - test_non_default_target.py - test_component_manager.py - test_build.py - test_bootloader.py - test_git.py - test_kconfig.py - test_partition.py - test_reproducible_build.py - test_sdkconfig.py - test_versions.py - test_common.py - test_components.py - test_cmake.py - test_idf_extension.py - test_rebuild.py pytest_build_system_macos: extends: diff --git a/.gitlab/ci/test-win.yml b/.gitlab/ci/test-win.yml index c617358959f..bf2a116e91a 100644 --- a/.gitlab/ci/test-win.yml +++ b/.gitlab/ci/test-win.yml @@ -170,19 +170,3 @@ pytest_buildv2_system_win: --parallel-index ${CI_NODE_INDEX} --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} - -- - test_non_default_target.py - test_component_manager.py - test_build.py - test_bootloader.py - test_git.py - test_kconfig.py - test_partition.py - test_reproducible_build.py - test_sdkconfig.py - test_versions.py - test_common.py - test_components.py - test_cmake.py - test_idf_extension.py - test_rebuild.py diff --git a/components/bootloader_support/CMakeLists.txt b/components/bootloader_support/CMakeLists.txt index 070686bc0bd..3e5620771ff 100644 --- a/components/bootloader_support/CMakeLists.txt +++ b/components/bootloader_support/CMakeLists.txt @@ -72,7 +72,13 @@ endif() if(BOOTLOADER_BUILD OR CONFIG_APP_BUILD_TYPE_RAM) set(include_dirs "include" "bootloader_flash/include" "private_include") - set(priv_requires micro-ecc spi_flash efuse esp_bootloader_format esp_app_format esptool_py) + # micro-ecc is only needed for secure boot sources built under BOOTLOADER_BUILD. + # For CONFIG_APP_BUILD_TYPE_RAM (loadable ELF app) we don't build those sources, + # and micro-ecc lives in the bootloader subproject so it isn't available in app builds. + set(priv_requires spi_flash efuse esp_bootloader_format esp_app_format esptool_py) + if(BOOTLOADER_BUILD) + list(APPEND priv_requires micro-ecc) + endif() # `esp_hal_ana_conv` is required by bootloader_random_esp32xx.c list(APPEND priv_requires esp_hal_wdt esp_hal_gpio esp_hal_uart esp_hal_ana_conv esp_hal_rtc_timer esp_hal_security esp_hal_clock) diff --git a/components/bt/Kconfig b/components/bt/Kconfig index 9bd635734db..d9c2fdc7536 100644 --- a/components/bt/Kconfig +++ b/components/bt/Kconfig @@ -66,7 +66,7 @@ menu "Bluetooth" menu "Controller Options" depends on BT_CONTROLLER_ENABLED - source "$IDF_PATH/components/bt/controller/$IDF_TARGET/Kconfig.in" + osource "$IDF_PATH/components/bt/controller/$IDF_TARGET/Kconfig.in" endmenu config BT_RELEASE_IRAM diff --git a/components/esp-tls/CMakeLists.txt b/components/esp-tls/CMakeLists.txt index b5eabfe4dc5..468456e67e2 100644 --- a/components/esp-tls/CMakeLists.txt +++ b/components/esp-tls/CMakeLists.txt @@ -12,6 +12,11 @@ endif() set(priv_req http_parser esp_timer) if(NOT ${IDF_TARGET} STREQUAL "linux") list(APPEND priv_req lwip) +else() + # For linux target on Build system v2, add lwip to PRIV_REQUIRES when CONFIG_LWIP_ENABLE is set. + if(CONFIG_LWIP_ENABLE AND IDF_BUILD_V2) + list(APPEND priv_req lwip) + endif() endif() idf_component_register(SRCS "${srcs}" @@ -27,8 +32,7 @@ if(NOT ${IDF_TARGET} STREQUAL "linux") idf_component_get_property(lwip lwip COMPONENT_LIB) set_property(TARGET ${lwip} APPEND PROPERTY LINK_INTERFACE_MULTIPLICITY 5) else() - # Check if LWIP in the build for linux target to adapt esp-tls compatibility layer - idf_build_get_property(build_components BUILD_COMPONENTS) + # For linux target, define ESP_TLS_WITH_LWIP if LWIP is enabled in the build if(CONFIG_LWIP_ENABLE) target_compile_definitions(${COMPONENT_LIB} PRIVATE ESP_TLS_WITH_LWIP=1) endif() diff --git a/components/esp_coex/CMakeLists.txt b/components/esp_coex/CMakeLists.txt index 3160d55da2b..252d3402eca 100644 --- a/components/esp_coex/CMakeLists.txt +++ b/components/esp_coex/CMakeLists.txt @@ -50,5 +50,6 @@ if(CONFIG_ESP_COEX_ENABLED) endif() target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") + target_link_libraries(${COMPONENT_LIB} INTERFACE "-u esp_coex_init_include_func") endif() endif() diff --git a/components/esp_coex/src/coexist.c b/components/esp_coex/src/coexist.c index 1cb67c66f65..f78395f26ff 100644 --- a/components/esp_coex/src/coexist.c +++ b/components/esp_coex/src/coexist.c @@ -1,11 +1,12 @@ /* - * SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include "esp_coexist.h" #include "private/esp_coexist_internal.h" +#include "esp_private/startup_internal.h" #include "soc/soc_caps.h" #if CONFIG_EXTERNAL_COEX_ENABLE @@ -295,3 +296,17 @@ esp_err_t esp_coex_wifi_i154_enable(void) return ESP_OK; } #endif + +#if CONFIG_ESP_COEX_SW_COEXIST_ENABLE || CONFIG_ESP_COEX_EXTERNAL_COEXIST_ENABLE +void esp_coex_init_include_func(void) +{ + // Hook to force the linker to include this file +} + +ESP_SYSTEM_INIT_FN(init_coexist, SECONDARY, BIT(0), 204) +{ + esp_coex_adapter_register(&g_coex_adapter_funcs); + coex_pre_init(); + return ESP_OK; +} +#endif // CONFIG_ESP_COEX_SW_COEXIST_ENABLE || CONFIG_ESP_COEX_EXTERNAL_COEXIST_ENABLE diff --git a/components/esp_phy/CMakeLists.txt b/components/esp_phy/CMakeLists.txt index b566f006e8c..7c827338a06 100644 --- a/components/esp_phy/CMakeLists.txt +++ b/components/esp_phy/CMakeLists.txt @@ -10,6 +10,8 @@ if(IDF_TARGET STREQUAL "esp32p4") return() endif() +set(embed_files) + if(CONFIG_ESP_PHY_ENABLED) set(srcs "src/phy_override.c" "src/lib_printf.c" "src/phy_common.c") diff --git a/components/esp_system/CMakeLists.txt b/components/esp_system/CMakeLists.txt index 99942a7c8e0..47935d218b8 100644 --- a/components/esp_system/CMakeLists.txt +++ b/components/esp_system/CMakeLists.txt @@ -153,10 +153,6 @@ if(CONFIG_VFS_SUPPORT_IO) idf_component_optional_requires(PRIVATE vfs) endif() -if(CONFIG_SW_COEXIST_ENABLE OR CONFIG_EXTERNAL_COEX_ENABLE) - idf_component_optional_requires(PRIVATE esp_coex) -endif() - if(NOT BOOTLOADER_BUILD) if(CONFIG_SPIRAM) idf_component_optional_requires(PRIVATE esp_psram) diff --git a/components/esp_system/startup_funcs.c b/components/esp_system/startup_funcs.c index 86578e6148c..9be0d219066 100644 --- a/components/esp_system/startup_funcs.c +++ b/components/esp_system/startup_funcs.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -23,10 +23,6 @@ #include "hal/uart_types.h" #include "hal/uart_ll.h" -#if CONFIG_SW_COEXIST_ENABLE || CONFIG_EXTERNAL_COEX_ENABLE -#include "private/esp_coexist_internal.h" -#endif - #if CONFIG_PM_ENABLE || CONFIG_PM_WORKAROUND_FREQ_LIMIT_ENABLED #include "esp_pm.h" #include "esp_private/pm_impl.h" @@ -127,15 +123,6 @@ ESP_SYSTEM_INIT_FN(init_apb_dma, SECONDARY, BIT(0), 203) } #endif -#if CONFIG_SW_COEXIST_ENABLE || CONFIG_EXTERNAL_COEX_ENABLE -ESP_SYSTEM_INIT_FN(init_coexist, SECONDARY, BIT(0), 204) -{ - esp_coex_adapter_register(&g_coex_adapter_funcs); - coex_pre_init(); - return ESP_OK; -} -#endif // CONFIG_SW_COEXIST_ENABLE || CONFIG_EXTERNAL_COEX_ENABLE - #if SOC_RECOVERY_BOOTLOADER_SUPPORTED ESP_SYSTEM_INIT_FN(init_bootloader_offset, SECONDARY, BIT(0), 205) { diff --git a/components/esp_system/system_init_fn.txt b/components/esp_system/system_init_fn.txt index f3f0fe1f1a2..ba6d891fb12 100644 --- a/components/esp_system/system_init_fn.txt +++ b/components/esp_system/system_init_fn.txt @@ -114,7 +114,7 @@ SECONDARY: 151: nvs_sec_provider_register_hmac_scheme in components/nvs_sec_prov SECONDARY: 201: init_pm in components/esp_system/startup_funcs.c on BIT(0) SECONDARY: 202: init_pm_flash_freq_limit in components/esp_pm/pm_c5_flash_freq_limit.c on BIT(0) SECONDARY: 203: init_apb_dma in components/esp_system/startup_funcs.c on BIT(0) -SECONDARY: 204: init_coexist in components/esp_system/startup_funcs.c on BIT(0) +SECONDARY: 204: init_coexist in components/esp_coex/src/coexist.c on BIT(0) SECONDARY: 205: init_bootloader_offset in components/esp_system/startup_funcs.c on BIT(0) # usb_console needs to create an esp_timer at startup. diff --git a/components/esp_wifi/CMakeLists.txt b/components/esp_wifi/CMakeLists.txt index 1a9e8094ba1..13e3b8a3e28 100644 --- a/components/esp_wifi/CMakeLists.txt +++ b/components/esp_wifi/CMakeLists.txt @@ -13,10 +13,26 @@ if( NOT CONFIG_ESP_WIFI_ENABLED "src/wifi_netif.c" "src/wifi_default_ap.c") + # In build system v2, idf_component_optional_requires() includes the target + # component into the build immediately, so these stub sources are compiled + # even on targets where Wi-Fi is not supported. The stubs need esp_event and + # esp_netif headers, so those dependencies must be declared here for v2. + # + # In build system v1, idf_component_optional_requires() only links a + # component that is already part of the build. On non-Wi-Fi targets, this + # component is never pulled in, so the stubs are never compiled and the + # dependencies are not needed. Additionally, the CMAKE_BUILD_EARLY_EXPANSION + # guard ensures that the stubs are not compiled for v1. + set(priv_reqs "") + if(IDF_BUILD_V2) + set(priv_reqs "esp_event" "esp_netif") + endif() + # This component provides "esp_wifi" "wifi_apps/nan_app" headers if WiFi not enabled # (implementation supported optionally in a managed component esp_wifi_remote) idf_component_register(SRCS "${srcs}" - INCLUDE_DIRS "include" "wifi_apps/nan_app/include") + INCLUDE_DIRS "include" "wifi_apps/nan_app/include" + PRIV_REQUIRES ${priv_reqs}) return() endif() diff --git a/components/freertos/CMakeLists.txt b/components/freertos/CMakeLists.txt index 1401601f715..3e7a24f1f5a 100644 --- a/components/freertos/CMakeLists.txt +++ b/components/freertos/CMakeLists.txt @@ -193,6 +193,11 @@ if(arch STREQUAL "linux") PROPERTIES COMPILE_OPTIONS "-Wno-strict-prototypes" ) + if(APPLE) + target_link_libraries(${COMPONENT_LIB} INTERFACE "-u _app_main") + else() + target_link_libraries(${COMPONENT_LIB} INTERFACE "-u app_main") + endif() else() idf_component_get_property(COMPONENT_DIR freertos COMPONENT_DIR) diff --git a/components/linux/CMakeLists.txt b/components/linux/CMakeLists.txt index e446de0e9db..d2716494317 100644 --- a/components/linux/CMakeLists.txt +++ b/components/linux/CMakeLists.txt @@ -3,6 +3,7 @@ if(NOT "${target}" STREQUAL "linux") return() endif() +set(srcs) set(includes "include") if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") list(APPEND srcs getrandom.c assert_func.c) diff --git a/components/partition_table/CMakeLists.txt b/components/partition_table/CMakeLists.txt index da57cbea2b1..e4697b27e89 100644 --- a/components/partition_table/CMakeLists.txt +++ b/components/partition_table/CMakeLists.txt @@ -134,7 +134,12 @@ if(${target} STREQUAL "linux" AND EXISTS ${partition_csv}) # target. This is a hack, since that target name is an implementation detail # of the build system. - add_dependencies(__idf_build_target partition-table) + if(IDF_BUILD_V2) + # Build system v2 does not support __idf_build_target target, so add to ALL target. + add_custom_target(partition-table-linux-build ALL DEPENDS partition-table) + else() + add_dependencies(__idf_build_target partition-table) + endif() endif() # Add signing steps diff --git a/components/soc/CMakeLists.txt b/components/soc/CMakeLists.txt index 19ab9b15f1b..c075f731ecb 100644 --- a/components/soc/CMakeLists.txt +++ b/components/soc/CMakeLists.txt @@ -2,6 +2,8 @@ idf_build_get_property(target IDF_TARGET) set(target_folder "${target}") +set(srcs) + # On Linux the soc component is a simple wrapper, without much functionality if(NOT ${target} STREQUAL "linux") if(CONFIG_IDF_TARGET_ESP32H4 AND NOT CONFIG_ESP32H4_SELECTS_REV_MP) # TODO: ESP32H4 IDF-13835 diff --git a/components/tcp_transport/CMakeLists.txt b/components/tcp_transport/CMakeLists.txt index dabca0c0ccd..9c4fce482df 100644 --- a/components/tcp_transport/CMakeLists.txt +++ b/components/tcp_transport/CMakeLists.txt @@ -16,6 +16,13 @@ endif() set(req esp-tls) if(NOT ${IDF_TARGET} STREQUAL "linux") list(APPEND req lwip esp_timer) +else() + # Add esp_timer to REQUIRES for the linux target when Build system v2 is used + # since Build system v2 does not support BUILD_COMPONENTS and cannot link to + # esp_timer conditionally if LWIP is in the build. + if(CONFIG_LWIP_IPV4 AND IDF_BUILD_V2) + list(APPEND req esp_timer) + endif() endif() idf_component_register(SRCS "${srcs}" @@ -23,9 +30,10 @@ idf_component_register(SRCS "${srcs}" PRIV_INCLUDE_DIRS "private_include" REQUIRES ${req}) -if(${IDF_TARGET} STREQUAL "linux") +if(${IDF_TARGET} STREQUAL "linux" AND NOT IDF_BUILD_V2) # Check if LWIP in the build for linux target to add esp_timer to the dependencies # since socks_proxy transport needs it and lwip & linux build could use it + # Note: Build System v2 adds dependency on esp_timer during component registration idf_build_get_property(build_components BUILD_COMPONENTS) if("lwip" IN_LIST build_components) idf_component_get_property(esp_timer esp_timer COMPONENT_LIB) diff --git a/components/ulp/cmake/IDFULPProject.cmake b/components/ulp/cmake/IDFULPProject.cmake index fbab4d69928..ec5d85e9db1 100644 --- a/components/ulp/cmake/IDFULPProject.cmake +++ b/components/ulp/cmake/IDFULPProject.cmake @@ -49,7 +49,7 @@ function(ulp_apply_default_sources ulp_app_name) list(APPEND ULP_PREPRO_ARGS -I${sdkconfig_dir}) list(APPEND ULP_PREPRO_ARGS -I${IDF_PATH}/components/esp_system/ld) - target_include_directories(${ulp_app_name} PRIVATE ${COMPONENT_INCLUDES}) + target_include_directories(${ulp_app_name} PRIVATE ${COMPONENT_INCLUDES} ${sdkconfig_dir}) # Pre-process the linker script if(CONFIG_ULP_COPROC_TYPE_RISCV) diff --git a/docs/en/api-guides/build-system-v2.rst b/docs/en/api-guides/build-system-v2.rst index bdb9c998cbc..fa87f8b0ee9 100644 --- a/docs/en/api-guides/build-system-v2.rst +++ b/docs/en/api-guides/build-system-v2.rst @@ -7,6 +7,8 @@ Build System v2 ESP-IDF CMake-based build system v2, referred to in this documentation simply as v2 or build system, is a successor to the original CMake-based :doc:`/api-guides/build-system`, referred to as v1. The v2 addresses limitations introduced in the previous version while trying to maintain backward compatibility for components written for v1. The most significant changes include the ability to use Kconfig variables to specify component dependencies, the removal of early component evaluation using CMake script mode, and support for writing components using the native CMake approach. While v2 aims to be as backward compatible with v1 as possible, meaning most components written for v1 should work without modification with v2, there are design differences between v1 and v2 that may require changes in v1 components to work with v2. The incompatibilities are described in :ref:`cmakev2-breaking-changes`. +Example applications for Build System v2 are described in the :idf_file:`Build System v2 examples README `. + Creating a New Project ====================== @@ -274,6 +276,34 @@ LINKER_SCRIPTS: Also ensure that the ``esp_chip_info`` function is retained in the final binary even when section garbage collection, ``--gc-sections``, is enabled. This is required because ``esp_target_info.ld`` defines ``esp_target_chip_info`` as an alias for ``esp_chip_info``, and without forcing the linker to include it, the underlying ``esp_chip_info`` function could be discarded as unused. +.. _cmakev2-build-event-callbacks: + +Build Event Callback Framework +============================== + +The build system allows components to register callbacks that are invoked at specific points in the build lifecycle. This provides a generic way for components to run custom steps (for example, running a tool on the executable or adding dependencies) without relying on internal build targets or properties. + +Components register a callback in their ``project_include.cmake`` using :cmakev2:ref:`idf_component_register_build_event_callback`. The callback must be a CMake function defined in the same file. At the specified event, the build system invokes the callback and passes the relevant CMake target as the first argument (for example, the executable target for ``POST_ELF``). + +Currently supported events: + +- **POST_ELF** — Fired after the executable target is created and linked, but before the binary (``.bin``) image is generated. The callback receives the executable target name. Use this to perform actions on the ELF by attaching a ``POST_BUILD`` command to the executable with ``add_custom_command(TARGET ... POST_BUILD ...)``. + +Example: perform actions on the ELF after linking: + +.. code-block:: cmake + + # In project_include.cmake + function(my_post_elf_hook target) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND my_tool "$" + COMMENT "Running my_tool on the executable") + endfunction() + + idf_component_register_build_event_callback(EVENT POST_ELF CALLBACK my_post_elf_hook) + +Additional build events may be added in future when required. + .. _cmakev2-breaking-changes: Breaking Changes for v1 Components @@ -326,6 +356,13 @@ The hello_world example ``CMakeLists_v2.txt`` for v2. idf::spi_flash ) +``idf_build_add_post_elf_dependency`` and ``idf_build_get_post_elf_dependencies`` are Unavailable +------------------------------------------------------------------------------------------------- + +In v1, components that need to run a step after the executable is linked but before the binary image is generated use ``idf_build_add_post_elf_dependency`` to register a dependency and ``idf_build_get_post_elf_dependencies`` to retrieve the list of such dependencies (see the :doc:`build system ` API). These functions are **not available** in v2. + +In v2, use the :ref:`Build Event Callback Framework ` instead. Register a **POST_ELF** callback with :cmakev2:ref:`idf_component_register_build_event_callback` in your component's ``project_include.cmake``. The callback receives the executable target name; use it to attach a ``POST_BUILD`` command (e.g. with ``add_custom_command(TARGET ... POST_BUILD ...)``) or to add custom targets that depend on the executable. This achieves the same ordering (run after ELF, before binary) without relying on internal build properties. + The ``BUILD_COMPONENTS`` Build Property is Unavailable ------------------------------------------------------ @@ -432,6 +469,34 @@ Strict Component Precedence The v2 strictly adheres to the component precedence for components with the same name, as described in :ref:`cmake-components-same-name`. While v1 allows components discovered in directories specified with the ``EXTRA_COMPONENT_DIRS`` variable to be overridden by `Local Directory Dependencies`_ specified in the ``idf_component.yml`` manifest file, this is no longer possible in v2. +The Behavior of ``idf_component_optional_requires`` has Changed +--------------------------------------------------------------- + +In v1, the ``idf_component_optional_requires`` function adds a dependency on a specified component only if that component is already included in the build (for instance, if it is already required by another component). To achieve this, v1 examines the ``BUILD_COMPONENTS`` build property, which is generated during the early evaluation phase and lists all components involved in the build. + +In v2, there is no early collection phase and ``BUILD_COMPONENTS`` does not exist. The build system discovers components as it evaluates dependencies. So v2 cannot use the same "only if already in the build" check; it has to choose a different rule. + +The build system supports two behaviors, controlled by the ``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` build property: + +* **IMMEDIATE (default)** — When a component calls ``idf_component_optional_requires(type req_component)``, the build system includes ``req_component`` and links it to the caller if it is recognized (discovered). No check is made whether the rest of the project actually needs that component. This is safe for multi-binary projects (multiple executables or binaries), but it can pull in more components than necessary and increase build time. + +* **DEFERRED** — The build system does not include or link immediately. It records the request and resolves it later in :cmakev2:ref:`idf_build_library`: the optional component is linked only if it ends up in that library's dependency graph. This matches v1 semantics and keeps the number of linked components minimal. It **must not** be used when building more than one library (see below). + +A multi-binary project is one that creates more than one executable or binary (for example, several application executables built from the same tree). Such a project calls :cmakev2:ref:`idf_build_library` or :cmakev2:ref:`idf_build_executable` more than once. In v2, component targets are shared globally across all libraries. If ``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` is set to **DEFERRED**, the build system resolves optional requirements when it processes each library. When it processes the second or a later library, it may add new links to component targets that are already used by the first library. The first library's metadata (such as the list of linker fragments or linked components) was already computed when that library was processed and is not updated. As a result, linker script generation and section placement for the first library can be incorrect or stale. For this reason, DEFERRED mode is not allowed when more than one library is built; the build fails with an error in that case. **IMMEDIATE** mode does not have this problem, because optional requirements are applied during component evaluation, before any per-library metadata is computed. The side effect of the IMMEDIATE mode is that it can pull in more components than necessary and increase build time. + +:cmakev2:ref:`idf_project_default` (the usual entry point for a single-executable project) sets ``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` to **DEFERRED** before building the default executable when no libraries have been created yet. So if your project uses ``idf_project_default()`` and builds only one executable, you get DEFERRED behavior automatically and do not need to do anything. + +If you do not use :cmakev2:ref:`idf_project_default` and instead call :cmakev2:ref:`idf_project_init` and then the lower-level API (:cmakev2:ref:`idf_build_executable`, :cmakev2:ref:`idf_build_library`) yourself, the default mode is **IMMEDIATE**. If you build only one library/executable and want the same efficient, v1-like behavior as ``idf_project_default``, you must set the mode to DEFERRED yourself after project init: + +.. code-block:: cmake + + idf_project_init() + idf_build_set_property(IDF_COMPONENT_OPTIONAL_REQUIRES_MODE DEFERRED) + idf_build_executable(my_app COMPONENTS main ...) + # ... rest of your project ... + +Do **not** set ``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` to ``DEFERRED`` if you build multiple libraries; the build will error. Keep the default IMMEDIATE in that case. + API Reference ============= diff --git a/examples/build_system/cmakev2/README.md b/examples/build_system/cmakev2/README.md new file mode 100644 index 00000000000..b0e6cc1c46a --- /dev/null +++ b/examples/build_system/cmakev2/README.md @@ -0,0 +1,155 @@ +# Build System v2 Examples + +This directory contains examples demonstrating **ESP-IDF Build System v2** (`cmakev2`). Build System v2 is the next-generation build system for ESP-IDF, offering improved architecture, better CMake integration, and enhanced features. Build System v2 attempts to be backward compatible with Build System v1 applications and components as much as possible. More information about Build System v2 can be found in the ESP-IDF Programming Guide under **API Guides** → **Build System v2**. + +> **Note:** Build System v2 is currently a **Technical Preview**. + +--- + +## Getting Started + +### Migrating an Existing Project + +Change your project's `CMakeLists.txt` from: + +```cmake +# Build System v1 +cmake_minimum_required(VERSION 3.22) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(my_project) +``` + +To: + +```cmake +# Build System v2 +cmake_minimum_required(VERSION 3.22) +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) +project(my_project C CXX ASM) +idf_project_default() +``` + +This update is sufficient for most ESP-IDF projects. Components and application code remain unchanged. + +### Building Examples + +Building any `cmakev2` example follows the standard workflow: + +```bash +cd examples/build_system/cmakev2/get-started/ +idf.py set-target +idf.py build +idf.py flash monitor +``` + +--- + +## Get-Started Examples + +These examples mirror the ones in `examples/get-started/` but use Build System v2. + +### hello_world + +[get-started/hello_world/](./get-started/hello_world/) + +The simplest ESP-IDF project with `cmakev2`. It prints "Hello World" and demonstrates the minimal project structure required: a `CMakeLists.txt` with `idf_project_default()` and a main component. Start here to understand how a basic v2 project is set up. + +--- + +## Build System Features Examples + +These examples demonstrate advanced build system capabilities unique to or enhanced in `cmakev2`. The components used in the examples in this section are registered with `idf_component_register`, which ensures that newly written components remain compatible with Build System v1. + +### component_manager + +[features/component_manager/](./features/component_manager/) + +Shows how to use the ESP Component Registry with `cmakev2`. Declares dependencies in `idf_component.yml`, which are automatically downloads from ESP Component Registry during the build. + +### import_lib + +[features/import_lib/](./features/import_lib/) + +Demonstrates importing third-party CMake libraries using CMake's `ExternalProject_Add()` module. The example downloads and builds [tinyxml2](https://github.com/leethomason/tinyxml2) from GitHub, wraps it as an IDF component, and uses it to parse XML data. This approach is recommended for C++ libraries that use exceptions, as it properly handles ESP-IDF's C++ runtime integration through `PRIV_REQUIRES cxx`. For simpler C libraries, see `import_lib_direct` which demonstrates direct integration without component wrappers. + +### import_prebuilt + +[features/import_prebuilt/](./features/import_prebuilt/) + +Shows how to import pre-compiled static libraries into your project using `add_prebuilt_library()`. The example includes a `prebuilt/` directory containing a component that gets compiled separately, with its output (`libprebuilt.a` and headers) consumed by the main application. Useful for distributing proprietary libraries or speeding up builds with pre-compiled components. + +### multi_config + +[features/multi_config/](./features/multi_config/) + +Demonstrates building multiple configurations of a single application using CMake presets. The example defines development and production presets, each with different `sdkconfig.defaults` files, separate build directories, and conditional source file compilation. Useful for building binaries for different product variants or deployment environments from one codebase. + +Build with presets: +```bash +idf.py --preset default build # Development build +idf.py --preset prod1 build # Product 1 build +idf.py --preset prod2 build # Product 2 build +``` + +### plugins + +[features/plugins/](./features/plugins/) + +Demonstrates link-time plugin registration using the `WHOLE_ARCHIVE` component property. Plugins register themselves automatically at startup using `__attribute__((constructor))` functions, without requiring explicit function calls from the main application. The example shows both dynamic registration (constructor functions) and static registration (linker sections with `KEEP()`). Useful for building extensible applications where plugins can be added or removed by simply including or excluding components. + +### idf_as_lib + +[features/idf_as_lib/](./features/idf_as_lib/) + +Shows how to use ESP-IDF components as a library in external CMake projects. The example showcases how a non-IDF project can invoke the ESP-IDF build system to create a library which the executable can link against. + +--- + +## cmakev2-Specific Examples + +The following examples demonstrate capabilities that are unique to Build System v2 and cannot be achieved with Build System v1. + +### conditional_component + +[features/conditional_component/](./features/conditional_component/) + +Demonstrates conditional component inclusion driven by Kconfig options, with all components written as pure CMake static libraries (no `idf_component_register`). + +### multi_binary + +[features/multi_binary/](./features/multi_binary/) + +Demonstrates building multiple independent firmware binaries from a single project in one build command. Each binary has its own entry point, component dependencies, and flash target. + +The project defines two applications (`app1` and `app2`) with shared and distinct components: + +Build and flash: +```bash +idf.py build # Creates both app1.bin and app2.bin +idf.py app1-flash monitor # Flash and monitor app1 +idf.py app2-flash monitor # Flash and monitor app2 +``` + +This is useful for creating firmware variants with different features, manufacturing test firmware alongside production firmware, or modular applications where different binaries serve different purposes. + +### import_lib_direct + +[features/import_lib_direct/](./features/import_lib_direct/) + +Demonstrates a cmakev2-specific capability of importing external CMake libraries directly at the project level using `FetchContent` without wrapping them as IDF components. The example downloads [lwjson](https://github.com/MaJerle/lwjson), a lightweight JSON parser, and links it directly to the main component using standard CMake `target_link_libraries()`. This pattern works well for pure C libraries and simplifies third-party library integration. + +--- + +## Further Reading + +For comprehensive documentation on Build System v2 APIs, migration details, and advanced usage, see the Build System v2 guide in the ESP-IDF Programming Guide under **API Guides** → **Build System v2**. + +--- + +## Contributing + +When adding new examples: +1. Use `cmakev2` APIs where applicable +2. Include a README.md explaining the example +3. Test on multiple targets (esp32, esp32c3, esp32s3) +4. Document which `cmakev2` features are demonstrated diff --git a/examples/build_system/cmakev2/features/component_manager/CMakeLists.txt b/examples/build_system/cmakev2/features/component_manager/CMakeLists.txt new file mode 100644 index 00000000000..5fd23c2a7fb --- /dev/null +++ b/examples/build_system/cmakev2/features/component_manager/CMakeLists.txt @@ -0,0 +1,7 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) +project(component_manager C CXX ASM) +idf_project_default() diff --git a/examples/build_system/cmakev2/features/component_manager/README.md b/examples/build_system/cmakev2/features/component_manager/README.md new file mode 100644 index 00000000000..b1b437202a8 --- /dev/null +++ b/examples/build_system/cmakev2/features/component_manager/README.md @@ -0,0 +1,68 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Using the component manager for downloading dependencies + +This example demonstrates how to use [IDF Component Manager](https://pypi.org/project/idf-component-manager/) for downloading dependencies from [ESP Component Registry](https://components.espressif.com). More details and use cases of IDF Component Manager can be found in the programming guide under `API Guides` -> `Tools` -> `IDF Component Manager`. + +## How to use the example +### Hardware Required + +This example is designed to work with any commonly available development kit. + +### Build and Flash + +Run `idf.py reconfigure` to configure this project. During CMake execution the component manager will process data from the manifest file `./main/idf_component.yml` where 2 dependencies are defined: + +- `idf: ">=6.0"` - Specifies required version of ESP-IDF. +- `example/cmp: ">=3.3.3"` - Defines dependency on [example/cmp](https://components.espressif.com/component/example/cmp) component that is used by the main component. + +CMake Output: +``` +... +-- IDF: Component manager round 1... +NOTICE: Dependencies lock doesn't exist, solving dependencies. +...NOTICE: Updating lock file at /Users/sudeepmohanty/esp/esp-idf/examples/build_system/cmakev2/features/component_manager/dependencies.lock +NOTICE: Processing 2 dependencies: +NOTICE: [1/2] example/cmp (3.3.9~1) +NOTICE: [2/2] idf (6.1.0) +... +``` + +Content of the `./managed_components` directory after successful build: +``` +▶ find ./managed_components +./managed_components +./managed_components/example__cmp +./managed_components/example__cmp/CMakeLists.txt +./managed_components/example__cmp/LICENSE +./managed_components/example__cmp/changelog.md +./managed_components/example__cmp/idf_component.yml +./managed_components/example__cmp/include +./managed_components/example__cmp/include/cmp.h +./managed_components/example__cmp/README.md +./managed_components/example__cmp/examples +./managed_components/example__cmp/examples/cmp_ex +./managed_components/example__cmp/examples/cmp_ex/CMakeLists.txt +./managed_components/example__cmp/examples/cmp_ex/README.md +./managed_components/example__cmp/examples/cmp_ex/main +./managed_components/example__cmp/examples/cmp_ex/main/CMakeLists.txt +./managed_components/example__cmp/examples/cmp_ex/main/idf_component.yml +./managed_components/example__cmp/examples/cmp_ex/main/cmp_ex.c +./managed_components/example__cmp/cmp.c +./managed_components/example__cmp/.component_hash +``` + +Flash the project and run the serial monitor to view the output: + +``` +idf.py -p PORT flash monitor +``` + +### Example Output + +The example outputs a line from the `cmp_hello` function from the component downloaded by the component manager. + +``` +Hello from example component! +``` diff --git a/examples/build_system/cmakev2/features/component_manager/main/CMakeLists.txt b/examples/build_system/cmakev2/features/component_manager/main/CMakeLists.txt new file mode 100644 index 00000000000..9bc2623419c --- /dev/null +++ b/examples/build_system/cmakev2/features/component_manager/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "component_manager.c" + INCLUDE_DIRS ".") diff --git a/examples/build_system/cmakev2/features/component_manager/main/component_manager.c b/examples/build_system/cmakev2/features/component_manager/main/component_manager.c new file mode 100644 index 00000000000..6cfbeeb121d --- /dev/null +++ b/examples/build_system/cmakev2/features/component_manager/main/component_manager.c @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "cmp.h" + +void app_main(void) +{ + cmp_hello(); +} diff --git a/examples/build_system/cmakev2/features/component_manager/main/idf_component.yml b/examples/build_system/cmakev2/features/component_manager/main/idf_component.yml new file mode 100644 index 00000000000..8b221801e52 --- /dev/null +++ b/examples/build_system/cmakev2/features/component_manager/main/idf_component.yml @@ -0,0 +1,36 @@ +dependencies: + # Required IDF version + idf: ">=6.0" + + # Defining a dependency from the ESP Component Registry: + # https://components.espressif.com/component/example/cmp + example/cmp: "^3.3.3" + + # # Other ways to define dependencies + # + # # For components maintained by Espressif only name can be used. + # # Same as `espressif/cmp` + # component: "~1.0.0" + # + # # Or in a longer form with extra parameters + # component2: + # version: ">=2.0.0" + # + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect for the `main` component. + # # All dependencies of `main` are public by default. + # public: true + # + # # For components hosted on non-default registry: + # service_url: "https://componentregistry.company.com" + # + # # For components in git repository: + # test_component: + # path: test_component + # git: ssh://git@gitlab.com/user/components.git + # + # # For test projects during component development + # # components can be used from a local directory + # # with relative or absolute path + # some_local_component: + # path: ../../projects/component diff --git a/examples/build_system/cmakev2/features/conditional_component/CMakeLists.txt b/examples/build_system/cmakev2/features/conditional_component/CMakeLists.txt new file mode 100644 index 00000000000..83ea4f670fa --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/CMakeLists.txt @@ -0,0 +1,41 @@ +# This example demonstrates conditional component inclusion with Build System v2 + +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) + +project(conditional_component C CXX ASM) + +# Initialize the project +idf_project_init() + +# Build the executable. +# main component provides the main function and conditionally +# includes logging_util and math_util. esptool_py is required +# for binary generation. +idf_build_executable(${CMAKE_PROJECT_NAME}.elf + COMPONENTS main esptool_py) + +# Generate binary from executable +idf_build_binary(${CMAKE_PROJECT_NAME}.elf + OUTPUT_FILE "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin" + TARGET ${CMAKE_PROJECT_NAME}_binary) + +# Create flash target +idf_flash_binary(${CMAKE_PROJECT_NAME}_binary + TARGET app-flash + NAME "app" + FLASH) + +# Check binary size +idf_check_binary_size(${CMAKE_PROJECT_NAME}_binary) + +# Create app target +add_custom_target(app ALL DEPENDS ${CMAKE_PROJECT_NAME}_binary) + +# Generate metadata +idf_build_generate_metadata(BINARY ${CMAKE_PROJECT_NAME}_binary) + +# Create utility targets for configuration +idf_create_menuconfig(${CMAKE_PROJECT_NAME}.elf TARGET menuconfig) +idf_create_confserver(${CMAKE_PROJECT_NAME}.elf TARGET confserver) diff --git a/examples/build_system/cmakev2/features/conditional_component/README.md b/examples/build_system/cmakev2/features/conditional_component/README.md new file mode 100644 index 00000000000..9a6af2472da --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/README.md @@ -0,0 +1,41 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Conditional Component Inclusion Example + +This example demonstrates how to conditionally include components at build time using Build System v2's pure CMake approach. Components are included or excluded based on Kconfig configuration options, and all components are written using standard CMake functions rather than IDF-specific abstractions. + +## Overview + +In Build System v2, components can be written as plain CMake static libraries using `add_library`, `target_link_libraries`, and `target_include_directories`. A component is brought into the build on demand by calling `idf_component_include()`, which sets up the component's target name and invokes its CMakeLists.txt. Dependencies on other components are expressed using the `idf::` alias targets. + +This example uses that mechanism to conditionally pull in two utility components — `logging_util` and `math_util` — from within the `main` component, based on Kconfig options. + +## Project Structure + +The project is initialized manually with `idf_project_init()` and `idf_build_executable()`, which gives precise control over which components are seeded into the build. The `main` component itself is a pure CMake static library. It conditionally includes `logging_util` and/or `math_util` at configure time by calling `idf_component_include()` inside `if(CONFIG_...)` guards and linking the resulting targets with `target_link_libraries`. + +`logging_util` depends on IDF's `log` component for `ESP_LOG*` macros. It pulls `log` into the build with `idf_component_include(log)` and links it via the `idf::log` alias target. `math_util` has no IDF dependencies and is a self-contained static library. + +## Configuration Options + +Two Kconfig options control which components are included: + +- `CONFIG_EXAMPLE_ENABLE_LOGGING` — includes and links the `logging_util` component. +- `CONFIG_EXAMPLE_ENABLE_MATH` — includes and links the `math_util` component. + +Both default to enabled. Toggle them via menuconfig under **Conditional Component Example Configuration**. + +## Building + +```bash +cd examples/build_system/cmakev2/conditional_component +idf.py set-target +idf.py build +``` + +To change which components are included, run `idf.py menuconfig` before rebuilding. + +## Output + +With both components enabled the serial output shows info and warning log messages from `logging_util` and the results of add, subtract, and multiply operations from `math_util`. Disabling either option removes the corresponding section and replaces it with a "DISABLED" message. diff --git a/examples/build_system/cmakev2/features/conditional_component/components/logging_util/CMakeLists.txt b/examples/build_system/cmakev2/features/conditional_component/components/logging_util/CMakeLists.txt new file mode 100644 index 00000000000..daa3b0e8640 --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/components/logging_util/CMakeLists.txt @@ -0,0 +1,8 @@ +# This component provides the logging utility functions. + +# Explicitly pull in the IDF log component before using it. +idf_component_include(log) + +add_library(${COMPONENT_TARGET} STATIC logging_util.c) +target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${COMPONENT_TARGET} PRIVATE idf::log) diff --git a/examples/build_system/cmakev2/features/conditional_component/components/logging_util/include/logging_util.h b/examples/build_system/cmakev2/features/conditional_component/components/logging_util/include/logging_util.h new file mode 100644 index 00000000000..932b3e90b5c --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/components/logging_util/include/logging_util.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Log an informational message + * + * @param message The message to log + */ +void log_info(const char *message); + +/** + * @brief Log a warning message + * + * @param message The message to log + */ +void log_warn(const char *message); + +/** + * @brief Log an error message + * + * @param message The message to log + */ +void log_error(const char *message); + +#ifdef __cplusplus +} +#endif diff --git a/examples/build_system/cmakev2/features/conditional_component/components/logging_util/logging_util.c b/examples/build_system/cmakev2/features/conditional_component/components/logging_util/logging_util.c new file mode 100644 index 00000000000..ce0a214ee48 --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/components/logging_util/logging_util.c @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "logging_util.h" +#include "esp_log.h" + +static const char *TAG = "logging_util"; + +void log_info(const char *message) +{ + ESP_LOGI(TAG, "%s", message); +} + +void log_warn(const char *message) +{ + ESP_LOGW(TAG, "%s", message); +} + +void log_error(const char *message) +{ + ESP_LOGE(TAG, "%s", message); +} diff --git a/examples/build_system/cmakev2/features/conditional_component/components/math_util/CMakeLists.txt b/examples/build_system/cmakev2/features/conditional_component/components/math_util/CMakeLists.txt new file mode 100644 index 00000000000..032a70e2a78 --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/components/math_util/CMakeLists.txt @@ -0,0 +1,3 @@ +# This component provides the math utility functions. +add_library(${COMPONENT_TARGET} STATIC math_util.c) +target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") diff --git a/examples/build_system/cmakev2/features/conditional_component/components/math_util/include/math_util.h b/examples/build_system/cmakev2/features/conditional_component/components/math_util/include/math_util.h new file mode 100644 index 00000000000..67efc3e2fbe --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/components/math_util/include/math_util.h @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Add two integers + * + * @param a First operand + * @param b Second operand + * @return Sum of a and b + */ +int math_add(int a, int b); + +/** + * @brief Subtract two integers + * + * @param a First operand + * @param b Second operand + * @return a minus b + */ +int math_subtract(int a, int b); + +/** + * @brief Multiply two integers + * + * @param a First operand + * @param b Second operand + * @return Product of a and b + */ +int math_multiply(int a, int b); + +#ifdef __cplusplus +} +#endif diff --git a/examples/build_system/cmakev2/features/conditional_component/components/math_util/math_util.c b/examples/build_system/cmakev2/features/conditional_component/components/math_util/math_util.c new file mode 100644 index 00000000000..69de0485d32 --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/components/math_util/math_util.c @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "math_util.h" + +int math_add(int a, int b) +{ + return a + b; +} + +int math_subtract(int a, int b) +{ + return a - b; +} + +int math_multiply(int a, int b) +{ + return a * b; +} diff --git a/examples/build_system/cmakev2/features/conditional_component/main/CMakeLists.txt b/examples/build_system/cmakev2/features/conditional_component/main/CMakeLists.txt new file mode 100644 index 00000000000..3f236e833e2 --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/main/CMakeLists.txt @@ -0,0 +1,21 @@ +# This component provides the main function and conditionally +# includes logging_util and math_util based on the Kconfig options. + +# Provide the main component as a static library +add_library(${COMPONENT_TARGET} STATIC main.c) + +if(CONFIG_EXAMPLE_ENABLE_LOGGING) + idf_component_include(logging_util) + target_link_libraries(${COMPONENT_TARGET} PRIVATE idf::logging_util) + idf_msg("Conditional component 'logging_util' is ENABLED") +else() + idf_msg("Conditional component 'logging_util' is DISABLED") +endif() + +if(CONFIG_EXAMPLE_ENABLE_MATH) + idf_component_include(math_util) + target_link_libraries(${COMPONENT_TARGET} PRIVATE idf::math_util) + idf_msg("Conditional component 'math_util' is ENABLED") +else() + idf_msg("Conditional component 'math_util' is DISABLED") +endif() diff --git a/examples/build_system/cmakev2/features/conditional_component/main/Kconfig.projbuild b/examples/build_system/cmakev2/features/conditional_component/main/Kconfig.projbuild new file mode 100644 index 00000000000..df6250aebc5 --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/main/Kconfig.projbuild @@ -0,0 +1,17 @@ +menu "Conditional Component Example Configuration" + + config EXAMPLE_ENABLE_LOGGING + bool "Enable Logging Utility Component" + default y + help + Enable the logging_util component to demonstrate conditional + component inclusion in Build System v2. + + config EXAMPLE_ENABLE_MATH + bool "Enable Math Utility Component" + default y + help + Enable the math_util component to demonstrate conditional + component inclusion in Build System v2. + +endmenu diff --git a/examples/build_system/cmakev2/features/conditional_component/main/main.c b/examples/build_system/cmakev2/features/conditional_component/main/main.c new file mode 100644 index 00000000000..e46f9b75c3b --- /dev/null +++ b/examples/build_system/cmakev2/features/conditional_component/main/main.c @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "sdkconfig.h" + +#ifdef CONFIG_EXAMPLE_ENABLE_LOGGING +#include "logging_util.h" +#endif + +#ifdef CONFIG_EXAMPLE_ENABLE_MATH +#include "math_util.h" +#endif + +void app_main(void) +{ + printf("Conditional Component Example\n"); + printf("==============================\n\n"); + + printf("This example demonstrates Build System v2's idf_component_include()\n"); + printf("function for conditional component inclusion.\n\n"); + +#ifdef CONFIG_EXAMPLE_ENABLE_LOGGING + printf("Logging component is ENABLED\n"); + log_info("This is an info message from logging_util"); + log_warn("This is a warning message from logging_util"); +#else + printf("Logging component is DISABLED\n"); +#endif + + printf("\n"); + +#ifdef CONFIG_EXAMPLE_ENABLE_MATH + printf("Math component is ENABLED\n"); + int a = 10, b = 5; + printf(" add(%d, %d) = %d\n", a, b, math_add(a, b)); + printf(" subtract(%d, %d) = %d\n", a, b, math_subtract(a, b)); + printf(" multiply(%d, %d) = %d\n", a, b, math_multiply(a, b)); +#else + printf("Math component is DISABLED\n"); +#endif + + printf("\n"); + printf("You can change the component configuration via menuconfig:\n"); + printf(" idf.py menuconfig -> Conditional Component Example Configuration\n"); +} diff --git a/examples/build_system/cmakev2/features/idf_as_lib/CMakeLists.txt b/examples/build_system/cmakev2/features/idf_as_lib/CMakeLists.txt new file mode 100644 index 00000000000..b31fb1ce48c --- /dev/null +++ b/examples/build_system/cmakev2/features/idf_as_lib/CMakeLists.txt @@ -0,0 +1,55 @@ +# A standard CMake project that uses ESP-IDF as a library. +# When ESP_PLATFORM is set (idf.py invocation), IDF components are bundled +# into a single library and linked into the executable. Without it the +# project builds a plain host executable with no IDF dependency. + +cmake_minimum_required(VERSION 3.22) + +if(ESP_PLATFORM) + include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) +endif() + +project(idf_as_lib C CXX ASM) + +# Single source file for both builds. IDF code paths inside main.c are +# guarded by #ifdef ESP_PLATFORM. +add_executable(${CMAKE_PROJECT_NAME}.elf main.c) + +if(ESP_PLATFORM) + # Initialise the IDF build system and bundle the required components + # into a single linkable library. + idf_project_init() + + # Create ESP-IDF library with specified components. + idf_build_library(idf_components COMPONENTS spi_flash esp_system) + + # Apply IDF build properties to the custom executable so it receives + # include paths, compile definitions, and compile options that IDF normally adds. + idf_build_get_property(include_directories INCLUDE_DIRECTORIES GENERATOR_EXPRESSION) + target_include_directories(${CMAKE_PROJECT_NAME}.elf PRIVATE "${include_directories}") + + idf_build_get_property(compile_definitions COMPILE_DEFINITIONS GENERATOR_EXPRESSION) + target_compile_definitions(${CMAKE_PROJECT_NAME}.elf PRIVATE "${compile_definitions}") + + idf_build_get_compile_options(compile_options) + target_compile_options(${CMAKE_PROJECT_NAME}.elf PRIVATE "${compile_options}") + + # Link the bundled IDF library. This brings in component headers, + # sdkconfig includes, and all necessary linker options. + target_link_libraries(${CMAKE_PROJECT_NAME}.elf PRIVATE idf_components) + + # Binary and flash targets + idf_build_binary(${CMAKE_PROJECT_NAME}.elf + OUTPUT_FILE "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin" + TARGET ${CMAKE_PROJECT_NAME}_binary) + + idf_flash_binary(${CMAKE_PROJECT_NAME}_binary + TARGET app-flash + NAME "app" + FLASH) + + idf_check_binary_size(${CMAKE_PROJECT_NAME}_binary) + idf_build_generate_metadata(BINARY ${CMAKE_PROJECT_NAME}_binary) + idf_build_generate_flasher_args() + add_custom_target(app ALL DEPENDS ${CMAKE_PROJECT_NAME}_binary) +endif() diff --git a/examples/build_system/cmakev2/features/idf_as_lib/README.md b/examples/build_system/cmakev2/features/idf_as_lib/README.md new file mode 100644 index 00000000000..1c55de12b2f --- /dev/null +++ b/examples/build_system/cmakev2/features/idf_as_lib/README.md @@ -0,0 +1,55 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# ESP-IDF as a Library in a Standard CMake Project + +This example demonstrates how to use ESP-IDF as a library inside a standard CMake project. The same source files produce either a plain host executable or an ESP-IDF firmware image depending on how the project is invoked. + +## How it works + +When `idf.py` is used it sets the `ESP_PLATFORM` CMake variable before invoking CMake. A single project-level CMakeLists.txt checks for this variable: if set, it initialises the IDF build system, bundles the required IDF components into one linkable library via `idf_build_library`, compiles the application sources, links them against that library, and produces the final binary and flash targets. Without `ESP_PLATFORM` the same file produces a minimal host executable with no IDF dependency. + +`main.c` is the only source file. All IDF API calls inside it are guarded by `#ifdef ESP_PLATFORM`, so the same file compiles cleanly for both targets. On ESP it defines `app_main`; on the host it defines a plain `main`. + +## Project Layout + +``` +idf_as_lib/ +├── CMakeLists.txt ← Single file: host + ESP build logic +├── main.c ← One file, two entry points (host & ESP) +└── sdkconfig ← ESP-IDF configuration +``` + +## How to build the host app + +``` +cmake -B build-host . +cmake --build build-host +./build-host/idf_as_lib.elf +``` + +### Expected output + +``` +Hello from host build +Run with idf.py set-target && idf.py build for ESP build. +``` + +## How to build the ESP-IDF app + +``` +idf.py set-target +idf.py build flash monitor +``` + +### Expected output + +``` +Hello from ESP-IDF build +idf_lib initialized (IDF as library) +This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, silicon revision v3.0, 2MB external flash +Minimum free heap size: 303764 bytes +Restarting in 10... +... +Restarting now. +``` \ No newline at end of file diff --git a/examples/build_system/cmakev2/features/idf_as_lib/main.c b/examples/build_system/cmakev2/features/idf_as_lib/main.c new file mode 100644 index 00000000000..a76cef8f3bc --- /dev/null +++ b/examples/build_system/cmakev2/features/idf_as_lib/main.c @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * Single source file for both host and ESP-IDF builds. + * - Host: plain main(), no IDF dependency. + * - ESP-IDF: app_main(), uses IDF APIs guarded by ESP_PLATFORM. + */ + +#include +#include + +#ifdef ESP_PLATFORM +#include "esp_chip_info.h" +#include "esp_flash.h" +#include "esp_system.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#endif + +#ifdef ESP_PLATFORM +void app_main(void) +#else +int main(void) +#endif +{ + printf("Hello from %s build\n", +#ifdef ESP_PLATFORM + "ESP-IDF" +#else + "host" +#endif + ); + +#ifdef ESP_PLATFORM + /* --- chip info --------------------------------------------------------- */ + esp_chip_info_t chip_info; + uint32_t flash_size; + esp_chip_info(&chip_info); + + printf("This is %s chip with %d CPU core(s), WiFi%s%s, ", + CONFIG_IDF_TARGET, + chip_info.cores, + (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", + (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : ""); + + unsigned major_rev = chip_info.revision / 100; + unsigned minor_rev = chip_info.revision % 100; + printf("silicon revision v%d.%d, ", major_rev, minor_rev); + + if (esp_flash_get_size(NULL, &flash_size) == ESP_OK) { + printf("%" PRIu32 " MB %s flash\n", flash_size / (1024 * 1024), + (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external"); + } + + printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size()); + + /* --- countdown and restart -------------------------------------------- */ + for (int i = 10; i >= 0; i--) { + printf("Restarting in %d...\n", i); + vTaskDelay(1000 / portTICK_PERIOD_MS); + } + printf("Restarting now.\n"); + fflush(stdout); + esp_restart(); +#else + printf("Run with idf.py set-target && idf.py build for ESP-IDF build.\n"); + return 0; +#endif +} diff --git a/examples/build_system/cmakev2/features/import_lib/CMakeLists.txt b/examples/build_system/cmakev2/features/import_lib/CMakeLists.txt new file mode 100644 index 00000000000..92937ffd783 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) + +project(import_lib C CXX ASM) + +idf_project_default() diff --git a/examples/build_system/cmakev2/features/import_lib/README.md b/examples/build_system/cmakev2/features/import_lib/README.md new file mode 100644 index 00000000000..421700fa65a --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib/README.md @@ -0,0 +1,41 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Import Third-Party CMake Library Example + +This example demonstrates how to import third-party CMake libraries. + +## Example Flow + +[tinyxml2](https://github.com/leethomason/tinyxml2) is a small C++ XML parser. + +It is imported, without modification, into the [tinyxml2](components/tinyxml2/) component. Please refer to the component CMakeLists.txt file for the description of the process: [components/tinyxml2/CMakeLists.txt](components/tinyxml2/CMakeLists.txt). + +To demonstrate the library being used, a sample XML is embedded into the project. This sample XML is then read and parsed using `tinyxml2`. Please refer to the [main](main/) component for details. + +### Output + +``` +I (317) example: Setting up... +I (317) example: Copying sample XML to filesystem... +I (647) example: Reading XML file +I (657) example: Read XML data: + + + Tove + Jani + Reminder + Don't forget me this weekend! + + +I (667) example: Parsed XML data: + +To: Tove +From: Jani +Heading: Reminder +Body: Don't forget me this weekend! +I (677) example: Example end +``` +--- + +There is a discussion on importing third-party CMake libraries in the programming guide under `API Guides` -> `Build System` -> `Using Third-Party CMake Projects with Components` diff --git a/examples/build_system/cmakev2/features/import_lib/components/tinyxml2/CMakeLists.txt b/examples/build_system/cmakev2/features/import_lib/components/tinyxml2/CMakeLists.txt new file mode 100644 index 00000000000..5c20751bb7e --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib/components/tinyxml2/CMakeLists.txt @@ -0,0 +1,67 @@ +# This component demonstrates how to add an existing third-party library as a component +# to ESP-IDF build system. +# +# Since we are wrapping the library inside a component, +# the component has to be registered first: +idf_component_register() + +# To build a third-party library, ExternalProject CMake module can be used. +# ExternalProject offers many features which are impossible to demonstrate +# in a single example. Please refer to its documentation for more info: +# https://cmake.org/cmake/help/latest/module/ExternalProject.html +include(ExternalProject) + +# Define the location where tinyxml2 will be installed: +set(TINYXML2_INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/tinyxml2_install) + +# This function downloads the project, calls CMake to configure it, +# builds the project and installs it to the specified location: +externalproject_add(tinyxml2_proj + # Download the source code of the third party project from the following URL. + # (Two URLs are provided, the 2nd one is the mirror for Chinese users) + URL https://github.com/leethomason/tinyxml2/archive/refs/tags/9.0.0.zip + https://dl.espressif.com/dl/tinyxml2/9.0.0.zip + # (Downloading is not the only option; the library can also be located in your source tree. + # Consult ExternalProject_Add function documentation for other options.) + + # Specify arguments to be passed when running CMake for this subproject. + # Note that ExternalProject_Add also works with non-CMake projects, so this + # is just an example. + CMAKE_ARGS + # Use the same CMake toolchain file as for the main project. + -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + # tinyxml2-specific settings: disable building everything except for the static library + -Dtinyxml2_BUILD_TESTING=FALSE + -Dtinyxml2_SHARED_LIBS=FALSE + # Pass the install directory to the subproject. + -DCMAKE_INSTALL_PREFIX= + + # These options are set so that Ninja immediately outputs + # the subproject build to the terminal. Otherwise it looks like the + # build process "hangs" while the subproject is being built. + USES_TERMINAL_DOWNLOAD TRUE + USES_TERMINAL_CONFIGURE TRUE + USES_TERMINAL_BUILD TRUE + + # Specify the installation directory for the subproject + INSTALL_DIR ${TINYXML2_INSTALL_DIR} + # Let CMake know that the library is generated by the subproject build step. + BUILD_BYPRODUCTS "${TINYXML2_INSTALL_DIR}/lib/libtinyxml2.a" +) + +# Now that the subproject build is set up, we need to consume the results +# of the build: the header file and the static library. +# To do this, define an imported CMake library: +add_prebuilt_library(tinyxml2_lib "${TINYXML2_INSTALL_DIR}/lib/libtinyxml2.a" + # tinyxml calls certain C++ support library functions (_Unwind_Resume and similar) + # so a dependency on IDF's cxx component is added here: + PRIV_REQUIRES cxx) +target_include_directories(tinyxml2_lib INTERFACE "${TINYXML2_INSTALL_DIR}/include") +add_dependencies(tinyxml2_lib tinyxml2_proj) + +# Link the imported library to the current component. +target_link_libraries(${COMPONENT_LIB} INTERFACE tinyxml2_lib) + +# To use tinyxml2 in another component, add 'tinyxml2' (the name of this component) +# to PRIV_REQUIRES or REQUIRES list its idf_component_register call. +# See ../../main/CMakeLists.txt for an example. diff --git a/examples/build_system/cmakev2/features/import_lib/main/CMakeLists.txt b/examples/build_system/cmakev2/features/import_lib/main/CMakeLists.txt new file mode 100644 index 00000000000..6e9d7d94ca9 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib/main/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register(SRCS "import_lib_example_main.cpp" + INCLUDE_DIRS "." + PRIV_REQUIRES tinyxml2 fatfs) + +# Create a FAT filesystem image from the contents of data/ subdirectory, +# The image will be flashed into the 'storage' partition when 'idf.py flash' is used. +fatfs_create_spiflash_image(storage data FLASH_IN_PROJECT) diff --git a/examples/build_system/cmakev2/features/import_lib/main/data/sample.xml b/examples/build_system/cmakev2/features/import_lib/main/data/sample.xml new file mode 100644 index 00000000000..d3f0eca227c --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib/main/data/sample.xml @@ -0,0 +1,7 @@ + + + Tove + Jani + Reminder + Don't forget me this weekend! + diff --git a/examples/build_system/cmakev2/features/import_lib/main/import_lib_example_main.cpp b/examples/build_system/cmakev2/features/import_lib/main/import_lib_example_main.cpp new file mode 100644 index 00000000000..bc58d29100a --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib/main/import_lib_example_main.cpp @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "esp_err.h" +#include "esp_log.h" +#include "esp_vfs_fat.h" +#include "tinyxml2.h" + +static const char *TAG = "example"; + + +extern "C" void app_main(void) +{ + ESP_LOGI(TAG, "Initializing the filesystem"); + esp_vfs_fat_mount_config_t mount_config = {}; + mount_config.max_files = 1; + + wl_handle_t wl_handle = WL_INVALID_HANDLE; + esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl("/spiflash", "storage", &mount_config, &wl_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to mount FATFS (%s)", esp_err_to_name(err)); + return; + } + + // Load the XML file from the filesystem and parse it using tinyxml2 + ESP_LOGI(TAG, "Reading XML file"); + tinyxml2::XMLDocument data; + data.LoadFile("/spiflash/sample.xml"); + + tinyxml2::XMLPrinter printer; + data.Print(&printer); + + ESP_LOGI(TAG, "Read XML data:\n%s", printer.CStr()); + + const char* to_data = data.FirstChildElement("note")->FirstChildElement("to")->GetText(); + const char* from_data = data.FirstChildElement("note")->FirstChildElement("from")->GetText(); + const char* heading_data = data.FirstChildElement("note")->FirstChildElement("heading")->GetText(); + const char* body_data = data.FirstChildElement("note")->FirstChildElement("body")->GetText(); + + ESP_LOGI(TAG, "Parsed XML data:\n\nTo: %s\nFrom: %s\nHeading: %s\nBody: %s", + to_data, from_data, heading_data, body_data); + + // Clean up + esp_vfs_fat_spiflash_unmount_rw_wl("/spiflash", wl_handle); + + ESP_LOGI(TAG, "Example end"); +} diff --git a/examples/build_system/cmakev2/features/import_lib/partitions_example.csv b/examples/build_system/cmakev2/features/import_lib/partitions_example.csv new file mode 100644 index 00000000000..d8fe545ba28 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib/partitions_example.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +storage, data, fat, , 528K, diff --git a/examples/build_system/cmakev2/features/import_lib/sdkconfig.defaults b/examples/build_system/cmakev2/features/import_lib/sdkconfig.defaults new file mode 100644 index 00000000000..b9bb0c0a5dc --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_example.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions_example.csv" diff --git a/examples/build_system/cmakev2/features/import_lib_direct/CMakeLists.txt b/examples/build_system/cmakev2/features/import_lib_direct/CMakeLists.txt new file mode 100644 index 00000000000..3f8b8faa9a0 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib_direct/CMakeLists.txt @@ -0,0 +1,11 @@ +# This example demonstrates a cmakev2 capability of importing an external +# C library via FetchContent, built by its own CMake, and linked directly +# to an IDF component without any IDF component wrapper for the library. + +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) + +project(import_lib_direct C CXX ASM) + +idf_project_default() diff --git a/examples/build_system/cmakev2/features/import_lib_direct/README.md b/examples/build_system/cmakev2/features/import_lib_direct/README.md new file mode 100644 index 00000000000..f844daa8471 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib_direct/README.md @@ -0,0 +1,61 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Import External C Library Directly (cmakev2) + +This example demonstrates importing an external C library that is a pure CMake project. It is downloaded at configure time, built by its own CMake, and linked directly to the IDF app without wrapping it as an IDF component. + +## Overview + +In Build System v1, integrating third-party CMake libraries required wrapping them as IDF components (see the `import_lib` example). With cmakev2, CMake's `FetchContent` module can be used to fetch and build an external CMake library (lwjson) and link the library to an IDF component using `target_link_libraries`. + +This example uses [lwjson](https://github.com/MaJerle/lwjson), a lightweight JSON parser for embedded systems. + +## Project Structure + +``` +import_lib_direct/ +├── CMakeLists.txt # Initialize the IDF project +├── README.md +└── main/ + ├── CMakeLists.txt # FetchContent for lwjson, register component, link + └── main.c # Uses lwjson API +``` + +## How It Works + +1. The **main component's CMakeLists.txt** uses `FetchContent_Declare()` and `FetchContent_MakeAvailable()` to download and build lwjson at configure time. The fetched sources land in `./build/_deps/`. + +2. After registering the main omponent with `idf_component_register`, it links the fetched library: + ```cmake + target_link_libraries(${COMPONENT_LIB} PUBLIC lwjson) + ``` + +3. **main.c** uses the lwjson API (`lwjson_init`, `lwjson_parse`, `lwjson_find`, etc.) to parse a sample JSON string and print device name, cores, features, and specs. + +## How to Use + +Build and flash the example: + +```bash +idf.py set-target +idf.py build +idf.py flash monitor +``` + +## Expected Output + +``` +I (275) import_lib_direct: lwjson library imported directly (downloaded, built, linked) without IDF component wrapper +I (275) import_lib_direct: Parsing JSON string... +I (285) import_lib_direct: Device name: ESP32 +I (285) import_lib_direct: Number of cores: 2 +I (285) import_lib_direct: Features: +I (295) import_lib_direct: - WiFi +I (295) import_lib_direct: - Bluetooth +I (295) import_lib_direct: - GPIO +I (305) import_lib_direct: Specifications: +I (305) import_lib_direct: Flash: 4MB +I (305) import_lib_direct: RAM: 520KB +I (315) import_lib_direct: Example complete! +``` \ No newline at end of file diff --git a/examples/build_system/cmakev2/features/import_lib_direct/main/CMakeLists.txt b/examples/build_system/cmakev2/features/import_lib_direct/main/CMakeLists.txt new file mode 100644 index 00000000000..1a80a6f9dea --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib_direct/main/CMakeLists.txt @@ -0,0 +1,15 @@ +# Fetch the external lwjson library at configure time. +include(FetchContent) +fetchcontent_declare( + lwjson + GIT_REPOSITORY https://github.com/MaJerle/lwjson.git + GIT_TAG v1.8.1 + GIT_SHALLOW TRUE +) +fetchcontent_makeavailable(lwjson) + +idf_component_register(SRCS "main.c" + INCLUDE_DIRS ".") + +# Link the fetched lwjson library to this component +target_link_libraries(${COMPONENT_LIB} PUBLIC lwjson) diff --git a/examples/build_system/cmakev2/features/import_lib_direct/main/main.c b/examples/build_system/cmakev2/features/import_lib_direct/main/main.c new file mode 100644 index 00000000000..b9a82b121d1 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_lib_direct/main/main.c @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * This example demonstrates importing an external C library (lwjson) + * via FetchContent, built by its own CMake, and linked directly to this IDF component + * without any IDF component wrapper. + */ + +#include +#include +#include "esp_log.h" +#include "lwjson/lwjson.h" + +static const char *TAG = "import_lib_direct"; + +#define LWJSON_TOKENS 64 + +/* Sample JSON string to parse */ +static const char *sample_json = + "{" + " \"name\": \"ESP32\"," + " \"cores\": 2," + " \"features\": [\"WiFi\", \"Bluetooth\", \"GPIO\"]," + " \"specs\": {" + " \"flash\": \"4MB\"," + " \"ram\": \"520KB\"" + " }" + "}"; + +void app_main(void) +{ + ESP_LOGI(TAG, "lwjson library imported directly (downloaded, built, linked) without IDF component wrapper"); + + static lwjson_token_t tokens[LWJSON_TOKENS]; + lwjson_t lwobj; + + if (lwjson_init(&lwobj, tokens, LWJSON_TOKENS) != lwjsonOK) { + ESP_LOGE(TAG, "lwjson_init failed"); + return; + } + + if (lwjson_parse(&lwobj, sample_json) != lwjsonOK) { + ESP_LOGE(TAG, "lwjson_parse failed"); + lwjson_free(&lwobj); + return; + } + + ESP_LOGI(TAG, "Parsing JSON string..."); + + const lwjson_token_t *name_t = lwjson_find(&lwobj, "name"); + if (name_t != NULL) { + size_t len; + const char *s = lwjson_get_val_string(name_t, &len); + if (s != NULL) { + ESP_LOGI(TAG, "Device name: %.*s", (int)len, s); + } + } + + const lwjson_token_t *cores_t = lwjson_find(&lwobj, "cores"); + if (cores_t != NULL && cores_t->type == LWJSON_TYPE_NUM_INT) { + ESP_LOGI(TAG, "Number of cores: %lld", (long long)lwjson_get_val_int(cores_t)); + } + + const lwjson_token_t *features_t = lwjson_find(&lwobj, "features"); + if (features_t != NULL && features_t->type == LWJSON_TYPE_ARRAY) { + ESP_LOGI(TAG, "Features:"); + const lwjson_token_t *child = lwjson_get_first_child(features_t); + while (child != NULL) { + if (child->type == LWJSON_TYPE_STRING) { + size_t len; + const char *s = lwjson_get_val_string(child, &len); + if (s != NULL) { + ESP_LOGI(TAG, " - %.*s", (int)len, s); + } + } + child = child->next; + } + } + + const lwjson_token_t *flash_t = lwjson_find(&lwobj, "specs.flash"); + const lwjson_token_t *ram_t = lwjson_find(&lwobj, "specs.ram"); + if (flash_t != NULL || ram_t != NULL) { + ESP_LOGI(TAG, "Specifications:"); + if (flash_t != NULL) { + size_t len; + const char *s = lwjson_get_val_string(flash_t, &len); + if (s != NULL) { + ESP_LOGI(TAG, " Flash: %.*s", (int)len, s); + } + } + if (ram_t != NULL) { + size_t len; + const char *s = lwjson_get_val_string(ram_t, &len); + if (s != NULL) { + ESP_LOGI(TAG, " RAM: %.*s", (int)len, s); + } + } + } + + lwjson_free(&lwobj); + + ESP_LOGI(TAG, "Example complete!"); +} diff --git a/examples/build_system/cmakev2/features/import_prebuilt/CMakeLists.txt b/examples/build_system/cmakev2/features/import_prebuilt/CMakeLists.txt new file mode 100644 index 00000000000..e4e7d0a254f --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) +project(import_prebuilt C CXX ASM) +idf_project_default() diff --git a/examples/build_system/cmakev2/features/import_prebuilt/README.md b/examples/build_system/cmakev2/features/import_prebuilt/README.md new file mode 100644 index 00000000000..b390b278aad --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/README.md @@ -0,0 +1,40 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Import Prebuilt Library Example + +This example illustrates how to import a prebuilt static library in the ESP-IDF build system. + +## Example Flow + +Users need to first build the project in the [prebuilt](prebuilt) subdirectory: + +``` +cd prebuilt +idf.py build +``` + +This builds a component named [prebuilt](prebuilt/components/prebuilt), which has private dependency on ESP-IDF components `spi_flash`, `log` and `app_update` (see [its CMakeLists.txt](prebuilt/components/prebuilt/CMakeLists.txt)). Once built, the archive file `libprebuilt.a`, along with the header file `prebuilt.h`, is automatically copied to the [`main` component](main) of this example project. + +The [`main` component's CMakeLists.txt](main/CMakeLists.txt) demonstrates how to import `libprebuilt.a` and link it to `main` so that the definitions inside can be used. +It also demonstrates how to specify the same dependencies the original component had so as to properly resolve symbols used inside the prebuilt library. + +Users can then return to this directory and build the main example: + +``` +cd .. +idf.py build +``` + + +### Output + +The example simply outputs the current running partition. + +``` +I (319) prebuilt: The running partition is 'factory'! +``` + +--- + +There is a discussion on importing prebuilt libraries in the programming guide under `API Guides` -> `Build System` -> `Using Prebuilt Libraries with Components` diff --git a/examples/build_system/cmakev2/features/import_prebuilt/main/CMakeLists.txt b/examples/build_system/cmakev2/features/import_prebuilt/main/CMakeLists.txt new file mode 100644 index 00000000000..6c0888f23c2 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/main/CMakeLists.txt @@ -0,0 +1,10 @@ +idf_component_register(SRCS "main.c" + INCLUDE_DIRS ".") + +# Import the library, specifying a target name and the library path. +# The private dependencies of the library is also specified. +add_prebuilt_library(prebuilt "libprebuilt.a" + PRIV_REQUIRES spi_flash app_update log) + +# `main` calls a function from the library, so link it to `main` +target_link_libraries(${COMPONENT_LIB} PRIVATE prebuilt) diff --git a/examples/build_system/cmakev2/features/import_prebuilt/main/main.c b/examples/build_system/cmakev2/features/import_prebuilt/main/main.c new file mode 100644 index 00000000000..7ba461af7c7 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/main/main.c @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +// Include the prebuilt library's header file so as to be able +// to reference `prebuilt_func` here. +#include "prebuilt.h" + +void app_main(void) +{ + prebuilt_func(); +} diff --git a/examples/build_system/cmakev2/features/import_prebuilt/main/project_include.cmake b/examples/build_system/cmakev2/features/import_prebuilt/main/project_include.cmake new file mode 100644 index 00000000000..a724d85f901 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/main/project_include.cmake @@ -0,0 +1,16 @@ +# For users checking this example, ignore the following code. This is so that +# the prebuilt project is built automatically in ESP-IDF CI. +if("$ENV{CI}") + # otherwise these file won't be rebuilt when switching the built target within the same job + file(REMOVE + ${CMAKE_SOURCE_DIR}/prebuilt/sdkconfig + ${CMAKE_SOURCE_DIR}/main/libprebuilt.a + ${CMAKE_SOURCE_DIR}/main/prebuilt.h + ) + file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/prebuilt/build) + + idf_build_get_property(python PYTHON) + idf_build_get_property(idf_path IDF_PATH) + execute_process(COMMAND ${python} "${idf_path}/tools/idf.py" build + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/prebuilt) +endif() diff --git a/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/CMakeLists.txt b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/CMakeLists.txt new file mode 100644 index 00000000000..db665b1db05 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) +project(prebuilt C CXX ASM) +idf_project_default() diff --git a/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/components/prebuilt/CMakeLists.txt b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/components/prebuilt/CMakeLists.txt new file mode 100644 index 00000000000..b58f0b3f062 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/components/prebuilt/CMakeLists.txt @@ -0,0 +1,10 @@ +idf_component_register(SRCS prebuilt.c + INCLUDE_DIRS "." + PRIV_REQUIRES app_update spi_flash log) + +# After build, copy the archive file and header file to parent example directory's main component +add_custom_command(TARGET ${COMPONENT_LIB} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_SOURCE_DIR}/../main + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/prebuilt.h ${CMAKE_SOURCE_DIR}/../main + COMMENT "Copying built archive file and header to parent example directory...") diff --git a/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/components/prebuilt/prebuilt.c b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/components/prebuilt/prebuilt.c new file mode 100644 index 00000000000..46fad63131f --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/components/prebuilt/prebuilt.c @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "esp_ota_ops.h" +#include "esp_partition.h" +#include "esp_log.h" + +const char *TAG = "prebuilt"; + +void prebuilt_func(void) +{ + const esp_partition_t* running_partition = esp_ota_get_running_partition(); + ESP_LOGI(TAG, "The running partition is '%s'!", running_partition->label); +} diff --git a/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/components/prebuilt/prebuilt.h b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/components/prebuilt/prebuilt.h new file mode 100644 index 00000000000..9f0abe05825 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/components/prebuilt/prebuilt.h @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once + +void prebuilt_func(void); diff --git a/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/main/CMakeLists.txt b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/main/CMakeLists.txt new file mode 100644 index 00000000000..6ee0c4445ae --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "main.c" + PRIV_REQUIRES prebuilt esptool_py + INCLUDE_DIRS "") diff --git a/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/main/main.c b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/main/main.c new file mode 100644 index 00000000000..7fc141a1315 --- /dev/null +++ b/examples/build_system/cmakev2/features/import_prebuilt/prebuilt/main/main.c @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include + +void app_main(void) +{ + printf("Hello World!\n"); +} diff --git a/examples/build_system/cmakev2/features/multi_binary/CMakeLists.txt b/examples/build_system/cmakev2/features/multi_binary/CMakeLists.txt new file mode 100644 index 00000000000..1706a09a957 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) + +project(multi_binary C CXX ASM) + +# Manual initialization for fine-grained control over component inclusion +idf_project_init() + +# Build the first executable: app1 +# Links: app1_main, component1, component2 +idf_build_executable(app1.elf + COMPONENTS app1_main component1 component2) + +# Build the second executable: app2 +# Links: app2_main, component1, component2, component3 +idf_build_executable(app2.elf + COMPONENTS app2_main component1 component2 component3) + +# Generate binaries and flash targets + +# App1 binary +idf_build_binary(app1.elf + OUTPUT_FILE "${CMAKE_BINARY_DIR}/app1.bin" + TARGET app1_binary) + +idf_flash_binary(app1_binary + TARGET app1-flash + NAME "app1" + FLASH) + +# Generate metadata only for the primary app1 binary +idf_build_generate_metadata(BINARY app1_binary) + +# Create menuconfig and confserver targets for app1 binary +idf_create_menuconfig(app1.elf TARGET app1-menuconfig) +idf_create_confserver(app1.elf TARGET app1-confserver) + +# App2 binary +idf_build_binary(app2.elf + OUTPUT_FILE "${CMAKE_BINARY_DIR}/app2.bin" + TARGET app2_binary) + +idf_flash_binary(app2_binary + TARGET app2-flash + NAME "app2" + FLASH) + +# Create menuconfig and confserver targets for app2 binary +idf_create_menuconfig(app2.elf TARGET app2-menuconfig) +idf_create_confserver(app2.elf TARGET app2-confserver) + +# Make both binaries part of the default "app" target +add_custom_target(app ALL DEPENDS app1.bin app2.bin) diff --git a/examples/build_system/cmakev2/features/multi_binary/README.md b/examples/build_system/cmakev2/features/multi_binary/README.md new file mode 100644 index 00000000000..06ec856b264 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/README.md @@ -0,0 +1,92 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Multi-Binary Example: Multiple Apps with Different Component Sets + +This example demonstrates **Build System v2's** capability to build multiple independent firmware binaries from a single project in one build command. Each binary has its own entry point component with different dependencies. + +## Overview + +Two independent executables are built simultaneously: + +1. **`app1.bin`** - Links `app1_main`, `component1`, `component2` +2. **`app2.bin`** - Links `app2_main`, `component1`, `component2`, `component3` + +Each app has its own dedicated entry point component that explicitly declares which components it needs. This avoids complex conditional logic and makes dependencies clear. + +## Project Structure + +All components live under `components/`. Each `idf_build_executable` call picks exactly the subset it requires. + +``` +multi_binary/ +├── CMakeLists.txt +└── components/ + ├── app1_main/ # Entry point for app1 + │ ├── CMakeLists.txt + │ └── app1_main.c + ├── app2_main/ # Entry point for app2 + │ ├── CMakeLists.txt + │ └── app2_main.c + ├── component1/ # Shared component + │ ├── CMakeLists.txt + │ ├── component1.c + │ └── component1.h + ├── component2/ # Shared component + │ ├── CMakeLists.txt + │ ├── component2.c + │ └── component2.h + └── component3/ # Only linked into app2 + ├── CMakeLists.txt + ├── component3.c + └── component3.h +``` + +## Building + +### Build both binaries in a single command: + +```bash +cd examples/build_system/cmakev2/features/multi_binary +idf.py set-target +idf.py build +``` + +This generates both `app1.bin` and `app2.bin` in the `build/` directory. + +## Flashing + +Flash app1: +```bash +idf.py app1-flash monitor +``` + +Flash app2: +```bash +idf.py app2-flash monitor +``` + +You can switch between apps without rebuilding - just select which one to flash. + +### Configuration (menuconfig) + +When invoking a custom menuconfig target via `idf.py`, you must pass `--no-hints`. For example, use `idf.py app1-menuconfig --no-hints` (plain `idf.py app1-menuconfig` may not work because `idf.py` redirects stdout by default, which breaks the curses-based menu). + +> **Note:** Although multiple binaries can be produced from a single project, each component is evaluated only once using the current configuration. If a different configuration is needed for a component, a new project must be created. You cannot use the same component with different configurations within a single project. + +## Expected Output + +### app1 Output + +``` +I (xxx) component1: Hello from component1! +I (xxx) component2: Hello from component2! +``` + +### app2 Output + +``` +I (xxx) component1: Hello from component1! +I (xxx) component2: Hello from component2! +I (xxx) component3: Hello from component3! +``` \ No newline at end of file diff --git a/examples/build_system/cmakev2/features/multi_binary/components/app1_main/CMakeLists.txt b/examples/build_system/cmakev2/features/multi_binary/components/app1_main/CMakeLists.txt new file mode 100644 index 00000000000..089b4abe26f --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/app1_main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "app1_main.c" + PRIV_REQUIRES component1 component2) diff --git a/examples/build_system/cmakev2/features/multi_binary/components/app1_main/app1_main.c b/examples/build_system/cmakev2/features/multi_binary/components/app1_main/app1_main.c new file mode 100644 index 00000000000..eab34da3570 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/app1_main/app1_main.c @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "component1.h" +#include "component2.h" + +void app_main(void) +{ + component1_print_hello(); + component2_print_hello(); + + while (1) { + vTaskDelay(pdMS_TO_TICKS(10000)); + } +} diff --git a/examples/build_system/cmakev2/features/multi_binary/components/app2_main/CMakeLists.txt b/examples/build_system/cmakev2/features/multi_binary/components/app2_main/CMakeLists.txt new file mode 100644 index 00000000000..f7e79ff6b97 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/app2_main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "app2_main.c" + PRIV_REQUIRES component1 component2 component3) diff --git a/examples/build_system/cmakev2/features/multi_binary/components/app2_main/app2_main.c b/examples/build_system/cmakev2/features/multi_binary/components/app2_main/app2_main.c new file mode 100644 index 00000000000..c187322124d --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/app2_main/app2_main.c @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "component1.h" +#include "component2.h" +#include "component3.h" + +void app_main(void) +{ + component1_print_hello(); + component2_print_hello(); + component3_print_hello(); + + while (1) { + vTaskDelay(pdMS_TO_TICKS(10000)); + } +} diff --git a/examples/build_system/cmakev2/features/multi_binary/components/component1/CMakeLists.txt b/examples/build_system/cmakev2/features/multi_binary/components/component1/CMakeLists.txt new file mode 100644 index 00000000000..5d4ba1e4668 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/component1/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "component1.c" + INCLUDE_DIRS ".") diff --git a/examples/build_system/cmakev2/features/multi_binary/components/component1/component1.c b/examples/build_system/cmakev2/features/multi_binary/components/component1/component1.c new file mode 100644 index 00000000000..225d0202d32 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/component1/component1.c @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "esp_log.h" + +static const char *TAG = "component1"; + +void component1_print_hello(void) +{ + ESP_LOGI(TAG, "Hello from component1!"); +} diff --git a/examples/build_system/cmakev2/features/multi_binary/components/component1/component1.h b/examples/build_system/cmakev2/features/multi_binary/components/component1/component1.h new file mode 100644 index 00000000000..1ba5db88e5f --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/component1/component1.h @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef COMPONENT1_H +#define COMPONENT1_H + +void component1_print_hello(void); + +#endif // COMPONENT1_H diff --git a/examples/build_system/cmakev2/features/multi_binary/components/component2/CMakeLists.txt b/examples/build_system/cmakev2/features/multi_binary/components/component2/CMakeLists.txt new file mode 100644 index 00000000000..2555f994749 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/component2/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "component2.c" + INCLUDE_DIRS ".") diff --git a/examples/build_system/cmakev2/features/multi_binary/components/component2/component2.c b/examples/build_system/cmakev2/features/multi_binary/components/component2/component2.c new file mode 100644 index 00000000000..16839754469 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/component2/component2.c @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "esp_log.h" + +static const char *TAG = "component2"; + +void component2_print_hello(void) +{ + ESP_LOGI(TAG, "Hello from component2!"); +} diff --git a/examples/build_system/cmakev2/features/multi_binary/components/component2/component2.h b/examples/build_system/cmakev2/features/multi_binary/components/component2/component2.h new file mode 100644 index 00000000000..d3c2d0a5fea --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/component2/component2.h @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef COMPONENT2_H +#define COMPONENT2_H + +void component2_print_hello(void); + +#endif // COMPONENT2_H diff --git a/examples/build_system/cmakev2/features/multi_binary/components/component3/CMakeLists.txt b/examples/build_system/cmakev2/features/multi_binary/components/component3/CMakeLists.txt new file mode 100644 index 00000000000..5c0dfffa861 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/component3/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "component3.c" + INCLUDE_DIRS ".") diff --git a/examples/build_system/cmakev2/features/multi_binary/components/component3/component3.c b/examples/build_system/cmakev2/features/multi_binary/components/component3/component3.c new file mode 100644 index 00000000000..37b8528b658 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/component3/component3.c @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "esp_log.h" +#include "sdkconfig.h" + +static const char *TAG = "component3"; + +void component3_print_hello(void) +{ + ESP_LOGI(TAG, "Hello from component3!"); +} diff --git a/examples/build_system/cmakev2/features/multi_binary/components/component3/component3.h b/examples/build_system/cmakev2/features/multi_binary/components/component3/component3.h new file mode 100644 index 00000000000..cd5069ae95e --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_binary/components/component3/component3.h @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef COMPONENT3_H +#define COMPONENT3_H + +void component3_print_hello(void); + +#endif // COMPONENT3_H diff --git a/examples/build_system/cmakev2/features/multi_config/.gitignore b/examples/build_system/cmakev2/features/multi_config/.gitignore new file mode 100644 index 00000000000..ac46c39a798 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/.gitignore @@ -0,0 +1,2 @@ +build_prod1/ +build_prod2/ diff --git a/examples/build_system/cmakev2/features/multi_config/CMakeLists.txt b/examples/build_system/cmakev2/features/multi_config/CMakeLists.txt new file mode 100644 index 00000000000..50b6b40aba7 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.22) + +# In this example, sdkconfig file is placed into the build directory. +# This allows building development and production configs side by side, +# without having them influence each other. +set(SDKCONFIG "${CMAKE_BINARY_DIR}/sdkconfig") + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) +project(multi_config C CXX ASM) +idf_project_default() diff --git a/examples/build_system/cmakev2/features/multi_config/CMakePresets.json b/examples/build_system/cmakev2/features/multi_config/CMakePresets.json new file mode 100644 index 00000000000..a8ee8dfe828 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/CMakePresets.json @@ -0,0 +1,34 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "default", + "binaryDir": "build/default", + "displayName": "Default (development)", + "description": "Development configuration", + "cacheVariables": { + "SDKCONFIG": "./build/default/sdkconfig" + } + }, + { + "name": "prod1", + "binaryDir": "build/prod1", + "displayName": "Product 1", + "description": "Production configuration for product 1", + "cacheVariables": { + "SDKCONFIG_DEFAULTS": "sdkconfig.defaults.prod_common;sdkconfig.defaults.prod1", + "SDKCONFIG": "./build/prod1/sdkconfig" + } + }, + { + "name": "prod2", + "binaryDir": "build/prod2", + "displayName": "Product 2", + "description": "Production configuration for product 2", + "cacheVariables": { + "SDKCONFIG_DEFAULTS": "sdkconfig.defaults.prod_common;sdkconfig.defaults.prod2", + "SDKCONFIG": "./build/prod2/sdkconfig" + } + } + ] +} diff --git a/examples/build_system/cmakev2/features/multi_config/README.md b/examples/build_system/cmakev2/features/multi_config/README.md new file mode 100644 index 00000000000..fff9c17537b --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/README.md @@ -0,0 +1,132 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Multiple Build Configurations Example + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +This example demonstrates how to build multiple configurations of a single application. This can be useful in the following cases: + +* Building binaries for multiple similar products from single codebase +* Building the application for development or production hardware +* Optimizing the application differently for development and production + +This example contains three build configurations: + +* Development configuration, described by `sdkconfig.defaults` file. This configuration is used by default if the application is built using `idf.py build`. +* Production configuration for product 1 ("Blinky Smart Light"), described in `sdkconfig.prod1` file. This configuration is not built by default, however it can be built as shown in the next section. It is used together `sdkconfig.prod_common`, common configuration file for all products. +* Production configuration for product 2 ("Blinky Smart Switch"), described in `sdkconfig.prod2` file. Differs from `prod1` configuration only in product name. + +For each configuration, a few configuration options are set: + +* Project-specific Kconfig options, `CONFIG_EXAMPLE_PRODUCT_NAME` and `CONFIG_EXAMPLE_FUNC_IMPL`. These options are declared in [component Kconfig.projbuild](main/Kconfig.projbuild). These are used to demonstrate how to create and set project-specific options. These options are set differently in `sdkconfig.defaults` and `sdkconfig.prod_common` files. + - `CONFIG_EXAMPLE_PRODUCT_NAME` is a simple `string` option. It is used to set the product name. + - `CONFIG_EXAMPLE_FUNC_IMPL` is a `choice` option. It is used to select which of the two source files, [func_dev.c](main/func_dev.c) or [func_prod.c](main/func_prod.c), is compiled and linked. See [component CMakeLists.txt file](main/CMakeLists.txt) for related logic. +* ESP-IDF configuration options, `CONFIG_COMPILER_OPTIMIZATION_SIZE`, `CONFIG_BOOTLOADER_LOG_LEVEL_NONE`, `CONFIG_LOG_DEFAULT_LEVEL_NONE` are set in `sdkconfig.prod_common` to illustrate a typical production configuration where log messages are disabled and optimization for size is used. + +## How to Use Example + +### Development build + +To build the development configuration (specified in `sdkconfig.defaults`), specify it using --preset argument: + +``` +idf.py --preset default build +``` + +To flash the project and see the output, run: + +``` +idf.py --preset default -p PORT flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +### Production build + +To build one of the Production configurations, specify the name using idf.py --preset argument: + +``` +idf.py --preset prod1 build +``` + +To flash the project and see the output, run: + +``` +idf.py --preset prod1 -p PORT flash monitor +``` + +To build and run the app with `prod2` configuration, repeat the steps above, replacing `prod1` with `prod2`. + +### Specifying the preset for multiple commands + +To avoid having to specify `--preset` argument every time you run `idf.py`, you can set `IDF_PRESET` environment variable: + +For UNIX-like systems (Linux, macOS): +```shell +export IDF_PRESET=prod1 +``` + +For Windows (PowerShell): +```powershell +$ENV:IDF_PRESET='prod1' +``` + +For Windows (cmd.exe): +```shell +set IDF_PRESET=prod1 +``` + +Then subsequent commands will work with `prod1` configuration: +```shell +idf.py build +idf.py flash monitor +``` + +### Combining multiple files in `SDKCONFIG_DEFAULTS` + +`SDKCONFIG_DEFAULTS` build system variable selects the file which contains the default app configuration, used when no `sdkconfig` file is present. If not specified, `SDKCONFIG_DEFAULTS` is set to `"sdkconfig.defaults"`. + +`SDKCONFIG_DEFAULTS` can be set to a different name from the command line, using `-D` flag of `idf.py`, as shown above. It can also be set from the project CMakeLists.txt file, before `project.cmake` is included. + +It is possible to specify multiple files in this variable, separating them with semicolons. In the example given in the previous section, this is used to create a common config file for production builds and product-specific config files: + +* product 1: `sdkconfig.prod_common;sdkconfig.prod1` +* product 2: `sdkconfig.prod_common;sdkconfig.prod2` + +This way the common options do not need to be repeated in each of `sdkconfig.prodN` files. + +### Generated `sdkconfig` file + +In this example, `sdkconfig` file is placed into the build directory, instead of the project root directory as it is done by default. This allows development and production builds to exist side by side. The location of `sdkconfig` file is set using `SDKCONFIG` variable in [project CMakeLists.txt](CMakeLists.txt) file. + +## Example Output + +### Development build output + +``` +I (310) cpu_start: Starting scheduler on PRO CPU. +I (0) cpu_start: Starting scheduler on APP CPU. +This app is built for running on: Blinky Development Board +func() from func_dev.c (Development) called. +See README.md for building and running other app configurations. +``` + +### Production build output + +When building with `-DSDKCONFIG_DEFAULTS="sdkconfig.prod_common;sdkconfig.prod1"` option: + +``` +This app is built for running on: Blinky Smart Light +func() from func_prod.c (Production) called. +See README.md for building and running other app configurations. +``` + +When building with `-DSDKCONFIG_DEFAULTS="sdkconfig.prod_common;sdkconfig.prod2"` option: + +``` +This app is built for running on: Blinky Smart Switch +func() from func_prod.c (Production) called. +See README.md for building and running other app configurations. +``` + diff --git a/examples/build_system/cmakev2/features/multi_config/main/CMakeLists.txt b/examples/build_system/cmakev2/features/multi_config/main/CMakeLists.txt new file mode 100644 index 00000000000..d5b4d1778f2 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/main/CMakeLists.txt @@ -0,0 +1,8 @@ +idf_component_register(SRCS "multi_config_example_main.c" + INCLUDE_DIRS ".") + +if(CONFIG_EXAMPLE_FUNC_IMPL_DEV) + target_sources(${COMPONENT_LIB} PRIVATE "func_dev.c") +elseif(CONFIG_EXAMPLE_FUNC_IMPL_PROD) + target_sources(${COMPONENT_LIB} PRIVATE "func_prod.c") +endif() diff --git a/examples/build_system/cmakev2/features/multi_config/main/Kconfig.projbuild b/examples/build_system/cmakev2/features/multi_config/main/Kconfig.projbuild new file mode 100644 index 00000000000..3711596bb10 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/main/Kconfig.projbuild @@ -0,0 +1,20 @@ +menu "Example Product Configuration" + + config EXAMPLE_PRODUCT_NAME + string "Product name" + default "Not set" + help + Product name used in the example + + choice EXAMPLE_FUNC_IMPL + prompt "Implementation of function 'func'" + help + Select one of the implementations of 'func' to be used in the app. + This setting is used in component CMakeLists.txt. + + config EXAMPLE_FUNC_IMPL_DEV + bool "Development (func_dev.c)" + config EXAMPLE_FUNC_IMPL_PROD + bool "Production (func_prod.c)" + endchoice +endmenu diff --git a/examples/build_system/cmakev2/features/multi_config/main/func.h b/examples/build_system/cmakev2/features/multi_config/main/func.h new file mode 100644 index 00000000000..9b89bb0e172 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/main/func.h @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief This function has different implementations depending on the product type. + * + * See func_dev.c and func_prod.c. Which of the files is compiled is determined in + * CMakeLists.txt, + */ +void func(void); + +#ifdef __cplusplus +} +#endif diff --git a/examples/build_system/cmakev2/features/multi_config/main/func_dev.c b/examples/build_system/cmakev2/features/multi_config/main/func_dev.c new file mode 100644 index 00000000000..e220e76d3a7 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/main/func_dev.c @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include "func.h" + +void func(void) +{ + printf("func() from func_dev.c (Development) called.\n"); +} diff --git a/examples/build_system/cmakev2/features/multi_config/main/func_prod.c b/examples/build_system/cmakev2/features/multi_config/main/func_prod.c new file mode 100644 index 00000000000..2eb2aa0c847 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/main/func_prod.c @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include "func.h" + +void func(void) +{ + printf("func() from func_prod.c (Production) called.\n"); +} diff --git a/examples/build_system/cmakev2/features/multi_config/main/multi_config_example_main.c b/examples/build_system/cmakev2/features/multi_config/main/multi_config_example_main.c new file mode 100644 index 00000000000..cf7e7af07e6 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/main/multi_config_example_main.c @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "sdkconfig.h" +#include "func.h" + +void app_main(void) +{ + printf("This app is built for running on: " CONFIG_EXAMPLE_PRODUCT_NAME "\n"); + + /* This will call func() either from func_dev.c or func_prod.c, depending on + * the build configuration. + */ + func(); + + printf("See README.md for building and running other app configurations.\n"); +} diff --git a/examples/build_system/cmakev2/features/multi_config/sdkconfig.defaults b/examples/build_system/cmakev2/features/multi_config/sdkconfig.defaults new file mode 100644 index 00000000000..ab4daca7417 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/sdkconfig.defaults @@ -0,0 +1,6 @@ +# In this example, the default build (obtained with 'idf.py build') +# targets a hypothetical development platform. +CONFIG_EXAMPLE_PRODUCT_NAME="Blinky Development Board" + +# This selects 'func_dev.c' file to be compiled and linked, see CMakeLists.txt. +CONFIG_EXAMPLE_FUNC_IMPL_DEV=y diff --git a/examples/build_system/cmakev2/features/multi_config/sdkconfig.defaults.prod1 b/examples/build_system/cmakev2/features/multi_config/sdkconfig.defaults.prod1 new file mode 100644 index 00000000000..c21593e5628 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/sdkconfig.defaults.prod1 @@ -0,0 +1,2 @@ +# This build configuration is for a specific product: +CONFIG_EXAMPLE_PRODUCT_NAME="Blinky Smart Light" diff --git a/examples/build_system/cmakev2/features/multi_config/sdkconfig.defaults.prod2 b/examples/build_system/cmakev2/features/multi_config/sdkconfig.defaults.prod2 new file mode 100644 index 00000000000..91d982e9309 --- /dev/null +++ b/examples/build_system/cmakev2/features/multi_config/sdkconfig.defaults.prod2 @@ -0,0 +1,2 @@ +# This build configuration is for a specific product: +CONFIG_EXAMPLE_PRODUCT_NAME="Blinky Smart Switch" diff --git a/examples/build_system/cmakev2/features/plugins/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/CMakeLists.txt new file mode 100644 index 00000000000..655c2d75c26 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) +project(plugins C CXX ASM) + +# Use project-level initialization instead of idf_project_default +idf_project_init() + +# Create executable with explicit component specification +# This approach includes plugin components in the executable +idf_build_executable(${CMAKE_PROJECT_NAME}.elf + COMPONENTS main plugins plugin_hello plugin_nihao) + +# Generate binary +idf_build_binary(${CMAKE_PROJECT_NAME}.elf + OUTPUT_FILE "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin" + TARGET ${CMAKE_PROJECT_NAME}_binary) + +# Create flash target +idf_flash_binary(${CMAKE_PROJECT_NAME}_binary + TARGET app-flash + NAME "app" + FLASH) + +# Check binary size +idf_check_binary_size(${CMAKE_PROJECT_NAME}_binary) + +# Create app target +add_custom_target(app ALL DEPENDS ${CMAKE_PROJECT_NAME}_binary) + +# Generate metadata +idf_build_generate_metadata(BINARY ${CMAKE_PROJECT_NAME}_binary) + +# Create other utility targets +idf_create_menuconfig(${CMAKE_PROJECT_NAME}.elf + TARGET menuconfig) diff --git a/examples/build_system/cmakev2/features/plugins/README.md b/examples/build_system/cmakev2/features/plugins/README.md new file mode 100644 index 00000000000..37c81a22dd5 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/README.md @@ -0,0 +1,132 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Link Time Plugins Registration + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +This example demonstrates features of ESP-IDF build system related to link time registration of plugins. Link time registration of plugins is often used to add multiple implementations of a certain feature without having to make the application aware of all these implementations. With this approach, adding a new implementation is often as simple as adding a new source file or a new component. Aside from plugins, link time registration may be used for other purposes, such as automatic registration of test cases. + +# Overview of link time registration + +When using link time registration, there are typically two challenges: getting the plugin code linked into the application and enumerating the plugins at run time. The following sections explain these problems and the solutions to them. + +## Ensuring that the plugin code is included into the executable + +When GNU linker (ld) links a static library, it considers each object file in the library separately. The object file is ignored if it doesn't resolve any unresolved references known to the linker at that moment. With link-time plugin registration this is typically the case — the application doesn't explicitly reference any plugins, so the linker sees no reason to include the respective object files into the executable. + +Aside from adding an explicit reference from the application to the plugin object file, there are two common ways to resolve this issue: + +1. Link the object file of the plugin directly to the executable, and not via a static library. +2. Instruct the linker to include every object file of a library into the executable, even those which don't resolve any references from the rest of the application. For GNU ld this can be achieved by surrounding the library on the linker command line with `-Wl,--whole-archive` and `-Wl,--no-whole-archive` flags. + +ESP-IDF build system implements the 2nd approach by providing a `WHOLE_ARCHIVE` component property. It can be set in component CMakeLists.txt in two ways. One is to add `WHOLE_ARCHIVE` option when calling `idf_component_register`: + +```cmake +idf_component_register(SRCS file.c + WHOLE_ARCHIVE) +``` + +Another is to call `idf_component_set_property` after registering the component: +```cmake +idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE) +``` + +This will instruct the build system to surround the component library with whole-archive flags on the linker command line, ensuring that all object files from the library get included into the final application. + +Note that the linker also performs "garbage collection" at the end of the linking process, eliminating all functions and global variables which are not referenced anywhere. This is addressed in the current example using `__attribute__((constructor))` function attribute (for dynamic registration) and `KEEP()` linker fragment flag (for static registration). + +## Registering and enumerating the plugins + +To make use of the plugins linked into the application, the application must somehow enumerate them. There are 2 common ways to register and enumerate the plugins: dynamic and static. This example demonstrates both approaches. + +### Dynamic registration (or self-registration) + +With this approach, each plugin module has a function with `__attribute__((constructor))` attribute (in C) or a static global object with a non-trivial constructor (in C++). Startup code calls all constructor functions before the application entry point (`app_main`) is executed. Plugin constructor functions then register themselves by calling a function defined in the application. As an example, this registration function can, add structures describing the plugins into a linked list. + +### Static registration + +This approach relies on plugin description structures being collected into an array at link time. + +For each plugin, a structure describing the plugin (or a pointer to it) is placed into some special input section using `__attribute((section(".plugins_desc")))`. Using the linker script generator features in ESP-IDF, all entries from this input section can be gathered into a continuous array, surrounded by some symbols (e.g. `_plugins_array_start`, `_plugins_array_end`). At run time, the application casts the `&_plugins_array_start` pointer to the plugin description structure pointer and then iterates over structures collected from all plugins. + +## Example code overview + +Unlike `cmakev1` which used a global `COMPONENTS` variable, `cmakev2` uses an explicit dependency-driven model. For plugin architectures, this example demonstrates the use of `idf_build_executable()` with specific components to ensure plugins are properly included and linked. + +```cmake +idf_project_init() +idf_build_executable(${CMAKE_PROJECT_NAME}.elf + COMPONENTS main plugins plugin_hello plugin_nihao) +``` + +This example contains 4 components: + +* `main` — Only calls two sample functions defined in `plugins` component. +* `plugins` — The main part of the plugin system. + + For dynamic registration, it provides an API which plugin components call to register themselves (`example_plugin_register`). + + It also provides two sample functions for the application: + - `example_plugins_list`: prints the list of registered plugins. This function demonstrates static registration. + - `example_plugins_greet`: calls a function defined by each plugin with a given argument. This function demonstrates working with dynamically registered plugins. +* `plugin_hello` and `plugin_nihao` — two almost identical components, each provides one plugin. + + Note that multiple plugins may also be defined in the same component. + +Please refer to the comments in the example code for a more detailed description. + +## How to use example + +### Hardware Required + +This example runs on any ESP development board, no special hardware is required. + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type `Ctrl-]`.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +``` +Nihao plugin performing self-registration... +Successfully registered plugin 'Nihao' +Hello plugin performing self-registration... +Successfully registered plugin 'Hello' +I (325) cpu_start: Starting scheduler on PRO CPU. +I (0) cpu_start: Starting scheduler on APP CPU. +List of plugins: +- Plugin 'Hello', function greet=0x400d4f40 +0x400d4f40: plugin_hello_greet at /home/user/esp/esp-idf/examples/build_system/cmake/plugins/build/../components/plugin_hello/plugin_hello.c:14 + +- Plugin 'Nihao', function greet=0x400d4f70 +0x400d4f70: plugin_nihao_greet at /home/user/esp/esp-idf/examples/build_system/cmake/plugins/build/../components/plugin_nihao/plugin_nihao.c:14 + +Calling greet function of plugin 'Hello'... +Hello, World! +Done with greet function of plugin 'Hello'. +Calling greet function of plugin 'Nihao'... +你好 World! +Done with greet function of plugin 'Nihao'. +``` + +## Troubleshooting + +When implementing the approaches described in this example, the following issues may occur: + +* Plugin self-registration function (constructor function) is never called. To troubleshoot this, check the application .map file — it is generated in the `build` directory of the project. Look for the object file where the constructor function is defined. If the object file and the constructor function are missing, it means that the object file was discarded. Double-check that the `WHOLE_ARCHIVE` property of the component is set correctly. Verify that on the linker command line, the component library is surrounded by `-Wl,--whole-archive`, `-Wl,--no-whole-archive`. To see the linker command line, build the project with verbose (-v) flag. +* With static registration, the plugin description structure is missing from the link-time array. Same as in the case above, start by examining the map file. + - If the plugin object file is missing from the map file, double-check that the `WHOLE_ARCHIVE` property of the component is set correctly (see the instructions above). + - If the plugin object file is present, but the plugin description structure is missing, check that the linker fragment rule and `__attribute((section(...)))` use the same section name. Check that the linker fragment rule uses `KEEP()` flag. + - If the plugin description structure is in the map file but is not located inside the link-time array (is located in some other section), check the generated linker script found in the build directory (`build/esp-idf/esp_system/ld/sections.ld`). Check that the rules for placing the plugin description structure have correct precedence with respect to other rules in the linker script. + diff --git a/examples/build_system/cmakev2/features/plugins/components/plugin_hello/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/components/plugin_hello/CMakeLists.txt new file mode 100644 index 00000000000..1a1ee9fbe4f --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugin_hello/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS plugin_hello.c + PRIV_REQUIRES plugins + WHOLE_ARCHIVE) diff --git a/examples/build_system/cmakev2/features/plugins/components/plugin_hello/plugin_hello.c b/examples/build_system/cmakev2/features/plugins/components/plugin_hello/plugin_hello.c new file mode 100644 index 00000000000..fc4c4ed7555 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugin_hello/plugin_hello.c @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include "plugins_api.h" + +/** + * This is an example function implemented by the plugin. + */ +static void plugin_hello_greet(const char* arg) +{ + if (arg == NULL) { + return; + } + printf("Hello, %s!\n", arg); +} + +/* The code below demonstrates both static and dynamic registration approaches. */ + +/** + * Static registration of this plugin can be achieved by defining the plugin description + * structure and placing it into .plugins_desc section. + * The name of the section and its placement is determined by linker.lf file in 'plugins' component. + */ +static const example_plugin_desc_t __attribute__((section(".plugins_desc"),used)) PLUGIN = { + .name = "Hello", + .greet = &plugin_hello_greet +}; + +/** + * Dynamic registration of this plugin can be achieved by calling plugin registration function + * ('example_plugin_register') from a "constructor" function. Constructor function is called automatically + * during application startup. + */ +static void __attribute__((constructor)) plugin_hello_self_register(void) +{ + printf("Hello plugin performing self-registration...\n"); + example_plugin_register(&(example_plugin_desc_t){ + .name = "Hello", + .greet = &plugin_hello_greet + }); +} diff --git a/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/CMakeLists.txt new file mode 100644 index 00000000000..b215ea454a5 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register(SRCS plugin_nihao.c + PRIV_REQUIRES plugins) + +# This is equivalent to adding WHOLE_ARCHIVE option to the idf_component_register call above: +idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE) diff --git a/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/plugin_nihao.c b/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/plugin_nihao.c new file mode 100644 index 00000000000..0e5c7ba4ae8 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/plugin_nihao.c @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include "plugins_api.h" + +/** + * This is an example function implemented by the plugin. + */ +static void plugin_nihao_greet(const char* arg) +{ + if (arg == NULL) { + return; + } + printf("你好 %s!\n", arg); +} + +/* The code below demonstrates both static and dynamic registration approaches. */ + +/** + * Static registration of this plugin can be achieved by defining the plugin description + * structure and placing it into .plugins_desc section. + * The name of the section and its placement is determined by linker.lf file in 'plugins' component. + */ +static const example_plugin_desc_t __attribute__((section(".plugins_desc"),used)) PLUGIN = { + .name = "Nihao", + .greet = &plugin_nihao_greet +}; + +/** + * Dynamic registration of this plugin can be achieved by calling plugin registration function + * ('example_plugin_register') from a "constructor" function. Constructor function is called automatically + * during application startup. + */ +static void __attribute__((constructor)) plugin_nihao_self_register(void) +{ + printf("Nihao plugin performing self-registration...\n"); + example_plugin_register(&(example_plugin_desc_t){ + .name = "Nihao", + .greet = &plugin_nihao_greet + }); +} diff --git a/examples/build_system/cmakev2/features/plugins/components/plugins/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/components/plugins/CMakeLists.txt new file mode 100644 index 00000000000..c132c9bfad1 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugins/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS plugins.c + INCLUDE_DIRS include + LDFRAGMENTS linker.lf) diff --git a/examples/build_system/cmakev2/features/plugins/components/plugins/include/plugins_api.h b/examples/build_system/cmakev2/features/plugins/components/plugins/include/plugins_api.h new file mode 100644 index 00000000000..6ff6e76e6e1 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugins/include/plugins_api.h @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/* This structure describes the plugin to the rest of the application */ +typedef struct { + /* A pointer to the plugin name */ + const char* name; + + /* A function which the plugin provides to the application. + * In this example, this function prints something to the console + * depending on the value of the argument 'arg'. + */ + void (*greet)(const char* arg); +} example_plugin_desc_t; + +/** + * @brief Register the plugin with the application + * This function is called from each plugin's "constructor" function. + * It adds the plugin to the list. + * @param plugin_desc Pointer to the structure which describes the given plugin. + */ +void example_plugin_register(const example_plugin_desc_t* plugin_desc); + +/** + * @brief Print the list of registered plugins to the console. + * This function is called from the application. + */ +void example_plugins_list(void); + +/** + * @brief Invoke 'greet' function of each registered plugin with the given argument. + * This function is called from the application. + * @param arg argument to pass to plugins' greet functions. + */ +void example_plugins_greet(const char* arg); + + +#ifdef __cplusplus +} +#endif diff --git a/examples/build_system/cmakev2/features/plugins/components/plugins/linker.lf b/examples/build_system/cmakev2/features/plugins/components/plugins/linker.lf new file mode 100644 index 00000000000..0b1dafff45d --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugins/linker.lf @@ -0,0 +1,13 @@ +[sections:plugins_desc] +entries: + .plugins_desc + +[scheme:plugins_desc_default] +entries: + plugins_desc -> flash_rodata + +[mapping:plugins_desc] +archive: * +entries: + * (plugins_desc_default); + plugins_desc -> flash_rodata KEEP() SORT(name) SURROUND(plugins_array) diff --git a/examples/build_system/cmakev2/features/plugins/components/plugins/plugins.c b/examples/build_system/cmakev2/features/plugins/components/plugins/plugins.c new file mode 100644 index 00000000000..0019d35cb2b --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugins/plugins.c @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include +#include +#include "plugins_api.h" + +/** + * Demonstration of dynamic registration (self-registration): + * + * - example_plugin_register function is called from "constructor" functions of each plugin. + * Information about the plugin is passed inside 'example_plugin_desc_t' structure. + * This function adds each plugin description into linked list (s_plugins_list). + * + * - example_plugins_greet function iterates over the linked list. + */ + +struct plugin_record { + example_plugin_desc_t plugin_desc; + LIST_ENTRY(plugin_record) list_entry; +}; + +static LIST_HEAD(plugins_list, plugin_record) s_plugins_list = LIST_HEAD_INITIALIZER(s_plugins_list); + +void example_plugin_register(const example_plugin_desc_t* plugin_desc) +{ + struct plugin_record *record = (struct plugin_record *) malloc(sizeof(struct plugin_record)); + if (record == NULL) { + abort(); + } + memcpy(&record->plugin_desc, plugin_desc, sizeof(*plugin_desc)); + + static struct plugin_record *tail = NULL; + if (tail == NULL) { + LIST_INSERT_HEAD(&s_plugins_list, record, list_entry); + } else { + LIST_INSERT_AFTER(tail, record, list_entry); + } + tail = record; + printf("Successfully registered plugin '%s'\n", plugin_desc->name); +} + +void example_plugins_greet(const char* arg) +{ + struct plugin_record *it; + LIST_FOREACH(it, &s_plugins_list, list_entry) { + printf("Calling greet function of plugin '%s'...\n", it->plugin_desc.name); + (*it->plugin_desc.greet)(arg); + printf("Done with greet function of plugin '%s'.\n", it->plugin_desc.name); + } +} + +/** + * Demonstration of static registration. + * Symbols '_plugins_array_start' and '_plugins_array_end' mark the beginning and end + * of the array where 'example_plugin_desc_t' structures are placed by the linker. + * The names of these variables are determined by linker.lf in 'plugins' component, + * look for 'SURROUND(plugins_array)'. + */ + +void example_plugins_list(void) +{ + printf("List of plugins:\n"); + extern const example_plugin_desc_t _plugins_array_start; + extern const example_plugin_desc_t _plugins_array_end; + for (const example_plugin_desc_t* it = &_plugins_array_start; it != &_plugins_array_end; ++it) { + printf("- Plugin '%s', function greet=%p\n", it->name, it->greet); + } +} diff --git a/examples/build_system/cmakev2/features/plugins/main/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/main/CMakeLists.txt new file mode 100644 index 00000000000..2c35b46ce1c --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "plugins_example_main.c" + INCLUDE_DIRS "." + PRIV_REQUIRES plugins) diff --git a/examples/build_system/cmakev2/features/plugins/main/plugins_example_main.c b/examples/build_system/cmakev2/features/plugins/main/plugins_example_main.c new file mode 100644 index 00000000000..22af957366f --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/main/plugins_example_main.c @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "plugins_api.h" + +void app_main(void) +{ + example_plugins_list(); + example_plugins_greet("World"); +} diff --git a/examples/build_system/cmakev2/get-started/hello_world/CMakeLists.txt b/examples/build_system/cmakev2/get-started/hello_world/CMakeLists.txt new file mode 100644 index 00000000000..95f65e6ff4c --- /dev/null +++ b/examples/build_system/cmakev2/get-started/hello_world/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) + +project(hello_world C CXX ASM) + +idf_project_default() diff --git a/examples/build_system/cmakev2/get-started/hello_world/README.md b/examples/build_system/cmakev2/get-started/hello_world/README.md new file mode 100644 index 00000000000..61af59cc831 --- /dev/null +++ b/examples/build_system/cmakev2/get-started/hello_world/README.md @@ -0,0 +1,53 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Hello World Example + +Starts a FreeRTOS task to print "Hello World". + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +## How to use example + +Follow detailed instructions provided specifically for this example. + +Select the instructions depending on Espressif chip installed on your development board: + +- [ESP32 Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/stable/get-started/index.html) +- [ESP32-S2 Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/get-started/index.html) + + +## Example folder contents + +The project **hello_world** contains one source file in C language [hello_world_main.c](main/hello_world_main.c). The file is located in folder [main](main). + +ESP-IDF projects are built using CMake. The project build configuration is contained in `CMakeLists.txt` files that provide set of directives and instructions describing the project's source files and targets (executable, library, or both). + +Below is short explanation of remaining files in the project folder. + +``` +├── CMakeLists.txt +├── pytest_hello_world.py Python script used for automated testing +├── main +│ ├── CMakeLists.txt +│ └── hello_world_main.c +└── README.md This is the file you are currently reading +``` + +For more information on structure and contents of ESP-IDF projects, please refer to Section [Build System v2](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/build-system-v2.html) of the ESP-IDF Programming Guide. + +## Troubleshooting + +* Program upload failure + + * Hardware connection is not correct: run `idf.py -p PORT monitor`, and reboot your board to see if there are any output logs. + * The baud rate for downloading is too high: lower your baud rate in the `menuconfig` menu, and try again. + +## Technical support and feedback + +Please use the following feedback channels: + +* For technical queries, go to the [esp32.com](https://esp32.com/) forum +* For a feature request or bug report, create a [GitHub issue](https://github.com/espressif/esp-idf/issues) + +We will get back to you as soon as possible. diff --git a/tools/cmakev2/test/components/hello_world_example/CMakeLists.txt b/examples/build_system/cmakev2/get-started/hello_world/main/CMakeLists.txt similarity index 100% rename from tools/cmakev2/test/components/hello_world_example/CMakeLists.txt rename to examples/build_system/cmakev2/get-started/hello_world/main/CMakeLists.txt diff --git a/tools/cmakev2/test/components/hello_world_example/hello_world_main.c b/examples/build_system/cmakev2/get-started/hello_world/main/hello_world_main.c similarity index 90% rename from tools/cmakev2/test/components/hello_world_example/hello_world_main.c rename to examples/build_system/cmakev2/get-started/hello_world/main/hello_world_main.c index 95cab254a1a..ba30858a6b1 100644 --- a/tools/cmakev2/test/components/hello_world_example/hello_world_main.c +++ b/examples/build_system/cmakev2/get-started/hello_world/main/hello_world_main.c @@ -1,8 +1,9 @@ /* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD * - * SPDX-License-Identifier: Apache-2.0 + * SPDX-License-Identifier: CC0-1.0 */ + #include #include #include "sdkconfig.h" @@ -31,7 +32,7 @@ void app_main(void) unsigned major_rev = chip_info.revision / 100; unsigned minor_rev = chip_info.revision % 100; printf("silicon revision v%d.%d, ", major_rev, minor_rev); - if (esp_flash_get_size(NULL, &flash_size) != ESP_OK) { + if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) { printf("Get flash size failed"); return; } diff --git a/examples/build_system/cmakev2/get-started/hello_world/sdkconfig.ci b/examples/build_system/cmakev2/get-started/hello_world/sdkconfig.ci new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/ci/exclude_check_tools_files.txt b/tools/ci/exclude_check_tools_files.txt index 1eac90962a3..439807a04e6 100644 --- a/tools/ci/exclude_check_tools_files.txt +++ b/tools/ci/exclude_check_tools_files.txt @@ -51,3 +51,4 @@ tools/templates/sample_component/main.c tools/templates/sample_project/CMakeLists.txt tools/templates/sample_project/main/CMakeLists.txt tools/templates/sample_project/main/main.c +tools/test_build_system/buildv2_test_app/main/KConfig.projbuild diff --git a/tools/cmakev2/build.cmake b/tools/cmakev2/build.cmake index 79e806a3bf2..bbdf8c532c3 100644 --- a/tools/cmakev2/build.cmake +++ b/tools/cmakev2/build.cmake @@ -112,6 +112,32 @@ function(__dump_build_properties) endforeach() endfunction() +#[[ + __idf_build_dispatch_build_event( ) + + *event[in]* + + Build event name. Currently only ``POST_ELF`` is supported. Other build + events may be extended when required. + + *target[in]* + + Name of the primary CMake target at this event point. Passed as the + sole argument to every registered callback so that callbacks can + operate on the correct target without querying build properties. + For ``POST_ELF`` this is the executable target name. + + Internal dispatcher called by the build system at well-defined lifecycle + points. Invokes every CMake function registered for + ``event`` via ``idf_component_register_build_event_callback``. +#]] +function(__idf_build_dispatch_build_event event target) + idf_build_get_property(callbacks "__BUILD_EVENT_CALLBACKS_${event}") + foreach(cb IN LISTS callbacks) + cmake_language(CALL "${cb}" "${target}") + endforeach() +endfunction() + #[[ __get_library_interface_or_die(LIBRARY OUTPUT ) @@ -336,6 +362,12 @@ function(idf_build_library library) target_link_libraries("${library}" INTERFACE "${component_interface}") endforeach() + # Process optional requirements in DEFERRED mode only (no-op in IMMEDIATE or when unset). + idf_build_get_property(opt_req_mode IDF_COMPONENT_OPTIONAL_REQUIRES_MODE) + if("${opt_req_mode}" STREQUAL "DEFERRED") + __idf_component_process_optional_requires() + endif() + # Get all targets transitively linked to the library interface target. __get_target_dependencies(TARGET "${library}" OUTPUT dependencies) @@ -597,6 +629,8 @@ function(idf_build_executable executable) if(ARG_MAPFILE_TARGET AND "${linker_type}" STREQUAL "GNU") set(mapfile "${CMAKE_BINARY_DIR}/${ARG_NAME}.map") target_link_options(${executable} PRIVATE "LINKER:--Map=${mapfile}") + # Add cross-reference table to the map file + target_link_options(${executable} PRIVATE "LINKER:--cref") add_custom_command( OUTPUT "${mapfile}" DEPENDS ${executable} @@ -608,6 +642,9 @@ function(idf_build_executable executable) endif() set_target_properties(${executable} PROPERTIES LIBRARY_INTERFACE ${library}) + + # Dispatch POST_ELF event once the executable target exists + __idf_build_dispatch_build_event(POST_ELF "${executable}") endfunction() #[[ @@ -715,35 +752,55 @@ endfunction() .. code-block:: cmake - idf_build_generate_metadata( + idf_build_generate_metadata([BINARY ] + [EXECUTABLE ] [OUTPUT_FILE ]) - *binary[in]* + *BINARY[in,opt]* Binary target for which to generate a metadata file. + *EXECUTABLE[in,opt]* + + Executable target for which to generate a metadata file. + *OUTPUT_FILE[in,opt]* Optional output file path for storing the metadata. If not provided, the default path ``/project_description.json`` is used. - Generate metadata for the specified ``binary`` and store it in the - specified ``OUTPUT_FILE``. If no ``OUTPUT_FILE`` is provided, the default - location ``/project_description.json`` will be used. + Generate metadata for the specified ``binary`` or ``executable`` target and + store it in the specified ``OUTPUT_FILE``. If no ``OUTPUT_FILE`` is + provided, the default location ``/project_description.json`` will be + used. #]] -function(idf_build_generate_metadata binary) +function(idf_build_generate_metadata) set(options) - set(one_value OUTPUT_FILE) + set(one_value OUTPUT_FILE BINARY EXECUTABLE) set(multi_value) cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) - # The EXECUTABLE_TARGET property is set by the idf_build_binary or - # the idf_sign_binary function. - get_target_property(executable "${binary}" EXECUTABLE_TARGET) - if(NOT executable) - idf_die("Binary target '${binary}' is missing 'EXECUTABLE_TARGET' property.") + if(NOT DEFINED ARG_BINARY AND NOT DEFINED ARG_EXECUTABLE) + idf_die("BINARY or EXECUTABLE option is required") endif() - __get_executable_library_or_die(TARGET "${executable}" OUTPUT library) + + if(DEFINED ARG_BINARY) + # The EXECUTABLE_TARGET property is set by the idf_build_binary or + # the idf_sign_binary function. + get_target_property(ARG_EXECUTABLE "${ARG_BINARY}" EXECUTABLE_TARGET) + if(NOT ARG_EXECUTABLE) + idf_die("Binary target '${ARG_BINARY}' is missing 'EXECUTABLE_TARGET' property.") + endif() + + # The BINARY_PATH property is set by the idf_build_binary or + # the idf_sign_binary function. + get_target_property(binary_path ${ARG_BINARY} BINARY_PATH) + if(NOT binary_path) + idf_die("Binary target '${ARG_BINARY}' is missing 'BINARY_PATH' property.") + endif() + get_filename_component(PROJECT_BIN "${binary_path}" NAME) + endif() + __get_executable_library_or_die(TARGET "${ARG_EXECUTABLE}" OUTPUT library) idf_build_get_property(PROJECT_NAME PROJECT_NAME) idf_build_get_property(PROJECT_VER PROJECT_VER) @@ -752,14 +809,7 @@ function(idf_build_generate_metadata binary) idf_build_get_property(BUILD_DIR BUILD_DIR) idf_build_get_property(SDKCONFIG SDKCONFIG) idf_build_get_property(SDKCONFIG_DEFAULTS SDKCONFIG_DEFAULTS) - set(PROJECT_EXECUTABLE "$") - # The BINARY_PATH property is set by the idf_build_binary or - # the idf_sign_binary function. - get_target_property(binary_path ${binary} BINARY_PATH) - if(NOT binary_path) - idf_die("Binary target '${binary}' is missing 'BINARY_PATH' property.") - endif() - get_filename_component(PROJECT_BIN "${binary_path}" NAME) + set(PROJECT_EXECUTABLE "$") if(NOT PROJECT_BIN) set(PROJECT_BIN "") endif() diff --git a/tools/cmakev2/compat.cmake b/tools/cmakev2/compat.cmake index 842b30deea1..70fe67b3ea1 100644 --- a/tools/cmakev2/compat.cmake +++ b/tools/cmakev2/compat.cmake @@ -196,6 +196,106 @@ function(target_linker_script target deptype scriptfiles) endfunction() #[[ + __idf_component_process_optional_requires() + + Called by idf_build_library() before LIBRARY_COMPONENTS_LINKED is computed. + For each pending (caller, type, req) entry recorded by + idf_component_optional_requires, links req's interface target to the + caller's real target if both are present in this library. +#]] +function(__idf_component_process_optional_requires) + # Nothing to do if no component has called idf_component_optional_requires. + idf_build_get_property(callers __DEFERRED_OPTIONAL_CALLERS) + if(NOT callers) + return() + endif() + + # DEFERRED mode + multiple libraries is disallowed + idf_build_get_property(libraries_list LIBRARY_INTERFACES) + list(LENGTH libraries_list lib_count) + if(lib_count GREATER 1) + idf_die("DEFERRED optional requires mode cannot be used when building " + "multiple libraries (detected ${lib_count} libraries). " + "Set IDF_COMPONENT_OPTIONAL_REQUIRES_MODE to IMMEDIATE for multi-library projects.") + endif() + + # Fetch the components included in the project (single library only here, + # so this equals the library's component set). + idf_build_get_property(components_included COMPONENTS_INCLUDED) + set(library_component_interfaces "") + foreach(comp_name IN LISTS components_included) + __get_component_interface(COMPONENT "${comp_name}" OUTPUT dep_interface) + if(dep_interface AND NOT "${dep_interface}" IN_LIST library_component_interfaces) + list(APPEND library_component_interfaces "${dep_interface}") + endif() + endforeach() + + # For every caller that recorded optional requirements, check whether it is + # part of this library and, if so, apply any pending pairs whose + # requirement is also in the library. + foreach(caller_target IN LISTS callers) + # Resolve the caller target to its component interface. + __get_component_interface(COMPONENT "${caller_target}" OUTPUT caller_interface) + if(NOT caller_interface) + continue() + endif() + + # Skip if this caller is not linked into the current library. + if(NOT "${caller_interface}" IN_LIST library_component_interfaces) + continue() + endif() + + # Fetch this caller's pending pairs and the pairs already processed in + # earlier idf_build_library() calls. + idf_build_get_property(pairs "__OPT_REQ_${caller_target}") + idf_build_get_property(done "__OPT_REQ_DONE_${caller_target}") + + foreach(pair IN LISTS pairs) + # Skip pairs already processed for a previous library. The + # target_link_libraries call and property updates are permanent + # global mutations; repeating them would be redundant. + if("${pair}" IN_LIST done) + continue() + endif() + + # Decode the "type::::req_interface" entry. + string(REPLACE "::::" ";" split "${pair}") + list(GET split 0 link_type) + list(GET split 1 req_interface) + + # Skip if the optional requirement is not part of this library. + if(NOT "${req_interface}" IN_LIST library_component_interfaces) + continue() + endif() + + # Link the caller's real target to the requirement's interface target. + target_link_libraries("${caller_target}" "${link_type}" "${req_interface}") + + # Update the caller's REQUIRES or PRIV_REQUIRES property. + idf_component_get_property(req_name "${req_interface}" COMPONENT_NAME) + if("${link_type}" STREQUAL "PRIVATE") + idf_component_get_property(existing "${caller_interface}" PRIV_REQUIRES) + if(NOT "${req_name}" IN_LIST existing) + idf_component_set_property("${caller_interface}" PRIV_REQUIRES + "${req_name}" APPEND) + endif() + elseif("${link_type}" STREQUAL "PUBLIC" OR "${link_type}" STREQUAL "INTERFACE") + idf_component_get_property(existing "${caller_interface}" REQUIRES) + if(NOT "${req_name}" IN_LIST existing) + idf_component_set_property("${caller_interface}" REQUIRES + "${req_name}" APPEND) + endif() + endif() + + # Mark this pair as done so it is not processed again for subsequent + # libraries. + idf_build_set_property("__OPT_REQ_DONE_${caller_target}" "${pair}" APPEND) + endforeach() + endforeach() +endfunction() + + +#[[api .. cmakev2:function:: idf_component_optional_requires .. code-block:: cmake @@ -211,22 +311,87 @@ endfunction() The component name that should be added as a dependency to the evaluated component. It may be provided multiple times. - Add a dependency on a specific component only if it is included in the - build. + Add a dependency on a specific component only if the component is + recognized by the build system. The behavior is controlled by the + ``IDF_COMPONENT_OPTIONAL_REQUIRES_MODE`` build property: + + * **IMMEDIATE** (default): Include the component and link it to the + caller if it is discovered. Safe for multi-library projects but may + pull in more components than strictly needed. + * **DEFERRED**: Do not include or link immediately; record the request + and resolve it in :cmakev2:ref:`idf_build_library` so that the + component is linked only when it is part of the library's dependency + graph. Matches v1 semantics and reduces unnecessary components, but + must not be used when building multiple libraries (see docs). + + .. note:: + This function should be avoided in cmakev2, where + dependencies should be added based on configuration options. This is purely + for backward compatibility with cmakev1. + + .. warning:: + In multi-library projects with **DEFERRED** mode, optional requires + resolved when processing a later library apply globally to shared + component targets. Earlier libraries then link that optional component + too, but their per-library metadata (e.g. linker fragments) was + already computed and is not updated. DEFERRED mode is therefore + disallowed when more than one library is built. #]] -function(idf_component_optional_requires req_type) +function(idf_component_optional_requires type) set(optional_reqs ${ARGN}) - foreach(req ${optional_reqs}) - __get_component_interface(COMPONENT "${req}" OUTPUT req_interface) - if("${req_interface}" STREQUAL "NOTFOUND") - continue() - endif() - idf_component_get_property(req_alias "${req}" COMPONENT_ALIAS) - # The component alias is created only after the component is included, - # meaning the add_subdirectory command for it has been called. This can - # be used to detect if a component has already been added to the build. - target_link_libraries(${COMPONENT_TARGET} ${req_type} "$<$:${req_interface}>") - endforeach() + + idf_build_get_property(mode IDF_COMPONENT_OPTIONAL_REQUIRES_MODE) + if(NOT mode) + set(mode IMMEDIATE) + endif() + + if("${mode}" STREQUAL "DEFERRED") + # DEFERRED mode: record for resolution in idf_build_library. + # The component is linked only if it is part of the library's dependency graph. + foreach(req ${optional_reqs}) + __get_component_interface(COMPONENT "${req}" OUTPUT req_interface) + if("${req_interface}" STREQUAL "NOTFOUND") + continue() + endif() + + idf_build_get_property(callers __DEFERRED_OPTIONAL_CALLERS) + if(NOT "${COMPONENT_TARGET}" IN_LIST callers) + idf_build_set_property(__DEFERRED_OPTIONAL_CALLERS "${COMPONENT_TARGET}" APPEND) + endif() + + # Store the optional requirement in the __OPT_REQ_ property. + idf_build_set_property("__OPT_REQ_${COMPONENT_TARGET}" + "${type}::::${req_interface}" APPEND) + endforeach() + else() + # IMMEDIATE mode: include and link discovered components unconditionally. + foreach(req ${optional_reqs}) + __get_component_interface(COMPONENT "${req}" OUTPUT req_interface) + if("${req_interface}" STREQUAL "NOTFOUND") + continue() + endif() + idf_component_include("${req}") + + if("${type}" STREQUAL "PRIVATE") + set(req_type PRIV_REQUIRES) + elseif("${type}" STREQUAL "PUBLIC") + set(req_type REQUIRES) + else() + set(req_type "") + endif() + + if(req_type) + idf_component_get_property(req_name "${req_interface}" COMPONENT_NAME) + idf_component_get_property(target_reqs "${COMPONENT_NAME}" ${req_type}) + if(NOT "${req_name}" IN_LIST target_reqs) + idf_component_set_property("${COMPONENT_NAME}" ${req_type} "${req_name}" APPEND) + target_link_libraries(${COMPONENT_TARGET} ${type} ${req_interface}) + endif() + else() + target_link_libraries(${COMPONENT_TARGET} ${type} ${req_interface}) + endif() + endforeach() + endif() endfunction() #[[ @@ -404,7 +569,7 @@ function(idf_component_register) idf_build_get_property(compile_definitions COMPILE_DEFINITIONS GENERATOR_EXPRESSION) add_compile_definitions("${compile_definitions}") - __get_compile_options(OUTPUT compile_options) + idf_build_get_compile_options(compile_options) add_compile_options("${compile_options}") idf_build_get_property(common_component_interfaces __COMMON_COMPONENT_INTERFACES) diff --git a/tools/cmakev2/component.cmake b/tools/cmakev2/component.cmake index b38680cc993..a0262a9ac8f 100644 --- a/tools/cmakev2/component.cmake +++ b/tools/cmakev2/component.cmake @@ -885,12 +885,12 @@ function(idf_component_include name) # helps in detecting and reporting circular dependencies, such as # C1->C2->C1. In this scenario, C2 can still use the C1 interface target, # but C1 will only be fully evaluated after C2 has been evaluated. - if("${component_name}" IN_LIST __DEPENDENCY_CHAIN) - idf_dbg("Component '${name}' in circular dependency chain '${__DEPENDENCY_CHAIN}'") + if("${component_interface}" IN_LIST __DEPENDENCY_CHAIN) + idf_dbg("Component '${component_interface}' in circular dependency chain '${__DEPENDENCY_CHAIN}'") return() endif() - list(APPEND __DEPENDENCY_CHAIN "${name}") + list(APPEND __DEPENDENCY_CHAIN "${component_interface}") # Evaluate the CMakeLists.txt file of the component. idf_component_get_property(component_build_dir "${component_name}" COMPONENT_BUILD_DIR) add_subdirectory("${component_directory}" "${component_build_dir}") @@ -1055,6 +1055,65 @@ function(idf_component_include name) idf_build_get_property(compile_definitions COMPILE_DEFINITIONS GENERATOR_EXPRESSION) target_compile_definitions("${component_real_target}" PRIVATE "${compile_definitions}") - __get_compile_options(OUTPUT compile_options) + idf_build_get_compile_options(compile_options) target_compile_options("${component_real_target}" BEFORE PRIVATE "${compile_options}") endfunction() + +#[[api +.. cmakev2:function:: idf_component_register_build_event_callback + + .. code-block:: cmake + + idf_component_register_build_event_callback(EVENT CALLBACK ) + + *EVENT[in]* + + Build lifecycle event at which the callback will be invoked. Currently + only ``POST_ELF`` is supported. + + *CALLBACK[in]* + + Name of a CMake function defined in the component's project_include.cmake file. + The build system calls this function with the primary CMake target as its argument + at the specified event point. For ``POST_ELF`` this is the executable target. + + Example:: + + # project_include.cmake + function(my_component_post_elf_hook target) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND my_tool "$") + endfunction() + + idf_component_register_build_event_callback( + EVENT POST_ELF + CALLBACK my_component_post_elf_hook) + +#]] +function(idf_component_register_build_event_callback) + set(options) + set(one_value EVENT CALLBACK) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_EVENT) + idf_die("idf_component_register_build_event_callback: EVENT option is required") + endif() + + if(NOT DEFINED ARG_CALLBACK) + idf_die("idf_component_register_build_event_callback: CALLBACK option is required") + endif() + + set(valid_events POST_ELF) + if(NOT "${ARG_EVENT}" IN_LIST valid_events) + idf_die("idf_component_register_build_event_callback: unknown event '${ARG_EVENT}'. " + "Valid events: ${valid_events}") + endif() + + if(NOT COMMAND "${ARG_CALLBACK}") + idf_die("idf_component_register_build_event_callback: callback '${ARG_CALLBACK}' " + "is not a known CMake function. Define it before calling this function.") + endif() + + idf_build_set_property("__BUILD_EVENT_CALLBACKS_${ARG_EVENT}" "${ARG_CALLBACK}" APPEND) +endfunction() diff --git a/tools/cmakev2/idf.cmake b/tools/cmakev2/idf.cmake index 7aaf3f64add..631309e7a63 100644 --- a/tools/cmakev2/idf.cmake +++ b/tools/cmakev2/idf.cmake @@ -266,16 +266,15 @@ endfunction() #[[ __init_toolchain() - Determine the IDF_TOOLCHAIN value from the IDF_TOOLCHAIN environment - variable or the CMake cache variable. If none of these are set, use the - default gcc toolchain. Ensure there are no inconsistencies in the - IDF_TOOLCHAIN values set in different locations. Also ensure that the + Determine the toolchain file, set IDF_TOOLCHAIN_FILE build property and + global CMAKE_TOOLCHAIN_FILE CMake variable. Also ensure that the CMAKE_TOOLCHAIN_FILE is set to the correct file according to the current IDF_TARGET. - Set the IDF_TOOLCHAIN and IDF_TOOLCHAIN_FILE build properties. Also, - configure the IDF_TOOLCHAIN CMake cache variable and set the - CMAKE_TOOLCHAIN_FILE global variable. + Note: The IDF_TOOLCHAIN build property is set after the toolchain + configuration in ``idf_project_init``. The ``tools/cmake/toolchain.cmake`` + is included in the toolchain file and it sets the IDF_TOOLCHAIN variable in + CMake's cache. #]] function(__init_toolchain) set(cache_toolchain $CACHE{IDF_TOOLCHAIN}) @@ -318,9 +317,7 @@ function(__init_toolchain) idf_die("Toolchain file ${toolchain_file} not found") endif() - set(IDF_TOOLCHAIN ${toolchain} CACHE STRING "IDF Build Toolchain Type") set(CMAKE_TOOLCHAIN_FILE "${toolchain_file}" PARENT_SCOPE) - idf_build_set_property(IDF_TOOLCHAIN "${toolchain}") idf_build_set_property(IDF_TOOLCHAIN_FILE "${toolchain_file}") endfunction() diff --git a/tools/cmakev2/kconfig.cmake b/tools/cmakev2/kconfig.cmake index ceda49dbde8..de38a6ab7b3 100644 --- a/tools/cmakev2/kconfig.cmake +++ b/tools/cmakev2/kconfig.cmake @@ -68,6 +68,32 @@ function(__init_kconfig) idf_build_set_property(CONFIG_DIR "${config_dir}") endfunction() +#[[ + __create_sdkconfig_orig_copy() + + Create a copy of the sdkconfig file in the build directory to preserve + all original options, including those from managed components that are + not yet known to kconfgen. The copy is referenced via __SDKCONFIG_ORIG + and used as the --config input for kconfgen, so that unknown options + are not dropped during intermediate sdkconfig regeneration rounds. + + After the component manager has fetched all components (and their Kconfig + definitions are available), __SDKCONFIG_ORIG is reset to point to the + real sdkconfig so that subsequent operations (menuconfig, etc.) read + and write the actual file. +#]] +function(__create_sdkconfig_orig_copy) + idf_build_get_property(sdkconfig SDKCONFIG) + idf_build_get_property(build_dir BUILD_DIR) + set(sdkconfig_orig "${build_dir}/sdkconfig.orig") + if(EXISTS "${sdkconfig}") + file(COPY_FILE "${sdkconfig}" "${sdkconfig_orig}" ONLY_IF_DIFFERENT) + else() + set(sdkconfig_orig "${sdkconfig}") + endif() + idf_build_set_property(__SDKCONFIG_ORIG "${sdkconfig_orig}") +endfunction() + #[[ .. cmakev2:function:: __should_generate_sdkconfig @@ -407,9 +433,10 @@ function(__create_executable_config_env_file executable) foreach(component_interface IN LISTS component_interfaces) __idf_component_get_property_unchecked(component_kconfig "${component_interface}" __KCONFIG) __idf_component_get_property_unchecked(component_projbuild "${component_interface}" __KCONFIG_PROJBUILD) + __idf_component_get_property_unchecked(component_real_target "${component_interface}" COMPONENT_REAL_TARGET) if(component_kconfig) - if("${component_interface}" IN_LIST component_interfaces_linked) + if("${component_interface}" IN_LIST component_interfaces_linked AND component_real_target) list(APPEND kconfigs "${component_kconfig}") else() list(APPEND kconfigs_excluded "${component_kconfig}") @@ -417,7 +444,7 @@ function(__create_executable_config_env_file executable) endif() if(component_projbuild) - if("${component_interface}" IN_LIST component_interfaces_linked) + if("${component_interface}" IN_LIST component_interfaces_linked AND component_real_target) list(APPEND kconfigs_projbuild "${component_projbuild}") else() list(APPEND kconfigs_projbuild_excluded "${component_projbuild}") @@ -629,12 +656,18 @@ function(__generate_kconfig_outputs) set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY ADDITIONAL_CLEAN_FILES "${sdkconfig_header}" "${sdkconfig_cmake}") - # Store output paths in build properties + # Store output paths in build properties (internal) idf_build_set_property(__SDKCONFIG_HEADER "${sdkconfig_header}") idf_build_set_property(__SDKCONFIG_CMAKE "${sdkconfig_cmake}") idf_build_set_property(__SDKCONFIG_JSON "${sdkconfig_json}") idf_build_set_property(__SDKCONFIG_JSON_MENUS "${sdkconfig_json_menus}") + # Public aliases for backward compatibility with components (e.g. ULP) + idf_build_set_property(SDKCONFIG_HEADER "${sdkconfig_header}") + idf_build_set_property(SDKCONFIG_CMAKE "${sdkconfig_cmake}") + idf_build_set_property(SDKCONFIG_JSON "${sdkconfig_json}") + idf_build_set_property(SDKCONFIG_JSON_MENUS "${sdkconfig_json_menus}") + idf_msg("Generated Kconfig outputs in ${config_dir}") endfunction() @@ -670,12 +703,20 @@ function(__create_base_kconfgen_command sdkconfig sdkconfig_defaults) endforeach() endif() + # Use __SDKCONFIG_ORIG for --config so that unknown options from managed + # components are preserved during intermediate kconfgen runs. Falls back + # to the real sdkconfig when __SDKCONFIG_ORIG is not yet set. + idf_build_get_property(sdkconfig_orig __SDKCONFIG_ORIG) + if(NOT sdkconfig_orig) + set(sdkconfig_orig "${sdkconfig}") + endif() + # Create base kconfgen command set(base_kconfgen_cmd ${python} -m kconfgen --list-separator=semicolon --kconfig "${root_kconfig}" --sdkconfig-rename "${root_sdkconfig_rename}" - --config "${sdkconfig}" + --config "${sdkconfig_orig}" ${defaults_args} --env "IDF_BUILD_V2=y") diff --git a/tools/cmakev2/manager.cmake b/tools/cmakev2/manager.cmake index 7bf8d6e6582..c0488e6ec01 100644 --- a/tools/cmakev2/manager.cmake +++ b/tools/cmakev2/manager.cmake @@ -83,6 +83,15 @@ function(__fetch_components_from_registry) idf_die("IDF Component Manager error: ${cmgr_result}") endif() endwhile() + + # All managed components are now fetched and their Kconfig definitions + # are available. Point __SDKCONFIG_ORIG back to the real sdkconfig so + # that subsequent operations (menuconfig, save-defconfig, confserver) + # read and write the actual file, not the preserved copy. + idf_build_get_property(sdkconfig SDKCONFIG) + idf_build_set_property(__SDKCONFIG_ORIG "${sdkconfig}") + idf_build_get_property(sdkconfig_defaults SDKCONFIG_DEFAULTS) + __create_base_kconfgen_command("${sdkconfig}" "${sdkconfig_defaults}") endfunction() #[[ diff --git a/tools/cmakev2/project.cmake b/tools/cmakev2/project.cmake index 0e0f1626fee..b26a9a49a1e 100644 --- a/tools/cmakev2/project.cmake +++ b/tools/cmakev2/project.cmake @@ -419,8 +419,6 @@ function(__init_project_configuration) if("${linker_type}" STREQUAL "GNU") set(target_upper "${idf_target}") string(TOUPPER ${target_upper} target_upper) - # Add cross-reference table to the map file - list(APPEND link_options "-Wl,--cref") # Add this symbol as a hint for esp_idf_size to guess the target name list(APPEND link_options "-Wl,--defsym=IDF_TARGET_${target_upper}=0") # Check if linker supports --no-warn-rwx-segments @@ -560,6 +558,11 @@ macro(idf_project_init) # Ensure this function is executed only once throughout the entire # project. + # The IDF_TOOLCHAIN variable is established as a CMake cache variable + # during the toolchain initialization process in + # ``tools/cmake/toolchain.cmake``. + idf_build_set_property(IDF_TOOLCHAIN "${IDF_TOOLCHAIN}") + # Warn about the use of deprecated variables. deprecate_variable(COMPONENTS) deprecate_variable(EXCLUDE_COMPONENTS) @@ -576,6 +579,9 @@ macro(idf_project_init) # Discover and initialize components __init_components() + # Save original sdkconfig before kconfgen may drop unknown options + __create_sdkconfig_orig_copy() + # Generate initial sdkconfig with discovered components __generate_sdkconfig() @@ -598,6 +604,15 @@ macro(idf_project_init) # Ensure this is done after including the sdkconfig. __init_idf_target_arch() + # Make build properties available as CMake variables for backward + # compatibility with project_include.cmake files (e.g. ULP component + # references ${SDKCONFIG_HEADER} and ${SDKCONFIG_CMAKE} directly). + idf_build_get_property(build_properties BUILD_PROPERTIES) + foreach(build_property IN LISTS build_properties) + idf_build_get_property(val ${build_property}) + set(${build_property} "${val}") + endforeach() + # Include all project_include.cmake files for the components that have # been discovered. idf_build_get_property(component_interfaces COMPONENT_INTERFACES) @@ -707,7 +722,7 @@ function(__project_default) COMPONENTS main MAPFILE_TARGET "${executable}_mapfile") - if(CONFIG_APP_BUILD_GENERATE_BINARIES) + if(CONFIG_APP_BUILD_GENERATE_BINARIES AND TARGET idf::esptool_py) # Is it possible to have a configuration where # CONFIG_APP_BUILD_GENERATE_BINARIES is not set? @@ -726,7 +741,7 @@ function(__project_default) TARGET app-flash NAME "app" FLASH) - idf_build_generate_metadata("${executable}_binary_signed") + idf_build_generate_metadata(BINARY "${executable}_binary_signed") else() idf_build_binary("${executable}" OUTPUT_FILE "${build_dir}/${executable}.bin" @@ -743,10 +758,12 @@ function(__project_default) idf_create_dfu("${executable}_binary" TARGET dfu) - idf_build_generate_metadata("${executable}_binary") + idf_build_generate_metadata(BINARY "${executable}_binary") endif() idf_build_generate_flasher_args() + else() + idf_build_generate_metadata(EXECUTABLE "${executable}") endif() idf_create_menuconfig("${executable}" @@ -791,6 +808,11 @@ endfunction() #]] macro(idf_project_default) idf_project_init() + + # Use DEFERRED optional-requires resolution only when this will be the sole + # library being built. + idf_build_set_property(IDF_COMPONENT_OPTIONAL_REQUIRES_MODE DEFERRED) + # Only the idf_project_init macro needs be called within the global scope, # as it includes the project_include.cmake files and the cmake version of # the configuration. The remaining functionality of the idf_project_default diff --git a/tools/cmakev2/test/CMakeLists.txt b/tools/cmakev2/test/CMakeLists.txt deleted file mode 100644 index 0981ec92afb..00000000000 --- a/tools/cmakev2/test/CMakeLists.txt +++ /dev/null @@ -1,296 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD -# SPDX-License-Identifier: Apache-2.0 - -# Simple project for basic cmakev2 testing -# Run: cmake -S . -B build -cmake_minimum_required(VERSION 3.22) - -include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) - -project(cmakev2 - VERSION 1.2.3 - LANGUAGES C CXX ASM) - -add_custom_target(flash) - -idf_project_init() - -idf_build_set_property(__BUILD_COMPONENT_DEPGRAPH_ENABLED 1) - -# Test component priority -function(test_component_priority) - # Set the idf component to be replaced with a testing component of higher - # priority. - set(component_name "esp_system") - - # Check that idf component is between discovered components. - __get_component_interface(COMPONENT "${component_name}" - OUTPUT component_interface) - if("${component_interface}" STREQUAL "NOTFOUND") - idf_die("Component '${component_name}' not found") - endif() - - # Check that idf component has "idf_components" as source. - idf_component_get_property(component_source - ${component_interface} - COMPONENT_SOURCE) - if(NOT "${component_source}" STREQUAL "idf_components") - idf_die("Unexpected idf component '${component_name}' source '${component_source}'") - endif() - - # Create fake component with same name as idf component. - set(component_dir "${CMAKE_CURRENT_BINARY_DIR}/${component_name}") - file(MAKE_DIRECTORY "${component_dir}") - file(TOUCH "${component_dir}/CMakeLists.txt") - - # Initialize fake component with higher "project_components" priority. - idf_build_get_property(component_prefix PREFIX) - __init_component(DIRECTORY "${component_dir}" - PREFIX "${component_prefix}" - SOURCE "project_components") - - # Check that the idf component was replaced with fake component. - idf_component_get_property(component_source - ${component_interface} - COMPONENT_SOURCE) - if(NOT "${component_source}" STREQUAL "project_components") - idf_die("Unexpected fake component '${component_name}' source '${component_source}'") - endif() - - idf_component_get_property(component_dir - ${component_interface} - COMPONENT_DIR) - if(NOT "${component_dir}" STREQUAL "${CMAKE_CURRENT_BINARY_DIR}/${component_name}") - idf_die("Unexpected fake component '${component_name}' directory '${component_dir}'") - endif() - __dump_component_properties("${component_name}") -endfunction() - -# Test that IDF_VERSION and IDF_VER build property is set -function(test_idf_version) - if(NOT DEFINED IDF_VERSION_MAJOR OR - NOT DEFINED IDF_VERSION_MINOR OR - NOT DEFINED IDF_VERSION_PATCH OR - NOT DEFINED ENV{IDF_VERSION}) - idf_die("IDF_VERSION not set") - endif() - idf_build_get_property(idf_ver IDF_VER) - if(NOT idf_ver) - idf_die("IDF_VER build property not set") - endif() -endfunction() - -# Test that Python interpreter is set -function(test_python) - if(NOT DEFINED PYTHON OR "${PYTHON}" STREQUAL "") - idf_die("PYTHON variable not defined or empty") - endif() -endfunction() - -# Test that toolchain is properly set -function(test_toolchain) - if(NOT IDF_TOOLCHAIN) - idf_die("IDF_TOOLCHAIN variable not defined or empty") - endif() - if(NOT CMAKE_TOOLCHAIN_FILE) - idf_die("CMAKE_TOOLCHAIN_FILE variable not defined or empty") - endif() - idf_build_get_property(toolchain IDF_TOOLCHAIN) - if(NOT toolchain) - idf_die("IDF_TOOLCHAIN build property not set") - endif() - idf_build_get_property(toolchain_file IDF_TOOLCHAIN_FILE) - if(NOT toolchain_file) - idf_die("IDF_TOOLCHAIN_FILE build property not set") - endif() -endfunction() - -# Test idf_build_library -function(test_idf_build_library) - # Create idflibtest with specific set of components and test that it - # contains them. - set(components app_trace app_update bootloader bootloader_support bt cmock) - - set(idflib idflibtest) - idf_build_library("${idflib}" COMPONENTS "${components}") - - if(NOT TARGET "${idflib}") - idf_die("'${idflib}' not created") - endif() - - idf_library_get_property(lib_components "${idflib}" LIBRARY_COMPONENTS) - if(NOT "${lib_components}" STREQUAL "${components}") - idf_die("Library '${idflib}' components '${lib_components}' do not match " - "expected components '${components}'") - endif() - - # Create idflibtest2 without specifying COMPONENTS and test that it - # contains all discovered components. - set(idflib idflibtest2) - idf_build_library("${idflib}") - - if(NOT TARGET "${idflib}") - idf_die("'${idflib}' not created") - endif() - - idf_library_get_property(lib_components "${idflib}" LIBRARY_COMPONENTS) - idf_build_get_property(component_names COMPONENTS_DISCOVERED) - if(NOT "${lib_components}" STREQUAL "${component_names}") - idf_die("Library '${idflib}' components '${lib_components}' do not match " - "COMPONENTS_DISCOVERED") - endif() -endfunction() - -function(test_kconfig) - # Check that kconfig output files were generated - idf_build_get_property(config_dir CONFIG_DIR) - set(output_files sdkconfig.h sdkconfig.cmake sdkconfig.json) - - foreach(file ${output_files}) - set(file_path "${config_dir}/${file}") - if(NOT EXISTS "${file_path}") - idf_die("Missing kconfig output: ${file_path}") - endif() - file(SIZE "${file_path}" file_size) - if(file_size EQUAL 0) - idf_die("Empty kconfig output: ${file_path}") - endif() - endforeach() - - # Check that kconfig targets were created - set(targets menuconfig confserver save-defconfig) - foreach(target ${targets}) - if(NOT TARGET ${target}) - idf_die("Missing kconfig target: ${target}") - endif() - endforeach() -endfunction() - -# Test that PROJECT_NAME and PROJECT_VER build property is set and contain -# values provided in project() call. -function(test_project_properties) - idf_build_get_property(project_name PROJECT_NAME) - if(NOT project_name) - idf_die("PROJECT_NAME build property not set") - endif() - if(NOT "${project_name}" STREQUAL "${PROJECT_NAME}") - idf_die("PROJECT_NAME build property '${project_name}' != '${PROJECT_NAME}'") - endif() - idf_build_get_property(project_ver PROJECT_VER) - if(NOT project_ver) - idf_die("PROJECT_VER build property not set") - endif() - if(NOT "${project_ver}" STREQUAL "${PROJECT_VERSION}") - idf_die("PROJECT_VER build property '${project_ver}' != '${PROJECT_VERSION}'") - endif() -endfunction() - -# Test that the targets for component1 and component2 are created when -# component2 is included. -function(test_include_component) - idf_build_library(idflibtest3 COMPONENTS component2) - idf_component_get_property(component_real_target - component2 - COMPONENT_REAL_TARGET) - if(NOT TARGET ${component_real_target}) - idf_die("Missing component2 target") - endif() - - # Test that component1 is included as a dependency of component2. - idf_component_get_property(component_real_target - component1 - COMPONENT_REAL_TARGET) - if(NOT TARGET ${component_real_target}) - idf_die("Missing component1 target") - endif() -endfunction() - -# Add two executables fatfs_example and hello_world_example, generate -# binary images for them and add flash and menuconfig targets. -# After configuration these can be build with -# idf.py hello_world_example_bin -# idf.py fatfs_example_bin -# or simply -# idf.py hello_world_example-flash monitor -# idf.py fatfs-flash monitor -# The menuconfig can be invoked with -# idf.py --no-hints menuconfig-hello_world -# idf.py --no-hints menuconfig-fatfs -# The confserver can be invoked with -# idf.py confserver-hello_world -# idf.py confserver-fatfs -function(test_executable) - idf_build_executable(fatfs_example - COMPONENTS fatfs_example - MAPFILE_TARGET fatfs_example_mapfile) - idf_build_binary(fatfs_example - TARGET fatfs_example_bin - OUTPUT_FILE fatfs_example.bin) - idf_check_binary_size(fatfs_example_bin) - idf_flash_binary(fatfs_example_bin - TARGET fatfs_example-flash - NAME fatfs_example) - idf_create_menuconfig(fatfs_example - TARGET menuconfig-fatfs) - idf_build_generate_metadata(fatfs_example_bin - OUTPUT_FILE project_description_fatfs.json) - idf_build_generate_depgraph(fatfs_example - OUTPUT_FILE component_deps_fatfs.dot) - idf_create_confserver(fatfs_example - TARGET confserver-fatfs) - - if(TARGET fatfs_example_mapfile) - idf_create_size_report(fatfs_example_mapfile - TARGET fatfs-size) - endif() - - - idf_build_executable(hello_world_example - COMPONENTS hello_world_example - MAPFILE_TARGET hello_world_example_mapfile) - idf_build_binary(hello_world_example - TARGET hello_world_example_bin - OUTPUT_FILE hello_world_example.bin) - idf_check_binary_size(hello_world_example_bin) - idf_flash_binary(hello_world_example_bin - TARGET hello_world_example-flash - NAME hello_world_example) - idf_create_menuconfig(hello_world_example - TARGET menuconfig-hello_world) - idf_build_generate_metadata(hello_world_example_bin - OUTPUT_FILE project_description_hello_world.json) - idf_build_generate_depgraph(hello_world_example - OUTPUT_FILE component_deps_hello_world.dot) - idf_create_confserver(hello_world_example - TARGET confserver-hello_world) - - if(TARGET hello_world_example_mapfile) - idf_create_size_report(hello_world_example_mapfile - TARGET hello-size) - endif() -endfunction() - -# Run tests -test_idf_version() -test_python() -test_toolchain() -test_idf_build_library() -test_project_properties() -test_include_component() -test_executable() - -# Create default project -idf_project_default() - -# The kconfig test also verifies whether kconfig-related targets, such as -# menuconfig, are created. These targets are now generated within -# idf_project_default rather than globally, so this test should be run only -# after the default project is created. -test_kconfig() - -# Call this test last because it replaces the ESP system component. -test_component_priority() - -__dump_all_properties() - -message("ALL TESTS PASSED") diff --git a/tools/cmakev2/test/components/component1/CMakeLists.txt b/tools/cmakev2/test/components/component1/CMakeLists.txt deleted file mode 100644 index 7f126c75dc6..00000000000 --- a/tools/cmakev2/test/components/component1/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -add_library(${COMPONENT_TARGET} component1.c) -target_include_directories(${COMPONENT_TARGET} PUBLIC ${CMAKE_CURRENT_LIST_DIR}) diff --git a/tools/cmakev2/test/components/component1/Kconfig b/tools/cmakev2/test/components/component1/Kconfig deleted file mode 100644 index 747f971d653..00000000000 --- a/tools/cmakev2/test/components/component1/Kconfig +++ /dev/null @@ -1,5 +0,0 @@ -menu "component1 configuration" - config COMPONENT1_OPTION - prompt "component1 option" - bool -endmenu diff --git a/tools/cmakev2/test/components/component1/component1.c b/tools/cmakev2/test/components/component1/component1.c deleted file mode 100644 index 682fc662cdf..00000000000 --- a/tools/cmakev2/test/components/component1/component1.c +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#include - -void component1_func(void) -{ - printf("component1\n"); -} diff --git a/tools/cmakev2/test/components/component1/component1.h b/tools/cmakev2/test/components/component1/component1.h deleted file mode 100644 index 3157eb52d3b..00000000000 --- a/tools/cmakev2/test/components/component1/component1.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#ifndef _COMPONENT1_H_ -#define _COMPONENT1_H_ -void component1_func(void); -#endif diff --git a/tools/cmakev2/test/components/component2/CMakeLists.txt b/tools/cmakev2/test/components/component2/CMakeLists.txt deleted file mode 100644 index 2471d235114..00000000000 --- a/tools/cmakev2/test/components/component2/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -idf_component_include(component1 INTERFACE component1) - -add_library(${COMPONENT_TARGET} component2.c) -target_include_directories(${COMPONENT_TARGET} PUBLIC ${CMAKE_CURRENT_LIST_DIR}) -target_link_libraries(${COMPONENT_TARGET} PRIVATE ${component1}) diff --git a/tools/cmakev2/test/components/component2/Kconfig b/tools/cmakev2/test/components/component2/Kconfig deleted file mode 100644 index e047729a8b7..00000000000 --- a/tools/cmakev2/test/components/component2/Kconfig +++ /dev/null @@ -1,5 +0,0 @@ -menu "component2 configuration" - config COMPONENT2_OPTION - prompt "component2 option" - bool -endmenu diff --git a/tools/cmakev2/test/components/component2/component2.c b/tools/cmakev2/test/components/component2/component2.c deleted file mode 100644 index 6bcd9f3526d..00000000000 --- a/tools/cmakev2/test/components/component2/component2.c +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#include -#include "component1.h" - -void component2_func(void) -{ - component1_func(); - printf("component2\n"); -} diff --git a/tools/cmakev2/test/components/component2/component2.h b/tools/cmakev2/test/components/component2/component2.h deleted file mode 100644 index 0eb7756fcd9..00000000000 --- a/tools/cmakev2/test/components/component2/component2.h +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#ifndef _COMPONENT2_H_ -#define _COMPONENT2_H_ - -void component2_func(void); -#endif diff --git a/tools/cmakev2/test/components/fatfs_example/CMakeLists.txt b/tools/cmakev2/test/components/fatfs_example/CMakeLists.txt deleted file mode 100644 index 25c6ce0b7c8..00000000000 --- a/tools/cmakev2/test/components/fatfs_example/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -idf_component_register(SRCS "fatfs_getting_started_main.c" - PRIV_REQUIRES vfs fatfs lwip - INCLUDE_DIRS ".") diff --git a/tools/cmakev2/test/components/fatfs_example/fatfs_getting_started_main.c b/tools/cmakev2/test/components/fatfs_example/fatfs_getting_started_main.c deleted file mode 100644 index 2b93095d1ce..00000000000 --- a/tools/cmakev2/test/components/fatfs_example/fatfs_getting_started_main.c +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#include -#include -#include -#include "esp_vfs.h" -#include "esp_vfs_fat.h" -#include "sdkconfig.h" - -static const char *TAG = "example"; - -// Mount path for the partition -const char *base_path = "/spiflash"; - -// Handle of the wear levelling library instance -static wl_handle_t s_wl_handle = WL_INVALID_HANDLE; - -void app_main(void) -{ - ESP_LOGI(TAG, "Mounting FAT filesystem"); - // To mount device we need name of device partition, define base_path - // and allow format partition in case if it is new one and was not formatted before - const esp_vfs_fat_mount_config_t mount_config = { - .max_files = 4, // Number of files that can be open at a time - .format_if_mount_failed = true, // If true, try to format the partition if mount fails - .allocation_unit_size = CONFIG_WL_SECTOR_SIZE, // Size of allocation unit, cluster size. - .use_one_fat = false, // Use only one FAT table (reduce memory usage), but decrease reliability of file system in case of power failure. - }; - - // Mount FATFS filesystem located on "storage" partition in read-write mode - esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl(base_path, "storage", &mount_config, &s_wl_handle); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to mount FATFS (%s)", esp_err_to_name(err)); - return; - } - - ESP_LOGI(TAG, "Filesystem mounted"); - - ESP_LOGI(TAG, "Opening file"); - - const char *filename = "/spiflash/example.txt"; - - FILE *f = fopen(filename, "wb"); - if (f == NULL) { - perror("fopen"); // Print reason why fopen failed - ESP_LOGE(TAG, "Failed to open file for writing"); - return; - } - - fprintf(f, "Hello World!\n"); - fclose(f); - - ESP_LOGI(TAG, "File written"); - - // Open file for reading - ESP_LOGI(TAG, "Reading file"); - - f = fopen(filename, "r"); - if (f == NULL) { - ESP_LOGE(TAG, "Failed to open file for reading"); - return; - } - - char line[128]; - - fgets(line, sizeof(line), f); - fclose(f); - - // strip newline - char *pos = strchr(line, '\n'); - if (pos) { - *pos = '\0'; - } - - ESP_LOGI(TAG, "Read from file: '%s'", line); - - // Unmount FATFS - ESP_LOGI(TAG, "Unmounting FAT filesystem"); - - ESP_ERROR_CHECK(esp_vfs_fat_spiflash_unmount_rw_wl(base_path, s_wl_handle)); - - ESP_LOGI(TAG, "Done"); -} diff --git a/tools/cmakev2/test/components/main/CMakeLists.txt b/tools/cmakev2/test/components/main/CMakeLists.txt deleted file mode 100644 index 28ab405bd66..00000000000 --- a/tools/cmakev2/test/components/main/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -idf_component_register(SRCS "hello_world_main.c" - PRIV_REQUIRES spi_flash - INCLUDE_DIRS "") diff --git a/tools/cmakev2/test/components/main/hello_world_main.c b/tools/cmakev2/test/components/main/hello_world_main.c deleted file mode 100644 index 95cab254a1a..00000000000 --- a/tools/cmakev2/test/components/main/hello_world_main.c +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#include -#include -#include "sdkconfig.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_chip_info.h" -#include "esp_flash.h" -#include "esp_system.h" - -void app_main(void) -{ - printf("Hello world!\n"); - - /* Print chip information */ - esp_chip_info_t chip_info; - uint32_t flash_size; - esp_chip_info(&chip_info); - printf("This is %s chip with %d CPU core(s), %s%s%s%s, ", - CONFIG_IDF_TARGET, - chip_info.cores, - (chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "", - (chip_info.features & CHIP_FEATURE_BT) ? "BT" : "", - (chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "", - (chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : ""); - - unsigned major_rev = chip_info.revision / 100; - unsigned minor_rev = chip_info.revision % 100; - printf("silicon revision v%d.%d, ", major_rev, minor_rev); - if (esp_flash_get_size(NULL, &flash_size) != ESP_OK) { - printf("Get flash size failed"); - return; - } - - printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024), - (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external"); - - printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size()); - - for (int i = 10; i >= 0; i--) { - printf("Restarting in %d seconds...\n", i); - vTaskDelay(1000 / portTICK_PERIOD_MS); - } - printf("Restarting now.\n"); - fflush(stdout); - esp_restart(); -} diff --git a/tools/cmakev2/utilities.cmake b/tools/cmakev2/utilities.cmake index 36715986407..ceabc67d1ff 100644 --- a/tools/cmakev2/utilities.cmake +++ b/tools/cmakev2/utilities.cmake @@ -609,26 +609,19 @@ function(__split) endfunction() #[[ - __get_compile_options(OUTPUT ) + idf_build_get_compile_options() - *OUTPUT[out]* + *variable* - List of generator expressions for C, CXX, and ASM compile options + Variable name in which the list of generator expressions for C, CXX, + and ASM compile options will be stored. Gather the compilation options from COMPILE_OPTIONS, C_COMPILE_OPTIONS, CXX_COMPILE_OPTIONS, and ASM_COMPILE_OPTIONS build properties into a single list using generator expressions. This list can then be used with the target_compile_options call. #]] -function(__get_compile_options) - set(options) - set(one_value OUTPUT) - set(multi_value) - cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) - - if(NOT DEFINED ARG_OUTPUT) - idf_die("OUTPUT option is required") - endif() +function(idf_build_get_compile_options output) idf_build_get_property(compile_options COMPILE_OPTIONS GENERATOR_EXPRESSION) idf_build_get_property(c_compile_options C_COMPILE_OPTIONS GENERATOR_EXPRESSION) idf_build_get_property(cxx_compile_options CXX_COMPILE_OPTIONS GENERATOR_EXPRESSION) @@ -643,7 +636,7 @@ function(__get_compile_options) foreach(option IN LISTS asm_compile_options) list(APPEND compile_options $<$:${option}>) endforeach() - set(${ARG_OUTPUT} "${compile_options}" PARENT_SCOPE) + set(${output} "${compile_options}" PARENT_SCOPE) endfunction() #[[ @@ -819,7 +812,12 @@ function(target_add_binary_data target embed_file embed_type) idf_build_get_property(build_dir BUILD_DIR) idf_build_get_property(idf_path IDF_PATH) - get_filename_component(embed_file "${embed_file}" ABSOLUTE) + # The target_add_binary_data function is also called within the + # idf_component_include function, which is not executed in the component + # directory context. Therefore, ensure that the absolute path of the + # embedded file is resolved relative to the component directory. + idf_component_get_property(component_directory "${target}" COMPONENT_DIR) + get_filename_component(embed_file "${embed_file}" ABSOLUTE BASE_DIR "${component_directory}") get_filename_component(name "${embed_file}" NAME) set(embed_srcfile "${build_dir}/${name}.S") @@ -841,6 +839,22 @@ function(target_add_binary_data target embed_file embed_type) WORKING_DIRECTORY "${build_dir}" VERBATIM) + # A file generated by `add_custom_command` can be used as a dependency only + # within the directory context where the target was created (see + # https://cmake.org/cmake/help/latest/command/add_custom_command.html). + # Since embedded files are collected in the EMBED_FILES and EMBED_TXTFILES + # component properties, and the source files are generated in the + # idf_component_include function, which is not called in the component’s + # directory context, we must create an explicit target and add it as a + # dependency of the component target. To avoid potential target name + # collisions with embedded files that share the same name but reside in + # different directories, add a hash of the full embedded file path to + # the generated target name. + string(MD5 hash "${embed_file}") + string(MAKE_C_IDENTIFIER "gen_${name}_${hash}" embed_srcfile_target) + add_custom_target(${embed_srcfile_target} DEPENDS "${embed_srcfile}") + add_dependencies(${target} ${embed_srcfile_target}) + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY ADDITIONAL_CLEAN_FILES "${embed_srcfile}") target_sources("${target}" PRIVATE "${embed_srcfile}") diff --git a/tools/test_build_system/buildv2/__init__.py b/tools/test_build_system/buildv2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/test_build_system/buildv2/test_build.py b/tools/test_build_system/buildv2/test_build.py new file mode 100644 index 00000000000..c4f295cd822 --- /dev/null +++ b/tools/test_build_system/buildv2/test_build.py @@ -0,0 +1,379 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import json +import logging +import subprocess +from pathlib import Path + +import pytest +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import replace_in_file + + +@pytest.mark.usefixtures('test_app_copy') +def test_build_executable(idf_py: IdfPyFunc) -> None: + """idf_build_executable creates an ELF named after the executable target by default; + when NAME and SUFFIX are provided, the output filename reflects those options instead.""" + logging.info('Testing idf_build_executable default and custom NAME/SUFFIX output naming') + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + 'idf_build_executable(build_test_app COMPONENTS main)\n' + 'idf_build_executable(custom_app COMPONENTS main NAME custom_name SUFFIX .custom_elf)\n' + 'idf_build_generate_metadata(EXECUTABLE build_test_app)\n', + ) + + idf_py('build') + assert Path('build/build_test_app.elf').exists(), ( + 'idf_build_executable should produce build_test_app.elf with default naming' + ) + assert Path('build/custom_name.custom_elf').exists(), ( + 'idf_build_executable should produce custom_name.custom_elf with NAME and SUFFIX' + ) + + +@pytest.mark.usefixtures('test_app_copy') +def test_build_library(idf_py: IdfPyFunc) -> None: + """idf_build_library with COMPONENTS respects the filter; without it, includes all.""" + logging.info('Testing idf_build_library with and without COMPONENTS filter') + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + 'idf_build_library(mylib_specific COMPONENTS main)\n' + 'idf_library_get_property(specific_comps mylib_specific LIBRARY_COMPONENTS)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/lib_comps.txt" "${specific_comps}")\n' + 'idf_build_library(mylib_all)\n' + 'idf_library_get_property(all_comps mylib_all LIBRARY_COMPONENTS)\n' + 'idf_build_get_property(discovered COMPONENTS_DISCOVERED)\n' + 'list(LENGTH all_comps all_count)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/lib_comps_count.txt" "${all_count}")\n' + 'if("${all_comps}" STREQUAL "${discovered}")\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/lib_comps_match.txt" "MATCH")\n' + 'else()\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/lib_comps_match.txt"' + ' "MISMATCH\\nLIBRARY: ${all_comps}\\nDISCOVERED: ${discovered}")\n' + 'endif()\n' + 'idf_build_executable(build_test_app COMPONENTS main)\n' + 'idf_build_generate_metadata(EXECUTABLE build_test_app)\n', + ) + + idf_py('reconfigure') + + # Verify COMPONENTS filter was respected + comps_text = Path('build/lib_comps.txt').read_text().strip() + assert comps_text == 'main', f'Library with COMPONENTS main should list only "main", got: {comps_text!r}' + + # Verify absence of filter includes all discovered components + count = int(Path('build/lib_comps_count.txt').read_text().strip()) + assert count > 1, f'Library without COMPONENTS should include all discovered components (>1), got: {count}' + + match_result = Path('build/lib_comps_match.txt').read_text().strip() + assert match_result == 'MATCH', f'Library without COMPONENTS should match COMPONENTS_DISCOVERED: {match_result}' + + +@pytest.mark.usefixtures('test_app_copy') +def test_build_binary(idf_py: IdfPyFunc) -> None: + """idf_build_binary converts an ELF into a .bin image and a .md5sum checksum file, + setting BINARY_PATH and EXECUTABLE_TARGET on the created target. When MAPFILE_TARGET is + provided to idf_build_executable, the target is created with a MAPFILE_PATH property + pointing to the link map file. TARGET is a required argument; omitting it aborts + configuration with a clear error message.""" + logging.info('Testing idf_build_binary artifacts, properties, MAPFILE_TARGET, and TARGET error') + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + 'idf_build_executable(myapp COMPONENTS main MAPFILE_TARGET myapp_mapfile)\n' + 'idf_build_binary(myapp\n' + ' TARGET myapp_bin\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/myapp.bin")\n' + # Dump target properties to files so Python can assert them + 'get_target_property(_bpath myapp_bin BINARY_PATH)\n' + 'get_target_property(_etarget myapp_bin EXECUTABLE_TARGET)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/binary_path.txt" "${_bpath}")\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/exec_target.txt" "${_etarget}")\n' + # Guard MAPFILE_TARGET with if(TARGET) + 'if(TARGET myapp_mapfile)\n' + ' get_target_property(_mpath myapp_mapfile MAPFILE_PATH)\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/mapfile_path.txt" "${_mpath}")\n' + 'else()\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/mapfile_path.txt" "NO_MAPFILE_TARGET")\n' + 'endif()\n' + 'add_custom_target(app ALL DEPENDS myapp_bin)\n' + 'idf_build_generate_metadata(EXECUTABLE myapp)\n', + ) + + idf_py('build') + + # Binary image and MD5 checksum file must exist + assert Path('build/myapp.bin').exists(), 'idf_build_binary should create the .bin image' + assert Path('build/myapp.bin.md5sum').exists(), 'idf_build_binary should create the .bin.md5sum checksum file' + + # BINARY_PATH must point to the output file + binary_path = Path('build/binary_path.txt').read_text().strip() + assert 'myapp.bin' in binary_path, f'BINARY_PATH should reference myapp.bin, got: {binary_path!r}' + + # EXECUTABLE_TARGET must point back to the ELF target + exec_target = Path('build/exec_target.txt').read_text().strip() + assert exec_target == 'myapp', f'EXECUTABLE_TARGET should be "myapp", got: {exec_target!r}' + + # MAPFILE_TARGET: target and MAPFILE_PATH property + mapfile_path_content = Path('build/mapfile_path.txt').read_text().strip() + if mapfile_path_content != 'NO_MAPFILE_TARGET': + ninja = Path('build/build.ninja').read_text() + assert 'myapp_mapfile' in ninja, '"myapp_mapfile" target not found in build.ninja' + assert '.map' in mapfile_path_content, ( + f'MAPFILE_PATH should reference a .map file, got: {mapfile_path_content!r}' + ) + + # TARGET is required: remove it from the invocation and verify configuration aborts. + cmake_before = Path('CMakeLists.txt').read_text() + replace_in_file( + 'CMakeLists.txt', + 'idf_build_binary(myapp\n TARGET myapp_bin\n OUTPUT_FILE "${CMAKE_BINARY_DIR}/myapp.bin")\n', + 'idf_build_binary(myapp\n OUTPUT_FILE "${CMAKE_BINARY_DIR}/myapp.bin")\n', + ) + assert Path('CMakeLists.txt').read_text() != cmake_before, 'replace_in_file did not modify CMakeLists.txt' + with pytest.raises(subprocess.CalledProcessError) as exc_info: + idf_py('reconfigure') + err_output = (exc_info.value.stdout or '') + (exc_info.value.stderr or '') + assert 'TARGET option is required' in err_output, 'Expected "TARGET option is required" in cmake error output' + + +@pytest.mark.usefixtures('test_app_copy') +def test_sign_binary(idf_py: IdfPyFunc) -> None: + """idf_sign_binary requires CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES; without it the + configuration aborts with a message naming the missing option. When the option is enabled + idf_sign_binary registers the signing target in the build graph and sets BINARY_PATH and + EXECUTABLE_TARGET on it.""" + logging.info('Testing idf_sign_binary config-disabled and config-enabled paths') + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + 'idf_build_executable(myapp COMPONENTS main)\n' + 'idf_build_binary(myapp TARGET myapp_bin\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/myapp.bin")\n' + 'idf_sign_binary(myapp_bin TARGET myapp_signed\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/myapp_signed.bin")\n' + # Dump idf_sign_binary target properties to files so Python can assert them + 'get_target_property(_signed_bpath myapp_signed BINARY_PATH)\n' + 'get_target_property(_signed_etarget myapp_signed EXECUTABLE_TARGET)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/signed_binary_path.txt" "${_signed_bpath}")\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/signed_exec_target.txt" "${_signed_etarget}")\n' + 'idf_build_generate_metadata(EXECUTABLE myapp)\n', + ) + + # Config disabled: idf_sign_binary must abort with a clear diagnostic. + with pytest.raises(subprocess.CalledProcessError) as exc_info: + idf_py('reconfigure') + err_output = (exc_info.value.stdout or '') + (exc_info.value.stderr or '') + assert 'SECURE_BOOT_BUILD_SIGNED_BINARIES' in err_output, ( + 'Expected "SECURE_BOOT_BUILD_SIGNED_BINARIES" in cmake error output' + ) + + # Config enabled: drive the Kconfig dependency chain through sdkconfig.defaults. + # Delete any sdkconfig written by the failed run above so the new defaults are not + # shadowed by the existing file (sdkconfig takes priority over sdkconfig.defaults). + sdkconfig = Path('sdkconfig') + if sdkconfig.exists(): + sdkconfig.unlink() + Path('sdkconfig.defaults').write_text('CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT=y\n') + + idf_py('reconfigure') + + # Signing target must appear in the build graph + ninja = Path('build/build.ninja').read_text() + assert 'myapp_signed' in ninja, '"myapp_signed" signing target not found in build.ninja' + + # BINARY_PATH on the signed target must point to the signed output file + signed_binary_path = Path('build/signed_binary_path.txt').read_text().strip() + assert 'myapp_signed.bin' in signed_binary_path, ( + f'BINARY_PATH on signed target should reference myapp_signed.bin, got: {signed_binary_path!r}' + ) + + # EXECUTABLE_TARGET must be propagated from the unsigned binary to the signed target + signed_exec_target = Path('build/signed_exec_target.txt').read_text().strip() + assert signed_exec_target == 'myapp', ( + f'EXECUTABLE_TARGET should be propagated as "myapp", got: {signed_exec_target!r}' + ) + + +@pytest.mark.usefixtures('test_app_copy') +def test_generate_metadata_and_flash(idf_py: IdfPyFunc) -> None: + """idf_build_generate_metadata produces project description JSON for EXECUTABLE and BINARY + modes with optional custom output paths. idf_flash_binary registers a named flash target, + with the FLASH flag also wiring it into the global flash target.""" + logging.info('Testing idf_build_generate_metadata and idf_flash_binary') + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + 'idf_build_executable(myapp COMPONENTS main)\n' + 'idf_build_binary(myapp TARGET myapp_bin\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/myapp.bin")\n' + 'idf_flash_binary(myapp_bin TARGET myapp-flash NAME myapp FLASH)\n' + 'idf_build_generate_metadata(EXECUTABLE myapp)\n' + 'idf_build_generate_metadata(BINARY myapp_bin\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/binary_desc.json")\n', + ) + + idf_py('reconfigure') + + ninja = Path('build/build.ninja').read_text() + + # idf_flash_binary: named flash target must appear in the build graph + assert 'myapp-flash' in ninja, '"myapp-flash" target not found in build.ninja' + + # idf_build_generate_metadata(EXECUTABLE): default output path + assert Path('build/project_description.json').exists(), ( + 'project_description.json not created by idf_build_generate_metadata(EXECUTABLE)' + ) + proj_desc = json.loads(Path('build/project_description.json').read_text()) + assert 'build_components' in proj_desc, 'project_description.json should contain "build_components" key' + + # idf_build_generate_metadata(BINARY): custom output path + assert Path('build/binary_desc.json').exists(), ( + 'binary_desc.json not created by idf_build_generate_metadata(BINARY)' + ) + binary_desc = json.loads(Path('build/binary_desc.json').read_text()) + assert 'build_components' in binary_desc, 'binary_desc.json should contain "build_components" key' + + +@pytest.mark.usefixtures('test_app_copy') +def test_check_binary_size_and_signed(idf_py: IdfPyFunc) -> None: + """idf_check_binary_size adds a partition-fit target when APP_BUILD_TYPE_APP_2NDBOOT is + enabled. idf_check_binary_signed is a no-op when signed applications are not required; + when SECURE_SIGNED_APPS is enabled without SECURE_BOOT_BUILD_SIGNED_BINARIES it attaches + a POST_BUILD signing reminder to the binary target.""" + logging.info('Testing idf_check_binary_size and idf_check_binary_signed') + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + 'idf_build_executable(myapp COMPONENTS main)\n' + 'idf_build_binary(myapp TARGET myapp_bin\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/myapp.bin")\n' + 'idf_check_binary_size(myapp_bin)\n' + 'idf_check_binary_signed(myapp_bin)\n' + # Write a sentinel so Python can check whether the size-check target was created + 'if(TARGET myapp_bin_check_size)\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/check_size_target.txt" "EXISTS")\n' + 'else()\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/check_size_target.txt" "ABSENT")\n' + 'endif()\n' + 'idf_build_generate_metadata(EXECUTABLE myapp)\n', + ) + + # --- Phase 1: default config (no-op paths) --------------------------------- + idf_py('reconfigure') + + ninja = Path('build/build.ninja').read_text() + + # idf_check_binary_size: when APP_BUILD_TYPE_APP_2NDBOOT is enabled a size-check + # custom target is created and added as a dependency of the binary target + check_size_status = Path('build/check_size_target.txt').read_text().strip() + if check_size_status == 'EXISTS': + assert 'myapp_bin_check_size' in ninja, '"myapp_bin_check_size" target not found in build.ninja' + + # --- Phase 2: idf_check_binary_signed active path -------------------------- + # SECURE_SIGNED_APPS=y but SECURE_BOOT_BUILD_SIGNED_BINARIES=n: + # the function attaches a POST_BUILD "not signed" reminder to the binary target. + sdkconfig = Path('sdkconfig') + if sdkconfig.exists(): + sdkconfig.unlink() + Path('sdkconfig.defaults').write_text( + 'CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT=y\nCONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=n\n' + ) + # ECDSA scheme + no build-time signing requires signature_verification_key.bin to exist. + # Content is not validated at configure time, so an empty file suffices here. + Path('signature_verification_key.bin').write_bytes(b'') + + idf_py('reconfigure') + + # CMake serialises the POST_BUILD echo into build.ninja or cmake helper scripts + ninja2 = Path('build/build.ninja').read_text() + cmake_scripts = ''.join( + p.read_text(errors='replace') for p in Path('build/CMakeFiles').rglob('*.cmake') if p.is_file() + ) + assert 'not signed' in ninja2 + cmake_scripts, ( + 'Expected POST_BUILD signing reminder from idf_check_binary_signed in build graph' + ) + + +@pytest.mark.usefixtures('test_app_copy') +def test_property_apis(idf_py: IdfPyFunc) -> None: + """library property and build property APIs""" + + logging.info('Testing property APIs') + + # Use the library already created by idf_project_default() (idf_build_executable + # creates library_${executable}, i.e. library_build_test_app). Do not add a + # second idf_build_library() or compat.cmake will fail (DEFERRED + multi-library). + replace_in_file( + 'CMakeLists.txt', + '# placeholder_after_project_default', + # --- idf_library_set_property / idf_library_get_property (on default library) --- + 'idf_library_set_property(library_build_test_app MY_CUSTOM_PROP "test_value")\n' + 'idf_library_get_property(lib_prop_val library_build_test_app MY_CUSTOM_PROP)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/lib_prop.txt" "${lib_prop_val}")\n' + # --- idf_build_set_property / idf_build_get_property --- + 'idf_build_set_property(MY_BUILD_PROP "build_test_value")\n' + 'idf_build_get_property(build_prop_val MY_BUILD_PROP)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/build_prop.txt" "${build_prop_val}")\n' + # --- idf_build_set_property APPEND --- + 'idf_build_set_property(MY_LIST "a")\n' + 'idf_build_set_property(MY_LIST "b" APPEND)\n' + 'idf_build_get_property(list_val MY_LIST)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/build_list.txt" "${list_val}")\n', + ) + + idf_py('reconfigure') + + # idf_library_set_property / idf_library_get_property checks + assert Path('build/lib_prop.txt').read_text().strip() == 'test_value', ( + 'idf_library_set/get_property round-trip failed' + ) + + # idf_build_set_property / idf_build_get_property checks + assert Path('build/build_prop.txt').read_text().strip() == 'build_test_value', ( + 'idf_build_set/get_property round-trip failed' + ) + + # APPEND should produce a list containing both values + list_result = Path('build/build_list.txt').read_text().strip() + assert 'a' in list_result and 'b' in list_result, ( + f'APPEND property should contain "a" and "b", got: {list_result!r}' + ) + + +@pytest.mark.usefixtures('test_app_copy') +def test_depgraph_generation(idf_py: IdfPyFunc) -> None: + """idf_build_generate_depgraph() should produce a DOT file""" + logging.info('Testing depgraph generation') + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + 'idf_build_set_property(__BUILD_COMPONENT_DEPGRAPH_ENABLED YES)\n' + 'idf_build_executable(build_test_app COMPONENTS main)\n' + 'idf_build_generate_depgraph(build_test_app)\n' + 'idf_build_generate_metadata(EXECUTABLE build_test_app)\n', + ) + + idf_py('reconfigure') + + dot_file = Path('build/component_deps.dot') + assert dot_file.exists(), 'component_deps.dot not created by idf_build_generate_depgraph' + dot_content = dot_file.read_text() + assert 'digraph' in dot_content, 'component_deps.dot should contain a digraph definition' + assert '->' in dot_content, 'component_deps.dot should contain at least one dependency edge (->)' diff --git a/tools/test_build_system/buildv2/test_build_event_callbacks.py b/tools/test_build_system/buildv2/test_build_event_callbacks.py new file mode 100644 index 00000000000..d03a2b20f75 --- /dev/null +++ b/tools/test_build_system/buildv2/test_build_event_callbacks.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +from pathlib import Path + +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import get_snapshot + + +def _write_project_include(test_app_copy: Path, content: str) -> None: + """Write content to the main component's project_include.cmake file.""" + project_include = test_app_copy / 'main' / 'project_include.cmake' + project_include.write_text(content, encoding='utf-8') + + +def test_post_elf_callback_fires_before_binary(test_app_copy: Path, idf_py: IdfPyFunc) -> None: + """ + Verify a POST_ELF callback fires after the executable target exists but before + the binary (.bin) is generated. The callback attaches a POST_BUILD step to the + executable via add_custom_command(TARGET ... POST_BUILD). + """ + _write_project_include( + test_app_copy, + '\n'.join( + [ + 'function(__test_post_elf_cb target)', + ' add_custom_command(TARGET ${target} POST_BUILD', + ' COMMAND ${CMAKE_COMMAND} -E sleep 1', + ' COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_BINARY_DIR}/postelf_file")', + 'endfunction()', + 'idf_component_register_build_event_callback(EVENT POST_ELF CALLBACK __test_post_elf_cb)', + ] + ), + ) + + idf_py('build') + + elf_file = test_app_copy / 'build' / 'build_test_app.elf' + postelf_file = test_app_copy / 'build' / 'postelf_file' + bin_timestamp = test_app_copy / 'build' / 'build_test_app.bin' + + assert elf_file.exists(), 'ELF file must exist' + assert postelf_file.exists(), 'post-elf file must be created' + assert bin_timestamp.exists(), 'bin timestamp must exist' + + snap = get_snapshot([str(elf_file), str(postelf_file), str(bin_timestamp)]) + mtimes = dict(snap.info) + assert mtimes[str(postelf_file)] > mtimes[str(elf_file)], 'post-ELF file must be created after ELF file' + assert mtimes[str(bin_timestamp)] > mtimes[str(postelf_file)], ( + 'Binary generation must occur after post-ELF dependency' + ) diff --git a/tools/test_build_system/buildv2/test_component.py b/tools/test_build_system/buildv2/test_component.py new file mode 100644 index 00000000000..89a6cbc3d1a --- /dev/null +++ b/tools/test_build_system/buildv2/test_component.py @@ -0,0 +1,299 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import logging +from pathlib import Path + +import pytest +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import replace_in_file + + +@pytest.mark.usefixtures('test_app_copy') +def test_native_cmake_component_builds(idf_py: IdfPyFunc) -> None: + """add_library(${COMPONENT_TARGET} ...) without idf_component_register(). + + COMPONENT_TARGET is pre-set before the component's CMakeLists.txt runs, + so components can create their library target directly with add_library(). + """ + logging.info('Testing native CMake component with add_library') + + comp_dir = Path('components/native_lib_comp') + comp_dir.mkdir(parents=True, exist_ok=True) + (comp_dir / 'CMakeLists.txt').write_text( + 'add_library(${COMPONENT_TARGET} STATIC "native_lib.c")\n' + 'target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}")\n' + ) + (comp_dir / 'native_lib.c').write_text('void native_lib_func(void) {}') + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES native_lib_comp\n # placeholder_inside_idf_component_register', + ) + + idf_py('build') + assert Path('build/build_test_app.elf').exists() + + +@pytest.mark.usefixtures('test_app_copy') +def test_native_cmake_depends_on_idf_component(idf_py: IdfPyFunc) -> None: + """idf_component_include() lets native components depend on IDF components. + + The INTERFACE output variable provides the target name for linking. + """ + logging.info('Testing native CMake component with IDF dependency via idf_component_include') + + comp_dir = Path('components/native_idf_dep') + comp_dir.mkdir(parents=True, exist_ok=True) + (comp_dir / 'CMakeLists.txt').write_text( + 'idf_component_include(log INTERFACE log_iface)\n' + 'add_library(${COMPONENT_TARGET} STATIC "native_idf.c")\n' + 'target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}")\n' + 'target_link_libraries(${COMPONENT_TARGET} PRIVATE ${log_iface})\n' + ) + (comp_dir / 'native_idf.c').write_text( + '#include "esp_log.h"\nvoid native_idf_func(void) { ESP_LOGI("test", "hello"); }\n' + ) + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES native_idf_dep\n # placeholder_inside_idf_component_register', + ) + + idf_py('build') + assert Path('build/build_test_app.elf').exists() + + +@pytest.mark.usefixtures('test_app_copy') +def test_native_cmake_transitive_deps(idf_py: IdfPyFunc) -> None: + """Transitive deps between native CMake components via idf_component_include().""" + logging.info('Testing native CMake transitive dependencies') + + comp_a = Path('components/native_comp_a') + comp_a.mkdir(parents=True, exist_ok=True) + (comp_a / 'CMakeLists.txt').write_text( + 'add_library(${COMPONENT_TARGET} STATIC "comp_a.c")\n' + 'target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}")\n' + ) + (comp_a / 'comp_a.c').write_text('void func_native_a(void) {}') + + comp_b = Path('components/native_comp_b') + comp_b.mkdir(parents=True, exist_ok=True) + (comp_b / 'CMakeLists.txt').write_text( + 'idf_component_include(native_comp_a INTERFACE native_a_iface)\n' + 'add_library(${COMPONENT_TARGET} STATIC "comp_b.c")\n' + 'target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}")\n' + 'target_link_libraries(${COMPONENT_TARGET} PRIVATE ${native_a_iface})\n' + ) + (comp_b / 'comp_b.c').write_text('void func_native_b(void) {}') + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES native_comp_b\n # placeholder_inside_idf_component_register', + ) + + idf_py('build') + assert Path('build/build_test_app.elf').exists() + + +@pytest.mark.usefixtures('test_app_copy') +def test_mixed_native_and_register_components(idf_py: IdfPyFunc) -> None: + """Projects mixing add_library() and idf_component_register() components.""" + logging.info('Testing mixed native and v1-style components') + + native_comp = Path('components/native_mixed') + native_comp.mkdir(parents=True, exist_ok=True) + (native_comp / 'CMakeLists.txt').write_text( + 'add_library(${COMPONENT_TARGET} STATIC "native_m.c")\n' + 'target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}")\n' + ) + (native_comp / 'native_m.c').write_text('void native_m_func(void) {}') + + v1_comp = Path('components/v1_mixed') + v1_comp.mkdir(parents=True, exist_ok=True) + (v1_comp / 'CMakeLists.txt').write_text('idf_component_register(SRCS "v1_m.c")\n') + (v1_comp / 'v1_m.c').write_text('void v1_m_func(void) {}') + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES native_mixed v1_mixed\n # placeholder_inside_idf_component_register', + ) + + idf_py('build') + assert Path('build/build_test_app.elf').exists() + + +@pytest.mark.usefixtures('test_app_copy') +def test_fetchcontent_available_in_component(idf_py: IdfPyFunc) -> None: + """FetchContent is available in a v2 component: declare a dependency, make it available, and link it.""" + logging.info('Testing FetchContent available in v2 component with real usage') + + comp_dir = Path('components/fc_comp') + comp_dir.mkdir(parents=True, exist_ok=True) + fetched_dep = comp_dir / 'fetched_dep' + fetched_dep.mkdir(parents=True, exist_ok=True) + + (fetched_dep / 'fetched.c').write_text('void fetched_dep_func(void) {}') + (fetched_dep / 'fetched.h').write_text('void fetched_dep_func(void);\n') + (fetched_dep / 'CMakeLists.txt').write_text( + 'add_library(fetched_dep_lib STATIC fetched.c)\n' + 'target_include_directories(fetched_dep_lib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")\n' + ) + + (comp_dir / 'fc.c').write_text('#include "fetched.h"\nvoid fc_func(void) { fetched_dep_func(); }\n') + (comp_dir / 'CMakeLists.txt').write_text( + 'include(FetchContent)\n' + 'FetchContent_Declare(fetched_dep SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/fetched_dep")\n' + 'FetchContent_MakeAvailable(fetched_dep)\n' + 'idf_component_register(SRCS "fc.c")\n' + 'target_link_libraries(${COMPONENT_TARGET} PRIVATE fetched_dep_lib)\n' + 'target_include_directories(${COMPONENT_TARGET} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/fetched_dep")\n' + ) + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES fc_comp\n # placeholder_inside_idf_component_register', + ) + + idf_py('build') + assert Path('build/build_test_app.elf').exists() + + +@pytest.mark.usefixtures('test_app_copy') +def test_idf_component_include_returns_interface(idf_py: IdfPyFunc) -> None: + """idf_component_include() INTERFACE output variable returns the interface target name.""" + logging.info('Testing idf_component_include INTERFACE output variable') + + comp = Path('components/iface_test_comp') + comp.mkdir(parents=True, exist_ok=True) + (comp / 'CMakeLists.txt').write_text('idf_component_register(SRCS "iface.c")\n') + (comp / 'iface.c').write_text('void iface_func(void) {}') + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES iface_test_comp\n # placeholder_inside_idf_component_register', + ) + + replace_in_file( + 'CMakeLists.txt', + '# placeholder_after_project_default', + 'idf_component_include(iface_test_comp INTERFACE iface_var)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/iface_output.txt" "${iface_var}")\n', + ) + + idf_py('reconfigure') + + iface_val = Path('build/iface_output.txt').read_text().strip() + assert len(iface_val) > 0, 'idf_component_include INTERFACE should return a non-empty target name' + + +@pytest.mark.usefixtures('test_app_copy') +def test_component_format_cmakev1_vs_cmakev2(idf_py: IdfPyFunc) -> None: + """COMPONENT_FORMAT should be CMAKEV1 for idf_component_register, CMAKEV2 for native.""" + logging.info('Testing COMPONENT_FORMAT detection') + + v1_comp = Path('components/format_v1') + v1_comp.mkdir(parents=True, exist_ok=True) + (v1_comp / 'CMakeLists.txt').write_text('idf_component_register(SRCS "v1.c")\n') + (v1_comp / 'v1.c').write_text('void v1_func(void) {}') + + v2_comp = Path('components/format_v2') + v2_comp.mkdir(parents=True, exist_ok=True) + (v2_comp / 'CMakeLists.txt').write_text( + 'add_library(${COMPONENT_TARGET} STATIC "v2.c")\n' + 'target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}")\n' + ) + (v2_comp / 'v2.c').write_text('void v2_func(void) {}') + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES format_v1 format_v2\n # placeholder_inside_idf_component_register', + ) + + replace_in_file( + 'CMakeLists.txt', + '# placeholder_after_project_default', + 'idf_component_get_property(v1_fmt format_v1 COMPONENT_FORMAT)\n' + 'idf_component_get_property(v2_fmt format_v2 COMPONENT_FORMAT)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/format_check.txt" "${v1_fmt}:${v2_fmt}")\n', + ) + + idf_py('build') + result = Path('build/format_check.txt').read_text().strip() + assert result == 'CMAKEV1:CMAKEV2', f'Expected CMAKEV1:CMAKEV2, got {result!r}' + + +@pytest.mark.usefixtures('test_app_copy') +def test_idf_component_optional_requires(idf_py: IdfPyFunc) -> None: + """idf_component_optional_requires silently skips unknown components.""" + logging.info('Testing idf_component_optional_requires with unknown component') + + comp = Path('components/opt_req_comp') + comp.mkdir(parents=True, exist_ok=True) + (comp / 'CMakeLists.txt').write_text( + 'idf_component_register(SRCS "opt.c")\nidf_component_optional_requires(PRIVATE nonexistent_comp_xyz)\n' + ) + (comp / 'opt.c').write_text('void opt_func(void) {}') + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES opt_req_comp\n # placeholder_inside_idf_component_register', + ) + + idf_py('build') + assert Path('build/build_test_app.elf').exists(), 'Build should succeed even with nonexistent optional dependency' + + +@pytest.mark.usefixtures('test_app_copy') +def test_idf_component_set_get_property_apis(idf_py: IdfPyFunc) -> None: + """Test idf_component_set_property and idf_component_get_property APIs work correctly.""" + logging.info('Testing idf_component_set_property and idf_component_get_property APIs') + + replace_in_file( + 'CMakeLists.txt', + '# placeholder_after_project_default', + 'idf_component_set_property(main CUSTOM_PROP "custom_value")\n' + 'idf_component_set_property(main TEST_LIST "item1")\n' + 'idf_component_set_property(main TEST_LIST "item2" APPEND)\n' + 'idf_component_get_property(custom main CUSTOM_PROP)\n' + 'idf_component_get_property(list_prop main TEST_LIST)\n' + 'idf_component_get_property(name main COMPONENT_NAME)\n' + 'idf_component_get_property(dir main COMPONENT_DIR)\n' + 'idf_component_get_property(lib main COMPONENT_LIB)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/properties_test.txt"\n' + ' "CUSTOM=${custom}\\n"\n' + ' "LIST=${list_prop}\\n"\n' + ' "NAME=${name}\\n"\n' + ' "DIR=${dir}\\n"\n' + ' "LIB=${lib}\\n"\n' + ')\n', + ) + + idf_py('reconfigure') + result = Path('build/properties_test.txt').read_text().strip() + + # Verify set/get works for custom property + assert 'CUSTOM=custom_value' in result, ( + 'idf_component_set_property and idf_component_get_property should work for custom properties' + ) + + # Verify append works + assert 'LIST=item1;item2' in result or ('item1' in result and 'item2' in result), ( + 'idf_component_set_property APPEND should work correctly' + ) + + # Verify standard properties are retrievable + assert 'NAME=main' in result, 'idf_component_get_property should retrieve COMPONENT_NAME' + assert 'DIR=' in result and len(result.split('DIR=')[1].split('\n')[0]) > 0, ( + 'idf_component_get_property should retrieve COMPONENT_DIR' + ) + assert 'LIB=' in result and len(result.split('LIB=')[1].split('\n')[0]) > 0, ( + 'idf_component_get_property should retrieve COMPONENT_LIB' + ) diff --git a/tools/test_build_system/buildv2/test_linux_build.py b/tools/test_build_system/buildv2/test_linux_build.py new file mode 100644 index 00000000000..38af71367bb --- /dev/null +++ b/tools/test_build_system/buildv2/test_linux_build.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import logging +import sys +from pathlib import Path + +import pytest +from test_build_system_helpers import IdfPyFunc + + +@pytest.mark.skipif(sys.platform == 'win32', reason='Unix test') +@pytest.mark.usefixtures('test_app_copy') +def test_linux_target_build(idf_py: IdfPyFunc) -> None: + logging.info('Can build for Linux target') + idf_py('--preview', '-DIDF_TARGET=linux', 'build') + elf = Path('build/build_test_app.elf') + assert elf.exists(), 'ELF file was not produced by the Linux build' + assert elf.stat().st_size > 0, 'ELF file is empty — link step may have failed silently' diff --git a/tools/test_build_system/buildv2/test_migration.py b/tools/test_build_system/buildv2/test_migration.py new file mode 100644 index 00000000000..e6eed42f6f1 --- /dev/null +++ b/tools/test_build_system/buildv2/test_migration.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import json +import logging +from pathlib import Path + +import pytest +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import replace_in_file + + +@pytest.mark.usefixtures('test_app_copy') +def test_v1_component_works_in_v2(idf_py: IdfPyFunc) -> None: + """Standard v1-style idf_component_register() component should work in v2""" + logging.info('Testing v1 component compatibility in v2') + + # The buildv2_test_app already uses idf_component_register - just build it + idf_py('build') + assert Path('build/build_test_app.elf').exists() + # Verify the v1-style component (main) was discovered and included + proj_desc = json.loads(Path('build/project_description.json').read_text()) + assert 'main' in proj_desc.get('build_components', []), ( + 'v1-style "main" component should appear in build_components' + ) + + +@pytest.mark.usefixtures('test_app_copy') +def test_idf_build_v2_available(idf_py: IdfPyFunc) -> None: + """IDF_BUILD_V2 should be available as both CMake variable and build property.""" + logging.info('Testing IDF_BUILD_V2 availability') + + comp_dir = Path('components/v2_guard_comp') + comp_dir.mkdir(parents=True, exist_ok=True) + + # Write marker in v2 branch + (comp_dir / 'CMakeLists.txt').write_text( + 'if(IDF_BUILD_V2)\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/v2_guard_check.txt" "V2_BRANCH")\n' + ' idf_component_register(SRCS "v2_guard.c")\n' + 'else()\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/v2_guard_check.txt" "V1_BRANCH")\n' + ' idf_component_register(SRCS "v2_guard.c")\n' + 'endif()\n' + ) + (comp_dir / 'v2_guard.c').write_text('void v2_guard_func(void) {}') + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES v2_guard_comp\n # placeholder_inside_idf_component_register', + ) + + replace_in_file( + 'CMakeLists.txt', + '# placeholder_after_project_default', + 'idf_build_get_property(is_v2 IDF_BUILD_V2)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/idf_v2_property.txt" "${is_v2}")\n', + ) + + idf_py('reconfigure') + + # Verify marker in v2 branch + guard_check = Path('build/v2_guard_check.txt').read_text().strip() + assert guard_check == 'V2_BRANCH', f'Expected IDF_BUILD_V2 guard to take V2_BRANCH, got: {guard_check!r}' + + # Verify build property is available + property_file = Path('build/idf_v2_property.txt') + assert property_file.exists(), 'IDF_BUILD_V2 property check file not created' + value = property_file.read_text().strip() + assert value in ('TRUE', 'true', 'YES', 'yes', 'y', '1', 'ON', 'on'), ( + f'IDF_BUILD_V2 property should be truthy, got: {value!r}' + ) + + +@pytest.mark.usefixtures('test_app_copy') +def test_minimal_build_ignored_with_warning(idf_py: IdfPyFunc) -> None: + """Setting MINIMAL_BUILD via idf_build_set_property should produce an obsolete warning.""" + logging.info('Testing MINIMAL_BUILD property warning') + + replace_in_file( + 'CMakeLists.txt', '# placeholder_after_project_default', 'idf_build_set_property(MINIMAL_BUILD ON)\n' + ) + + result = idf_py('reconfigure') + combined = (result.stdout or '') + (result.stderr or '') + assert 'MINIMAL_BUILD' in combined, 'MINIMAL_BUILD property name should appear in warning output' + lower = combined.lower() + assert 'obsolete' in lower or 'ignored' in lower or 'deprecated' in lower, ( + 'Warning should mention MINIMAL_BUILD is obsolete, ignored, or deprecated' + ) + + +@pytest.mark.usefixtures('test_app_copy') +def test_build_components_not_available(idf_py: IdfPyFunc) -> None: + """BUILD_COMPONENTS is not available in v2.""" + logging.info('Testing BUILD_COMPONENTS unavailability in v2') + + replace_in_file( + 'CMakeLists.txt', + '# placeholder_after_project_default', + # Check the CMake variable + 'if(DEFINED BUILD_COMPONENTS)\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/build_components_check.txt" "FOUND")\n' + 'else()\n' + ' file(WRITE "${CMAKE_BINARY_DIR}/build_components_check.txt" "NOT_FOUND")\n' + 'endif()\n' + # Using BUILD_COMPONENTS property must cause a fatal error in v2 + 'idf_build_get_property(_bc BUILD_COMPONENTS)\n', + ) + + result = idf_py('reconfigure', check=False) + + assert result.returncode != 0, 'reconfigure must fail when BUILD_COMPONENTS property is used' + combined = (result.stdout or '') + (result.stderr or '') + assert 'BUILD_COMPONENTS' in combined, 'Error output must mention BUILD_COMPONENTS' + assert 'not supported' in combined.lower(), 'Error output must state that BUILD_COMPONENTS is not supported' + + check_file = Path('build/build_components_check.txt') + assert check_file.exists(), 'Variable check file should be written before the fatal error' + assert check_file.read_text().strip() == 'NOT_FOUND', 'BUILD_COMPONENTS variable should not be available in v2' diff --git a/tools/test_build_system/buildv2/test_multi_binary.py b/tools/test_build_system/buildv2/test_multi_binary.py new file mode 100644 index 00000000000..22cf80772f0 --- /dev/null +++ b/tools/test_build_system/buildv2/test_multi_binary.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import json +import logging +from pathlib import Path + +import pytest +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import replace_in_file + + +def _create_app2_component(base_dir: Path) -> None: + """Create a second application component for multi-binary tests.""" + app2_dir = base_dir / 'components' / 'app2' + app2_dir.mkdir(parents=True, exist_ok=True) + (app2_dir / 'CMakeLists.txt').write_text('idf_component_register(SRCS "app2.c")\n') + (app2_dir / 'app2.c').write_text('void app_main(void) {}\n') + + +@pytest.mark.usefixtures('test_app_copy') +def test_multi_binary_all_features(idf_py: IdfPyFunc) -> None: + """Comprehensive test covering all multi-binary features in a single build.""" + logging.info('Testing multi-binary: all features in single comprehensive build') + + _create_app2_component(Path('.')) + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + # Create two executables with mapfile targets for size reports + 'idf_build_executable(app1 COMPONENTS main MAPFILE_TARGET app1_mapfile)\n' + 'idf_build_executable(app2 COMPONENTS app2 MAPFILE_TARGET app2_mapfile)\n' + # Create binaries with flash targets + 'idf_build_binary(app1\n' + ' TARGET app1_bin\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/app1.bin")\n' + 'idf_build_binary(app2\n' + ' TARGET app2_bin\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/app2.bin")\n' + # Add flash targets - test both FLASH flag behaviors: + # app1: FLASH flag adds to global 'flash' target (idf.py flash) + # app2: No FLASH flag creates only named target (idf.py app2-flash) + 'idf_flash_binary(app1_bin TARGET app1-flash NAME app1 FLASH)\n' + 'idf_flash_binary(app2_bin TARGET app2-flash NAME app2)\n' + # Check binary size for app1 + 'idf_check_binary_size(app1_bin)\n' + # Build both binaries + 'add_custom_target(app ALL DEPENDS app1_bin app2_bin)\n' + # Generate metadata for both executables and binaries + 'idf_build_generate_metadata(EXECUTABLE app1)\n' + 'idf_build_generate_metadata(EXECUTABLE app2\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/project_description_app2.json")\n' + 'idf_build_generate_metadata(BINARY app1_bin\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/project_description_bin.json")\n' + # Create menuconfig and confserver targets + 'idf_create_menuconfig(app1 TARGET menuconfig-app1)\n' + 'idf_create_menuconfig(app2 TARGET menuconfig-app2)\n' + 'idf_create_confserver(app1 TARGET confserver-app1)\n' + 'idf_create_confserver(app2 TARGET confserver-app2)\n' + # Create depgraph targets + 'idf_build_set_property(__BUILD_COMPONENT_DEPGRAPH_ENABLED YES)\n' + 'idf_build_generate_depgraph(app1)\n' + 'idf_build_generate_depgraph(app2\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/component_deps_app2.dot")\n' + # Create size report targets + 'if(TARGET app1_mapfile)\n' + ' idf_create_size_report(app1_mapfile TARGET size-app1)\n' + 'endif()\n' + 'if(TARGET app2_mapfile)\n' + ' idf_create_size_report(app2_mapfile TARGET size-app2)\n' + 'endif()\n' + # Generate flasher args + 'idf_build_generate_flasher_args()\n', + ) + + idf_py('build') + + # Verify both executables are created + assert Path('build/app1.elf').exists(), 'app1.elf not created' + assert Path('build/app2.elf').exists(), 'app2.elf not created' + + # Verify both binaries are created + assert Path('build/app1.bin').exists(), 'app1.bin should be produced' + assert Path('build/app2.bin').exists(), 'app2.bin should be produced' + + # Verify executable metadata + proj_desc = json.loads(Path('build/project_description.json').read_text()) + build_components = proj_desc.get('build_components', []) + assert 'main' in build_components, f'main component should be in app1 build_components, got: {build_components}' + + # Verify per-binary metadata + assert Path('build/project_description.json').exists(), 'Default project_description.json not created' + assert Path('build/project_description_app2.json').exists(), 'Per-binary project_description_app2.json not created' + assert Path('build/project_description_bin.json').exists(), ( + 'Binary metadata project_description_bin.json not created' + ) + + # Verify flasher_args.json is generated and references both apps + flasher_args_path = Path('build/flasher_args.json') + assert flasher_args_path.exists(), 'flasher_args.json not generated for multi-binary build' + flasher_args = json.loads(flasher_args_path.read_text()) + flash_files = flasher_args.get('flash_files', {}) + flash_values = ' '.join(str(v) for v in flash_files.values()) + assert 'app1' in flash_values, f'flasher_args.json should reference app1, got flash_files: {flash_files}' + + # Read build.ninja once for all checks + ninja = Path('build/build.ninja').read_text() + + # Verify flash targets + assert 'app1-flash' in ninja, '"app1-flash" target not found in build.ninja' + assert 'app2-flash' in ninja, '"app2-flash" target not found in build.ninja' + + # Verify configure targets + assert 'menuconfig-app1' in ninja, '"menuconfig-app1" target not found' + assert 'menuconfig-app2' in ninja, '"menuconfig-app2" target not found' + assert 'confserver-app1' in ninja, '"confserver-app1" target not found' + assert 'confserver-app2' in ninja, '"confserver-app2" target not found' + assert Path('build/component_deps.dot').exists(), 'component_deps.dot not created for app1' + assert Path('build/component_deps_app2.dot').exists(), 'component_deps_app2.dot not created for app2' + + # Verify size report targets + assert 'size-app1' in ninja, '"size-app1" target not found in build.ninja' + assert 'size-app2' in ninja, '"size-app2" target not found in build.ninja' diff --git a/tools/test_build_system/buildv2/test_project.py b/tools/test_build_system/buildv2/test_project.py new file mode 100644 index 00000000000..92d40dd660a --- /dev/null +++ b/tools/test_build_system/buildv2/test_project.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import json +import logging +import subprocess +from pathlib import Path + +import pytest +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import replace_in_file + + +@pytest.mark.usefixtures('test_app_copy') +def test_project_init_explicit_steps_without_binary(idf_py: IdfPyFunc) -> None: + """idf_project_init() + idf_build_executable() produces ELF but no .bin when idf_build_binary() is omitted.""" + logging.info('Testing idf_project_init with explicit steps but without idf_build_binary') + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + 'idf_build_executable(build_test_app COMPONENTS main)\n' + 'idf_build_generate_metadata(EXECUTABLE build_test_app)\n', + ) + + idf_py('build') + assert Path('build/project_description.json').exists(), 'project_description.json not produced' + assert Path('build/build_test_app.elf').exists(), 'ELF not created despite idf_build_executable()' + assert not Path('build/build_test_app.bin').exists(), '.bin should not exist when idf_build_binary() is omitted' + + +@pytest.mark.usefixtures('test_app_copy') +def test_project_default_produces_all_artifacts(idf_py: IdfPyFunc) -> None: + """idf_project_default() should produce ELF, .bin, project_description.json, + flasher_args.json, and standard build targets (menuconfig, uf2, size).""" + logging.info('Testing idf_project_default full artifact generation') + + idf_py('build') + + assert Path('build/build_test_app.elf').exists(), 'ELF not produced' + assert Path('build/build_test_app.bin').exists(), '.bin not produced' + assert Path('build/project_description.json').exists(), 'project_description.json not produced' + assert Path('build/flasher_args.json').exists(), 'flasher_args.json not produced' + + flasher_args = json.loads(Path('build/flasher_args.json').read_text()) + assert 'flash_files' in flasher_args, 'flasher_args.json should contain flash_files' + + ninja_targets_result = subprocess.run( + ['ninja', '-t', 'targets', 'all'], + capture_output=True, + text=True, + cwd='build', + check=True, + ) + ninja_targets = ninja_targets_result.stdout + for target in ( + 'menuconfig', + 'confserver', + 'save-defconfig', + 'config-report', + 'uf2', + 'uf2-app', + 'size', + 'app-flash', + ): + assert f'{target}:' in ninja_targets, f'"{target}" target not found in ninja targets' + + +@pytest.mark.usefixtures('test_app_copy') +def test_project_init_requires_cmake_project(idf_py: IdfPyFunc) -> None: + """idf_project_init() called before cmake project() should fail.""" + logging.info('Testing idf_project_init before project() fails') + + replace_in_file('CMakeLists.txt', '# placeholder_after_include_project_cmake', 'idf_project_init()\n') + replace_in_file('CMakeLists.txt', 'idf_project_default()', '# project_default removed for ordering test\n') + + cmake_content = Path('CMakeLists.txt').read_text() + assert 'idf_project_init()' in cmake_content, 'replace_in_file did not inject idf_project_init()' + + with pytest.raises(subprocess.CalledProcessError): + idf_py('reconfigure') + + +@pytest.mark.usefixtures('test_app_copy') +def test_build_generate_flasher_args(idf_py: IdfPyFunc) -> None: + """idf_build_generate_flasher_args() generates flasher_args.json with correct content.""" + logging.info('Testing idf_build_generate_flasher_args flasher_args.json generation') + + replace_in_file( + 'CMakeLists.txt', + 'idf_project_default()', + 'idf_project_init()\n' + 'idf_build_executable(build_test_app COMPONENTS main)\n' + 'idf_build_binary(build_test_app\n' + ' OUTPUT_FILE "${CMAKE_BINARY_DIR}/build_test_app.bin"\n' + ' TARGET build_test_app_binary)\n' + 'idf_flash_binary(build_test_app_binary\n' + ' TARGET app-flash\n' + ' NAME "app"\n' + ' FLASH)\n' + 'idf_check_binary_size(build_test_app_binary)\n' + 'add_custom_target(app ALL DEPENDS build_test_app_binary)\n' + 'idf_build_generate_metadata(BINARY build_test_app_binary)\n' + 'idf_build_generate_flasher_args()\n', + ) + + idf_py('build') + + # Verify flasher_args.json is created + flasher_args_path = Path('build/flasher_args.json') + assert flasher_args_path.exists(), 'flasher_args.json not generated by idf_build_generate_flasher_args' + + # Verify flasher_args.json contains expected structure + flasher_args = json.loads(flasher_args_path.read_text()) + assert 'flash_files' in flasher_args, 'flasher_args.json should contain flash_files key' + assert 'flash_settings' in flasher_args, 'flasher_args.json should contain flash_settings key' + assert 'extra_esptool_args' in flasher_args, 'flasher_args.json should contain extra_esptool_args' + assert 'chip' in flasher_args.get('extra_esptool_args', {}), 'extra_esptool_args should contain chip information' diff --git a/tools/test_build_system/buildv2/test_sdkconfig.py b/tools/test_build_system/buildv2/test_sdkconfig.py new file mode 100644 index 00000000000..ac5716b3ae9 --- /dev/null +++ b/tools/test_build_system/buildv2/test_sdkconfig.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import json +import logging +from pathlib import Path + +import pytest +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import file_contains +from test_build_system_helpers import replace_in_file + + +@pytest.mark.usefixtures('test_app_copy') +def test_kconfig_menus_json_not_generated(idf_py: IdfPyFunc) -> None: + """v2 should NOT generate kconfig_menus.json (v1 generates it).""" + logging.info('Testing that kconfig_menus.json is NOT generated in v2') + + idf_py('reconfigure') + + assert not Path('build/config/kconfig_menus.json').exists(), 'kconfig_menus.json should not be generated in v2' + + +@pytest.mark.usefixtures('test_app_copy') +def test_all_discovered_components_contribute_kconfig(idf_py: IdfPyFunc) -> None: + """All discovered components contribute Kconfig even if not in the dependency tree.""" + logging.info('Testing all discovered components contribute Kconfig') + + comp_dir = Path('components/kconfig_only_comp') + comp_dir.mkdir(parents=True, exist_ok=True) + (comp_dir / 'CMakeLists.txt').write_text('idf_component_register()\n') + (comp_dir / 'Kconfig').write_text( + 'config KCONFIG_ONLY_COMP_TEST_OPTION\n bool "Test option from kconfig_only_comp"\n default n\n' + ) + + idf_py('reconfigure') + + sdkconfig_json = json.loads(Path('build/config/sdkconfig.json').read_text()) + assert any('KCONFIG_ONLY_COMP_TEST_OPTION' in key for key in sdkconfig_json), ( + 'KCONFIG_ONLY_COMP_TEST_OPTION should appear in sdkconfig.json - ' + 'all discovered components must contribute Kconfig in v2' + ) + + +@pytest.mark.usefixtures('test_app_copy') +def test_config_report_target(idf_py: IdfPyFunc) -> None: + """idf_create_config_report() creates a config-report target that produces a report file.""" + logging.info('Testing config-report target') + + idf_py('reconfigure') + ninja = Path('build/build.ninja').read_text() + assert 'config-report' in ninja, '"config-report" target not found in build.ninja' + + idf_py('config-report') + assert Path('build/config/kconfig_parse_report.json').exists(), ( + 'config-report should produce kconfig_parse_report.json' + ) + + +@pytest.mark.usefixtures('test_app_copy') +def test_save_defconfig_target(idf_py: IdfPyFunc) -> None: + """idf_create_save_defconfig() creates a save-defconfig target.""" + logging.info('Testing save-defconfig target') + + idf_py('reconfigure') + ninja = Path('build/build.ninja').read_text() + assert 'save-defconfig' in ninja, '"save-defconfig" target not found in build.ninja' + + +@pytest.mark.usefixtures('test_app_copy') +def test_kconfig_driven_requires(idf_py: IdfPyFunc) -> None: + """CONFIG_ variables should be usable in REQUIRES.""" + logging.info('Testing Kconfig-driven REQUIRES') + + opt_comp = Path('components/optional_comp') + opt_comp.mkdir(parents=True, exist_ok=True) + (opt_comp / 'CMakeLists.txt').write_text('idf_component_register(SRCS "optional.c")\n') + (opt_comp / 'optional.c').write_text('void optional_func(void) {}') + (opt_comp / 'Kconfig').write_text('config OPTIONAL_COMP_ENABLE\n bool "Enable optional_comp"\n default n\n') + + cond_comp = Path('components/cond_dep_comp') + cond_comp.mkdir(parents=True, exist_ok=True) + (cond_comp / 'CMakeLists.txt').write_text( + 'if(CONFIG_OPTIONAL_COMP_ENABLE)\n' + ' set(opt_reqs optional_comp)\n' + 'else()\n' + ' set(opt_reqs "")\n' + 'endif()\n' + 'idf_component_register(SRCS "cond.c" REQUIRES ${opt_reqs})\n' + ) + (cond_comp / 'cond.c').write_text('void cond_func(void) {}') + + replace_in_file( + 'main/CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES cond_dep_comp\n # placeholder_inside_idf_component_register', + ) + + Path('sdkconfig.defaults').write_text('CONFIG_OPTIONAL_COMP_ENABLE=y\n') + + idf_py('build') + + proj_desc = json.loads(Path('build/project_description.json').read_text()) + build_components = proj_desc.get('build_components', []) + assert 'optional_comp' in build_components, ( + f'optional_comp should be in build_components when CONFIG_OPTIONAL_COMP_ENABLE=y ' + f'(Kconfig-driven REQUIRES), got: {build_components}' + ) + + +def test_sdkconfig_preserves_managed_component_options(idf_py: IdfPyFunc, test_app_copy: Path) -> None: + """Managed component Kconfig options should survive reconfigure rounds. + + kconfgen's --output config pass can drop options it does not recognise + (e.g. from managed components discovered after the initial configure). + This test verifies the value is preserved across reconfigure. + """ + logging.info('sdkconfig options from managed components should be preserved during reconfigure') + + managed_comp_dir = test_app_copy / '..' / 'managed_comp' + managed_comp_dir.mkdir(parents=True) + (managed_comp_dir / 'CMakeLists.txt').write_text('idf_component_register()\n') + (managed_comp_dir / 'Kconfig').write_text( + 'config MANAGED_COMP_TEST_OPTION\n int "Managed component test option"\n default 10\n' + ) + + (test_app_copy / 'main' / 'idf_component.yml').write_text( + 'dependencies:\n managed_comp:\n path: "../../managed_comp"\n' + ) + + (test_app_copy / 'sdkconfig').write_text('CONFIG_MANAGED_COMP_TEST_OPTION=30\n') + + idf_py('reconfigure') + + assert file_contains(test_app_copy / 'sdkconfig', 'CONFIG_MANAGED_COMP_TEST_OPTION=30') diff --git a/tools/test_build_system/buildv2/test_utilities.py b/tools/test_build_system/buildv2/test_utilities.py new file mode 100644 index 00000000000..d6001ea3738 --- /dev/null +++ b/tools/test_build_system/buildv2/test_utilities.py @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import logging +import subprocess +from pathlib import Path + +import pytest +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import replace_in_file + + +@pytest.mark.usefixtures('test_app_copy') +def test_idf_messaging_and_compile_options(idf_py: IdfPyFunc) -> None: + """idf_msg/idf_warn produce output; idf_build_get_compile_options returns flags.""" + logging.info('Testing idf messaging functions and compile options') + + replace_in_file( + 'CMakeLists.txt', + '# placeholder_after_project_default', + '# placeholder_after_project_default\n' + 'idf_msg("TEST_MSG_OUTPUT")\n' + 'idf_warn("TEST_WARN_OUTPUT")\n' + 'idf_build_get_compile_options(opts)\n' + 'file(WRITE "${CMAKE_BINARY_DIR}/compile_opts.txt" "${opts}")\n', + ) + + result = idf_py('reconfigure') + + combined = (result.stdout or '') + (result.stderr or '') + assert 'TEST_MSG_OUTPUT' in combined and 'TEST_WARN_OUTPUT' in combined, ( + 'Both idf_msg and idf_warn should produce output' + ) + + assert Path('build/compile_opts.txt').exists() + opts_content = Path('build/compile_opts.txt').read_text() + assert len(opts_content) > 0, 'compile options should not be empty' + + +@pytest.mark.usefixtures('test_app_copy') +def test_idf_die_aborts_configuration(idf_py: IdfPyFunc) -> None: + """idf_die() should abort cmake configuration with a clear message.""" + logging.info('Testing idf_die aborts configuration') + + replace_in_file( + 'CMakeLists.txt', + '# placeholder_after_project_default', + '# placeholder_after_project_default\nidf_die("FATAL_TEST_MESSAGE_UNIQUE_STRING")\n', + ) + + with pytest.raises(subprocess.CalledProcessError) as exc_info: + idf_py('reconfigure') + err_output = (exc_info.value.stdout or '') + (exc_info.value.stderr or '') + assert 'FATAL_TEST_MESSAGE_UNIQUE_STRING' in err_output, 'idf_die message should appear in the error output' + + +@pytest.mark.usefixtures('test_app_copy') +def test_idf_deprecated_produces_warning(idf_py: IdfPyFunc) -> None: + """idf_deprecated() should emit a deprecation warning.""" + logging.info('Testing idf_deprecated warning output') + + replace_in_file( + 'CMakeLists.txt', + '# placeholder_after_project_default', + '# placeholder_after_project_default\n' + 'set(CMAKE_WARN_DEPRECATED ON)\n' + 'idf_deprecated("DEPRECATED_TEST_MSG_UNIQUE")\n', + ) + + result = idf_py('reconfigure') + combined = (result.stdout or '') + (result.stderr or '') + assert 'DEPRECATED_TEST_MSG_UNIQUE' in combined, 'idf_deprecated message should appear in the output' diff --git a/tools/test_build_system/buildv2_test_app/CMakeLists.txt b/tools/test_build_system/buildv2_test_app/CMakeLists.txt index f911410a47a..258a917b850 100644 --- a/tools/test_build_system/buildv2_test_app/CMakeLists.txt +++ b/tools/test_build_system/buildv2_test_app/CMakeLists.txt @@ -8,8 +8,12 @@ include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) project(build_test_app C CXX ASM) +# placeholder_before_project_default + idf_project_default() +# placeholder_after_project_default + #[[ When doing idf.py add-dependency, the component manager expects the tools/cmake/project.cmake file to be included diff --git a/tools/test_build_system/conftest.py b/tools/test_build_system/conftest.py index 773f718e1eb..d8d85ce0a0e 100644 --- a/tools/test_build_system/conftest.py +++ b/tools/test_build_system/conftest.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import datetime import logging @@ -241,10 +241,23 @@ def idf_py(default_idf_env: EnvDict) -> IdfPyFunc: def pytest_collection_modifyitems(session: Session, config: Config, items: list[Item]) -> None: - if not config.getoption('--buildv2', False): - return + buildv2_dir = Path(__file__).parent / 'buildv2' + is_buildv2 = config.getoption('--buildv2', False) + for item in items: - marker = item.get_closest_marker('buildv2_skip') - if marker: - reason = marker.args[0] if marker.args else 'Skipped as this test is specific to build system v1.' - item.add_marker(pytest.mark.skip(reason=reason)) + if is_buildv2: + marker = item.get_closest_marker('buildv2_skip') + if marker: + reason = marker.args[0] if marker.args else 'Skipped as this test is specific to build system v1.' + item.add_marker(pytest.mark.skip(reason=reason)) + else: + if buildv2_dir in item.path.parents or item.path == buildv2_dir: + item.add_marker(pytest.mark.skip(reason='Skipped as build system v2 tests are disabled.')) + + +def pytest_report_header(config: Config) -> str: + """Add a clear header to the terminal output whether buildv1 or buildv2 testing is in progress.""" + if config.getoption('--buildv2'): + return 'Testing ESP-IDF CMake-based build system v2' + else: + return 'Testing ESP-IDF CMake-based build system v1' diff --git a/tools/test_build_system/test_build.py b/tools/test_build_system/test_build.py index 054a789fdd1..2f88c97f125 100644 --- a/tools/test_build_system/test_build.py +++ b/tools/test_build_system/test_build.py @@ -208,11 +208,6 @@ def test_build_uf2(idf_py: IdfPyFunc) -> None: assert_built(BOOTLOADER_BINS + APP_BINS + PARTITION_BIN + ['build/uf2.bin']) -# The bootloader_support component defines its requirements based on the -# sdkconfig values, specifically the CONFIG_APP_BUILD_TYPE_RAM used in this -# test. If CONFIG_APP_BUILD_TYPE_RAM is set, bootloader_support declares a -# dependency on micro-ecc. -@pytest.mark.buildv2_skip('bootloader_support component CMakeLists.txt is broken') def test_build_loadable_elf(idf_py: IdfPyFunc, test_app_copy: Path) -> None: logging.info('Loadable ELF build works') (test_app_copy / 'sdkconfig').write_text( diff --git a/tools/test_build_system/test_cmake.py b/tools/test_build_system/test_cmake.py index 2739c423079..5c881c803c1 100644 --- a/tools/test_build_system/test_cmake.py +++ b/tools/test_build_system/test_cmake.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import json import logging @@ -22,21 +22,32 @@ from test_build_system_helpers import run_idf_py # This test checks multiple targets in one test function. It would be better to have each target # tested in a isolated test case, but that would mean doing idf_copy each time, and copying takes most of the time @pytest.mark.usefixtures('idf_copy') -def test_build_custom_cmake_project(test_app_copy: Path) -> None: +def test_build_custom_cmake_project(test_app_copy: Path, request: pytest.FixtureRequest) -> None: # Test is compatible with any target. Random targets in the list are selected for performance reasons idf_path = Path(os.environ['IDF_PATH']) + is_buildv2 = request.config.getoption('buildv2', False) + if is_buildv2: + idf_as_lib_path = idf_path / 'examples' / 'build_system' / 'cmakev2' / 'features' / 'idf_as_lib' + base_cmake_args = ['-G', 'Ninja', '-DESP_PLATFORM=1'] + target_var = 'IDF_TARGET' + else: + idf_as_lib_path = idf_path / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib' + base_cmake_args = ['-G', 'Ninja'] + target_var = 'TARGET' + for target in ['esp32', 'esp32c2', 'esp32c3', 'esp32c6', 'esp32h2', 'esp32p4', 'esp32s2', 'esp32s3']: logging.info(f'Test build ESP-IDF as a library to a custom CMake projects for {target}') run_cmake_and_build( - str(idf_path / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib'), - '-G', - 'Ninja', + str(idf_as_lib_path), + *base_cmake_args, '-DCMAKE_TOOLCHAIN_FILE={}'.format(idf_path / 'tools' / 'cmake' / f'toolchain-{target}.cmake'), - f'-DTARGET={target}', + f'-D{target_var}={target}', ) assert file_contains((test_app_copy / 'build' / 'compile_commands.json'), '"command"') shutil.rmtree(test_app_copy / 'build') - os.remove(idf_path / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib' / 'sdkconfig') + sdkconfig_path = idf_as_lib_path / 'sdkconfig' + if sdkconfig_path.exists(): + os.remove(sdkconfig_path) @pytest.mark.skipif( @@ -45,16 +56,25 @@ def test_build_custom_cmake_project(test_app_copy: Path) -> None: ) @pytest.mark.usefixtures('idf_copy') @pytest.mark.usefixtures('test_app_copy') -def test_build_custom_cmake_project_host() -> None: +def test_build_custom_cmake_project_host(request: pytest.FixtureRequest) -> None: logging.info('Test build ESP-IDF as a library to a custom CMake projects for host') idf_path = Path(os.environ['IDF_PATH']) - run_cmake_and_build(str(idf_path / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib'), '-G', 'Ninja') + is_buildv2 = request.config.getoption('buildv2', False) + if is_buildv2: + idf_as_lib_path = idf_path / 'examples' / 'build_system' / 'cmakev2' / 'features' / 'idf_as_lib' + else: + idf_as_lib_path = idf_path / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib' + run_cmake_and_build(str(idf_as_lib_path), '-G', 'Ninja') -@pytest.mark.buildv2_skip('import_lib example uses cmakev1, not yet updated for buildv2 (IDF-14185)') -def test_build_cmake_library_with_toolchain_flags(test_app_copy: Path) -> None: +def test_build_cmake_library_with_toolchain_flags(test_app_copy: Path, request: pytest.FixtureRequest) -> None: logging.info('Building a project with CMake library imported with modified toolchain flags') idf_path = Path(os.environ['IDF_PATH']) + is_buildv2 = request.config.getoption('buildv2', False) + if is_buildv2: + import_lib_path = idf_path / 'examples' / 'build_system' / 'cmakev2' / 'features' / 'import_lib' + else: + import_lib_path = idf_path / 'examples' / 'build_system' / 'cmake' / 'import_lib' # Enable Picolibc to verify that all flags are passed correctly to the external project. # In case something is missing, the build will fail on linking stage. # Note: To enable Picolibc, IDF_EXPERIMENTAL_FEATURES must also be set for now. @@ -62,7 +82,6 @@ def test_build_cmake_library_with_toolchain_flags(test_app_copy: Path) -> None: '\n'.join(['CONFIG_IDF_EXPERIMENTAL_FEATURES=y', 'CONFIG_LIBC_PICOLIBC=y']) ) - import_lib_path = idf_path / 'examples' / 'build_system' / 'cmake' / 'import_lib' run_cmake_and_build( str(import_lib_path), '-G', @@ -103,22 +122,37 @@ def check_flag_in_compile_commands(build_dir: Path, flag_to_find: str) -> None: assert False, f'{flag_to_find} not found in {command}' -@pytest.mark.buildv2_skip('import_lib example uses cmakev1, not yet updated for buildv2 (IDF-14185)') -def test_build_cmake_library_psram_workaround(test_app_copy: Path) -> None: +def test_build_cmake_library_psram_workaround(test_app_copy: Path, request: pytest.FixtureRequest) -> None: logging.info( 'Building a project with CMake library imported and PSRAM workaround, all files compile with workaround' ) idf_path = Path(os.environ['IDF_PATH']) + is_buildv2 = request.config.getoption('buildv2', False) (test_app_copy / 'sdkconfig.defaults').write_text( '\n'.join(['CONFIG_SPIRAM=y', 'CONFIG_SPIRAM_CACHE_WORKAROUND=y']) ) - run_cmake( - '-G', - 'Ninja', - '-DCOMPONENTS=main;esp_psram', - '-DSDKCONFIG_DEFAULTS={}'.format(test_app_copy / 'sdkconfig.defaults'), - str(idf_path / 'examples' / 'build_system' / 'cmake' / 'import_lib'), - ) + if is_buildv2: + # Use buildv2_test_app and add esp_psram via CMakeLists placeholder + replace_in_file( + test_app_copy / 'main' / 'CMakeLists.txt', + '# placeholder_inside_idf_component_register', + 'REQUIRES esp_psram', + ) + run_cmake( + '..', + '-G', + 'Ninja', + '-DSDKCONFIG_DEFAULTS={}'.format(test_app_copy / 'sdkconfig.defaults'), + ) + else: + import_lib_path = idf_path / 'examples' / 'build_system' / 'cmake' / 'import_lib' + run_cmake( + '-G', + 'Ninja', + '-DCOMPONENTS=main;esp_psram', + '-DSDKCONFIG_DEFAULTS={}'.format(test_app_copy / 'sdkconfig.defaults'), + str(import_lib_path), + ) check_flag_in_compile_commands(test_app_copy, '-mfix-esp32-psram-cache-issue') @@ -147,38 +181,60 @@ def test_build_cmake_library_psram_strategies(idf_py: IdfPyFunc, test_app_copy: (test_app_copy / 'sdkconfig').unlink() -def test_defaults_unspecified_build_args(idf_copy: Path) -> None: - logging.info('Defaults set properly for unspecified idf_build_process args') - idf_as_lib_path = idf_copy / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib' +def test_defaults_unspecified_build_args(idf_copy: Path, request: pytest.FixtureRequest) -> None: + is_buildv2 = request.config.getoption('buildv2', False) + if is_buildv2: + logging.info('PROJECT_DIR set correctly by build system v2') + idf_as_lib_path = idf_copy / 'examples' / 'build_system' / 'cmakev2' / 'features' / 'idf_as_lib' + cmake_args = [ + '..', + '-G', + 'Ninja', + '-DCMAKE_TOOLCHAIN_FILE={}'.format(str(idf_copy / 'tools' / 'cmake' / 'toolchain-esp32.cmake')), + '-DESP_PLATFORM=1', + '-DIDF_TARGET=esp32', + ] + else: + logging.info('Defaults set properly for unspecified idf_build_process args') + idf_as_lib_path = idf_copy / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib' + cmake_args = [ + '..', + '-G', + 'Ninja', + '-DCMAKE_TOOLCHAIN_FILE={}'.format(str(idf_copy / 'tools' / 'cmake' / 'toolchain-esp32.cmake')), + '-DTARGET=esp32', + ] append_to_file( idf_as_lib_path / 'CMakeLists.txt', '\n'.join(['idf_build_get_property(project_dir PROJECT_DIR)', 'message("Project directory: ${project_dir}")']), ) - ret = run_cmake( - '..', - '-G', - 'Ninja', - '-DCMAKE_TOOLCHAIN_FILE={}'.format(str(idf_copy / 'tools' / 'cmake' / 'toolchain-esp32.cmake')), - '-DTARGET=esp32', - workdir=idf_as_lib_path, - ) + ret = run_cmake(*cmake_args, workdir=idf_as_lib_path) assert f'Project directory: {str(idf_as_lib_path.as_posix())}' in ret.stderr -def test_build_example_on_host(default_idf_env: EnvDict) -> None: +def test_build_example_on_host(default_idf_env: EnvDict, request: pytest.FixtureRequest) -> None: logging.info('Test if it can build the example to run on host') idf_path = Path(default_idf_env.get('IDF_PATH')) - idf_as_lib_path = Path(idf_path, 'examples', 'build_system', 'cmake', 'idf_as_lib') - try: - target = 'esp32' - run_cmake( + is_buildv2 = request.config.getoption('buildv2', False) + if is_buildv2: + idf_as_lib_path = Path(idf_path, 'examples', 'build_system', 'cmakev2', 'features', 'idf_as_lib') + cmake_configure_args = [ '..', - f'-DCMAKE_TOOLCHAIN_FILE={idf_path}/tools/cmake/toolchain-{target}.cmake', - f'-DTARGET={target}', + f'-DCMAKE_TOOLCHAIN_FILE={idf_path}/tools/cmake/toolchain-esp32.cmake', + '-DESP_PLATFORM=1', + '-DIDF_TARGET=esp32', '-GNinja', - workdir=idf_as_lib_path, - ) - + ] + else: + idf_as_lib_path = Path(idf_path, 'examples', 'build_system', 'cmake', 'idf_as_lib') + cmake_configure_args = [ + '..', + f'-DCMAKE_TOOLCHAIN_FILE={idf_path}/tools/cmake/toolchain-esp32.cmake', + '-DTARGET=esp32', + '-GNinja', + ] + try: + run_cmake(*cmake_configure_args, workdir=idf_as_lib_path) run_cmake('--build', '.', workdir=idf_as_lib_path) finally: shutil.rmtree(idf_as_lib_path / 'build', ignore_errors=True) diff --git a/tools/test_build_system/test_post_elf_dependency.py b/tools/test_build_system/test_post_elf_dependency.py index b5a188614a0..669d6a60e12 100644 --- a/tools/test_build_system/test_post_elf_dependency.py +++ b/tools/test_build_system/test_post_elf_dependency.py @@ -2,11 +2,13 @@ # SPDX-License-Identifier: Apache-2.0 from pathlib import Path +import pytest from test_build_system_helpers import append_to_file from test_build_system_helpers import get_snapshot from test_build_system_helpers import run_cmake_and_build +@pytest.mark.buildv2_skip('Post-ELF dependency API is not used in cmakev2. Component callback method replaces this') def test_post_elf_dependency_runs_before_bin(test_app_copy: Path) -> None: """ Verify idf_build_add_post_elf_dependency( ) adds a post-ELF step diff --git a/tools/test_build_system/test_spaces.py b/tools/test_build_system/test_spaces.py index 6d4ca8c9813..438bb4853b2 100644 --- a/tools/test_build_system/test_spaces.py +++ b/tools/test_build_system/test_spaces.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import logging import os @@ -21,6 +21,7 @@ def clean_app_dir(app_path: Path) -> None: shutil.rmtree(app_path / 'build', ignore_errors=True) +@pytest.mark.buildv2_skip('Examples (spiffsgen, ulp, etc.) not yet ported to buildv2') @pytest.mark.idf_copy_with_space def test_spaces_bundle1(idf_copy: Path) -> None: logging.info('Running test spaces bundle 1') @@ -29,9 +30,12 @@ def test_spaces_bundle1(idf_copy: Path) -> None: # test build ulp_fsm run_idf_py('build', workdir=(idf_copy / 'examples' / 'system' / 'ulp' / 'ulp_fsm' / 'ulp')) # test build ulp_riscv - run_idf_py('-DIDF_TARGET=esp32s2', 'build', workdir=(idf_copy / 'examples' / 'system' / 'ulp' / 'ulp_riscv' / 'gpio')) + run_idf_py( + '-DIDF_TARGET=esp32s2', 'build', workdir=(idf_copy / 'examples' / 'system' / 'ulp' / 'ulp_riscv' / 'gpio') + ) +@pytest.mark.buildv2_skip('Examples (flash_encryption, https_x509_bundle) not yet ported to buildv2') @pytest.mark.idf_copy_with_space def test_spaces_bundle2(idf_copy: Path) -> None: logging.info('Running test spaces bundle 2') @@ -41,33 +45,38 @@ def test_spaces_bundle2(idf_copy: Path) -> None: run_idf_py('build', workdir=(idf_copy / 'examples' / 'protocols' / 'https_x509_bundle')) +@pytest.mark.buildv2_skip('secure_boot test app not yet ported to buildv2') @pytest.mark.idf_copy_with_space def test_spaces_bundle3(idf_copy: Path) -> None: logging.info('Running test spaces bundle 3') - secure_boot_app_path = (idf_copy / 'tools' / 'test_apps' / 'security' / 'secure_boot') + secure_boot_app_path = idf_copy / 'tools' / 'test_apps' / 'security' / 'secure_boot' # test secure_boot_v1 - run_idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.00', 'build', - workdir=secure_boot_app_path) + run_idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.00', 'build', workdir=secure_boot_app_path) clean_app_dir(secure_boot_app_path) # test secure_boot_v2 - run_idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.01', 'build', - workdir=secure_boot_app_path) + run_idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.01', 'build', workdir=secure_boot_app_path) clean_app_dir(secure_boot_app_path) # test app_signing - run_idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.02', 'build', - workdir=secure_boot_app_path) + run_idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.02', 'build', workdir=secure_boot_app_path) clean_app_dir(secure_boot_app_path) # test secure_boot_release_mode - run_idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.04', '-DIDF_TARGET=esp32s2', 'build', - workdir=secure_boot_app_path) + run_idf_py( + '-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.04', + '-DIDF_TARGET=esp32s2', + 'build', + workdir=secure_boot_app_path, + ) @pytest.mark.xfail(sys.platform == 'win32', reason='Bug with reproducible build') # Use this bundle for tests which can be done with the default build_test_app -@pytest.mark.parametrize('dummy_', [ - # Dummy parameter with a space in it, used so that the test directory name contains a space - pytest.param('test spaces') -]) +@pytest.mark.parametrize( + 'dummy_', + [ + # Dummy parameter with a space in it, used so that the test directory name contains a space + pytest.param('test spaces') + ], +) @pytest.mark.idf_copy_with_space @pytest.mark.usefixtures('idf_copy') def test_spaces_bundle4(dummy_: str, idf_py: IdfPyFunc, test_app_copy: Path) -> None: @@ -91,9 +100,9 @@ def test_install_export_unix(idf_copy: Path) -> None: install_cmd = './install.sh esp32' export_cmd = '. ./export.sh' - logging.debug('running {} in {}'.format(install_cmd, idf_copy)) + logging.debug(f'running {install_cmd} in {idf_copy}') subprocess.check_call(install_cmd, env=env, shell=True, cwd=idf_copy) - logging.debug('running {} in {}'.format(export_cmd, idf_copy)) + logging.debug(f'running {export_cmd} in {idf_copy}') # The default shell used by subprocess.Popen on POSIX platforms is '/bin/sh', # which in esp-env Docker image is 'dash'. The export script doesn't support # IDF_PATH detection when used in dash, so we have to override the shell here. @@ -108,7 +117,7 @@ def test_install_export_win(idf_copy: Path) -> None: install_cmd = 'install.bat esp32' export_cmd = 'export.bat' - logging.debug('running {} in {}'.format(install_cmd, idf_copy)) + logging.debug(f'running {install_cmd} in {idf_copy}') subprocess.check_call(install_cmd, env=env, shell=True, cwd=idf_copy) - logging.debug('running {} in {}'.format(export_cmd, idf_copy)) + logging.debug(f'running {export_cmd} in {idf_copy}') subprocess.check_call(export_cmd, env=env, shell=True, cwd=idf_copy)