diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 9359f18ff1..5e0fa5dac0 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -214,6 +214,7 @@ /tools/ci/ @esp-idf-codeowners/ci /tools/cmake/ @esp-idf-codeowners/build-config /tools/cmake/toolchain-*.cmake @esp-idf-codeowners/toolchain +/tools/cmakev2/ @esp-idf-codeowners/build-config /tools/esp_app_trace/ @esp-idf-codeowners/debugging /tools/gdb_panic_server.py @esp-idf-codeowners/debugging /tools/kconfig*/ @esp-idf-codeowners/build-config diff --git a/.gitlab/ci/build.yml b/.gitlab/ci/build.yml index 2dcf6f735f..ed491abb37 100644 --- a/.gitlab/ci/build.yml +++ b/.gitlab/ci/build.yml @@ -255,6 +255,38 @@ pytest_build_system: extends: .test_build_system_template parallel: 3 +pytest_buildv2_system: + extends: + - .test_build_system_template + - .rules:labels:buildv2 + parallel: 3 + script: + - ${IDF_PATH}/tools/ci/test_configure_ci_environment.sh + - cd ${IDF_PATH}/tools/test_build_system + - run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME} + - pytest + --buildv2 + --cleanup-idf-copy + --parallel-count ${CI_NODE_TOTAL:-1} + --parallel-index ${CI_NODE_INDEX:-1} + --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 + pytest_build_system_macos: extends: - .test_build_system_template diff --git a/.gitlab/ci/dependencies/dependencies.yml b/.gitlab/ci/dependencies/dependencies.yml index 06dfb4bfce..db447461b2 100644 --- a/.gitlab/ci/dependencies/dependencies.yml +++ b/.gitlab/ci/dependencies/dependencies.yml @@ -76,3 +76,7 @@ - if-schedule-test-build-system-windows patterns: - build_system_win + +"labels:buildv2": + labels: + - buildv2 diff --git a/.gitlab/ci/rules.yml b/.gitlab/ci/rules.yml index ba0d32658a..ce89384f0f 100644 --- a/.gitlab/ci/rules.yml +++ b/.gitlab/ci/rules.yml @@ -46,6 +46,7 @@ .patterns-build_system: &patterns-build_system - "tools/cmake/**/*" + - "tools/cmakev2/**/*" - "tools/kconfig_new/**/*" - "tools/idf.py" - "tools/idf_py_actions/**/*" @@ -275,6 +276,9 @@ .if-label-build: &if-label-build if: '$BOT_LABEL_BUILD || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*build(?:,[^,\n\r]+)*$/i' +.if-label-buildv2: &if-label-buildv2 + if: '$BOT_LABEL_BUILDV2 || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*buildv2(?:,[^,\n\r]+)*$/i' + .if-label-docker: &if-label-docker if: '$BOT_LABEL_DOCKER || $CI_MERGE_REQUEST_LABELS =~ /^(?:[^,\n\r]+,)*docker(?:,[^,\n\r]+)*$/i' @@ -363,6 +367,12 @@ - <<: *if-dev-push changes: *patterns-downloadable-tools +.rules:labels:buildv2: + rules: + - <<: *if-revert-branch + when: never + - <<: *if-label-buildv2 + .rules:labels:nvs_coverage: rules: - <<: *if-revert-branch diff --git a/.gitlab/ci/test-win.yml b/.gitlab/ci/test-win.yml index e90e568e31..8bf2f62fa6 100644 --- a/.gitlab/ci/test-win.yml +++ b/.gitlab/ci/test-win.yml @@ -136,3 +136,45 @@ pytest_build_system_win_minimal_cmake: - cd ${IDF_PATH}\tools\test_build_system - idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME} - pytest -k cmake --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} + +pytest_buildv2_system_win: + extends: + - .test_build_system_template_win + - .rules:labels:buildv2 + parallel: 2 + needs: [] + tags: [windows-build, brew] + artifacts: + paths: + - XUNIT_RESULT.xml + - test_build_system + expire_in: 2 days + reports: + junit: XUNIT_RESULT.xml + when: always + script: + - .\install.ps1 --enable-ci + - . .\export.ps1 + - python "${SUBMODULE_FETCH_TOOL}" -s "all" + - cd ${IDF_PATH}\tools\test_build_system + - idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME} + - pytest + --buildv2 + --parallel-count ${CI_NODE_TOTAL} + --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 diff --git a/Kconfig b/Kconfig index 4a0abb9534..b3506cfd7d 100644 --- a/Kconfig +++ b/Kconfig @@ -721,6 +721,17 @@ mainmenu "Espressif IoT Development Framework Configuration" depends on "${IDF_MINIMAL_BUILD}" source "$COMPONENT_KCONFIGS_SOURCE_FILE" + + menu "Configuration for components not included in the build" + depends on "${IDF_BUILD_V2}" + osource "$COMPONENT_KCONFIGS_EXCLUDED_SOURCE_FILE" + endmenu + + endmenu + + menu "Configuration for components not included in the build" + depends on "${IDF_BUILD_V2}" + osource "$COMPONENT_KCONFIGS_PROJBUILD_EXCLUDED_SOURCE_FILE" endmenu config IDF_EXPERIMENTAL_FEATURES diff --git a/components/esp_common/CMakeLists.txt b/components/esp_common/CMakeLists.txt index 239b1640b0..54d5d5ebd2 100644 --- a/components/esp_common/CMakeLists.txt +++ b/components/esp_common/CMakeLists.txt @@ -35,10 +35,14 @@ set(optional_reqs ulp esp_hal_mspi esp_hw_support) -idf_build_get_property(build_components BUILD_COMPONENTS) -foreach(req ${optional_reqs}) - if(req IN_LIST build_components) - idf_component_get_property(req_lib ${req} COMPONENT_LIB) - target_link_libraries(${COMPONENT_LIB} PRIVATE ${req_lib}) - endif() -endforeach() +if(IDF_BUILD_V2) + idf_component_optional_requires(PRIVATE ${optional_reqs}) +else() + idf_build_get_property(build_components BUILD_COMPONENTS) + foreach(req ${optional_reqs}) + if(req IN_LIST build_components) + idf_component_get_property(req_lib ${req} COMPONENT_LIB) + target_link_libraries(${COMPONENT_LIB} PRIVATE ${req_lib}) + endif() + endforeach() +endif() diff --git a/components/esp_eth/CMakeLists.txt b/components/esp_eth/CMakeLists.txt index 0875def6f1..c31497d42e 100644 --- a/components/esp_eth/CMakeLists.txt +++ b/components/esp_eth/CMakeLists.txt @@ -4,8 +4,6 @@ if(${target} STREQUAL "linux") return() # This component is not supported by the POSIX/Linux simulator endif() -idf_build_get_property(components_to_build BUILD_COMPONENTS) - set(srcs) set(include) set(ld_fragments linker.lf) @@ -19,8 +17,9 @@ if(CONFIG_ETH_ENABLED) set(srcs "src/esp_eth.c" "src/phy/esp_eth_phy_802_3.c") set(include "include") - if(NOT CMAKE_BUILD_EARLY_EXPANSION) + if(NOT CMAKE_BUILD_EARLY_EXPANSION AND NOT IDF_BUILD_V2) # esp_netif related + idf_build_get_property(components_to_build BUILD_COMPONENTS) if(esp_netif IN_LIST components_to_build) list(APPEND srcs "src/esp_eth_netif_glue.c") endif() @@ -46,6 +45,9 @@ idf_component_register(SRCS "${srcs}" PRIV_REQUIRES ${priv_requires}) if(CONFIG_ETH_ENABLED) + if(IDF_BUILD_V2) + target_sources(${COMPONENT_TARGET} PRIVATE "$<$:src/esp_eth_netif_glue.c>") + endif() if(CONFIG_ETH_USE_SPI_ETHERNET) idf_component_optional_requires(PUBLIC esp_driver_spi) endif() diff --git a/components/esp_hal_i2c/CMakeLists.txt b/components/esp_hal_i2c/CMakeLists.txt index 60d93fe8dc..1c02433b16 100644 --- a/components/esp_hal_i2c/CMakeLists.txt +++ b/components/esp_hal_i2c/CMakeLists.txt @@ -1,5 +1,6 @@ idf_build_get_property(target IDF_TARGET) +set(includes) list(APPEND includes "${target}/include") list(APPEND includes "include") diff --git a/components/esp_libc/CMakeLists.txt b/components/esp_libc/CMakeLists.txt index 3c354f2932..9aaebf5825 100644 --- a/components/esp_libc/CMakeLists.txt +++ b/components/esp_libc/CMakeLists.txt @@ -77,10 +77,18 @@ else() list(APPEND ldfragments src/picolibc/libc.lf) endif() +set(priv_reqs soc spi_flash) + +if(IDF_BUILD_V2) + if(CONFIG_VFS_SUPPORT_IO) + list(APPEND priv_reqs vfs) + endif() +endif() + idf_component_register(SRCS "${srcs}" INCLUDE_DIRS platform_include PRIV_INCLUDE_DIRS priv_include - PRIV_REQUIRES soc spi_flash + PRIV_REQUIRES "${priv_reqs}" LDFRAGMENTS "${ldfragments}") # Toolchain libraries require code defined in this component diff --git a/components/esp_usb_cdc_rom_console/CMakeLists.txt b/components/esp_usb_cdc_rom_console/CMakeLists.txt index 01f3551f44..40dfb942c8 100644 --- a/components/esp_usb_cdc_rom_console/CMakeLists.txt +++ b/components/esp_usb_cdc_rom_console/CMakeLists.txt @@ -1,5 +1,6 @@ idf_build_get_property(target IDF_TARGET) +set(srcs) if(CONFIG_ESP_CONSOLE_USB_CDC) list(APPEND srcs "usb_console.c") diff --git a/components/heap/CMakeLists.txt b/components/heap/CMakeLists.txt index 8fa40265ed..af11108766 100644 --- a/components/heap/CMakeLists.txt +++ b/components/heap/CMakeLists.txt @@ -69,9 +69,13 @@ if(CONFIG_HEAP_TRACING) endforeach() endif() -if(NOT CMAKE_BUILD_EARLY_EXPANSION) - idf_build_get_property(build_components BUILD_COMPONENTS) - if(freertos IN_LIST build_components) - target_compile_options(${COMPONENT_TARGET} PRIVATE "-DMULTI_HEAP_FREERTOS") +if(IDF_BUILD_V2) + target_compile_options(${COMPONENT_TARGET} PRIVATE "$<$:-DMULTI_HEAP_FREERTOS>") +else() + if(NOT CMAKE_BUILD_EARLY_EXPANSION) + idf_build_get_property(build_components BUILD_COMPONENTS) + if(freertos IN_LIST build_components) + target_compile_options(${COMPONENT_TARGET} PRIVATE "-DMULTI_HEAP_FREERTOS") + endif() endif() endif() diff --git a/components/mbedtls/CMakeLists.txt b/components/mbedtls/CMakeLists.txt index 559a79431d..c9c2a9d322 100644 --- a/components/mbedtls/CMakeLists.txt +++ b/components/mbedtls/CMakeLists.txt @@ -192,9 +192,14 @@ endif() # net_sockets.c should only be compiled if BSD socket functions are available. # Do this by checking if lwip component is included into the build. if(CONFIG_LWIP_ENABLE) - list(APPEND mbedtls_target_sources "${COMPONENT_DIR}/port/net_sockets.c") - idf_component_get_property(lwip_lib lwip COMPONENT_LIB) - target_link_libraries(${COMPONENT_LIB} ${linkage_type} ${lwip_lib}) + if(IDF_BUILD_V2) + target_sources(${COMPONENT_LIB} PRIVATE "$<$:${COMPONENT_DIR}/port/net_sockets.c>") + target_link_libraries(${COMPONENT_LIB} ${linkage_type} "$<$:idf::lwip>") + else() + list(APPEND mbedtls_target_sources "${COMPONENT_DIR}/port/net_sockets.c") + idf_component_get_property(lwip_lib lwip COMPONENT_LIB) + target_link_libraries(${COMPONENT_LIB} ${linkage_type} ${lwip_lib}) + endif() endif() # Add port files to mbedtls targets diff --git a/components/xtensa/project_include.cmake b/components/xtensa/project_include.cmake index f0c5bee866..3ed44c2f59 100644 --- a/components/xtensa/project_include.cmake +++ b/components/xtensa/project_include.cmake @@ -1,3 +1,8 @@ +idf_build_get_property(idf_target_arch IDF_TARGET_ARCH) +if(NOT "${idf_target_arch}" STREQUAL "xtensa") + return() +endif() + # Check toolchain is configured properly in cmake if(CMAKE_C_COMPILER_ID MATCHES "Clang") # without '--target' option 'clang -dumpmachine' prints default target arch and it might be not Xtensa diff --git a/docs/conf_common.py b/docs/conf_common.py index 427f703223..b6feb4a241 100644 --- a/docs/conf_common.py +++ b/docs/conf_common.py @@ -8,10 +8,13 @@ # pylint: disable=undefined-variable import os.path import re +import sys from pathlib import Path from esp_docs.conf_docs import * # noqa: F403,F401 +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'tools', 'cmakev2'))) + if os.environ.get('IDF_PATH') is None: raise RuntimeError('IDF_PATH should be set, run export.sh before building docs') @@ -416,6 +419,7 @@ extensions += [ # noqa: F405 'esp_docs.esp_extensions.run_doxygen', 'esp_docs.esp_extensions.add_html_zip', 'linuxdoc.rstFlatTable', # https://return42.github.io/linuxdoc/linuxdoc-howto/table-markup.html#flat-table + 'esp_docs_cmakev2_extension', ] # Use wavedrompy as backend, instead of wavedrom-cli diff --git a/docs/en/api-guides/build-system-v2.rst b/docs/en/api-guides/build-system-v2.rst new file mode 100644 index 0000000000..bdb9c998cb --- /dev/null +++ b/docs/en/api-guides/build-system-v2.rst @@ -0,0 +1,470 @@ +Build System v2 +*************** + +.. attention:: + + Build System v2 is currently available as a **Technical Preview** intended for **testing and evaluation**. Features, functionality, and performance are **subject to change without notice**, and **production use is not recommended** at this stage. + +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`. + +Creating a New Project +====================== + +Below is the minimal project's ``CMakeLists.txt`` file, which should suffice for most projects: + +.. code-block:: cmake + + cmake_minimum_required(VERSION 3.22) + include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) + project(myProject C CXX ASM) + idf_project_default() + +The order of commands in the project's ``CMakeLists.txt`` is important. First, the ``cmake_minimum_required`` command must be called to specify the minimum required version of CMake. + +Next, include ``idf.cmake``, which provides the build system functionality. It performs the necessary build system initialization tasks and toolchain configuration, which must be completed before invoking CMake’s ``project()`` command. + +Once the build system is initialized, the ``project()`` command should be executed. It sets up project related variables, initializes the toolchain for the required languages, and performs other CMake specific initialization steps. + +Finally, the :cmakev2:ref:`idf_project_default` function creates the default project executable based on the ``main`` component and its transitive dependencies. It also generates binary images and adds related targets such as ``flash`` and ``menuconfig``. + +Creating a New Component +======================== + +This section explains how to create a new component. The preferred and generally sufficient method is described in :ref:`cmakev2-creating-compatible-component`. However, if you need more control or prefer a native CMake approach without concern for v1 compatibility, see :ref:`cmakev2-creating-v2-component`. + +.. important:: + + Always ensure that all variables used within the component's ``CMakeLists.txt`` are initialized before use. The component might be evaluated in the context of other component where some variables may already be set. A common example is appending to CMake's list without explicitly initializing the list variable first. + +.. _cmakev2-creating-compatible-component: + +Creating a Component Compatible with v1 and v2 +---------------------------------------------- + +The recommended method for creating a new component is to use the :cmakev2:ref:`idf_component_register` function. This function is provided in v2 to maintain backward compatibility with v1 and remains the preferred approach for creating components that work with both versions. The only difference between the v1 and v2 versions of this function is that the v2 version does not support the ``KCONFIG`` and ``KCONFIG_PROJBUILD`` options when using Kconfig filenames that do not follow the :ref:`cmakev2-standardized-kconfigs`. For components that require functionality beyond what :cmakev2:ref:`idf_component_register` provides, please refer to :ref:`cmakev2-breaking-changes`. + +.. code-block:: cmake + :caption: Minimal Component CMakeLists.txt + + idf_component_register(SRCS "foo.c" "bar.c" + INCLUDE_DIRS "include" + REQUIRES mbedtls) + +.. _cmakev2-creating-v2-component: + +Creating a Component for v2 Only +-------------------------------- + +.. important:: + + This section describes how to create a component for v2, which is not compatible or usable with v1. To create a component that works with both versions, it is recommended to follow :ref:`cmakev2-creating-compatible-component`. + +This section demonstrates how to create a new component using a simple component named ``esp_target_info`` as an example. This component provides the ``print_esp_target_info`` function, which, when invoked, displays basic information about the target. + +The layout of the component's directory is organized as follows: + +.. code-block:: bash + + esp_target_info + ├── include + │   └── esp_target_info.h + ├── srcs + │   └── esp_target_info.c + ├── CMakeLists.txt + ├── esp_target_info.ld + └── linker.lf + +The ``esp_target_info.h`` header file provides the ``print_esp_target_info`` function declaration as its sole public interface, intended for use by other components. + +.. code-block:: c + :caption: include/esp_target_info.h + + #ifndef _ESP_TARGET_INFO_ + #define _ESP_TARGET_INFO_ + void print_esp_target_info(void); + #endif + +The ``esp_target_info.c`` file contains the definition for the ``print_esp_target_info`` function. This function outputs the target name based on the configuration ``CONFIG_IDF_TARGET`` variable, the number of CPU cores as obtained from the ``esp_hw_support`` component, the available free heap size from the ``esp_system`` component, and the flash size from the ``spi_flash`` component. + +.. code-block:: c + :caption: srcs/esp_target_info.c + + #include + #include + #include "esp_chip_info.h" + #include "esp_flash.h" + #include "esp_system.h" + + void esp_target_chip_info(esp_chip_info_t*); + + void print_esp_target_info(void) + { + esp_chip_info_t chip_info; + uint32_t flash_size = 0; + + esp_target_chip_info(&chip_info); + esp_flash_get_size(NULL, &flash_size); + + printf("target: %s\n", CONFIG_IDF_TARGET); + printf("cpu cores: %d\n", chip_info.cores); + printf("free heap size: %" PRIu32 "\n", esp_get_minimum_free_heap_size()); + printf("flash size: %" PRIu32 "B\n", flash_size); + } + +The ``esp_target_info.ld`` linker script defines an additional symbol, ``esp_target_chip_info``, which serves as an alias for the ``esp_chip_info`` function. This alias is used in ``esp_target_info.c`` to obtain chip information instead of directly calling the ``esp_chip_info`` function. This serves no real purpose and is merely an example of a working linker script used in the demonstration. + +.. code-block:: bash + :caption: esp_target_info.ld + + esp_target_chip_info = esp_chip_info; + +The ``linker.lf`` is a linker fragment that determines the placement of ``print_esp_target_info`` in IRAM instead of flash. This is merely an example for demonstration purposes. For more details on linker fragments, please refer to :doc:`/api-guides/linker-script-generation`. + +.. code-block:: bash + :caption: linker.lf + + [mapping:esp_target_info] + archive: libesp_target_info.a + entries: + print_esp_target_info (noflash) + +Now, let's describe how the aforementioned sources can be integrated as a component for the build system by defining the component's ``CMakeLists.txt`` file. + +Create a static library target with the target name stored in the ``COMPONENT_TARGET`` variable for the ``srcs/esp_target_info.c`` source file, and add the ``include`` directory as the component's directory with public header files. + +.. code-block:: cmake + :caption: CMakeLists.txt + :linenos: + :emphasize-lines: 1-7 + + add_library(${COMPONENT_TARGET} STATIC + "srcs/esp_target_info.c" + ) + + target_include_directories(${COMPONENT_TARGET} PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/include + ) + + idf_component_include(esp_hw_support) + idf_component_include(spi_flash) + idf_component_include(esp_system) + + target_link_libraries(${COMPONENT_TARGET} PRIVATE + idf::esp_hw_support + idf::spi_flash + idf::esp_system + ) + + idf_component_set_property(${COMPONENT_TARGET} WHOLE_ARCHIVE TRUE) + idf_component_set_property(${COMPONENT_TARGET} LDFRAGMENTS linker.lf APPEND) + idf_component_set_property(${COMPONENT_TARGET} LINKER_SCRIPTS esp_target_info.ld APPEND) + + target_link_options(${COMPONENT_TARGET} INTERFACE "SHELL:-u esp_chip_info") + + +When a component is evaluated by the build system, the ``COMPONENT_TARGET`` variable is assigned a target name that the component is responsible for creating. This target represents the component within the build and is linked to the component interface target managed by the build system. The component interface target is what other components use to declare dependencies on this component. Other components can obtain a component’s interface target using the :cmakev2:ref:`idf_component_include` function and link to it as needed. + +Include all required components and link them to the component's target. + +.. code-block:: cmake + :caption: CMakeLists.txt + :linenos: + :emphasize-lines: 9-17 + + add_library(${COMPONENT_TARGET} STATIC + "srcs/esp_target_info.c" + ) + + target_include_directories(${COMPONENT_TARGET} PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/include + ) + + idf_component_include(esp_hw_support) + idf_component_include(spi_flash) + idf_component_include(esp_system) + + target_link_libraries(${COMPONENT_TARGET} PRIVATE + idf::esp_hw_support + idf::spi_flash + idf::esp_system + ) + + idf_component_set_property(${COMPONENT_TARGET} WHOLE_ARCHIVE TRUE) + idf_component_set_property(${COMPONENT_TARGET} LDFRAGMENTS linker.lf APPEND) + idf_component_set_property(${COMPONENT_TARGET} LINKER_SCRIPTS esp_target_info.ld APPEND) + + target_link_options(${COMPONENT_TARGET} INTERFACE "SHELL:-u esp_chip_info") + +.. note:: + + The v1 automatically adds component dependencies in certain situations. For instance, if the ``main`` component does not specify its dependencies, all components identified by the build system are added as dependencies to the ``main`` component. Additionally, component dependencies specified in ``idf_component.yaml`` for the component manager are also added as dependencies to the component. In contrast, the v2 does not automatically add any dependencies, all component dependencies must be explicitly stated. + +The ``esp_target_info`` component uses functionality from other components, namely ``esp_hw_support``, ``esp_system``, and ``spi_flash``, to collect basic information about the target. Therefore, it must declare its dependencies on these components. The component acquires the required component interface targets using the :cmakev2:ref:`idf_component_include` function. + +Each component is evaluated exactly once by the :cmakev2:ref:`idf_component_include` function. This function essentially invokes CMake’s ``add_subdirectory`` for the specified component directory and links the ``COMPONENT_TARGET`` created by that component with its interface target. + +The interface target can then be used by other components to declare dependencies. It can be referenced directly via a predefined alias named ``idf::``, which provides a convenient and literal way to reference a component’s interface target, as shown in the example above. + +The :cmakev2:ref:`idf_component_include` function also supports an ``INTERFACE`` option that allows storing the component’s interface target name in a variable. This approach is useful in more complex cases where the target needs to be passed to other CMake functions, stored in lists, or referenced programmatically. + +.. code-block:: cmake + :caption: Declaring component dependencies using interface target variable + + idf_component_include(spi_flash INTERFACE spi_flash_iface) + + target_link_libraries(${COMPONENT_TARGET} PRIVATE + ${spi_flash_iface} + ) + +Component dependencies can be conditionally expressed based on the configuration. When a component's ``CMakeLists.txt`` is evaluated by the build system, the configuration for all discovered components is available. If a component needs to declare a conditional dependency based on a configuration variable, it must ensure that the component is included in the build system first before using it by including the component with the :cmakev2:ref:`idf_component_include` function. For example, if a component has a conditional dependency on the ``vfs`` component based on the ``CONFIG_VFS_SUPPORT_IO`` configuration option being set, it needs to include the ``vfs`` component before linking it to the component target. + +.. code-block:: cmake + :caption: Declaring optional component dependency + + if(CONFIG_VFS_SUPPORT_IO) + idf_component_include(vfs) + target_link_libraries(${COMPONENT_TARGET} PRIVATE + idf::vfs + ) + endif() + +.. important:: + + Always ensure that the required component is included before using it. The presence of certain component configuration options does not guarantee that the component will be included in the project build. + +Set miscellaneous component properties. This primarily serves as an example to demonstrate other commonly used features that the component may configure. + +.. code-block:: cmake + :caption: CMakeLists.txt + :linenos: + :emphasize-lines: 19-23 + + add_library(${COMPONENT_TARGET} STATIC + "srcs/esp_target_info.c" + ) + + target_include_directories(${COMPONENT_TARGET} PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/include + ) + + idf_component_include(esp_hw_support) + idf_component_include(spi_flash) + idf_component_include(esp_system) + + target_link_libraries(${COMPONENT_TARGET} PRIVATE + idf::esp_hw_support + idf::spi_flash + idf::esp_system + ) + + idf_component_set_property(${COMPONENT_TARGET} WHOLE_ARCHIVE TRUE) + idf_component_set_property(${COMPONENT_TARGET} LDFRAGMENTS linker.lf APPEND) + idf_component_set_property(${COMPONENT_TARGET} LINKER_SCRIPTS esp_target_info.ld APPEND) + + target_link_options(${COMPONENT_TARGET} INTERFACE "SHELL:-u esp_chip_info") + +WHOLE_ARCHIVE: + Include all object files from the component's static library when linked. + +LDFRAGMENTS: + Include files from this property to be processed with the :doc:`/api-guides/linker-script-generation`. + +LINKER_SCRIPTS: + Include files from this property in the link command line as 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-breaking-changes: + +Breaking Changes for v1 Components +================================== + +While most of the v1 components should be usable without any modifications in v2, there are some differences between v1 and v2 that might require changes to v1 components. This section outlines the breaking changes that may occur if a v1 component is used with v2 and explains how the component can be modified to work with both v1 and v2. + +Updating Component for v2 Compatibility +---------------------------------------------- + +If a component needs to be evaluated with both v1 and v2, and requires modifications to function with v2, the ``IDF_BUILD_V2`` variable can be used by the component to determine whether it is being evaluated under v1 or v2, and adjust its behavior accordingly. The ``IDF_BUILD_V2`` is set when the component is evaluated with v2. This variable can be used to conditionally adjust only small parts of the existing v1 component, or a completely new ``CMakeLists.txt`` can be created for v2 and conditionally included in the v1 component's ``CMakeLists.txt``. For more information on how to write a v2 component, please see `Creating a New Component`_. + +Below is the ``hello_world`` example component with its ``CMakeLists.txt`` adjusted to include and evaluate the separate v2 component ``CMakeLists_v2.txt`` if evaluated under v2. + +.. note:: + + The existing v1 ``hello_world`` component should function without modification in v2. This is just a simple example of how ``IDF_BUILD_V2`` can be used to evaluate different component's ``CMakeLists.txt`` files for v1 and v2. + +Adjusted original v1 hello_world example ``CMakeLists.txt``. + +.. code-block:: cmake + + if(IDF_BUILD_V2) + # Include component CMake code for v2 and return. + include(CMakeLists_v2.txt) + return() + endif() + + # Here follows the original component CMake code for v1. + idf_component_register(SRCS "hello_world_main.c" + PRIV_REQUIRES spi_flash + INCLUDE_DIRS "") + + +The hello_world example ``CMakeLists_v2.txt`` for v2. + +.. code-block:: cmake + + idf_component_include(spi_flash) + + add_library(${COMPONENT_TARGET} STATIC + "hello_world_main.c" + ) + + target_include_directories(${COMPONENT_TARGET} PUBLIC + "${CMAKE_CURRENT_LIST_DIR}" + ) + + target_link_libraries(${COMPONENT_TARGET} PRIVATE + idf::spi_flash + ) + +The ``BUILD_COMPONENTS`` Build Property is Unavailable +------------------------------------------------------ + +The v1, by design, collects components built by the project in the ``BUILD_COMPONENTS`` build property. This is achieved by evaluating components early using the CMake script mode and collecting components based on their dependencies as provided in :cmakev2:ref:`idf_component_register`. Later, the ``BUILD_COMPONENTS`` are evaluated again using CMake's ``add_subdirectory``. This approach imposes several restrictions. First, components cannot express their dependencies based on Kconfig variables because Kconfig variables are not known during the early evaluation. Second, the CMake script mode does not allow CMake commands that define build targets or actions, which caused confusion about which commands can be used in a component and when they are actually evaluated. The v2 removes this two-stage component evaluation, and hence the ``BUILD_COMPONENTS`` build property doesn't exist in v2. Any attempt to obtain the ``BUILD_COMPONENTS`` build property with :cmakev2:ref:`idf_build_get_property` will result in an error. The ``BUILD_COMPONENTS`` build property was primarily used in v1 by components to discover which other components were being built by the project and to adjust their behavior, for example, by adding additional source files. In v2, the functionality of the ``BUILD_COMPONENTS`` build property can be replaced with the ``$`` generator expression. + +For example, the ``esp_eth`` component is compiled with the additional ``esp_eth_netif_glue.c`` file if the ``esp_netif`` component is also being built in v1 with: + +.. code-block:: cmake + + idf_build_get_property(components_to_build BUILD_COMPONENTS) + if(esp_netif IN_LIST components_to_build) + list(APPEND srcs "src/esp_eth_netif_glue.c") + endif() + +To use the ``esp_eth`` component with v2, the usage of ``BUILD_COMPONENTS`` has to be replaced with: + +.. code-block:: cmake + + if(IDF_BUILD_V2) + target_sources(${COMPONENT_TARGET} PRIVATE "$<$:src/esp_eth_netif_glue.c>") + else() + idf_build_get_property(components_to_build BUILD_COMPONENTS) + if(esp_netif IN_LIST components_to_build) + list(APPEND srcs "src/esp_eth_netif_glue.c") + endif() + endif() + +.. note:: + + The ``$`` generator expression should work for both v1 and v2. The ``IDF_BUILD_V2`` is used to demonstrate the different approaches in v1 and v2. + +.. _cmakev2-standardized-kconfigs: + +Standardized ``Kconfig`` and ``Kconfig.projbuild`` File Names +------------------------------------------------------------- + +The v1 allows the use of ``Kconfig`` and ``Kconfig.projbuild`` with custom file names in :cmakev2:ref:`idf_component_register`. This is possible because, in v1, the components participating in the project build are collected during early component evaluation, allowing for the specification of custom names for the Kconfig files. The v2 does not perform early evaluation, and the Kconfig files are collected based on the fixed ``Kconfig`` and ``Kconfig.projbuild`` file names, which must be present in the component's root directory. This can be resolved by renaming the Kconfig files that do not conform to this naming convention or by including them in Kconfig files that comply with this convention. + + +Component Configuration Visibility +---------------------------------- + +In v1, the ``sdkconfig`` is generated based on Kconfig files provided by components that are part of the project build. This means that if a component is not included in the project build, for instance, if it is not required as a dependency of another component, its configuration will not be visible or available when components are evaluated. In v2, the ``sdkconfig`` is generated based on Kconfig files for all available components. This means the presence of some configuration options does not guarantee that the component providing this configuration option is participating in the build, unlike in v1. + +For example, in v1, the following condition in the ``esp_vfs_console`` component links the ``vfs`` component to the ``esp_vfs_console`` component based on the ``CONFIG_VFS_SUPPORT_IO`` configuration option being set. This works in v1 because the ``CONFIG_VFS_SUPPORT_IO`` configuration option is not available if the ``vfs`` component is not participating in the project build. + +.. code-block:: cmake + + if(CONFIG_VFS_SUPPORT_IO) + target_link_libraries(${COMPONENT_LIB} PUBLIC idf::vfs) + # Make sure esp_vfs_console_register gets called at startup stage + target_link_libraries(${COMPONENT_LIB} INTERFACE "-u esp_vfs_include_console_register") + endif() + +In contrast, in v2, configuration for all discovered components is available. This means that even if a component does not participate in the project build, its configuration will still be accessible. If the ``CONFIG_VFS_SUPPORT_IO`` configuration option is set by default, the condition will evaluate as true in v2, even if the ``vfs`` component is not participating in the project build and the ``idf::vfs`` target does not exist. The ``$`` generator expression can be used to express such dependencies in v2. + +.. code-block:: cmake + + if(IDF_BUILD_V2) + target_link_libraries(${COMPONENT_LIB} PUBLIC "$<$:idf::vfs>") + # Make sure esp_vfs_console_register gets called at startup stage + target_link_libraries(${COMPONENT_LIB} INTERFACE "$<$:-u esp_vfs_include_console_register>") + else() + if(CONFIG_VFS_SUPPORT_IO) + target_link_libraries(${COMPONENT_LIB} PUBLIC idf::vfs) + # Make sure esp_vfs_console_register gets called at startup stage + target_link_libraries(${COMPONENT_LIB} INTERFACE "-u esp_vfs_include_console_register") + endif() + endif() + +.. note:: + + The ``$`` generator expression should work for both v1 and v2. The ``IDF_BUILD_V2`` is used to demonstrate the different approaches in v1 and v2. + +.. important:: + + This change applies not only to a component's ``CMakeLists.txt`` but also to its source files. The component's code can no longer assume that the functionality of other components is available simply because their Kconfig variables are set. For example, if the ``CONFIG_VFS_SUPPORT_IO`` configuration variable is set and the component's code depends on the functionality of the ``vfs`` component, it cannot merely check ``CONFIG_VFS_SUPPORT_IO`` in the source code. It must ensure that the ``vfs`` component is included in the project build and that the component has a declared dependency on the ``vfs`` component in its ``CMakeLists.txt``. + +Recursive Evaluation of Components +---------------------------------- + +In v1, the components participating in the project build are collected in the ``BUILD_COMPONENTS`` build property during the early evaluation phase, prior to their evaluation with ``add_subdirectory``. This means that v1 is aware of all components participating in the project build and their dependencies before CMake evaluates them with the ``add_subdirectory`` call. This allows v1 to evaluate components in a non-recursive manner and within a relatively predictable environment with minimal variables set. In contrast, v2 does not perform early component evaluation and components are added based on the requirements observed during their evaluation. This means a component can be evaluated recursively within the scope of another component's variables if it is a dependency of that component. In other words, if component A requires component B, then when B is evaluated, it can access the variables of A. Hence, it's important to ensure that all variables used by the component are properly initialized before usage. A typical problem involves CMake lists and the ``APPEND`` operation. + +.. code-block:: cmake + + # Wrong + list(APPEND srcs main.c) + + # Correct + set(srcs) + list(APPEND srcs main.c) + +The ``project_include.cmake`` Files are Included in a Non-Specific Order +------------------------------------------------------------------------ + +In v1, the ``project_include.cmake`` files from components are included in the order specified by the ``BUILD_COMPONENTS`` build property. The components in ``BUILD_COMPONENTS`` are sorted based on their requirements, as determined during early evaluation. This means a given component's ``project_include.cmake`` is included only after all ``project_include.cmake`` files for its dependencies have been included. This holds true as long as there is no cyclic dependency between components. In contrast, v2 does not perform early evaluation of components, and ``BUILD_COMPONENTS`` does not exist. Therefore, in v2, the ``project_include.cmake`` files are included for all discovered components, not just those participating in the project build, and they are included in the order in which the components are discovered by v2. As a result, cross-file functionality at the global scope between ``project_include.cmake`` files can no longer be relied upon in v2. + +.. note:: + + It is still possible to call functions or macros defined in another ``project_include.cmake`` file, provided they are invoked within a CMake function or another non-global scope. Only global-scope interactions are unreliable in v2. + +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. + +API Reference +============= + +This section contains an automatically generated API reference from the build +system v2 CMake source code. + +.. _cmakev2_variables: + +Variables +--------- + + +.. _cmakev2_functions: + +Functions +--------- + + +.. _cmakev2_macros: + +Macros +------ + + +.. cmakev2:include:: ../../../tools/cmakev2/build.cmake +.. cmakev2:include:: ../../../tools/cmakev2/compat.cmake +.. cmakev2:include:: ../../../tools/cmakev2/component.cmake +.. cmakev2:include:: ../../../tools/cmakev2/idf.cmake +.. cmakev2:include:: ../../../tools/cmakev2/kconfig.cmake +.. cmakev2:include:: ../../../tools/cmakev2/ldgen.cmake +.. cmakev2:include:: ../../../tools/cmakev2/manager.cmake +.. cmakev2:include:: ../../../tools/cmakev2/project.cmake +.. cmakev2:include:: ../../../tools/cmakev2/utilities.cmake + + +.. _Local Directory Dependencies: https://docs.espressif.com/projects/idf-component-manager/en/latest/reference/manifest_file.html#local-directory-dependencies diff --git a/docs/en/api-guides/index.rst b/docs/en/api-guides/index.rst index 6e1a2373fb..244cd14ab4 100644 --- a/docs/en/api-guides/index.rst +++ b/docs/en/api-guides/index.rst @@ -13,6 +13,7 @@ API Guides :SOC_BLE_MESH_SUPPORTED: esp-ble-mesh/ble-mesh-index bootloader build-system + build-system-v2 :SOC_SUPPORT_COEXISTENCE: coexist c cplusplus diff --git a/docs/zh_CN/api-guides/build-system-v2.rst b/docs/zh_CN/api-guides/build-system-v2.rst new file mode 100644 index 0000000000..82194a932c --- /dev/null +++ b/docs/zh_CN/api-guides/build-system-v2.rst @@ -0,0 +1 @@ +.. include:: ../../en/api-guides/build-system-v2.rst diff --git a/docs/zh_CN/api-guides/index.rst b/docs/zh_CN/api-guides/index.rst index 9bf06a3401..3a1f1d22ae 100644 --- a/docs/zh_CN/api-guides/index.rst +++ b/docs/zh_CN/api-guides/index.rst @@ -13,6 +13,7 @@ API 指南 :SOC_BLE_MESH_SUPPORTED: esp-ble-mesh/ble-mesh-index bootloader build-system + build-system-v2 :SOC_SUPPORT_COEXISTENCE: coexist c cplusplus diff --git a/tools/cmake/deduplicate_flags.cmake b/tools/cmake/deduplicate_flags.cmake new file mode 100644 index 0000000000..7398f2c743 --- /dev/null +++ b/tools/cmake/deduplicate_flags.cmake @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +# Remove duplicates from a string containing compilation flags +function(remove_duplicated_flags FLAGS UNIQFLAGS) + set(FLAGS_LIST "${FLAGS}") + # Convert the given flags, as a string, into a CMake list type + separate_arguments(FLAGS_LIST) + # Remove all the duplicated flags + list(REMOVE_DUPLICATES FLAGS_LIST) + # Convert the list back to a string + string(REPLACE ";" " " FLAGS_LIST "${FLAGS_LIST}") + # Return that string to the caller + set(${UNIQFLAGS} "${FLAGS_LIST}" PARENT_SCOPE) +endfunction() diff --git a/tools/cmake/toolchain-clang-esp32.cmake b/tools/cmake/toolchain-clang-esp32.cmake index 8710e635e2..698b7e0904 100644 --- a/tools/cmake/toolchain-clang-esp32.cmake +++ b/tools/cmake/toolchain-clang-esp32.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32c2.cmake b/tools/cmake/toolchain-clang-esp32c2.cmake index a2ed6b008a..3b6755d05b 100644 --- a/tools/cmake/toolchain-clang-esp32c2.cmake +++ b/tools/cmake/toolchain-clang-esp32c2.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32c3.cmake b/tools/cmake/toolchain-clang-esp32c3.cmake index a2ed6b008a..3b6755d05b 100644 --- a/tools/cmake/toolchain-clang-esp32c3.cmake +++ b/tools/cmake/toolchain-clang-esp32c3.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32c5.cmake b/tools/cmake/toolchain-clang-esp32c5.cmake index 41f9f6426d..a5138edf6f 100644 --- a/tools/cmake/toolchain-clang-esp32c5.cmake +++ b/tools/cmake/toolchain-clang-esp32c5.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32c6.cmake b/tools/cmake/toolchain-clang-esp32c6.cmake index 41f9f6426d..a5138edf6f 100644 --- a/tools/cmake/toolchain-clang-esp32c6.cmake +++ b/tools/cmake/toolchain-clang-esp32c6.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32c61.cmake b/tools/cmake/toolchain-clang-esp32c61.cmake index 41f9f6426d..a5138edf6f 100644 --- a/tools/cmake/toolchain-clang-esp32c61.cmake +++ b/tools/cmake/toolchain-clang-esp32c61.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32h2.cmake b/tools/cmake/toolchain-clang-esp32h2.cmake index a2ed6b008a..3b6755d05b 100644 --- a/tools/cmake/toolchain-clang-esp32h2.cmake +++ b/tools/cmake/toolchain-clang-esp32h2.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32h4.cmake b/tools/cmake/toolchain-clang-esp32h4.cmake index 21355e8215..5a5a707e97 100644 --- a/tools/cmake/toolchain-clang-esp32h4.cmake +++ b/tools/cmake/toolchain-clang-esp32h4.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32p4.cmake b/tools/cmake/toolchain-clang-esp32p4.cmake index 59e328d70b..c88781ae19 100644 --- a/tools/cmake/toolchain-clang-esp32p4.cmake +++ b/tools/cmake/toolchain-clang-esp32p4.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32s2.cmake b/tools/cmake/toolchain-clang-esp32s2.cmake index 50c14bd72f..2a92a1657e 100644 --- a/tools/cmake/toolchain-clang-esp32s2.cmake +++ b/tools/cmake/toolchain-clang-esp32s2.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-clang-esp32s3.cmake b/tools/cmake/toolchain-clang-esp32s3.cmake index 289bbbc717..2941e7c6a9 100644 --- a/tools/cmake/toolchain-clang-esp32s3.cmake +++ b/tools/cmake/toolchain-clang-esp32s3.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32.cmake b/tools/cmake/toolchain-esp32.cmake index 7c6759c862..afec940e25 100644 --- a/tools/cmake/toolchain-esp32.cmake +++ b/tools/cmake/toolchain-esp32.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32c2.cmake b/tools/cmake/toolchain-esp32c2.cmake index 87a50ea656..4f21adc72e 100644 --- a/tools/cmake/toolchain-esp32c2.cmake +++ b/tools/cmake/toolchain-esp32c2.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32c3.cmake b/tools/cmake/toolchain-esp32c3.cmake index 87a50ea656..4f21adc72e 100644 --- a/tools/cmake/toolchain-esp32c3.cmake +++ b/tools/cmake/toolchain-esp32c3.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32c5.cmake b/tools/cmake/toolchain-esp32c5.cmake index 597f7f0df9..e84b261e68 100644 --- a/tools/cmake/toolchain-esp32c5.cmake +++ b/tools/cmake/toolchain-esp32c5.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32c6.cmake b/tools/cmake/toolchain-esp32c6.cmake index 7ca12da3b4..ccfa5a60ed 100644 --- a/tools/cmake/toolchain-esp32c6.cmake +++ b/tools/cmake/toolchain-esp32c6.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32c61.cmake b/tools/cmake/toolchain-esp32c61.cmake index 597f7f0df9..e84b261e68 100644 --- a/tools/cmake/toolchain-esp32c61.cmake +++ b/tools/cmake/toolchain-esp32c61.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32h2.cmake b/tools/cmake/toolchain-esp32h2.cmake index 7ca12da3b4..ccfa5a60ed 100644 --- a/tools/cmake/toolchain-esp32h2.cmake +++ b/tools/cmake/toolchain-esp32h2.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32h21.cmake b/tools/cmake/toolchain-esp32h21.cmake index 7ca12da3b4..ccfa5a60ed 100644 --- a/tools/cmake/toolchain-esp32h21.cmake +++ b/tools/cmake/toolchain-esp32h21.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32h4.cmake b/tools/cmake/toolchain-esp32h4.cmake index 10cfa96f58..c4937c4ab5 100644 --- a/tools/cmake/toolchain-esp32h4.cmake +++ b/tools/cmake/toolchain-esp32h4.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32p4.cmake b/tools/cmake/toolchain-esp32p4.cmake index af66eeb506..33751e1af4 100644 --- a/tools/cmake/toolchain-esp32p4.cmake +++ b/tools/cmake/toolchain-esp32p4.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32s2.cmake b/tools/cmake/toolchain-esp32s2.cmake index 2e5fce33dd..b0763bde62 100644 --- a/tools/cmake/toolchain-esp32s2.cmake +++ b/tools/cmake/toolchain-esp32s2.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/toolchain-esp32s3.cmake b/tools/cmake/toolchain-esp32s3.cmake index 0210ecf032..50dd2d684a 100644 --- a/tools/cmake/toolchain-esp32s3.cmake +++ b/tools/cmake/toolchain-esp32s3.cmake @@ -1,4 +1,4 @@ -include($ENV{IDF_PATH}/tools/cmake/utilities.cmake) +include($ENV{IDF_PATH}/tools/cmake/deduplicate_flags.cmake) set(CMAKE_SYSTEM_NAME Generic) diff --git a/tools/cmake/utilities.cmake b/tools/cmake/utilities.cmake index dd244171ec..ed0e9002d9 100644 --- a/tools/cmake/utilities.cmake +++ b/tools/cmake/utilities.cmake @@ -1,3 +1,5 @@ +include(${CMAKE_CURRENT_LIST_DIR}/deduplicate_flags.cmake) + # set_default # # Define a variable to a default value if otherwise unset. @@ -457,17 +459,3 @@ function(add_deprecated_target_alias old_target new_target) ) add_dependencies(${old_target} ${new_target}) endfunction() - - -# Remove duplicates from a string containing compilation flags -function(remove_duplicated_flags FLAGS UNIQFLAGS) - set(FLAGS_LIST "${FLAGS}") - # Convert the given flags, as a string, into a CMake list type - separate_arguments(FLAGS_LIST) - # Remove all the duplicated flags - list(REMOVE_DUPLICATES FLAGS_LIST) - # Convert the list back to a string - string(REPLACE ";" " " FLAGS_LIST "${FLAGS_LIST}") - # Return that string to the caller - set(${UNIQFLAGS} "${FLAGS_LIST}" PARENT_SCOPE) -endfunction() diff --git a/tools/cmakev2/build.cmake b/tools/cmakev2/build.cmake new file mode 100644 index 0000000000..b7711a0f2f --- /dev/null +++ b/tools/cmakev2/build.cmake @@ -0,0 +1,1107 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +include_guard(GLOBAL) + +include(utilities) +include(CheckCCompilerFlag) +include(CheckCXXCompilerFlag) + +#[[api +.. cmakev2:function:: idf_build_set_property + + .. code-block:: cmake + + idf_build_set_property( [APPEND]) + + *property[in]* + + Property name. + + *value[in]* + + Property value. + + *APPEND* + + Append the value to the property's current value instead of replacing + it. + + Set the value of the specified property related to the ESP-IDF build. The + property is also added to the internal list of build properties if it isn't + already there. +#]] +function(idf_build_set_property property value) + set(options APPEND) + set(one_value) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + set(append) + if(ARG_APPEND) + set(append APPEND) + endif() + + __set_property(TARGET idf_build_properties + PROPERTY "${property}" + PROPERTIES BUILD_PROPERTIES + VALUE "${value}" + ${append}) +endfunction() + +#[[api +.. cmakev2:function:: idf_build_get_property + + .. code-block:: cmake + + idf_build_get_property( [GENERATOR_EXPRESSION]) + + *variable[out]* + + Variable to store the value in. + + *property[in]* + + Property name to get the value of. + + *GENERATOR_EXPRESSION[in]* + + Obtain the generator expression for the property rather than the actual + value. + + Get the value of the specified property related to the ESP-IDF build. +#]] +function(idf_build_get_property variable property) + set(options GENERATOR_EXPRESSION) + set(one_value) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if("${property}" STREQUAL BUILD_COMPONENTS) + idf_die("Build property 'BUILD_COMPONENTS' is not supported") + endif() + + set(genexpr) + if(ARG_GENERATOR_EXPRESSION) + set(genexpr GENERATOR_EXPRESSION) + endif() + + __get_property(TARGET idf_build_properties + PROPERTY "${property}" + OUTPUT value + ${genexpr}) + set(${variable} ${value} PARENT_SCOPE) +endfunction() + +#[[ + __dump_build_properties() + + Dump all build properties. +#]] +function(__dump_build_properties) + idf_build_get_property(properties BUILD_PROPERTIES) + idf_msg("build properties: ${properties}") + foreach(property IN LISTS properties) + idf_build_get_property(value ${property}) + idf_msg(" ${property}: ${value}") + endforeach() +endfunction() + +#[[ + __get_library_interface_or_die(LIBRARY + OUTPUT ) + + *LIBRARY[in]* + + Library interface or alias. + + *OUTPUT[out]* + + Output variable to store the library interface. + + Verify that "LIBRARY" is a known interface created by ``idf_build_library`` + or its alias. If it is, return the library interface; otherwise, terminate + the build process. +#]] +function(__get_library_interface_or_die) + set(options) + set(one_value LIBRARY OUTPUT) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_LIBRARY) + idf_die("LIBRARY option is required") + endif() + + if(NOT DEFINED ARG_OUTPUT) + idf_die("OUTPUT option is required") + endif() + + __get_real_target(TARGET ${ARG_LIBRARY} OUTPUT library_interface) + idf_build_get_property(library_interfaces LIBRARY_INTERFACES) + + if(NOT "${library_interface}" IN_LIST library_interfaces) + idf_die("Library interface '${ARG_LIBRARY}' does not exist") + endif() + set(${ARG_OUTPUT} ${library_interface} PARENT_SCOPE) +endfunction() + +#[[ +.. cmakev2:function:: idf_library_set_property + + .. code-block:: cmake + + idf_library_set_property( [APPEND]) + + *library[in]* + + Library interface target or alias. + + *property[in]* + + Property name. + + *value[in]* + + Property value. + + *APPEND* + + Append the value to the property's current value instead of replacing + it. + + Set the value of the specified library property. The property is also added + to the internal list of library properties if it isn't already there. +#]] +function(idf_library_set_property library property value) + set(options APPEND) + set(one_value) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + set(append) + if(ARG_APPEND) + set(append APPEND) + endif() + + __get_library_interface_or_die(LIBRARY "${library}" OUTPUT library_interface) + __set_property(TARGET "${library_interface}" + PROPERTY "${property}" + PROPERTIES LIBRARY_PROPERTIES + VALUE "${value}" + ${append}) +endfunction() + +#[[ +.. cmakev2:function:: idf_library_get_property + + .. code-block:: cmake + + idf_library_get_property( [GENERATOR_EXPRESSION]) + + *variable[out]* + + Variable to store the value in. + + *library[in]* + + Library interface target or alias. + + *property[in]* + + Property name to get the value of. + + *GENERATOR_EXPRESSION* + + Obtain the generator expression for the property rather than the actual + value. + + Retrieve the value of the specified library property. +#]] +function(idf_library_get_property variable library property) + set(options GENERATOR_EXPRESSION) + set(one_value) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + set(genexpr) + if(ARG_GENERATOR_EXPRESSION) + set(genexpr GENERATOR_EXPRESSION) + endif() + + __get_library_interface_or_die(LIBRARY "${library}" OUTPUT library_interface) + __get_property(TARGET "${library_interface}" + PROPERTY "${property}" + OUTPUT value + ${genexpr}) + set(${variable} ${value} PARENT_SCOPE) +endfunction() + +#[[ + __dump_library_properties() + + *libraries* + + List of library interfaces whose properties should be displayed. + + Dump all properties for the libraries listed in ````. +#]] +function(__dump_library_properties libraries) + foreach(library IN LISTS libraries) + idf_library_get_property(properties "${library}" LIBRARY_PROPERTIES) + idf_msg("library '${library}' properties: ${properties}") + foreach(property IN LISTS properties) + idf_library_get_property(value "${library}" "${property}") + idf_msg(" ${property}: ${value}") + endforeach() + endforeach() +endfunction() + +#[[ +.. cmakev2:function:: idf_build_library + + .. code-block:: cmake + + idf_build_library( + [COMPONENTS ...]) + + *library[in]* + + Name of the library interface to be created. + + *COMPONENTS[in,opt]* + + List of component names to add to the library. + + Create a new library interface target with the name specified in the + ``library`` option and link component targets to it based on the component + names provided in the ``COMPONENTS`` option. If ``COMPONENTS`` option is + not set, link component targets of all discovered components. + + List of library properties + + LIBRARY_COMPONENTS + List of component as specified by the ``COMPONENTS`` option. + + LIBRARY_COMPONENTS_LINKED + List of components linked to the library based on recursive evaluations + of the INTERFACE_LINK_LIBRARIES and LINK_LIBRARIES target properties. +#]] +function(idf_build_library library) + set(options) + set(one_value) + set(multi_value COMPONENTS) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + idf_build_get_property(project_initialized __PROJECT_INITIALIZED) + if(NOT project_initialized) + idf_die("The IDF project is not initialized. The 'idf_project_init()' must be called first.") + endif() + + if(NOT DEFINED ARG_COMPONENTS) + # The library should include all discovered components. + idf_build_get_property(component_names COMPONENTS_DISCOVERED) + set(ARG_COMPONENTS "${component_names}") + endif() + + # Create library interface. + add_library("${library}" INTERFACE) + idf_build_set_property(LIBRARY_INTERFACES "${library}" APPEND) + idf_library_set_property("${library}" LIBRARY_COMPONENTS "${ARG_COMPONENTS}") + + # Add global include directories, such as the directory containing + # sdkconfig, to the library's include directories. + idf_build_get_property(include_directories INCLUDE_DIRECTORIES GENERATOR_EXPRESSION) + target_include_directories("${library}" INTERFACE "${include_directories}") + + # Add link options. + idf_build_get_property(link_options LINK_OPTIONS) + target_link_options(${library} INTERFACE "${link_options}") + + # Include the requested components and link their interface targets to the + # library. + foreach(component_name IN LISTS ARG_COMPONENTS) + idf_component_include("${component_name}") + idf_component_get_property(component_interface "${component_name}" COMPONENT_INTERFACE) + target_link_libraries("${library}" INTERFACE "${component_interface}") + endforeach() + + # Get all targets transitively linked to the library interface target. + __get_target_dependencies(TARGET "${library}" OUTPUT dependencies) + + # Identify the components linked to the library by looking at all targets + # that are transitively linked to the library and mapping these targets to + # components. Store the linked component interfaces in + # LIBRARY_COMPONENTS_LINKED property. + set(component_interfaces_linked) + foreach(dep IN LISTS dependencies) + __get_component_interface(COMPONENT "${dep}" OUTPUT component_interface) + if(NOT component_interface) + continue() + endif() + if("${component_interface}" IN_LIST component_interfaces_linked) + continue() + endif() + + list(APPEND component_interfaces_linked "${component_interface}") + idf_component_get_property(component_name "${component_interface}" COMPONENT_NAME) + idf_library_set_property("${library}" LIBRARY_COMPONENTS_LINKED "${component_name}" APPEND) + endforeach() + + # Collect linker fragment files from all components linked to the library + # interface and store them in the __LDGEN_FRAGMENT_FILES files. This + # property is used by ldgen to generate template-based linker scripts. + foreach(component_interface IN LISTS component_interfaces_linked) + idf_component_get_property(component_ldfragments "${component_interface}" LDFRAGMENTS) + idf_component_get_property(component_directory "${component_interface}" COMPONENT_DIR) + __get_absolute_paths(PATHS "${component_ldfragments}" + BASE_DIR "${component_directory}" + OUTPUT ldfragments) + idf_library_set_property("${library}" __LDGEN_FRAGMENT_FILES "${ldfragments}" APPEND) + endforeach() + + # Collect archive files from all targets linked to the library interface + # and store them in the __LDGEN_DEPENDS and __LDGEN_LIBRARIES library + # properties. These properties are used by ldgen to generate linker scripts + # from templates. The __LDGEN_LIBRARIES property contains a list of + # TARGET_FILE generator expressions for archive files. + foreach(dep IN LISTS dependencies) + if(NOT TARGET "${dep}") + continue() + endif() + + get_target_property(type "${dep}" TYPE) + if("${type}" STREQUAL "INTERFACE_LIBRARY") + continue() + endif() + + idf_library_get_property(ldgen_depends "${library}" __LDGEN_DEPENDS) + if(NOT "${dep}" IN_LIST ldgen_depends) + idf_library_set_property("${library}" __LDGEN_LIBRARIES "$" APPEND) + idf_library_set_property("${library}" __LDGEN_DEPENDS ${dep} APPEND) + endif() + endforeach() + + # Create a sanitized library interface name that can be used as a suffix + # for files and targets specific to the library. + string(MAKE_C_IDENTIFIER "_${library}" suffix) + + foreach(component_interface IN LISTS component_interfaces_linked) + set(scripts) + idf_component_get_property(component_dir "${component_interface}" COMPONENT_DIR) + idf_component_get_property(component_build_dir "${component_interface}" COMPONENT_BUILD_DIR) + idf_component_get_property(preprocessed "${component_interface}" __LINKER_SCRIPTS_PREPROCESSED) + + # Add linker scripts that do not require ldgen processing. + idf_component_get_property(scripts_static "${component_interface}" LINKER_SCRIPTS) + __get_absolute_paths(PATHS "${scripts_static}" BASE_DIR "${component_dir}" OUTPUT scripts_static_abs) + foreach(script IN LISTS scripts_static_abs) + if(script MATCHES "\\.in$") + # If the linker script file name ends with the ".in" extension, + # preprocess it with the C preprocessor. + get_filename_component(script_name "${script}" NAME_WLE) + set(script_out "${component_build_dir}/ld/${script_name}") + string(MAKE_C_IDENTIFIER "gen_${script_name}" script_target) + + if(NOT preprocessed) + file(MAKE_DIRECTORY "${component_build_dir}/ld") + __preprocess_linker_script("${script}" "${script_out}") + # Add a custom target for the preprocessed script. + add_custom_target(${script_target} DEPENDS "${script_out}") + endif() + # Add dependency for the library interface to ensure the script is + # generated before linking. + add_dependencies("${library}" ${script_target}) + list(APPEND scripts "${script_out}") + else() + list(APPEND scripts "${script}") + endif() + endforeach() + + # Generate linker scripts from templates. + # LINKER_SCRIPTS_TEMPLATE and LINKER_SCRIPTS_GENERATED are parallel + # lists. The first holds the template linker script path, and the + # second holds the generated linker script path. + idf_component_get_property(template_scripts "${component_interface}" LINKER_SCRIPTS_TEMPLATE) + idf_component_get_property(generated_scripts "${component_interface}" LINKER_SCRIPTS_GENERATED) + + foreach(template script IN ZIP_LISTS template_scripts generated_scripts) + if(template MATCHES "\\.in$") + # If the linker script file name ends with the ".in" extension, + # preprocess it with the C preprocessor. + get_filename_component(template_name "${template}" NAME) + set(template_out "${component_build_dir}/ld/${template_name}") + if(NOT preprocessed) + file(MAKE_DIRECTORY "${component_build_dir}/ld") + __preprocess_linker_script("${template}" "${template_out}") + endif() + set(template "${template_out}") + endif() + set(script "${script}${suffix}") + __ldgen_process_template(LIBRARY "${library}" + TEMPLATE "${template}" + SUFFIX "${suffix}" + OUTPUT "${script}") + list(APPEND scripts "${script}") + # Add a custom target for the generated script and include it as a + # dependency for the library interface to ensure the script is + # generated before linking. + get_filename_component(basename "${script}" NAME) + string(MAKE_C_IDENTIFIER "${basename}" basename) + add_custom_target(gen_${basename} DEPENDS "${script}") + add_dependencies("${library}" gen_${basename}) + endforeach() + + # The static scripts for the given component have already been + # preprocessed and are identical across all libraries. Therefore, there + # is no need to preprocess them multiple times. Set the component + # property to indicate that the linker scripts have already been + # preprocessed. + idf_component_set_property("${component_interface}" __LINKER_SCRIPTS_PREPROCESSED YES) + + # Finally, add all preprocessed and ldgen-generated linker scripts to + # the library interface. + foreach(script IN LISTS scripts) + get_filename_component(script_dir "${script}" DIRECTORY) + get_filename_component(script_name "${script}" NAME) + # Add linker script directory to the linker search path. + target_link_directories("${library}" INTERFACE "${script_dir}") + # Add linker script to link. Regarding the usage of SHELL, see + # https://cmake.org/cmake/help/latest/command/target_link_options.html#option-de-duplication + target_link_options("${library}" INTERFACE "SHELL:-T ${script_name}") + # Add the linker script as a dependency to ensure the executable is + # re-linked if the script changes. + set_property(TARGET "${library}" APPEND PROPERTY INTERFACE_LINK_DEPENDS "${script}") + endforeach() + endforeach() +endfunction() + +#[[ +.. cmakev2:function:: idf_build_executable + + .. code-block:: cmake + + idf_build_executable( + [COMPONENTS ...] + [NAME ] + [SUFFIX ]) + + *executable[in]* + + Name of the executable target to be created. + + *COMPONENTS[in,opt]* + + List of component names to add to the library. + + *NAME[in,opt]* + + Optional preferred generated binary name. If not provided, the + ``executable`` name is used. + + *SUFFIX[in,opt]* + + Optional ``executable`` suffix. + + Create a new executable target using the name specified in the + ``executable`` argument, and link it to the library created with the + component names provided in the ``COMPONENTS`` option. If the + ``COMPONENTS`` option is not set, all discovered components are added to + the library. Optinaly set the executable name and suffix. +#]] +function(idf_build_executable executable) + set(options) + set(one_value NAME SUFFIX) + set(multi_value COMPONENTS) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_NAME) + set(ARG_NAME "${executable}") + endif() + + if(NOT DEFINED ARG_SUFFIX) + set(ARG_SUFFIX ".elf") + endif() + + idf_build_get_property(build_dir BUILD_DIR) + + set(library "library_${executable}") + idf_build_library(${library} COMPONENTS "${ARG_COMPONENTS}") + + set(executable_src ${CMAKE_BINARY_DIR}/executable_${executable}.c) + if(NOT EXISTS "${executable_src}") + file(TOUCH "${executable_src}") + endif() + add_executable(${executable} "${executable_src}") + + if(ARG_NAME) + set_target_properties(${executable} PROPERTIES OUTPUT_NAME ${ARG_NAME}) + endif() + + if(ARG_SUFFIX) + set_target_properties(${executable} PROPERTIES SUFFIX ${ARG_SUFFIX}) + endif() + + target_link_libraries(${executable} PRIVATE ${library}) +endfunction() + +#[[ + __get_components_metadata(COMPONENTS ... + OUTPUT ) + + *COMPONENTS[in]* + + List of components for which to generate metadata. + + *OUTPUT[out]* + + Output variable to store JSON metadata. + + Generate metadata in JSON format from ``COMPONENTS`` and store it in the + ``OUTPUT`` variable. +#]] +function(__get_components_metadata) + set(options) + set(one_value OUTPUT) + set(multi_value COMPONENTS) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_COMPONENTS) + idf_die("COMPONENTS option is required") + endif() + + if(NOT DEFINED ARG_OUTPUT) + idf_die("OUTPUT option is required") + endif() + + set(components_json "") + + foreach(name IN LISTS ARG_COMPONENTS) + idf_component_get_property(target ${name} COMPONENT_REAL_TARGET) + idf_component_get_property(alias ${name} COMPONENT_ALIAS) + idf_component_get_property(prefix ${name} __PREFIX) + idf_component_get_property(dir ${name} COMPONENT_DIR) + idf_component_get_property(type ${name} COMPONENT_TYPE) + idf_component_get_property(lib ${name} COMPONENT_LIB) + idf_component_get_property(reqs ${name} REQUIRES) + idf_component_get_property(include_dirs ${name} INCLUDE_DIRS) + idf_component_get_property(priv_reqs ${name} PRIV_REQUIRES) + idf_component_get_property(managed_reqs ${name} MANAGED_REQUIRES) + idf_component_get_property(managed_priv_reqs ${name} MANAGED_PRIV_REQUIRES) + if("${type}" STREQUAL "LIBRARY") + set(file "$") + + # The idf_component_register function is converting each source file path defined + # in SRCS into absolute one. But source files can be also added with cmake's + # target_sources and have relative paths. This is used for example in log + # component. Let's make sure all source files have absolute path. + set(sources "") + get_target_property(srcs ${lib} SOURCES) + foreach(src IN LISTS srcs) + get_filename_component(src "${src}" ABSOLUTE BASE_DIR "${dir}") + list(APPEND sources "${src}") + endforeach() + + else() + set(file "") + set(sources "") + endif() + + __make_json_list("${reqs}" OUTPUT reqs) + __make_json_list("${priv_reqs}" OUTPUT priv_reqs) + __make_json_list("${managed_reqs}" OUTPUT managed_reqs) + __make_json_list("${managed_priv_reqs}" OUTPUT managed_priv_reqs) + __make_json_list("${include_dirs}" OUTPUT include_dirs) + __make_json_list("${sources}" OUTPUT sources) + + string(JOIN "\n" component_json + " \"${name}\": {" + " \"alias\": \"${alias}\"," + " \"target\": \"${target}\"," + " \"prefix\": \"${prefix}\"," + " \"dir\": \"${dir}\"," + " \"type\": \"${type}\"," + " \"lib\": \"${lib}\"," + " \"reqs\": ${reqs}," + " \"priv_reqs\": ${priv_reqs}," + " \"managed_reqs\": ${managed_reqs}," + " \"managed_priv_reqs\": ${managed_priv_reqs}," + " \"file\": \"${file}\"," + " \"sources\": ${sources}," + " \"include_dirs\": ${include_dirs}" + " }" + ) + string(CONFIGURE "${component_json}" component_json) + if(NOT "${components_json}" STREQUAL "") + string(APPEND components_json ",\n") + endif() + string(APPEND components_json "${component_json}") + endforeach() + string(PREPEND components_json "{\n") + string(APPEND components_json "\n }") + set(${ARG_OUTPUT} "${components_json}" PARENT_SCOPE) +endfunction() + +#[[ +.. cmakev2:function:: idf_build_generate_metadata + + .. code-block:: cmake + + idf_build_generate_metadata( + [FILE ]) + + *executable[in]* + + 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 ``executable`` and store it in the + specified ``FILE``. If no ``FILE`` is provided, the default location + ``/project_description.json`` will be used. +#]] +function(idf_build_generate_metadata executable) + set(options) + set(one_value OUTPUT_FILE) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + __get_executable_library_or_die(TARGET "${executable}" OUTPUT library) + + idf_build_get_property(PROJECT_NAME PROJECT_NAME) + idf_build_get_property(PROJECT_VER PROJECT_VER) + idf_build_get_property(PROJECT_PATH PROJECT_DIR) + idf_build_get_property(IDF_PATH IDF_PATH) + 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 PROJECT_BIN executable property must be set by the idf_build_binary + # function. + get_target_property(PROJECT_BIN "${executable}" EXECUTABLE_BINARY) + if(NOT PROJECT_BIN) + set(PROJECT_BIN "") + endif() + if(CONFIG_APP_BUILD_TYPE_RAM) + set(PROJECT_BUILD_TYPE ram_app) + else() + set(PROJECT_BUILD_TYPE flash_app) + endif() + idf_build_get_property(IDF_VER IDF_VER) + + idf_build_get_property(common_component_reqs __COMPONENT_REQUIRES_COMMON) + list(SORT common_component_reqs) + __make_json_list("${common_component_reqs}" OUTPUT common_component_reqs_json) + + idf_library_get_property(build_components "${library}" LIBRARY_COMPONENTS_LINKED) + list(SORT build_components) + __make_json_list("${build_components}" OUTPUT build_components_json) + + set(build_component_paths) + set(COMPONENT_KCONFIGS) + set(COMPONENT_KCONFIGS_PROJBUILD) + foreach(component_name IN LISTS build_components) + idf_component_get_property(component_dir "${component_name}" COMPONENT_DIR) + idf_component_get_property(component_kconfig "${component_name}" __KCONFIG) + idf_component_get_property(component_kconfig_projbuild "${component_name}" __KCONFIG_PROJBUILD) + list(APPEND build_component_paths "${component_dir}") + if(component_kconfig) + list(APPEND COMPONENT_KCONFIGS "${component_kconfig}") + endif() + if(component_kconfig_projbuild) + list(APPEND COMPONENT_KCONFIGS_PROJBUILD "${component_kconfig_projbuild}") + endif() + endforeach() + __make_json_list("${build_component_paths}" OUTPUT build_component_paths_json) + + __get_components_metadata(COMPONENTS "${build_components}" OUTPUT build_component_info_json) + + idf_build_get_property(components_discovered COMPONENTS_DISCOVERED) + __get_components_metadata(COMPONENTS "${components_discovered}" OUTPUT all_component_info_json) + + __generate_gdbinit() + idf_build_get_property(gdbinit_files_prefix_map GDBINIT_FILES_PREFIX_MAP) + idf_build_get_property(gdbinit_files_symbols GDBINIT_FILES_SYMBOLS) + idf_build_get_property(gdbinit_files_py_extensions GDBINIT_FILES_PY_EXTENSIONS) + idf_build_get_property(gdbinit_files_connect GDBINIT_FILES_CONNECT) + __get_openocd_options(debug_arguments_openocd) + + if(NOT DEFINED ARG_OUTPUT_FILE) + set(ARG_OUTPUT_FILE "${BUILD_DIR}/project_description.json") + endif() + + get_filename_component(ARG_OUTPUT_FILE "${ARG_OUTPUT_FILE}" ABSOLUTE BASE_DIR "${BUILD_DIR}") + + configure_file("${IDF_PATH}/tools/cmake/project_description.json.in" "${ARG_OUTPUT_FILE}.templ") + file(READ "${ARG_OUTPUT_FILE}.templ" project_description_json_templ) + file(REMOVE "${ARG_OUTPUT_FILE}.templ") + file(GENERATE OUTPUT "${ARG_OUTPUT_FILE}" + CONTENT "${project_description_json_templ}") +endfunction() + +#[[ +.. cmakev2:function:: idf_build_binary + + .. code-block:: cmake + + idf_build_binary( + TARGET + OUTPUT_FILE ) + + *executable[in]* + + Executable target for which to generate a binary image file. + + *TARGET[in]* + + The name of the target that will be created for the generated binary + image. + + *OUTPUT_FILE[in]* + + Output file path for storing the binary image file. + + Create a binary image for the specified ``executable`` target and save it + in the file specified with the ``OUTPUT_FILE`` option. A custom target + named ``TARGET`` will be created for the generated binary image. The path + of the generated binary image will be also stored in the ``BINARY_PATH`` + property of the ``TARGET``. +#]] +function(idf_build_binary executable) + set(options) + set(one_value OUTPUT_FILE TARGET) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT TARGET idf::esptool_py) + idf_die("The 'esptool_py' component is not available") + endif() + + if(NOT CONFIG_APP_BUILD_GENERATE_BINARIES) + idf_die("Binary file generation is not enabled with 'CONFIG_APP_BUILD_GENERATE_BINARIES'") + endif() + + if(NOT TARGET "${executable}") + idf_die("The executable '${executable}' is not a cmake target") + endif() + + get_target_property(type "${executable}" TYPE) + if(NOT "${type}" STREQUAL "EXECUTABLE") + idf_die("The executable target '${executable}' is not of the EXECUTABLE type") + endif() + + idf_build_get_property(build_dir BUILD_DIR) + + get_target_property(executable_name ${executable} OUTPUT_NAME) + if(NOT executable_name) + set(executable_name "${executable}") + endif() + + if(NOT DEFINED ARG_TARGET) + idf_die("TARGET option is required") + endif() + + if(NOT DEFINED ARG_OUTPUT_FILE) + idf_die("OUTPUT_FILE option is required") + endif() + + idf_component_get_property(esptool_py_cmd esptool_py ESPTOOLPY_CMD) + idf_component_get_property(esptool_elf2image_args esptool_py ESPTOOL_PY_ELF2IMAGE_ARGS) + + get_filename_component(binary_name "${ARG_OUTPUT_FILE}" NAME) + + # Create a custom command and target to generate a binary from an ELF file. + add_custom_command(OUTPUT "${ARG_OUTPUT_FILE}" + COMMAND ${esptool_py_cmd} elf2image ${esptool_elf2image_args} + -o "${ARG_OUTPUT_FILE}" "$" + COMMAND ${CMAKE_COMMAND} -E echo "Generated ${ARG_OUTPUT_FILE}" + COMMAND ${CMAKE_COMMAND} -E md5sum "${ARG_OUTPUT_FILE}" > "${ARG_OUTPUT_FILE}.md5sum" + DEPENDS ${executable} + VERBATIM + WORKING_DIRECTORY ${build_dir} + COMMENT "Generating binary image '${binary_name}' from executable '${executable_name}'" + ) + + # Create a custom target to generate the binary file + add_custom_target(${ARG_TARGET} DEPENDS "${ARG_OUTPUT_FILE}") + + # The EXECUTABLE_BINARY property is used by idf_build_generate_metadata to + # store the name of the binary image. + set_target_properties(${executable} PROPERTIES EXECUTABLE_BINARY ${binary_name}) + + # Store the path of the binary file in the BINARY_PATH property of the + # custom binary target, which is used by the idf_flash_binary. + set_target_properties(${ARG_TARGET} PROPERTIES BINARY_PATH ${ARG_OUTPUT_FILE}) +endfunction() + +#[[ +.. cmakev2:function:: idf_sign_binary + + .. code-block:: cmake + + idf_sign_binary( + TARGET + OUTPUT_FILE + [KEYFILE ]) + + *binary[in]* + + Binary image target for which to generate a signed binary image file. + The ``binary`` target is created by the :cmakev2:ref:`idf_build_binary` + function. + + *TARGET[in]* + + The name of the target that will be created for the signed binary + image. + + *OUTPUT_FILE[in]* + + Output file path for storing the signed binary image file. + + *KEYFILE[in,opt]* + + Optional path to the key file that should be used for signing. If not + provided, the key file specified by the + ``CONFIG_SECURE_BOOT_SIGNING_KEY`` configuration option will be used. + + Sign binary image specified by ``binary`` target with ``KEYFILE`` and save + it in the file specified with the `OUTPUT_FILE` option. A custom target + named ``TARGET`` will be created for the signed binary image. The path of + the signed binary image will be also stored in the ``BINARY_PATH`` property + of the ``TARGET``. +#]] +function(idf_sign_binary binary) + set(options) + set(one_value OUTPUT_FILE TARGET KEYFILE) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT TARGET idf::esptool_py) + idf_die("The 'esptool_py' component is not available") + endif() + + if(NOT CONFIG_APP_BUILD_GENERATE_BINARIES) + idf_die("Binary file generation is not enabled with 'CONFIG_APP_BUILD_GENERATE_BINARIES'") + endif() + + if(NOT CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES) + idf_die("Binary file signing is not enabled with 'CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES'") + endif() + + if(NOT DEFINED ARG_TARGET) + idf_die("TARGET option is required") + endif() + + if(NOT DEFINED ARG_OUTPUT_FILE) + idf_die("OUTPUT_FILE option is required") + endif() + + if(ARG_KEYFILE) + set(keyfle "${ARG_KEYFILE}") + else() + idf_build_get_property(project_dir PROJECT_DIR) + get_filename_component(keyfile "${CONFIG_SECURE_BOOT_SIGNING_KEY}" ABSOLUTE BASE_DIR "${project_dir}") + endif() + + if(CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME) + set(secure_boot_version "1") + elseif(CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME OR CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME) + set(secure_boot_version "2") + endif() + + idf_component_get_property(espsecure_py_cmd esptool_py ESPSECUREPY_CMD) + 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(binary_name "${binary_path}" NAME) + get_filename_component(signed_binary_name "${ARG_OUTPUT_FILE}" NAME) + get_filename_component(key_name "${keyfile}" NAME) + + add_custom_command(OUTPUT "${ARG_OUTPUT_FILE}" + COMMAND ${espsecure_py_cmd} sign-data + --version ${secure_boot_version} --keyfile "${keyfile}" + -o "${ARG_OUTPUT_FILE}" "${binary_path}" + COMMAND ${CMAKE_COMMAND} -E md5sum "${ARG_OUTPUT_FILE}" > "${ARG_OUTPUT_FILE}.md5sum" + DEPENDS "${binary}" + VERBATIM + COMMENT "Signing '${binary_name}' with key '${key_name}' into '${signed_binary_name}'" + ) + add_custom_target(${ARG_TARGET} DEPENDS "${ARG_OUTPUT_FILE}") + + # Store the path of the binary file in the BINARY_PATH property of the + # custom signed binary target, which is used by the idf_flash_binary. + set_target_properties(${ARG_TARGET} PROPERTIES BINARY_PATH ${ARG_OUTPUT_FILE}) +endfunction() + +#[[ +.. cmakev2:function:: idf_flash_binary + + .. code-block:: cmake + + idf_flash_binary( + TARGET + [NAME ] + [FLASH]) + + *binary[in]* + + Binary image target for which to create flash target. The ``binary`` + target is created by the :cmakev2:ref:`idf_build_binary` function or + :cmakev2:ref:`idf_sign_binary`. + + *TARGET[in]* + + The name of the flash target that will be created for the binary image. + + *NAME[in,opt]* + + An optional name to be used as a prefix for the file containing + arguments for esptool, and as a logical name for the binary in + ``flasher_args.json``. If not specified, the name of the ``TARGET`` is + used. + + *FLASH[in,opt]* + + If specified, the binary will also be added to the global ``flash`` + target. + + Create a new flash target for ``binary`` using the name specified in the + ``TARGET`` option. The file path of the binary image should be stored in + the ``BINARY_PATH`` property of the ``binary`` target. +#]] +function(idf_flash_binary binary) + set(options FLASH) + set(one_value TARGET NAME) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT TARGET idf::esptool_py) + idf_die("The 'esptool_py' component is not available") + endif() + + if(NOT CONFIG_APP_BUILD_GENERATE_BINARIES) + idf_die("Binary file generation is not enabled with 'CONFIG_APP_BUILD_GENERATE_BINARIES'") + endif() + + if(NOT DEFINED ARG_TARGET) + idf_die("TARGET option is required") + endif() + + if(NOT DEFINED ARG_NAME) + set(ARG_NAME "${ARG_TARGET}") + endif() + + idf_component_get_property(main_args esptool_py FLASH_ARGS) + idf_component_get_property(sub_args esptool_py FLASH_SUB_ARGS) + + partition_table_get_partition_info(offset "--partition-boot-default" "offset") + get_target_property(binary_path ${binary} BINARY_PATH) + if(NOT binary_path) + idf_die("Binary target '${binary}' is missing 'BINARY_PATH' property.") + endif() + + idf_build_get_property(build_dir BUILD_DIR) + get_filename_component(binary_path "${binary_path}" ABSOLUTE BASE_DIR "${build_dir}") + + esptool_py_flash_target("${ARG_TARGET}" "${main_args}" "${sub_args}" + FILENAME_PREFIX "${ARG_NAME}") + esptool_py_flash_target_image("${ARG_TARGET}" "${ARG_NAME}" "${offset}" "${binary_path}") + add_dependencies("${ARG_TARGET}" "${binary}") + + if(ARG_FLASH) + esptool_py_flash_target_image(flash ${ARG_NAME} "${offset}" "${binary_path}") + add_dependencies(flash "${binary}") + endif() +endfunction() + +#[[ +.. cmakev2:function:: idf_check_binary_size + + .. code-block:: cmake + + idf_check_binary_size() + + *binary[in]* + + Binary image target to which to add a partition size check. The + ``binary`` target is created by the :cmakev2:ref:`idf_build_binary` or + :cmakev2:ref:`idf_sign_binary` function. + + Ensure that the generated binary image fits within the smallest available + application partition. The file path of the binary image should be stored + in the ``BINARY_PATH`` property of the ``binary`` target. +#]] +function(idf_check_binary_size binary) + if(NOT CONFIG_APP_BUILD_TYPE_APP_2NDBOOT) + return() + endif() + + get_target_property(binary_path ${binary} BINARY_PATH) + if(NOT binary_path) + idf_die("Binary target '${binary}' is missing 'BINARY_PATH' property.") + endif() + + partition_table_add_check_size_target("${binary}_check_size" + DEPENDS "${binary_path}" + BINARY_PATH "${binary_path}" + PARTITION_TYPE app) + add_dependencies("${binary}" "${binary}_check_size") +endfunction() + +#[[ +.. cmakev2:function:: idf_check_binary_signed + + .. code-block:: cmake + + idf_check_binary_signed() + + *binary[in]* + + Binary image target to which to add a partition size check. The + ``binary`` target is created by the ``idf_build_binary``. + + If the binary is not signed and signed applications are required with + CONFIG_SECURE_SIGNED_APPS, print a note indicating that the binary needs to + be signed manually. This situation can occur if the binary images are not + signed during the build because CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES is + not set. +#]] +function(idf_check_binary_signed binary) + if(NOT CONFIG_SECURE_SIGNED_APPS) + # Signed binaries are not required. + return() + endif() + + if(CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES) + # Binary should be already signed with idf_sign_binary. + return() + endif() + + get_target_property(binary_path ${binary} BINARY_PATH) + if(NOT binary_path) + idf_die("Binary target '${binary}' is missing 'BINARY_PATH' property.") + endif() + + if(CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME) + set(secure_boot_version "1") + elseif(CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME OR CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME) + set(secure_boot_version "2") + endif() + + idf_component_get_property(espsecure_py_cmd esptool_py ESPSECUREPY_CMD) + string(REPLACE ";" " " espsecure_py_cmd "${espsecure_py_cmd}") + get_filename_component(binary_name "${binary_path}" NAME) + + add_custom_command(TARGET "${binary}" POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo + "Binary '${binary_name}' not signed. Sign it before flashing with:" + COMMAND ${CMAKE_COMMAND} -E echo + "${espsecure_py_cmd} sign-data --keyfile KEYFILE --version ${secure_boot_version}" + "${binary_path}" + VERBATIM) +endfunction() diff --git a/tools/cmakev2/compat.cmake b/tools/cmakev2/compat.cmake new file mode 100644 index 0000000000..e473c22ac2 --- /dev/null +++ b/tools/cmakev2/compat.cmake @@ -0,0 +1,552 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +include_guard(GLOBAL) + +#[[ + check_expected_tool_version() + + Function to verify if the tool is using the expected version and issue a + warning if it is not. +#]] +function(check_expected_tool_version tool_name tool_path) + set(tool_version_warning "Check Getting Started documentation or proceed at own risk.\n") + set(tool_version_error "Check Getting Started documentation if the error continues.\n" + "You can override this error and proceed with build by defining the IDF_MAINTAINER environment variable.\n") + set(fixing_hint "Please try to run 'idf.py fullclean' to solve it.\n") + + idf_build_get_property(python PYTHON) + idf_build_get_property(idf_path IDF_PATH) + + set(ENV{IDF_TOOLS_VERSION_HELPER} "1") + # Use idf_tools.py to check if tool version is supported + execute_process( + COMMAND ${python} "${idf_path}/tools/idf_tools.py" + "check-tool-supported" "--tool-name" "${tool_name}" + "--exec-path" "${tool_path}" + OUTPUT_VARIABLE is_version_supported + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + + if(is_version_supported STREQUAL "False") + # Version is not supported. Need to get supported versions list to print them to user + execute_process( + COMMAND ${python} "${idf_path}/tools/idf_tools.py" + "get-tool-supported-versions" "--tool-name" "${tool_name}" + OUTPUT_VARIABLE tool_supported_versions + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + # IDF maintainers can build projects with not supported versions with just a warning + if($ENV{IDF_MAINTAINER}) + set(message_mode "WARNING") + else() + set(message_mode "FATAL_ERROR") + endif() + + idf_msg(${message_mode} "\n" + "Tool doesn't match supported version from list " + "${tool_supported_versions}: ${tool_path}\n" + ${fixing_hint}) + elseif(NOT is_version_supported STREQUAL "True") + idf_warn("Can not get version for tool: ${tool_path}\n" ${tool_version_warning}) + endif() + unset(ENV{IDF_TOOLS_VERSION_HELPER}) +endfunction() + +#[[ + __get_component_sources([SRCS ...] + [SRC_DIRS ...] + [EXCLUDE_SRCS ...] + OUTPUT ) + + *SRCS[in,opt]* + + Optional list of sources. + + *SRC_DIRS[in,opt]* + + Option list of source directories. + + *EXCLUDE_SRCS[in,opt]* + + Optional list of sources to exclude. + + *OUTPUT[out]* + + Output variable to store the list of component sources. + + This helper function gathers component sources from the arguments specified + in either ``SRCS`` or ``SRC_DIRS``. If both are provided, the ``SRC_DIRS`` + option is disregarded. The sources are collected as absolute paths relative + to the component's directory. If ``EXCLUDE_SRCS`` is specified, the source + files listed in it are excluded. The final list of component sources is + returned in the ``OUTPUT`` variable. + + The ``COMPONENT_NAME`` and ``COMPONENT_DIR`` variables are provided by the + build system when ``idf_component_register`` is called. +#]] +function(__get_component_sources) + set(options) + set(one_value OUTPUT) + set(multi_value SRCS SRC_DIRS EXCLUDE_SRCS) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_OUTPUT) + idf_die("OUTPUT option is required") + endif() + + if(DEFINED ARG_SRCS AND DEFINED ARG_SRC_DIRS) + idf_warn("SRCS and SRC_DIRS are both specified for '${COMPONENT_NAME}' " + "in '${COMPONENT_DIR}'. Ignoring SRC_DIRS.") + unset(ARG_SRC_DIRS) + endif() + + set(sources) + + if(ARG_SRCS) + __get_absolute_paths(PATHS "${ARG_SRCS}" BASE_DIR "${COMPONENT_DIR}" OUTPUT sources) + elseif(ARG_SRC_DIRS) + __get_absolute_paths(PATHS "${ARG_SRC_DIRS}" BASE_DIR "${COMPONENT_DIR}" OUTPUT dirs) + foreach(dir IN LISTS dirs) + if(NOT IS_DIRECTORY "${dir}") + idf_die("SRC_DIRS entry '${dir}' does not exist for component " + "'${COMPONENT_NAME}' in '${COMPONENT_DIR}'.") + endif() + + file(GLOB dir_sources "${dir}/*.c" "${dir}/*.cpp" "${dir}/*.S") + list(SORT dir_sources) + + if(NOT dir_sources) + idf_warn("No source files found for SRC_DIRS entry '${dir}' for " + "'${COMPONENT_NAME}' in '${COMPONENT_DIR}'.") + continue() + endif() + + list(APPEND sources "${dir_sources}") + endforeach() + endif() + + if(ARG_EXCLUDE_SRCS) + __get_absolute_paths(PATHS "${ARG_EXCLUDE_SRCS}" BASE_DIR "${COMPONENT_DIR}" OUTPUT exclude_srcs) + foreach(src IN LISTS exclude_srcs) + list(REMOVE_ITEM sources "${src}") + endforeach() + endif() + + list(REMOVE_DUPLICATES sources) + set(${ARG_OUTPUT} "${sources}" PARENT_SCOPE) +endfunction() + +#[[ +.. cmakev2:function:: target_linker_script + + .. code-block:: cmake + + target_linker_script( ... + [PROCESS ]) + + *target[in]* + + The component target to which linker script files should be added. + + *deptype[in]* + + This option is obsolete and maintained solely for backward + compatibility. + + *scriptfile[in]* + + Specifies the linker script file ``scriptfile`` to be added to the + link. Multiple files can be specified. + + *PROCESS[in,opt]* + + Specifies the ``output`` linker script, which is generated from the + ``linkerscript`` template. The ``linkerscript`` is processed with ldgen + to produce the ``output``. + + This function adds one or more linker scripts to the specified component + target, incorporating the linker script into the linking process. + + If the ``PROCESS`` option is specified, the last ``scriptfile`` listed is + processed using the ldgen command, and the generated ``output`` file is + used as the linker script during the linking process. This implies that + with the ``PROCESS`` option, it is logical to provide only a single + ``scriptfile`` as a template. +#]] +function(target_linker_script target deptype scriptfiles) + # The linker script files, templates, and their output filenames are stored + # only as component properties. The script files are generated and added to + # the library link interface in the idf_build_library function. + set(options) + set(one_value PROCESS) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + foreach(scriptfile ${scriptfiles}) + get_filename_component(scriptfile "${scriptfile}" ABSOLUTE) + idf_msg("Adding linker script ${scriptfile}") + if(ARG_PROCESS) + get_filename_component(output "${ARG_PROCESS}" ABSOLUTE) + idf_component_set_property("${target}" LINKER_SCRIPTS_TEMPLATE "${scriptfile}" APPEND) + idf_component_set_property("${target}" LINKER_SCRIPTS_GENERATED "${output}" APPEND) + else() + idf_component_set_property("${target}" LINKER_SCRIPTS ${scriptfile} APPEND) + endif() + endforeach() +endfunction() + +#[[ +.. cmakev2:function:: idf_component_optional_requires + + .. code-block:: cmake + + idf_component_optional_requires( ...) + + *type[in]* + + Type of dependency: PRIVATE, PUBLIC or INTERFACE + + *component[in]* + + 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. +#]] +function(idf_component_optional_requires req_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() +endfunction() + +#[[ + __init_common_components() + + Identify the commonly required components based on the target and + architecture, and store them in the __COMPONENT_REQUIRES_COMMON build + property. Their interfaces are stored in the __COMMON_COMPONENT_INTERFACES + build property. The commonly required component interfaces are + automatically linked to each cmakev1 component added through the + idf_component_register function. + + This function is called from the idf_component_register function and is + evaluated only once per project, as ensured by the + __COMMON_COMPONENTS_INITIALIZED build property. The cmakev2 components are + expected to properly specify all their dependencies, rather than relying on + common components to be automatically linked to them. Therefore, the common + components are relevant only within the context of cmakev1 components. +#]] +function(__init_common_components) + idf_build_get_property(common_components_initialized __COMMON_COMPONENTS_INITIALIZED) + if(common_components_initialized) + return() + endif() + + idf_build_get_property(idf_target IDF_TARGET) + idf_build_get_property(idf_target_arch IDF_TARGET_ARCH) + + # Define common components that are included as dependencies for each + # component. + if("${idf_target}" STREQUAL "linux") + set(requires_common freertos esp_hw_support heap log soc hal esp_rom esp_common esp_system linux) + else() + set(requires_common cxx esp_libc freertos esp_hw_support heap log soc hal esp_rom esp_common + esp_system ${idf_target_arch}) + endif() + idf_build_set_property(__COMPONENT_REQUIRES_COMMON "${requires_common}") + + # Set the common component interfaces first, before including them, so the + # idf_component_include function can see the complete list of common + # component interfaces. + set(common_component_interfaces "") + foreach(component_name IN LISTS requires_common) + idf_component_get_property(component_interface "${component_name}" COMPONENT_INTERFACE) + list(APPEND common_component_interfaces "${component_interface}") + endforeach() + idf_build_set_property(__COMMON_COMPONENT_INTERFACES "${common_component_interfaces}") + + foreach(component_name IN LISTS requires_common) + idf_component_include("${component_name}") + endforeach() + + idf_build_set_property(__COMMON_COMPONENTS_INITIALIZED YES) +endfunction() + +#[[api +.. cmakev2:function:: idf_component_register + + .. code-block:: cmake + + idf_component_register([SRCS ..] + [SRC_DIRS ...] + [EXCLUDE_SRCS ...] + [INCLUDE_DIRS ...] + [PRIV_INCLUDE_DIRS ...] + [LDFRAGMENTS ...] + [REQUIRES ...] + [PRIV_REQUIRES ...] + [REQUIRED_IDF_TARGETS ...] + [EMBED_FILES ...] + [EMBED_TXTFILES ...] + [WHOLE_ARCHIVE]) + + *SRCS[in,opt]* + + List of source files for the component. + + *SRC_DIRS[in,opt]* + + List of source directories to search for source files (.c, .cpp, .S); + ignored when SRCS is specified. + + *EXCLUDE_SRCS[in,opt]* + + List of source files to exclude from the designated source directories. + + *INCLUDE_DIRS[in,opt]* + + List of public include directories for the created component library. + + *PRIV_INCLUDE_DIRS[in,opt]* + + List of private include directories for the newly created component + library. + + *LDFRAGMENTS[in,opt]* + + List of linker script fragments for the component. + + *REQUIRES[in,opt]* + + List of publicly required components based on usage requirements. + + *PRIV_REQUIRES[in,opt]* + + List of privately required components based on usage requirements. + + *REQUIRED_IDF_TARGETS[in,opt]* + + List of IDF build targets supported exclusively by the component. + + *EMBED_FILES[in,opt]* + + List of binary files to embed with the component. + + *EMBED_TXTFILES[in,opt]* + + List of text files to embed with the component. + + *WHOLE_ARCHIVE[in,opt]* + + Link the component as --whole-archive. + + Register a new component with the build system using the provided options. + This function also automatically links all commonly required and managed + components to the component's target. +#]] +function(idf_component_register) + set(options WHOLE_ARCHIVE) + set(one_value KCONFIG KCONFIG_PROJBUILD) + set(multi_value SRCS SRC_DIRS EXCLUDE_SRCS + INCLUDE_DIRS PRIV_INCLUDE_DIRS LDFRAGMENTS REQUIRES + PRIV_REQUIRES REQUIRED_IDF_TARGETS EMBED_FILES EMBED_TXTFILES) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + # Initialize and include commonly required components. + __init_common_components() + + if(ARG_REQUIRED_IDF_TARGETS) + idf_build_get_property(idf_target IDF_TARGET) + if(NOT idf_target IN_LIST ARG_REQUIRED_IDF_TARGETS) + idf_die("Component ${COMPONENT_NAME} only supports targets: ${ARG_REQUIRED_IDF_TARGETS}") + endif() + endif() + + if(ARG_KCONFIG) + get_filename_component(filename "${ARG_KCONFIG}" NAME) + string(TOLOWER "${filename}" filename) + if(NOT "${filename}" STREQUAL "kconfig") + idf_die("The component '${COMPONENT_NAME}' in the '${COMPONENT_DIR}' directory " + "is specifying '${ARG_KCONFIG}' as the 'KCONFIG' option, which is not " + "the default 'Kconfig' filename.") + endif() + endif() + + if(ARG_KCONFIG_PROJBUILD) + get_filename_component(filename "${ARG_KCONFIG_PROJBUILD}" NAME) + string(TOLOWER "${filename}" filename) + if(NOT "${filename}" STREQUAL "kconfig.projbuild") + idf_die("The component '${COMPONENT_NAME}' in the '${COMPONENT_DIR}' directory " + "is specifying '${ARG_KCONFIG_PROJBUILD}' as the 'KCONFIG_PROJBUILD' option, " + "which is not the default 'Kconfig.projbuild' filename.") + endif() + endif() + + __get_component_sources(SRCS "${ARG_SRCS}" + SRC_DIRS "${ARG_SRC_DIRS}" + EXCLUDE_SRCS "${ARG_EXCLUDE_SRCS}" + OUTPUT sources) + + idf_build_get_property(include_directories INCLUDE_DIRECTORIES GENERATOR_EXPRESSION) + include_directories("${include_directories}") + + idf_build_get_property(compile_definitions COMPILE_DEFINITIONS GENERATOR_EXPRESSION) + add_compile_definitions("${compile_definitions}") + + __get_compile_options(OUTPUT compile_options) + add_compile_options("${compile_options}") + + idf_build_get_property(common_component_interfaces __COMMON_COMPONENT_INTERFACES) + idf_component_get_property(component_interface "${COMPONENT_NAME}" COMPONENT_INTERFACE) + list(REMOVE_ITEM common_component_interfaces ${component_interface}) + link_libraries(${common_component_interfaces}) + + __get_absolute_paths(PATHS "${ARG_INCLUDE_DIRS}" BASE_DIR "${COMPONENT_DIR}" OUTPUT include_dirs) + __get_absolute_paths(PATHS "${ARG_PRIV_INCLUDE_DIRS}" BASE_DIR "${COMPONENT_DIR}" OUTPUT priv_include_dirs) + foreach(dir IN LISTS include_dirs priv_include_dirs) + if(NOT IS_DIRECTORY ${dir}) + idf_die("The component '${COMPONENT_NAME}' in the '${COMPONENT_DIR}' directory " + "is specifying '${dir}' as its include directory, but the directory does " + "not exists.") + endif() + endforeach() + + if(sources OR ARG_EMBED_FILES OR ARG_EMBED_TXTFILES) + add_library("${COMPONENT_TARGET}" STATIC "${sources}") + + foreach(include_dir IN LISTS include_dirs) + target_include_directories("${COMPONENT_TARGET}" PUBLIC "${include_dir}") + endforeach() + + foreach(include_dir IN LISTS priv_include_dirs) + target_include_directories("${COMPONENT_TARGET}" PRIVATE "${include_dir}") + endforeach() + + set_target_properties(${COMPONENT_TARGET} PROPERTIES OUTPUT_NAME ${COMPONENT_NAME} LINKER_LANGUAGE C) + + set(component_type LIBRARY) + + else() + add_library("${COMPONENT_TARGET}" INTERFACE) + + foreach(include_dir IN LISTS include_dirs) + target_include_directories("${COMPONENT_TARGET}" INTERFACE "${include_dir}") + endforeach() + + set(component_type CONFIG_ONLY) + endif() + + foreach(req IN LISTS ARG_REQUIRES) + idf_component_include("${req}") + idf_component_get_property(req_interface "${req}" COMPONENT_INTERFACE) + if(${component_type} STREQUAL LIBRARY) + target_link_libraries("${COMPONENT_TARGET}" PUBLIC "${req_interface}") + else() + target_link_libraries("${COMPONENT_TARGET}" INTERFACE "${req_interface}") + endif() + endforeach() + + foreach(req IN LISTS ARG_PRIV_REQUIRES) + idf_component_include("${req}") + idf_component_get_property(req_interface "${req}" COMPONENT_INTERFACE) + if(${component_type} STREQUAL CONFIG_ONLY) + continue() + endif() + target_link_libraries("${COMPONENT_TARGET}" PRIVATE "${req_interface}") + endforeach() + + # Signal to idf_component_include that this component was included via the + # backward compatible idf_component_register function. + idf_component_set_property("${COMPONENT_NAME}" COMPONENT_FORMAT CMAKEV1) + + # The handling of WHOLE_ARCHIVE linkage is managed within the + # idf_component_include function. + idf_component_set_property("${COMPONENT_NAME}" WHOLE_ARCHIVE "${ARG_WHOLE_ARCHIVE}") + + idf_component_set_property("${COMPONENT_NAME}" SRCS "${sources}") + idf_component_set_property("${COMPONENT_NAME}" INCLUDE_DIRS "${ARG_INCLUDE_DIRS}") + idf_component_set_property("${COMPONENT_NAME}" PRIV_INCLUDE_DIRS "${ARG_PRIV_INCLUDE_DIRS}") + + # The addition of ldgen fragment files is managed by the + # idf_component_include function. + idf_component_set_property("${COMPONENT_NAME}" LDFRAGMENTS "${ARG_LDFRAGMENTS}") + + # Embedded files are managed in the idf_component_include function. + idf_component_set_property("${COMPONENT_NAME}" EMBED_FILES "${ARG_EMBED_FILES}") + idf_component_set_property("${COMPONENT_NAME}" EMBED_TXTFILES "${ARG_EMBED_TXTFILES}") + idf_component_set_property("${COMPONENT_NAME}" REQUIRES "${ARG_REQUIRES}") + idf_component_set_property("${COMPONENT_NAME}" PRIV_REQUIRES "${ARG_PRIV_REQUIRES}") + idf_component_set_property("${COMPONENT_NAME}" REQUIRED_IDF_TARGETS "${ARG_REQUIRED_IDF_TARGETS}") + idf_component_set_property("${COMPONENT_NAME}" COMPONENT_TYPE "${component_type}") +endfunction() + +#[[ +.. cmakev2:function:: idf_build_component + + .. code-block:: cmake + + idf_build_component( []) + + *component_dir[in]* + + Directory path of the component. + + *component_source[in,opt]* + + Source of the component. One of: + + * ``idf_components`` + * ``project_managed_components`` + * ``project_extra_components`` + * ``project_components`` + + Defaults to ``project_components``. + + Compatibility shim used by generated files (e.g. managed_components_list) + that initializes the component in build system v2 by delegating to + ``__init_component``. +#]] +function(idf_build_component component_dir) + if(${ARGC} EQUAL 1) + set(component_source "project_components") + else() + set(component_source ${ARGV1}) + endif() + + # Get the component name and check if it's already initialized + get_filename_component(component_name "${component_dir}" NAME) + __get_component_interface(COMPONENT "${component_name}" OUTPUT existing_interface) + + if(existing_interface) + # Component already exists, check if it's from the same directory and source + idf_component_get_property(existing_dir "${existing_interface}" COMPONENT_DIR) + idf_component_get_property(existing_source "${existing_interface}" COMPONENT_SOURCE) + + # If same directory and same source, skip re-initialization + # This handles the case where component manager may output the same component + # multiple times due to path separator issues on Windows. + # TODO: Remove this workaround once idf-component-manager is fixed (IDF-14260) + if("${component_dir}" STREQUAL "${existing_dir}" AND + "${component_source}" STREQUAL "${existing_source}") + # Component already initialized from the same location, skip duplicate + return() + endif() + + # Different directory or source, let __init_component handle the priority logic + endif() + + idf_build_get_property(component_prefix PREFIX) + __init_component(DIRECTORY "${component_dir}" + PREFIX "${component_prefix}" + SOURCE "${component_source}") +endfunction() diff --git a/tools/cmakev2/component.cmake b/tools/cmakev2/component.cmake new file mode 100644 index 0000000000..0b169753b2 --- /dev/null +++ b/tools/cmakev2/component.cmake @@ -0,0 +1,1026 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +include_guard(GLOBAL) + +include(utilities) +include(build) +include(kconfig) + +#[[api +.. cmakev2:function:: idf_component_set_property + + .. code-block:: cmake + + idf_component_set_property( [APPEND]) + + *component[in]* + + Component name, target, target alias or interface. + + *property[in]* + + Property name. + + *value[in]* + + Property value. + + *APPEND* + + Append the value to the property's current value instead of replacing + it. + + Set the value of the specified component property. The property is also + added to the internal list of component properties if it isn't already + there. +#]] +function(idf_component_set_property component property value) + set(options APPEND) + set(one_value) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + set(append) + if(ARG_APPEND) + set(append APPEND) + endif() + + __get_component_interface_or_die(COMPONENT "${component}" OUTPUT component_interface) + __set_property(TARGET "${component_interface}" + PROPERTY "${property}" + PROPERTIES COMPONENT_PROPERTIES + VALUE "${value}" + ${append}) +endfunction() + +#[[api +.. cmakev2:function:: idf_component_get_property + + .. code-block:: cmake + + idf_component_get_property( [GENERATOR_EXPRESSION]) + + *variable[out]* + + Variable to store the value in. + + *component[in]* + + Component name, target, target alias or interface. + + *property[in]* + + Property name to get the value of. + + *GENERATOR_EXPRESSION* + + Obtain the generator expression for the property rather than the actual + value. + + Retrieve the value of the specified component property. +#]] +function(idf_component_get_property variable component property) + set(options GENERATOR_EXPRESSION) + set(one_value) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + set(genexpr) + if(ARG_GENERATOR_EXPRESSION) + set(genexpr GENERATOR_EXPRESSION) + endif() + + __get_component_interface_or_die(COMPONENT "${component}" OUTPUT component_interface) + __get_property(TARGET "${component_interface}" + PROPERTY "${property}" + OUTPUT value + ${genexpr}) + set(${variable} ${value} PARENT_SCOPE) +endfunction() + +#[[ + __get_component_cached_interface( ) + + *variable[out]* + + Variable to store the cached component target name, or left empty if + not found. + + *component[in]* + + Component name to search for in the cache. + + Get the component target interface for a given ``component`` name. The cache + maps multiple component names to the component interface target. The name + can be a component name with or without a namespace, target, interface, or + alias. +#]] +function(__get_component_cached_interface variable component) + __get_property(TARGET __idf_component_interface_cache + PROPERTY "${component}" + OUTPUT interface) + set(${variable} "${interface}" PARENT_SCOPE) +endfunction() + +#[[ + __set_component_cached_interface( ) + + *component[in]* + + Component name with or without a namespace, target, interface, or + alias. + + *interface[in]* + + Component interface target. + + Set the name mapping for the ``component`` to the ``interface`` target in + the cache. +#]] +function(__set_component_cached_interface component interface) + __set_property(TARGET __idf_component_interface_cache + PROPERTY "${component}" + PROPERTIES INTERFACE_CACHE_PROPERTIES + VALUE "${interface}") +endfunction() + +#[[ + __dump_component_interface_cache() + + Dump content of internal component interface cache. +#]] +function(__dump_component_interface_cache) + __get_property(TARGET __idf_component_interface_cache + PROPERTY INTERFACE_CACHE_PROPERTIES + OUTPUT properties) + idf_msg("component interface cache: ${properties}") + foreach(property IN LISTS properties) + __get_property(TARGET __idf_component_interface_cache + PROPERTY "${property}" + OUTPUT value) + idf_msg(" ${property}: ${value}") + endforeach() +endfunction() + +#[[ + __init_component_interface_cache( ) + + *name[in]* + + Component name. The component name is the same as the component + directory name. + + *interface[in]* + + Component interface. + + *alias[in]* + + Component alias. + + *target[in]* + + Component target. + + *priority[in]* + + Component priority. + + Initialize the component interface cache for given component by adding + mappings from the component name, interface, alias, and target to the + component interface. The component interface cache is used by the + __get_component_interface function to quickly get a component interface + target based on the given component name, target, and alias. If the + component name contains a namespace prefix, the short name of the component + is also added to the cache. For example, for "espressif__led_strip," a + short name "led_strip" is added as a mapping to the "espressif__led_strip" + component target in the component interface cache. Since the short name may + cause ambiguity, component preference is used to determine which component + interface the short name should point to. +#]] +function(__init_component_interface_cache name interface alias target priority) + __set_component_cached_interface("${name}" "${interface}") + __set_component_cached_interface("${interface}" "${interface}") + __set_component_cached_interface("${alias}" "${interface}") + __set_component_cached_interface("${target}" "${interface}") + + string(REGEX REPLACE ".*__" "" short_name "${name}") + if("${short_name}" STREQUAL "${name}") + # The component name does not include a namespace, so there is nothing + # further to set. + return() + endif() + + __get_component_cached_interface(short_name_interface "${short_name}") + if(NOT short_name_interface) + # The component interface cache does not yet contain an entry for a + # component name without a namespace, so add it as the name for the + # given component interface. + idf_component_set_property("${name}" COMPONENT_SHORT_NAME "${short_name}") + __set_component_cached_interface("${short_name}" "${interface}") + return() + endif() + + # We have two components with the same short name but different namespaces. + # For example, there is "espressif__led_strip" and "my__led_strip", and the + # cache already contains a mapping of "led_strip" to + # "espressif__led_strip." + + idf_component_get_property(short_name_priority "${short_name}" COMPONENT_PRIORITY) + + if(${priority} GREATER ${short_name_priority}) + # The short component name in the cache currently refers to a component + # with lower priority. Update the short name to point to the component + # with higher priority. + idf_component_set_property("${short_name}" COMPONENT_SHORT_NAME "") + idf_component_set_property("${name}" COMPONENT_SHORT_NAME "${short_name}") + __set_component_cached_interface("${short_name}" "${interface}") + elseif(${priority} EQUAL ${short_name_priority}) + # The short component name refers to a component with the same + # priority. Resolve this ambiguity by completely removing the short + # component name from the cache. + idf_component_set_property("${short_name}" COMPONENT_SHORT_NAME "") + __set_component_cached_interface("${short_name}" "") + endif() +endfunction() + +#[[ + __get_component_paths(PATHS ... + [EXCLUDE_PATHS ...] + [SOURCE ] + [CHECK] + OUTPUT ) + + *PATHS[in]* + + List of paths to search for component directories. + + *EXCLUDE_PATHS[in,opt]* + + Optional list of paths to exclude from the search of component + directories. + + *SOURCE[in,opt]* + + Source of the ``PATHS``. If provided, it will be included in the error + message when ``CHECK`` is specified. + + *CHECK[in,opt]* + + Verify whether the paths listed in "PATHS" exist. If any path is + missing, abort the build process. + + *OUTPUT[out]* + + Output variable to store the list of found component directories. + + Search for component directories in the specified ``PATHS``, excluding + those in ``EXCLUDE_PATHS``, and store the list of absolute component paths + in the ``OUTPUT`` variable. If ``CHECK`` is specified, ensure that the + paths listed in ``PATHS`` exist, and stop the build process if they do not. +#]] +function(__get_component_paths) + set(options CHECK) + set(one_value SOURCE OUTPUT) + set(multi_value PATHS EXCLUDE_PATHS) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_PATHS) + idf_die("PATHS option is required") + endif() + + if(NOT DEFINED ARG_OUTPUT) + idf_die("OUTPUT option is required") + endif() + + __get_absolute_paths(PATHS "${ARG_PATHS}" OUTPUT include_paths) + __get_absolute_paths(PATHS "${ARG_EXCLUDE_PATHS}" OUTPUT exclude_paths) + + if(ARG_CHECK) + foreach(path IN LISTS include_paths) + if(NOT IS_DIRECTORY "${path}") + if(DEFINED ARG_SOURCE) + idf_die("Directory specified in '${ARG_SOURCE}' doesn't exist: '${path}'") + else() + idf_die("Directory doesn't exist: '${path}'") + endif() + endif() + endforeach() + endif() + + set(paths "${include_paths}") + set(component_paths "") + + while(paths) + list(POP_FRONT paths path) + + if(NOT IS_DIRECTORY "${path}" OR "${path}" IN_LIST exclude_paths) + continue() + endif() + + if(EXISTS "${path}/CMakeLists.txt") + list(APPEND component_paths "${path}") + elseif("${path}" IN_LIST include_paths) + file(GLOB dirs "${path}/*") + __get_absolute_paths(PATHS "${dirs}" OUTPUT dirs_abs) + list(APPEND paths "${dirs_abs}") + endif() + + endwhile() + + set(${ARG_OUTPUT} "${component_paths}" PARENT_SCOPE) +endfunction() + +#[[ + __get_component_interface(COMPONENT + OUTPUT ) + + *COMPONENT[int]* + + Component name, target, target alias or interface. + + *OUTPUT[out]* + + Output variable to store the component interface. + + Identify the component interface target using the ```` value, + which could be a component name with or without a namespace, target, target + alias, or interface. Return the component interface target, or NOTFOUND if + the interface cannot be located. + + component interface: _ + component target: __ + component alias: :: +#]] +function(__get_component_interface) + set(options) + set(one_value COMPONENT OUTPUT) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_COMPONENT) + idf_die("COMPONENT option is required") + endif() + + if(NOT DEFINED ARG_OUTPUT) + idf_die("OUTPUT option is required") + endif() + + set(component_interface NOTFOUND) + + __get_component_cached_interface(component_interface_cached "${ARG_COMPONENT}") + + if(component_interface_cached) + set(component_interface "${component_interface_cached}") + else() + # Check if the COMPONENT argument is not an alias. If it is an alias, + # resolve it to the real target and search the interface cache again + # using the real target. + __get_real_target(TARGET "${ARG_COMPONENT}" OUTPUT real_target) + if(real_target) + __get_component_cached_interface(component_interface_cached "${real_target}") + if(component_interface_cached) + set(component_interface "${component_interface_cached}") + # The component is referred to by a new alias, so it should be + # added to the component interface cache. + __set_component_cached_interface("${ARG_COMPONENT}" "${component_interface_cached}") + endif() + endif() + endif() + + set(${ARG_OUTPUT} ${component_interface} PARENT_SCOPE) +endfunction() + +#[[ + __get_component_interface_or_die(COMPONENT + OUTPUT ) + + *COMPONENT[in]* + + Component name, target, target alias or interface. + + *OUTPUT[out]* + + Output variable to store the component interface. + + A simple wrapper for ``__get_component_interface`` that aborts the build + process if the component interface is not found. +#]] +function(__get_component_interface_or_die) + set(options) + set(one_value COMPONENT OUTPUT) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_COMPONENT) + idf_die("COMPONENT option is required") + endif() + + if(NOT DEFINED ARG_OUTPUT) + idf_die("OUTPUT option is required") + endif() + + __get_component_interface(COMPONENT "${ARG_COMPONENT}" OUTPUT component_interface) + if("${component_interface}" STREQUAL "NOTFOUND") + idf_build_get_property(components_discovered COMPONENTS_DISCOVERED) + idf_die("Failed to resolve component '${ARG_COMPONENT}'.\n" + "Available components: ${components_discovered}") + endif() + set(${ARG_OUTPUT} ${component_interface} PARENT_SCOPE) +endfunction() + +#[[ + __get_component_priority(SOURCE + OUTPUT ) + + *SOURCE[in]* + + String identifying the component source. + + *OUTPUT[out]* + + Output variable to store the component priority. + + Return the priority number of a component, where a higher number indicates + a higher priority, based on the given ``source`` string. If the ``source`` + is not valid, return ``NOTFOUND``. +#]] +function(__get_component_priority) + set(options) + set(one_value SOURCE OUTPUT) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_SOURCE) + idf_die("SOURCE option is required") + endif() + + if(NOT DEFINED ARG_OUTPUT) + idf_die("OUTPUT option is required") + endif() + + if("${ARG_SOURCE}" STREQUAL "project_components") + set(priority 3) + elseif("${ARG_SOURCE}" STREQUAL "project_extra_components") + set(priority 2) + elseif("${ARG_SOURCE}" STREQUAL "project_managed_components") + set(priority 1) + elseif("${ARG_SOURCE}" STREQUAL "idf_components") + set(priority 0) + else() + set(priority NOTFOUND) + endif() + + set(${ARG_OUTPUT} "${priority}" PARENT_SCOPE) +endfunction() + +#[[ + __init_component(DIRECTORY + PREFIX + SOURCE ) + + *DIRECTORY[in]* + + ```` where the component is located. + + *PREFIX[in]* + + Prefix for component target names. + + *SOURCE[in]* + + String identifying the component source. + + Initialize the component by creating a component interface target, allowing + properties to be attached to it and also add initial component properties. + At this stage, the component is not included in the build. The actual + component target, named as specified in the `COMPONENT_LIB` property, is + created later when ``add_subdirectory`` is called for the component's + directory based on the project or other component requirements. + + Components are identified by the directory name they reside in. This means + that components with the same name might exist in different directory + paths. In such cases, the component with the higher priority is used. + Priority is determined by the component's source, as defined in + ``__get_component_priority``. If a component with a higher priority than an + existing one is initialized, its name, targets, and other properties remain + the same. Only the directory, priority, and source are updated in the + already initialized component. + + The name of the initialized component is added to the + ``COMPONENTS_DISCOVERED`` build property, and its interface target is added + to the ``COMPONENT_INTERFACES`` build property. These two lists are used by + ``__get_component_interface``, which searches for a component interface + based on the component name, alias, or targets, enabling the setting and + retrieval of component properties. +#]] +function(__init_component) + set(options) + set(one_value DIRECTORY PREFIX SOURCE) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_DIRECTORY) + idf_die("DIRECTORY option is required") + endif() + + if(NOT DEFINED ARG_SOURCE) + idf_die("SOURCE option is required") + endif() + + if(NOT DEFINED ARG_PREFIX) + idf_die("PREFIX option is required") + endif() + + set(component_source "${ARG_SOURCE}") + set(component_prefix "${ARG_PREFIX}") + get_filename_component(component_directory ${ARG_DIRECTORY} ABSOLUTE) + get_filename_component(component_name ${component_directory} NAME) + __get_component_priority(SOURCE "${component_source}" OUTPUT component_priority) + + if(NOT EXISTS "${component_directory}/CMakeLists.txt") + idf_die("Directory '${component_directory}' does not contain a component") + endif() + + if("${component_priority}" STREQUAL "NOTFOUND") + idf_die("Unknown component source '${component_source}' " + "for directory '${component_directory}'") + endif() + + # Collect Kconfig files from this component directory. + idf_build_get_property(target IDF_TARGET) + __collect_kconfig_files_from_directory("${component_directory}" "${target}" + component_kconfig component_projbuild component_rename) + + set(component_project_include "${component_directory}/project_include.cmake") + if(NOT EXISTS "${component_project_include}") + set(component_project_include "") + endif() + + # Track manifest presence + if(EXISTS "${component_directory}/idf_component.yml") + idf_build_set_property(__COMPONENTS_WITH_MANIFESTS "${component_directory}" APPEND) + endif() + + __get_component_interface(COMPONENT "${component_name}" OUTPUT existing_component_interface) + if(existing_component_interface) + # A component with the same name is already initialized. Check if it + # should be replaced with the component currently being initialized. + idf_component_get_property(existing_component_priority + "${existing_component_interface}" + COMPONENT_PRIORITY) + idf_component_get_property(existing_component_directory + "${existing_component_interface}" + COMPONENT_DIR) + idf_component_get_property(existing_component_source + "${existing_component_interface}" + COMPONENT_SOURCE) + + if(${component_priority} EQUAL ${existing_component_priority}) + idf_die("Component directory '${component_directory}' has the same " + "priority '${component_source}' as component directory " + "'${existing_component_directory}'") + + elseif(${component_priority} LESS ${existing_component_priority}) + idf_warn("Component directory '${component_directory}' has lower " + "priority '${component_source}' than component directory " + "'${existing_component_directory}' with priority " + "'${existing_component_source}' and will be ignored") + else() + idf_warn("Component '${component_name}' directory '${component_directory}' " + "with higher priority '${component_source}' will be used instead of " + "component directory '${existing_component_directory}' " + "with lower priority '${existing_component_source}'") + # The newly added component has a higher priority than the existing + # one. Since the component name and targets are identical, update + # the existing component with the new directory and priority. + + # Store the overridden component directory (original component directory) for compatibility with cmakev1 + idf_component_set_property("${component_name}" COMPONENT_OVERRIDEN_DIR "${existing_component_directory}") + + idf_component_set_property("${component_name}" COMPONENT_DIR "${component_directory}") + idf_component_set_property("${component_name}" COMPONENT_SOURCE "${component_source}") + idf_component_set_property("${component_name}" COMPONENT_PRIORITY ${component_priority}) + + # Update component properties with new Kconfig files + idf_component_set_property("${component_name}" __KCONFIG "${component_kconfig}") + idf_component_set_property("${component_name}" __KCONFIG_PROJBUILD "${component_projbuild}") + idf_component_set_property("${component_name}" __SDKCONFIG_RENAME "${component_rename}") + + # Update component project_include.cmake path. + idf_component_set_property("${component_name}" __PROJECT_INCLUDE "${component_project_include}") + endif() + + return() + endif() + + set(component_interface "${component_prefix}_${component_name}") + set(component_alias "${component_prefix}::${component_name}") + # Real component library target that needs to be created by the component. + set(component_target "_${component_interface}") + + idf_build_get_property(build_dir BUILD_DIR) + set(component_build_dir "${build_dir}/esp-idf/${component_name}") + + # Interface target is used to attach all component properties and is also + # used when the component is linked to other targets. + add_library("${component_interface}" INTERFACE) + + # Initialize the component interface cache for the included component. This + # establishes mappings between the component name (with or without + # namespace), target, interface, and alias to the component interface. The + # component interface cache is utilized in the __get_component_interface + # method to retrieve a component interface, for example, based on the + # component name. + __init_component_interface_cache("${component_name}" + "${component_interface}" + "${component_alias}" + "${component_target}" + "${component_priority}") + + idf_build_set_property(COMPONENTS_DISCOVERED "${component_name}" APPEND) + idf_build_set_property(COMPONENT_INTERFACES "${component_interface}" APPEND) + + idf_component_set_property("${component_name}" COMPONENT_LIB "${component_target}") + idf_component_set_property("${component_name}" COMPONENT_TARGET "${component_target}") + idf_component_set_property("${component_name}" COMPONENT_NAME "${component_name}") + idf_component_set_property("${component_name}" COMPONENT_DIR "${component_directory}") + idf_component_set_property("${component_name}" COMPONENT_BUILD_DIR "${component_build_dir}") + idf_component_set_property("${component_name}" COMPONENT_ALIAS "${component_alias}") + idf_component_set_property("${component_name}" COMPONENT_SOURCE "${component_source}") + idf_component_set_property("${component_name}" COMPONENT_INTERFACE "${component_interface}") + idf_component_set_property("${component_name}" COMPONENT_PRIORITY ${component_priority}) + idf_component_set_property("${component_name}" COMPONENT_INCLUDED NO) + idf_component_set_property("${component_name}" __PREFIX "${ARG_PREFIX}") + + # Set component properties for Kconfig files. + idf_component_set_property("${component_name}" __KCONFIG "${component_kconfig}") + idf_component_set_property("${component_name}" __KCONFIG_PROJBUILD "${component_projbuild}") + idf_component_set_property("${component_name}" __SDKCONFIG_RENAME "${component_rename}") + + # Set component project_include.cmake. + idf_component_set_property("${component_name}" __PROJECT_INCLUDE "${component_project_include}") +endfunction() + +#[[ + __dump_component_properties() + + *components[in]* + + List of components whose properties should be displayed. + + Dump all properties for the components listed in ``components``. +#]] +function(__dump_component_properties components) + foreach(component IN LISTS components) + idf_component_get_property(properties "${component}" COMPONENT_PROPERTIES) + idf_msg("component '${component}' properties: ${properties}") + foreach(property IN LISTS properties) + idf_component_get_property(value "${component}" "${property}") + idf_msg(" ${property}: ${value}") + endforeach() + endforeach() +endfunction() + +#[[ + __set_component_cmakev1_properties() + + *component[in]* + + Component name. + + To maintain backward compatibility with cmakev1 components, add the + component properties: INCLUDE_DIRS, PRIV_INCLUDE_DIRS, REQUIRES, + PRIV_REQUIRES, and SRCS. This is achieved by examining the real target + properties of the component and reconstructing the variables that are + otherwise explicitly provided with idf_component_register for cmakev1 + components. +#]] +function(__set_component_cmakev1_properties component_name) + idf_component_get_property(component_real_target "${component_name}" COMPONENT_REAL_TARGET) + idf_component_get_property(component_dir "${component_name}" COMPONENT_DIR) + + if(NOT component_real_target) + return() + endif() + + # Set SRCS and COMPONENT_TYPE properties + get_target_property(component_sources "${component_real_target}" SOURCES) + if(component_sources) + __get_absolute_paths(PATHS "${component_sources}" BASE_DIR "${component_dir}" OUTPUT sources) + idf_component_set_property("${component_name}" SOURCES "${sources}") + idf_component_set_property("${component_name}" COMPONENT_TYPE LIBRARY) + else() + idf_component_set_property("${component_name}" COMPONENT_TYPE CONFIG_ONLY) + endif() + + # Set INCLUDE_DIRS and PRIV_INCLUDE_DIRS properties + get_target_property(include_dirs "${component_real_target}" INCLUDE_DIRECTORIES) + if(NOT include_dirs) + # get_target_property may return include_dirs-NOTFOUND + set(include_dirs "") + endif() + __remove_genex(include_dirs) + + get_target_property(interface_include_dirs "${component_real_target}" INTERFACE_INCLUDE_DIRECTORIES) + if(NOT interface_include_dirs) + # get_target_property may return interface_include_dirs-NOTFOUND + set(interface_include_dirs "") + endif() + __remove_genex(interface_include_dirs) + + __list_intersection(interface_include_dirs include_dirs public_include_dirs_abs) + __list_difference(include_dirs public_include_dirs_abs priv_include_dirs_abs) + + __get_relative_paths(PATHS "${public_include_dirs_abs}" BASE_DIR "${component_dir}" OUTPUT public_include_dirs) + __get_relative_paths(PATHS "${priv_include_dirs_abs}" BASE_DIR "${component_dir}" OUTPUT priv_include_dirs) + + idf_component_set_property(${component_name} INCLUDE_DIRS "${public_include_dirs}") + idf_component_set_property(${component_name} PRIV_INCLUDE_DIRS "${priv_include_dirs}") + + # Set REQUIRES and PRIV_REQUIRES properties + get_target_property(link_libraries ${component_real_target} LINK_LIBRARIES) + set(link_components "") + foreach(lib IN LISTS link_libraries) + __get_component_interface(COMPONENT "${lib}" OUTPUT req_interface) + if(req_interface) + idf_component_get_property(req_name "${req_interface}" COMPONENT_NAME) + list(APPEND link_components "${req_name}") + endif() + endforeach() + + get_target_property(interface_link_libraries ${component_real_target} INTERFACE_LINK_LIBRARIES) + set(interface_link_components "") + foreach(lib IN LISTS interface_link_libraries) + __get_component_interface(COMPONENT "${lib}" OUTPUT req_interface) + if(req_interface) + idf_component_get_property(req_name "${req_interface}" COMPONENT_NAME) + list(APPEND interface_link_components "${req_name}") + endif() + endforeach() + + __list_intersection(interface_link_components link_components requires) + __list_difference(link_components requires priv_requires) + + idf_component_set_property(${component_name} REQUIRES "${requires}") + idf_component_set_property(${component_name} PRIV_REQUIRES "${priv_requires}") +endfunction() + +#[[api +.. cmakev2:function:: idf_component_include + + .. code-block:: cmake + + idf_component_include( + [INTERFACE ]) + + *name[in]* + + Component name. + + *INTERFACE[out,opt]* + + Optional variable where the name of the target interface for the + included component will be stored. + + This is a core function of the build system, responsible for including the + specified component, as indicated by the ``name`` option, into the build + process. It does this by evaluating the component through the + ``add_subdirectory`` call for the component's directory. The component is + responsible for creating its own CMake target, with the target name + provided via the ``COMPONENT_TARGET`` variable. This target is linked to + the component's interface target, which is managed internally by the build + system. The component's interface target is used by other components to + establish their dependencies. + + When the ``INTERFACE`` variable is provided, the name of the included + component interface target will be stored in it. +#]] +function(idf_component_include name) + set(options) + set(one_value INTERFACE) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + __get_component_interface_or_die(COMPONENT "${name}" + OUTPUT component_interface) + + # Check if the component is already included, meaning the add_subdirectory + # has already been called for it and the component has been processed. If + # the component is already included, it will have the COMPONENT_INCLUDED + # set to YES. + idf_component_get_property(component_included "${name}" COMPONENT_INCLUDED) + if(component_included) + idf_dbg("Component '${name}' is already included.") + # Set the parent variable ARG_INTERFACE to include the target name of + # the component interface. + if(DEFINED ARG_INTERFACE) + set(${ARG_INTERFACE} ${component_interface} PARENT_SCOPE) + endif() + return() + endif() + + idf_dbg("Including component '${name}'") + + idf_component_get_property(component_name "${name}" COMPONENT_NAME) + idf_component_get_property(component_interface "${name}" COMPONENT_INTERFACE) + idf_component_get_property(component_target "${name}" COMPONENT_TARGET) + idf_component_get_property(component_directory "${name}" COMPONENT_DIR) + idf_component_get_property(component_alias "${name}" COMPONENT_ALIAS) + idf_component_get_property(component_lib "${name}" COMPONENT_LIB) + idf_component_get_property(component_version "${name}" COMPONENT_VERSION) + + # The following cmakev1 backward-compatible variables are passed to the + # component's CMakeLists.txt. + set(COMPONENT_NAME ${component_name}) + set(COMPONENT_DIR ${component_directory}) + set(COMPONENT_ALIAS ${component_alias}) + set(COMPONENT_LIB ${component_lib}) + set(COMPONENT_VERSION ${component_version}) + set(ESP_PLATFORM 1) + + # The COMPONENT_TARGET is simply an alias for COMPONENT_LIB, which more + # accurately reflects its purpose in the cmakev2 build system. The + # component's responsibility is to create the target with the name + # specified in the COMPONENT_TARGET variable. + set(COMPONENT_TARGET ${component_target}) + + # To handle potential circular dependencies among components, maintain a + # record of currently evaluated components in the __DEPENDENCY_CHAIN. This + # 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}'") + return() + endif() + + list(APPEND __DEPENDENCY_CHAIN "${name}") + # 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}") + + # The component has been evaluated; remove it from the dependency chain. + list(POP_BACK __DEPENDENCY_CHAIN) + + # Mark the component as included and add it to the COMPONENTS_INCLUDED + # build property. Note that COMPONENTS_INCLUDED is read again because it + # might have been updated further down the dependency chain. + idf_component_set_property("${component_name}" COMPONENT_INCLUDED YES) + idf_build_get_property(components_included COMPONENTS_INCLUDED) + list(APPEND components_included "${component_name}") + idf_build_set_property(COMPONENTS_INCLUDED "${components_included}") + + idf_component_get_property(component_interface "${name}" COMPONENT_INTERFACE) + if(DEFINED ARG_INTERFACE) + set(${ARG_INTERFACE} ${component_interface} PARENT_SCOPE) + endif() + + if(NOT TARGET "${component_target}") + # The cmakev1 system allows component to return without creating any + # cmake target. For instance, a component might not be supported for a + # given IDF_TARGET. Ideally, we should fail and abort the build process + # in such cases. However, for compatibility reasons, the absence of + # component's target is silently ignored here. + idf_dbg("Component '${name}' lacks a target") + idf_component_set_property("${component_name}" COMPONENT_REAL_TARGET NOTFOUND) + return() + endif() + + # The component might use the COMPONENT_TARGET name as an alias for another + # target. In this case, although unlikely, identify the real target and + # store it in the COMPONENT_REAL_TARGET component property. + __get_real_target(TARGET "${component_target}" OUTPUT component_real_target) + idf_component_set_property("${component_name}" COMPONENT_REAL_TARGET "${component_real_target}") + + # Create a component interface alias, but only after the real target is + # known, meaning the component is included. The alias has a well-defined + # name and can be used, for example, in generator expressions without + # needing to get the COMPONENT_ALIAS property. + idf_component_get_property(component_alias "${name}" COMPONENT_ALIAS) + add_library("${component_alias}" ALIAS "${component_interface}") + + # Set component target type as COMPONENT_REAL_TARGET_TYPE property. + get_target_property(component_real_target_type "${component_real_target}" TYPE) + idf_component_set_property("${component_name}" COMPONENT_REAL_TARGET_TYPE "${component_real_target_type}") + + # Link the target created by the component to the component interface + # target. + if("${component_real_target_type}" STREQUAL "INTERFACE_LIBRARY") + target_link_libraries("${component_interface}" INTERFACE "${component_real_target}") + elseif("${component_real_target_type}" STREQUAL "STATIC_LIBRARY") + idf_component_get_property(whole_archive "${component_name}" WHOLE_ARCHIVE) + if(whole_archive) + idf_build_get_property(linker_type LINKER_TYPE) + if(linker_type STREQUAL "GNU") + target_link_options("${component_interface}" INTERFACE + "SHELL:-Wl,--whole-archive $ -Wl,--no-whole-archive") + target_link_libraries("${component_interface}" INTERFACE "${component_real_target}") + elseif(linker_type STREQUAL "Darwin") + target_link_options("${component_interface}" INTERFACE + "SHELL:-Wl,-force_load $") + target_link_libraries("${component_interface}" INTERFACE "${component_real_target}") + endif() + else() + target_link_libraries("${component_interface}" INTERFACE "${component_real_target}") + endif() + else() + idf_die("Unsupported target type '${component_real_target_type}' in component '${component_name}'") + endif() + + idf_component_get_property(embed_files "${component_name}" EMBED_FILES) + foreach(file IN LISTS embed_files) + target_add_binary_data(${COMPONENT_TARGET} "${file}" "BINARY") + endforeach() + + idf_component_get_property(embed_txtfiles "${component_name}" EMBED_TXTFILES) + foreach(file IN LISTS embed_txtfiles) + target_add_binary_data(${COMPONENT_TARGET} "${file}" "TEXT") + endforeach() + + # Inject managed dependencies if component manager is enabled + idf_build_get_property(idf_component_manager IDF_COMPONENT_MANAGER) + idf_component_get_property(component_format "${component_name}" COMPONENT_FORMAT) + if(idf_component_manager EQUAL 1) + idf_component_get_property(component_dir "${component_name}" COMPONENT_DIR) + # Check if component has manifest for managed dependency injection + if(EXISTS "${component_dir}/idf_component.yml") + __inject_requirements_for_component_from_manager("${component_name}") + + # Include any managed dependencies + idf_component_get_property(managed_requires "${component_name}" MANAGED_REQUIRES) + idf_component_get_property(managed_priv_requires "${component_name}" MANAGED_PRIV_REQUIRES) + + foreach(dep IN LISTS managed_requires managed_priv_requires) + if(dep) + idf_component_include("${dep}") + endif() + endforeach() + + # For cmakev1 components, automatically link managed dependencies to maintain + # backward compatibility. + if("${component_format}" STREQUAL "CMAKEV1") + idf_component_get_property(component_type "${component_name}" COMPONENT_TYPE) + + # Link managed public requirements + foreach(req IN LISTS managed_requires) + if(req) + idf_component_get_property(req_interface "${req}" COMPONENT_INTERFACE) + if(${component_type} STREQUAL LIBRARY) + target_link_libraries("${component_target}" PUBLIC "${req_interface}") + else() + target_link_libraries("${component_target}" INTERFACE "${req_interface}") + endif() + endif() + endforeach() + + # Link managed private requirements + foreach(req IN LISTS managed_priv_requires) + if(req) + idf_component_get_property(req_interface "${req}" COMPONENT_INTERFACE) + if(${component_type} STREQUAL CONFIG_ONLY) + continue() + endif() + target_link_libraries("${component_target}" PRIVATE "${req_interface}") + endif() + endforeach() + endif() + endif() + endif() + + # Components for cmakev1 use the idf_component_register call and are + # managed in cmakev2 through a shim. This shim sets the COMPONENT_FORMAT + # property to CMAKEV1 and applies global compilation options and + # definitions using CMake's directory-scoped function for backward + # compatibility. Therefore, no additional configuration is required. + if("${component_format}" STREQUAL "CMAKEV1") + return() + endif() + + # The component was not handled by the idf_component_register shim, + # consider it as a component for cmakev2. + idf_component_set_property("${component_name}" COMPONENT_FORMAT CMAKEV2) + + # Add INCLUDE_DIRS, PRIV_INCLUDE_DIRS, REQUIRES, PRIV_REQUIRES and SRCS + # component properties. + __set_component_cmakev1_properties("${component_name}") + + # The component's target is an interface library; nothing more needs to be + # set. + if("${component_real_target_type}" STREQUAL "INTERFACE_LIBRARY") + return() + endif() + + # Set the component archive file name. + set_target_properties(${component_real_target} PROPERTIES OUTPUT_NAME ${component_name} LINKER_LANGUAGE C) + + idf_build_get_property(include_directories INCLUDE_DIRECTORIES GENERATOR_EXPRESSION) + target_include_directories("${component_real_target}" BEFORE PRIVATE "${include_directories}") + + 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) + target_compile_options("${component_real_target}" BEFORE PRIVATE "${compile_options}") +endfunction() diff --git a/tools/cmakev2/esp_docs_cmakev2_extension.py b/tools/cmakev2/esp_docs_cmakev2_extension.py new file mode 100644 index 0000000000..4fae4b6acd --- /dev/null +++ b/tools/cmakev2/esp_docs_cmakev2_extension.py @@ -0,0 +1,172 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import re + +from docutils import nodes # type: ignore +from docutils.statemachine import StringList # type: ignore +from sphinx import addnodes +from sphinx.addnodes import pending_xref +from sphinx.application import Sphinx +from sphinx.builders import Builder +from sphinx.directives import ObjDescT +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain +from sphinx.environment import BuildEnvironment +from sphinx.roles import XRefRole +from sphinx.util.docutils import SphinxDirective +from sphinx.util.nodes import make_refnode +from sphinx.util.nodes import nested_parse_with_titles + +RST_COMMENT_RE = re.compile(r'(?sm)^#\[\[api\n(.*?)\n#\]\]') + + +class CMakeV2IncludeDirective(SphinxDirective): + required_arguments = 1 + + def run(self) -> list: + env = self.env + rel_filename, filename = env.relfn2path(self.arguments[0]) + + # Track the file as a dependency so Sphinx rebuilds if it changes. + env.note_dependency(rel_filename) + + # Read CMakeLists.txt file content. + with open(filename, encoding='utf-8') as f: + raw_content = f.read() + + # Extract all RST comments from the file. + rst_comments = RST_COMMENT_RE.findall(raw_content) + + parsed_nodes = [] + + for rst_comment in rst_comments: + # Temporary node to hold parsed comment. + node = nodes.section() + node.document = self.state.document + + string_list = StringList(rst_comment.splitlines(), source='') + nested_parse_with_titles(self.state, string_list, node) + + parsed_nodes.extend(node.children) + + if not hasattr(env, 'cmakev2_comment_nodes'): + env.cmakev2_comment_nodes = {} + + env.cmakev2_comment_nodes.setdefault(env.docname, []).extend(parsed_nodes) + + return [] + + +class CMakeV2Description(ObjectDescription): + def handle_signature(self, sig: str, signode: addnodes.desc_signature) -> ObjDescT: + signode += addnodes.desc_name(text=sig) + return sig + + def add_target_and_index(self, name: ObjDescT, sig: str, signode: addnodes.desc_signature) -> None: + # Register object target only (no index) + labelid = f'cmakev2-{self.cmakev2_type}-{sig}' + signode['ids'].append(labelid) + self.env.domaindata['cmakev2']['xrefs'][sig] = (self.env.docname, labelid) + + def run(self) -> list[nodes.Node]: + index, node = super().run() + # Add CMakeV2 custom attributes used when function, macro, and variable + # nodes are added to the doctree and sorted. + node['cmakev2-type'] = self.cmakev2_type + node['cmakev2-name'] = self.arguments[0] + return [node] + + +class CMakeV2VariableDirective(CMakeV2Description): + cmakev2_type = 'variable' + + +class CMakeV2FunctionDirective(CMakeV2Description): + cmakev2_type = 'function' + + +class CMakeV2MacroDirective(CMakeV2Description): + cmakev2_type = 'macro' + + +class CMakeV2Domain(Domain): + name = 'cmakev2' + label = 'ESP-IDF build system v2' + + roles = { + 'ref': XRefRole(), + } + + directives = { + 'variable': CMakeV2VariableDirective, + 'function': CMakeV2FunctionDirective, + 'macro': CMakeV2MacroDirective, + 'include': CMakeV2IncludeDirective, + } + + initial_data: dict = { + 'xrefs': {}, + } + + def resolve_xref( + self, + env: BuildEnvironment, + fromdocname: str, + builder: Builder, + typ: str, + target: str, + node: pending_xref, + contnode: nodes.Element, + ) -> nodes.Element | None: + xref = self.data['xrefs'].get(target) + if xref: + todocname, labelid = xref + + # Extract clean text from contnode and wrap in non-literal node + text = contnode.astext() + newnode = nodes.emphasis(text, text) + return make_refnode(builder, fromdocname, todocname, labelid, newnode) + return None + + +def insert_cmakev2_comment_nodes(app: Sphinx, doctree: nodes.document) -> None: + env = app.builder.env + # Get nodes parsed and created by CMakeV2IncludeDirective. + pending = getattr(env, 'cmakev2_comment_nodes', {}).get(env.docname, []) + + # Split nodes parsed in CMakeV2IncludeDirective into buckets based on their + # type. + buckets: dict = {'function': [], 'macro': [], 'variable': []} + for node in pending: + buckets[node['cmakev2-type']].append(node) + + # Sort functions, macros, and variables based on their names. + buckets_sorted = {} + for bucket_name, bucket_list in buckets.items(): + buckets_sorted[bucket_name] = sorted(bucket_list, key=lambda x: x['cmakev2-name']) + + # Traverse through the doctree to locate the target section labels (such as + # _cmakev2_variables) and insert the nodes sorted into the appropriate + # section. + for section in doctree.traverse(nodes.section): + if 'cmakev2-variables' in section['ids']: + section.extend(buckets_sorted['variable']) + elif 'cmakev2-functions' in section['ids']: + section.extend(buckets_sorted['function']) + elif 'cmakev2-macros' in section['ids']: + section.extend(buckets_sorted['macro']) + + +def setup(app: Sphinx) -> dict: + app.add_domain(CMakeV2Domain) + # The nodes generated with CMakeV2IncludeDirective need to be placed into the + # proper sections (functions, macros, variables), but the section labels + # are not known during the directive run() method. Place the nodes into + # section in the doctree-read event. + app.connect('doctree-read', insert_cmakev2_comment_nodes) + + return { + 'version': '0.1', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/tools/cmakev2/idf.cmake b/tools/cmakev2/idf.cmake new file mode 100644 index 0000000000..1972644de8 --- /dev/null +++ b/tools/cmakev2/idf.cmake @@ -0,0 +1,608 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +include_guard(GLOBAL) + +cmake_minimum_required(VERSION 3.22) + +# Update the CMAKE_MODULE_PATH to ensure that additional cmakev2 build system +# modules can be included. The third_party directory from cmakev1 is also +# included for third-party modules shared with cmakev1. +set(CMAKE_MODULE_PATH + "${CMAKE_CURRENT_LIST_DIR}" + "${CMAKE_CURRENT_LIST_DIR}/../cmake/third_party" + ${CMAKE_MODULE_PATH}) + +# The version.cmake file contains the IDF_VERSION variables, which are the same +# for both cmakev1 and cmakev2. +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/version.cmake) + +# The gdbinit.cmake file from cmakev1 contains a single function, +# __generate_gdbinit, which is used in the generation of +# project_description.json. +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/gdbinit.cmake) + +# The openocd.cmake file from cmakev1 contains a single function, +# __get_openocd_options, which is used in the generation of +# project_description.json. +include(${CMAKE_CURRENT_LIST_DIR}/../cmake/openocd.cmake) + +include(component) +include(build) +include(kconfig) +include(project) +include(manager) +include(compat) +include(ldgen) +include(GetGitRevisionDescription) +# For backward compatibility, since externalproject_add is used by +# project_include.cmake in the bootloader component. The ExternalProject +# should probably be included there instead. +include(ExternalProject) + +#[[ + __init_build_version() + + Set the global variables IDF_BUILD_V2, IDF_BUILD_VER and IDF_BUILD_VER_TAG, + as well as the build properties and environmental variables. +#]] +function(__init_build_version) + set(IDF_BUILD_V2 y PARENT_SCOPE) + set(IDF_BUILD_VER 2 PARENT_SCOPE) + set(IDF_BUILD_VER_TAG "v2" PARENT_SCOPE) + idf_build_set_property(IDF_BUILD_V2 y) + idf_build_set_property(IDF_BUILD_VER 2) + idf_build_set_property(IDF_BUILD_VER_TAG "v2") + set(ENV{IDF_BUILD_V2} y) + set(ENV{IDF_BUILD_VER} 2) + set(ENV{IDF_BUILD_VER_TAG} "v2") +endfunction() + +#[[ + __init_idf_path() + + Determine the IDF_PATH value, either from the IDF_PATH environmental + variable or based on the location of this file. Also check there is no + inconsistency between the two. + + Set the IDF_PATH global variable, environment variable and build property. +#]] +function(__init_idf_path) + get_filename_component(idf_path_infer "${CMAKE_CURRENT_LIST_DIR}/../.." REALPATH) + + if(NOT DEFINED ENV{IDF_PATH}) + idf_warn("IDF_PATH environment variable not found. " + "Setting IDF_PATH to '${idf_path_infer}'.") + set(idf_path "${idf_path_infer}") + else() + get_filename_component(idf_path_env "$ENV{IDF_PATH}" REALPATH) + if(NOT "${idf_path_env}" STREQUAL "${idf_path_infer}") + idf_warn("IDF_PATH environment variable is different from inferred IDF_PATH. " + "Check if your project's top-level CMakeLists.txt includes the right " + "CMake files. Environment IDF_PATH will be used for the build: " + "'${idf_path_env}'") + endif() + set(idf_path "${idf_path_env}") + endif() + + idf_build_set_property(IDF_PATH "${idf_path}") + set(IDF_PATH ${idf_path} PARENT_SCOPE) + set(ENV{IDF_PATH} ${idf_path}) +endfunction() + +#[[ + __init_git() + + Determine the executable. + + Set the GIT build property. +#]] +function(__init_git) + find_package(Git) + if(NOT GIT_FOUND) + idf_build_set_property(GIT NOTFOUND) + idf_warn("Git executable not found.") + return() + endif() + + idf_build_set_property(GIT "${GIT_EXECUTABLE}") +endfunction() + +#[[ + __init_idf_version() + + Determine the IDF version from the version.txt file. If it is not present, + use git-describe. If both previous attempts fail, use the IDF_VERSION from + the environment variables as a fallback. + + Set IDF_VER build property. +#]] +function(__init_idf_version) + idf_build_get_property(idf_path IDF_PATH) + idf_build_get_property(git GIT) + if(EXISTS "${idf_path}/version.txt") + file(STRINGS "${idf_path}/version.txt" idf_ver) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${idf_path}/version.txt") + else() + # Try to get the version using git-describe. + git_describe(idf_ver "${idf_path}" "--match=v*.*") + if(NOT idf_ver) + # The Git describe command failed unexpectedly, so the version is + # set to IDF_VERSION as specified in version.cmake. + set(idf_ver "$ENV{IDF_VERSION}") + endif() + endif() + # Trim IDF_VER to the necessary 32 characters. + string(SUBSTRING "${idf_ver}" 0 31 idf_ver) + idf_build_set_property(IDF_VER ${idf_ver}) +endfunction() + +#[[ + __init_python() + + Determine Python interpreter, either from the PYTHON CMake cache variable + or environmental variable or default it to "python". + + If the PYTHON_DEPS_CHECKED CMake cache variable is not set, check if all + Python packages dependencies are satisfied. For instance, if a tool calling + CMake has already performed this check, it doesn't need to be repeated. + + Set the global PYTHON variable, environment variable and build property. +#]] +function(__init_python) + __get_default_value(VARIABLE PYTHON + DEFAULT "python" + OUTPUT python) + file(TO_CMAKE_PATH ${python} python) + + idf_build_set_property(PYTHON "${python}") + set(PYTHON "${python}" PARENT_SCOPE) + + if(PYTHON_DEPS_CHECKED) + idf_dbg("Python dependencies have already been verified.") + return() + endif() + + idf_build_get_property(idf_path IDF_PATH) + + idf_msg("Checking Python dependencies...") + execute_process( + COMMAND "${python}" "${idf_path}/tools/idf_tools.py" "check-python-dependencies" + RESULT_VARIABLE result + ) + if(result EQUAL 1) + # The function check_python_dependencies returns an error code of 1 if + # it fails. + idf_die("Some Python dependencies must be installed. Check above message for details.") + elseif(NOT result EQUAL 0) + # This means that check_python_dependencies.py failed to run entirely, + # and the result should be an error message. + idf_die("Failed to run Python dependency check. Python: ${python}, Error: ${result}") + endif() +endfunction() + +#[[ + __init_idf_target() + + Determine the IDF_TARGET value from the IDF_TARGET environment variable, + the CMake cache variable, or the sdkconfig files. If none of these are set, + use the default esp32 target. Ensure there are no inconsistencies in the + IDF_TARGET values set in different locations. + + Set the IDF_TARGET as a global variable, in the CMake cache, as an + environment variable, and as a build property. +#]] +function(__init_idf_target) + set(sdkconfig_target "") + set(target "") + set(sdkconfig_file "") + + idf_build_get_property(sdkconfig SDKCONFIG) + idf_build_get_property(sdkconfig_defaults SDKCONFIG_DEFAULTS) + + foreach(config ${sdkconfig} ${sdkconfig_defaults}) + idf_dbg("Searching for target in '${config}'") + __get_sdkconfig_option(OPTION CONFIG_IDF_TARGET + SDKCONFIG "${config}" + OUTPUT sdkconfig_target) + if(sdkconfig_target) + set(sdkconfig_file "${config}") + break() + endif() + endforeach() + + __get_default_value(VARIABLE IDF_TARGET + DEFAULT NOTFOUND + OUTPUT target) + + if(NOT target) + if(sdkconfig_target) + idf_msg("IDF_TARGET is not set, guessed '${sdkconfig_target}' " + "from sdkconfig '${sdkconfig_file}'") + set(target "${sdkconfig_target}") + else() + idf_msg("IDF_TARGET not set, using default target: esp32") + set(target "esp32") + endif() + endif() + + # Verify that the chosen target aligns with the CMake cache. + set(cache_target $CACHE{IDF_TARGET}) + if(cache_target) + if(NOT "${cache_target}" STREQUAL "${target}") + idf_die("IDF_TARGET '${cache_target}' in CMake cache does not match " + "currently selected IDF_TARGET '${target}'. " + "To change the target, clear the build directory and sdkconfig file, " + "and build the project again.") + endif() + endif() + + # Verify that the chosen target aligns with the sdkconfig. + if(sdkconfig_target AND "${sdkconfig_file}" STREQUAL "${sdkconfig}") + if("$ENV{_IDF_PY_SET_TARGET_ACTION}" STREQUAL "1") + idf_dbg("The target consistency check for the target specified in ${sdkconfig} " + "was skipped because the set-target action is being executed.") + elseif(NOT "${sdkconfig_target}" STREQUAL "${target}") + idf_die("Target '${sdkconfig_target}' in sdkconfig '${sdkconfig}' " + "does not match currently selected IDF_TARGET '${target}'. " + "To change the target, clear the build directory and sdkconfig file, " + "and build the project again.") + endif() + endif() + + idf_build_set_property(IDF_TARGET "${target}") + set(ENV{IDF_TARGET} ${target}) + set(IDF_TARGET ${target} CACHE STRING "IDF Build Target") +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 + 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. +#]] +function(__init_toolchain) + set(cache_toolchain $CACHE{IDF_TOOLCHAIN}) + set(cache_toolchain_file $CACHE{CMAKE_TOOLCHAIN_FILE}) + + idf_build_get_property(idf_path IDF_PATH) + idf_build_get_property(idf_target IDF_TARGET) + + __get_default_value(VARIABLE IDF_TOOLCHAIN + DEFAULT gcc + OUTPUT toolchain) + if(cache_toolchain) + if(NOT "${cache_toolchain}" STREQUAL "${toolchain}") + idf_die("IDF_TOOLCHAIN '${cache_toolchain}' in CMake cache does not match " + "currently selected IDF_TOOLCHAIN '${toolchain}'. To change " + "the toolchain, clear the build directory and sdkconfig file, " + "and build the project again.") + endif() + endif() + + if("${toolchain}" STREQUAL "clang") + set(toolchain_type "clang-") + endif() + + # Check that the selected target is consistent with the toolchain file in + # the CMake cache. + if(cache_toolchain_file) + string(FIND "${cache_toolchain_file}" "-${toolchain_type}${idf_target}.cmake" found) + if(${found} EQUAL -1) + get_filename_component(cache_toolchain_file_stem "${cache_toolchain_file}" NAME_WE) + idf_die("CMAKE_TOOLCHAIN_FILE '${cache_toolchain_file_stem}' " + "does not match currently selected IDF_TARGET '${idf_target}'. " + "To change the target, clear the build directory and sdkconfig file, " + "and build the project again.") + endif() + endif() + + set(toolchain_file "${idf_path}/tools/cmake/toolchain-${toolchain_type}${idf_target}.cmake") + if(NOT EXISTS ${toolchain_file}) + 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() + +#[[ + __init_ccache() + + Enable ccache if requested through CCACHE_ENABLE. +#]] +function(__init_ccache) + if(NOT CCACHE_ENABLE) + return() + endif() + + find_program(CCACHE_FOUND ccache) + if(CCACHE_FOUND) + idf_msg("ccache will be used for faster recompilation") + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) + else() + idf_warn("enabled ccache in build but ccache program not found") + endif() +endfunction() + +#[[ + __init_components() + + Search for possible component directories categorized by their source, + which could be ``idf_components``, ``project_extra_components``, or + ``project_components``. Components added by the component manager are + initialized later as ``project_managed_components`` after the component + manager is called. + + The search respects the variables set by the user e.g. in the project's + CMakeLists.txt file. These are maintained for backward compatibility. + + COMPONENT_DIRS + If set, component directories are searched exclusively in the paths + provided in ``COMPONENT_DIRS``. + + EXTRA_COMPONENT_DIRS + Includes extra paths to search if ``COMPONENT_DIRS`` is not specified. + + EXTRA_COMPONENT_EXCLUDE_DIRS + List of paths to exclude from searching the component directories. + + Each component is initialized for every component directory found. +#]] +function(__init_components) + idf_build_get_property(idf_path IDF_PATH) + idf_build_get_property(prefix PREFIX) + + __get_component_paths(PATHS "${idf_path}/components" + OUTPUT idf_components) + if(COMPONENT_DIRS) + # The user explicitly stated the locations to search for components. + # For backward compatibility, check that the paths in + # COMPONENT_DIRS exist. + __get_component_paths(PATHS ${COMPONENT_DIRS} + EXCLUDE_PATHS ${EXTRA_COMPONENT_EXCLUDE_DIRS} + SOURCE "COMPONENT_DIRS" + CHECK + OUTPUT project_components) + else() + __get_component_paths(PATHS "${CMAKE_CURRENT_SOURCE_DIR}/main" + "${CMAKE_CURRENT_SOURCE_DIR}/components" + EXCLUDE_PATHS ${EXTRA_COMPONENT_EXCLUDE_DIRS} + OUTPUT project_components) + if(EXTRA_COMPONENT_DIRS) + # For backward compatibility, check that the paths in + # EXTRA_COMPONENT_DIRS exist. + __get_component_paths(PATHS ${EXTRA_COMPONENT_DIRS} + EXCLUDE_PATHS ${EXTRA_COMPONENT_EXCLUDE_DIRS} + SOURCE "EXTRA_COMPONENT_DIRS" + CHECK + OUTPUT project_extra_components) + endif() + endif() + + foreach(path IN LISTS idf_components) + __init_component(DIRECTORY "${path}" + PREFIX "${prefix}" + SOURCE "idf_components") + endforeach() + + foreach(path IN LISTS project_components) + __init_component(DIRECTORY "${path}" + PREFIX "${prefix}" + SOURCE "project_components") + endforeach() + + foreach(path IN LISTS project_extra_components) + __init_component(DIRECTORY "${path}" + PREFIX "${prefix}" + SOURCE "project_extra_components") + endforeach() +endfunction() + +#[[ + __init_submodules() + + Initialize submodules that are not yet initialized, and issue a warning for + submodules that do not match the recorded hash in the git tree. +#]] +function(__init_submodules) + idf_build_get_property(idf_path IDF_PATH) + idf_build_get_property(git GIT) + + # For internal use: Skip the submodule check if running on GitLab CI + # and the job is configured to not clone submodules. + if("$ENV{IDF_SKIP_CHECK_SUBMODULES}" STREQUAL "1") + idf_msg("skip submodule check on internal CI") + return() + endif() + + if(NOT git) + idf_warn("Git executable was not found. Git submodule checks will not be executed. " + "If you have any build issues at all, start by adding git executable to " + "the PATH and rerun cmake to not see this warning again.") + return() + endif() + + execute_process( + COMMAND ${git} submodule status + WORKING_DIRECTORY ${idf_path} + OUTPUT_VARIABLE status + ERROR_VARIABLE stderr + RESULT_VARIABLE rv + ) + + if(rv) + idf_warn("Git submodule status command failed(${rv}): ${stderr}" + "Git submodule checks will not be performed. ") + return() + endif() + + __split(STRING "${status}" + OUTPUT lines + REMOVE_EMPTY) + + # The output of the git submodule status command is not guaranteed to be + # stable. It may be necessary to check the GIT_VERSION_STRING and make + # adjustments in the future. + foreach(line IN LISTS lines) + string(REGEX MATCH "(.)[0-9a-f]+ ([^\( ]+) ?" _ignored "${line}") + set(status "${CMAKE_MATCH_1}") + set(submodule_path "${CMAKE_MATCH_2}") + + if("${status}" STREQUAL "-") # missing submodule + idf_msg("Initialising new submodule ${submodule_path}...") + execute_process( + COMMAND ${git} submodule update --init --recursive ${submodule_path} + WORKING_DIRECTORY ${idf_path} + ERROR_VARIABLE stderr + RESULT_VARIABLE rv + ) + if(rv) + idf_die("Git submodule '${submodule_path}' init failed(${rv}): ${stderr}") + endif() + + elseif(NOT "${status}" STREQUAL " ") + idf_warn("Git submodule ${submodule_path} is out of date. " + "Run the following command to fix: " + "git submodule update --init --recursive") + endif() + + # Ensure CMake is rerun if the submodule's .git file is modified or + # altered, such as in the case of an accidental deinitialization. + get_filename_component(submodule_abs_path ${submodule_path} ABSOLUTE BASE_DIR ${idf_path}) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${submodule_abs_path}/.git) + # If the HEAD file in the submodule's directory changes (i.e., if there + # are commit changes), it will at least display the 'out of date' + # warning. + set(submodule_head "${idf_path}/.git/modules/${submodule_path}/HEAD") + if(EXISTS "${submodule_head}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${submodule_head}) + endif() + + endforeach() +endfunction() + +#[[ + __init_idf_target_arch() + + Set the IDF_TARGET_ARCH value based on the sdkconfig. This means it must be + initialized after the sdkconfig is generated and its CMake version is + included. +#]] +function(__init_idf_target_arch) + if(CONFIG_IDF_TARGET_ARCH_XTENSA) + idf_build_set_property(IDF_TARGET_ARCH "xtensa") + elseif(CONFIG_IDF_TARGET_ARCH_RISCV) + idf_build_set_property(IDF_TARGET_ARCH "riscv") + else() + # Currently, no architecture is specified for Linux host builds. + idf_build_set_property(IDF_TARGET_ARCH "") + endif() +endfunction() + +#[[ + The idf_build_properties interface target is exclusively used to store + information about global build properties and is not linked or used in any + other way. This is created very early so that all the initialization + functions can use it. + + List of build properties + + IDF_PATH + Path to esp-idf directory. + + PREFIX + Prefix used for component target names. + + COMPONENTS_DISCOVERED + List of component names identified by the build system. These + components are initialized and can have properties attached to them. + However, they are not necessarily included in the build through + add_subdirectory. + + COMPONENT_INTERFACES + This is a list of component interface targets for the components in + ``COMPONENTS_DISCOVERED``. It is used when searching for a component, + such as by its name, to set or retrieve the component's properties. + + COMPONENTS_INCLUDED + This is a list of component names that were included in the build, + meaning their CMakeLists.txt files were processed with an + add_subdirectory call. Each component is evaluated exactly once, and + this list serves as a record of which components have already been + evaluated. Although each component can only be evaluated once, it can + be used in multiple idf_component_include calls. If a component is + requested to be included a second time, this list is checked. If the + component is already included, the idf_component_include function + simply returns, as there is nothing further to do except add a new + alias target if requested. +#]] +add_library(idf_build_properties INTERFACE) + +# The __idf_component_interface_cache target is used to maintain internal +# mappings between component identifiers, such as component name or alias, and +# the component interface target, which is the primary target for the +# component. +add_library(__idf_component_interface_cache INTERFACE) + +# Set build system prefix for component targets. +idf_build_set_property(PREFIX "idf") + +# Set project directory property. +idf_build_set_property(PROJECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + +# Set build directory property. +idf_build_set_property(BUILD_DIR "${CMAKE_BINARY_DIR}") + +# Enable the generation of compile_commands.json. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Initialize build system version. +__init_build_version() + +# Initialize IDF_PATH and set it as a global and environmental variable, as +# well as a build property. +__init_idf_path() + +# Determine git executable and set GIT build property. +__init_git() + +# Initialize git submodules. +__init_submodules() + +# Initialize the IDF_VER build property. +__init_idf_version() + +# Determine the Python interpreter and check package dependencies if necessary. +__init_python() + +# Initialize Kconfig system infrastructure. +__init_kconfig() + +# Set IDF_TARGET. +__init_idf_target() + +# Set IDF_TOOLCHAIN, IDF_TOOLCHAIN_FILE and CMAKE_TOOLCHAIN_FILE. +__init_toolchain() + +# Enable ccache if requested. +__init_ccache() + +#[[ + +At this point, the build system infrastructure is ready. +Project-specific operations (component discovery, Kconfig generation, +component manager, etc.) are handled in idf_project_init() after the +project() call. + +#]] diff --git a/tools/cmakev2/kconfig.cmake b/tools/cmakev2/kconfig.cmake new file mode 100644 index 0000000000..fa648833f2 --- /dev/null +++ b/tools/cmakev2/kconfig.cmake @@ -0,0 +1,836 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +# Kconfig utilities for ESP-IDF CMake build system v2 +# This module provides functions for Kconfig processing and sdkconfig generation + +include_guard(GLOBAL) + +include(utilities) +include(build) +include(component) + +#[[ + __init_kconfig() + + Initialize the Kconfig system infrastructure for the build. This function + performs the Kconfig initialization and setup by: + 1. Setting up SDKCONFIG and SDKCONFIG_DEFAULTS build properties + 2. Setting up ESP-IDF root Kconfig files and config directory + 3. Creating the default config directory + + This should be called after component discovery but before component + manager. + + Note: Regular component Kconfig files are collected during component + discovery. +#]] +function(__init_kconfig) + idf_build_get_property(idf_path IDF_PATH) + + # Initialize SDKCONFIG and SDKCONFIG_DEFAULTS build properties using environment + # variables, CMake cache variables, or default values. + if(EXISTS "${CMAKE_SOURCE_DIR}/sdkconfig.defaults") + set(sdkconfig_defaults "${CMAKE_SOURCE_DIR}/sdkconfig.defaults") + else() + set(sdkconfig_defaults "") + endif() + + __get_default_value(VARIABLE SDKCONFIG + DEFAULT "${CMAKE_SOURCE_DIR}/sdkconfig" + OUTPUT sdkconfig) + __get_default_value(VARIABLE SDKCONFIG_DEFAULTS + DEFAULT "${sdkconfig_defaults}" + OUTPUT sdkconfig_defaults) + + __get_absolute_paths(PATHS "${sdkconfig}" OUTPUT sdkconfig) + __get_absolute_paths(PATHS "${sdkconfig_defaults}" OUTPUT sdkconfig_defaults) + + set(sdkconfig_defaults_checked "") + foreach(sdkconfig_default ${sdkconfig_defaults}) + if(NOT EXISTS "${sdkconfig_default}") + idf_die("SDKCONFIG_DEFAULTS '${sdkconfig_default}' does not exist.") + endif() + list(APPEND sdkconfig_defaults_checked ${sdkconfig_default}) + endforeach() + + idf_build_set_property(SDKCONFIG "${sdkconfig}") + idf_build_set_property(SDKCONFIG_DEFAULTS "${sdkconfig_defaults_checked}") + + # Setup ESP-IDF root Kconfig and sdkconfig.rename files. + idf_build_set_property(__ROOT_KCONFIG "${idf_path}/Kconfig") + idf_build_set_property(__ROOT_SDKCONFIG_RENAME "${idf_path}/sdkconfig.rename") + + # Setup and create the default config directory. + idf_build_get_property(build_dir BUILD_DIR) + set(config_dir "${build_dir}/config") + file(MAKE_DIRECTORY "${config_dir}") + idf_build_set_property(CONFIG_DIR "${config_dir}") +endfunction() + +#[[ + __generate_sdkconfig() + + This function performs the complete Kconfig generation process: + 1. Collect Kconfig files from discovered components + 2. Collect Kconfig files from bootloader components + 3. Set up the Kconfig environment with all collected files + 4. Generate all output files (sdkconfig.h, sdkconfig.cmake, etc.) +#]] +function(__generate_sdkconfig) + idf_msg("Generating sdkconfig configuration...") + + # Collect Kconfig files from discovered components + __consolidate_component_kconfig_files() + + # Collect Kconfig files from bootloader components + __collect_kconfig_files_from_bootloader_components() + + # Prepare Kconfig environment using collected files + __setup_kconfig_environment() + + # Generate Kconfig outputs + __generate_kconfig_outputs() + + idf_msg("Generated sdkconfig configuration") +endfunction() + +# ============================================================================= +# KCONFIG COLLECTION FUNCTIONS +# ============================================================================= + +#[[ + __consolidate_component_kconfig_files() + + Consolidate Kconfig files from discovered components into global build + properties. This scans the COMPONENTS_DISCOVERED build property and for + each component, retrieves its Kconfig files from component properties and + adds them to the global __KCONFIGS, __KCONFIG_PROJBUILDS, and + __SDKCONFIG_RENAMES build properties. +#]] +function(__consolidate_component_kconfig_files) + idf_build_get_property(components_discovered COMPONENTS_DISCOVERED) + if(NOT components_discovered) + idf_die("No components discovered. This must be run after component discovery.") + endif() + + # Clean the build properties before adding new ones. This ensures that + # we don't have duplicate Kconfig files in the build properties. + idf_build_set_property(__KCONFIGS "") + idf_build_set_property(__KCONFIG_PROJBUILDS "") + idf_build_set_property(__SDKCONFIG_RENAMES "") + + # Iterate through all discovered components and consolidate their Kconfig files + foreach(component_name IN LISTS components_discovered) + # Get Kconfig files from component properties + idf_component_get_property(component_kconfig "${component_name}" __KCONFIG) + idf_component_get_property(component_projbuild "${component_name}" __KCONFIG_PROJBUILD) + idf_component_get_property(component_rename "${component_name}" __SDKCONFIG_RENAME) + + if(component_kconfig) + idf_build_set_property(__KCONFIGS "${component_kconfig}" APPEND) + endif() + + if(component_projbuild) + idf_build_set_property(__KCONFIG_PROJBUILDS "${component_projbuild}" APPEND) + endif() + + if(component_rename) + idf_build_set_property(__SDKCONFIG_RENAMES "${component_rename}" APPEND) + endif() + endforeach() +endfunction() + +#[[ + __collect_kconfig_files_from_bootloader_components() + + Collect Kconfig files from bootloader components and store them in build + properties. Bootloader components are located in the bootloader_components + directory of the project. This function only runs if the bootloader + component is discovered. +#]] +function(__collect_kconfig_files_from_bootloader_components) + # Check if bootloader component is discovered - only then collect bootloader components + idf_build_get_property(components_discovered COMPONENTS_DISCOVERED) + if(NOT "bootloader" IN_LIST components_discovered) + return() + endif() + + idf_build_get_property(idf_target IDF_TARGET) + idf_build_get_property(project_dir PROJECT_DIR) + + # Find bootloader component directories + __get_component_paths(PATHS "${project_dir}/bootloader_components" + OUTPUT bootloader_component_dirs) + + foreach(bootloader_component_dir ${bootloader_component_dirs}) + __collect_kconfig_files_from_directory("${bootloader_component_dir}" "${idf_target}" + bootloader_kconfig bootloader_projbuild bootloader_rename) + + # Set build properties. Bootloader components are only evaluated for Kconfig files at this stage. + if(bootloader_kconfig) + idf_build_set_property(__KCONFIGS "${bootloader_kconfig}" APPEND) + endif() + if(bootloader_projbuild) + idf_build_set_property(__KCONFIG_PROJBUILDS "${bootloader_projbuild}" APPEND) + endif() + if(bootloader_rename) + idf_build_set_property(__SDKCONFIG_RENAMES "${bootloader_rename}" APPEND) + endif() + endforeach() +endfunction() + +#[[ + __collect_kconfig_files_from_directory(directory target out_kconfigs out_projbuilds out_renames) + + Collect Kconfig files from a single directory. + + *directory[in]* + + Path to the directory to collect Kconfig files from. + + *target[in]* + + Target name for target-specific files. + + *out_kconfigs[out]* + + List of Kconfig files. + + *out_projbuilds[out]* + + List of projbuild files. + + *out_renames[out]* + + List of rename files. +#]] +function(__collect_kconfig_files_from_directory directory target out_kconfigs out_projbuilds out_renames) + file(GLOB all_files "${directory}/*") + + set(kconfig_files "") + set(projbuild_files "") + set(rename_files "") + + foreach(file IN LISTS all_files) + get_filename_component(filename "${file}" NAME) + string(TOLOWER "${filename}" filename_lower) + + # Check for Kconfig file + if(filename_lower STREQUAL "kconfig") + list(APPEND kconfig_files "${file}") + if(NOT filename STREQUAL "Kconfig") + idf_warn("${filename} file should be named 'Kconfig' (uppercase K, rest lowercase). + Full path to the file: ${file}") + endif() + + # Check for Kconfig.projbuild file + elseif(filename_lower STREQUAL "kconfig.projbuild") + list(APPEND projbuild_files "${file}") + if(NOT filename STREQUAL "Kconfig.projbuild") + idf_warn("${filename} file should be named 'Kconfig.projbuild' (uppercase K, rest lowercase). + Full path to the file: ${file}") + endif() + endif() + + # Check for sdkconfig.rename files + if(filename_lower STREQUAL "sdkconfig.rename") + list(APPEND rename_files "${file}") + endif() + + # Look for target-specific sdkconfig.rename files + if(target) + if(filename_lower STREQUAL "sdkconfig.rename.${target}") + list(APPEND rename_files "${file}") + endif() + endif() + endforeach() + + list(SORT kconfig_files) + list(SORT projbuild_files) + list(SORT rename_files) + + set(${out_kconfigs} "${kconfig_files}" PARENT_SCOPE) + set(${out_projbuilds} "${projbuild_files}" PARENT_SCOPE) + set(${out_renames} "${rename_files}" PARENT_SCOPE) +endfunction() + +# ============================================================================= +# KCONFIG ENVIRONMENT FUNCTIONS +# ============================================================================= + +#[[ + __setup_kconfig_environment() + + Setup the Kconfig environment for kconfgen. This function creates the + environment and prepares Kconfig source files. +#]] +function(__setup_kconfig_environment) + # Create the config.env file, which contains all environment variables for the python script + idf_build_get_property(build_dir BUILD_DIR) + set(config_env_path "${build_dir}/config.env") + __create_config_env_file("${config_env_path}") + + # Store environment path in build properties (used by kconfgen) + idf_build_set_property(__CONFIG_ENV_PATH "${config_env_path}") + + # Now prepare Kconfig source files using the prepare_kconfig_files.py script + idf_build_get_property(python PYTHON) + idf_build_get_property(idf_path IDF_PATH) + set(prepare_cmd ${python} "${idf_path}/tools/kconfig_new/prepare_kconfig_files.py" + --list-separator=semicolon + --env-file "${config_env_path}") + + idf_build_set_property(__PREPARE_KCONFIG_CMD "${prepare_cmd}") + + idf_dbg("Preparing Kconfig source files: ${prepare_cmd}") + execute_process( + COMMAND ${prepare_cmd} + RESULT_VARIABLE prepare_result + ) + + if(prepare_result) + idf_die("Failed to prepare Kconfig source files: ${prepare_result}") + endif() +endfunction() + +#[[ + __create_executable_config_env_file(executable) + + *executable[in]* + + Executable target for which to generate ``config.env`` file. + + Generate the ``config.env`` file for the specified ``executable``. The + configuration file will be stored in the build directory, within a + directory named after the ``executable`` target name. The ``kconfigs*.in`` + files, which are generated by ``prepare_kconfig_files.py``, will also be + stored in this directory. + + This function primarily prepares the arguments for the + ``__create_config_env_file`` function based on the components linked to the + ``executable``, ensuring that component Kconfig files are stored in the + appropriate ``kconfigs*.in`` files, depending on whether the component is + linked to the executable. + + The directory where the generated files are stored is added to the + ``executable`` ``CONFIG_ENV_DIR`` property. +#]] +function(__create_executable_config_env_file executable) + + get_target_property(config_env_dir "${executable}" CONFIG_ENV_DIR) + if(config_env_dir) + return() + endif() + + __get_executable_library_or_die(TARGET "${executable}" OUTPUT library) + + idf_library_get_property(components_linked "${library}" LIBRARY_COMPONENTS_LINKED) + + set(kconfigs "") + set(kconfigs_projbuild "") + set(kconfigs_excluded "") + set(kconfigs_projbuild_excluded "") + + idf_build_get_property(components_discovered COMPONENTS_DISCOVERED) + foreach(component_name IN LISTS components_discovered) + idf_component_get_property(component_kconfig "${component_name}" __KCONFIG) + idf_component_get_property(component_projbuild "${component_name}" __KCONFIG_PROJBUILD) + + if(component_kconfig) + if("${component_name}" IN_LIST components_linked) + list(APPEND kconfigs "${component_kconfig}") + else() + list(APPEND kconfigs_excluded "${component_kconfig}") + endif() + endif() + + if(component_projbuild) + if("${component_name}" IN_LIST components_linked) + list(APPEND kconfigs_projbuild "${component_projbuild}") + else() + list(APPEND kconfigs_projbuild_excluded "${component_projbuild}") + endif() + endif() + + endforeach() + + string(REGEX REPLACE "[^A-Za-z0-9_]" "_" executable_sanitized "${executable}") + idf_build_get_property(build_dir BUILD_DIR) + set(kconfigs_dir "${build_dir}/${executable_sanitized}") + + set(kconfigs_path "${kconfigs_dir}/kconfigs.in") + set(kconfigs_projbuild_path "${kconfigs_dir}/kconfigs_projbuild.in") + set(kconfigs_excluded_path "${kconfigs_dir}/kconfigs_excluded.in") + set(kconfigs_projbuild_excluded_path "${kconfigs_dir}/kconfigs_projbuild_excluded.in") + set(config_env_path "${kconfigs_dir}/config.env") + + __create_config_env_file("${config_env_path}" + KCONFIGS "${kconfigs}" + KCONFIGS_PROJBUILD "${kconfigs_projbuild}" + KCONFIGS_EXCLUDED "${kconfigs_excluded}" + KCONFIGS_PROJBUILD_EXCLUDED "${kconfigs_projbuild_excluded}") + + set_target_properties("${executable}" PROPERTIES CONFIG_ENV_DIR "${kconfigs_dir}") +endfunction() + +#[[ + __create_config_env_file(config_env_path + [KCONFIGS ...] + [KCONFIGS_PROJBUILD ...] + [KCONFIGS_EXCLUDED ...] + [KCONFIGS_PROJBUILD_EXCLUDED ...]) + + *config_env_path[in]* + + Path for the generated configuration environment file. + + *KCONFIGS[in,opt]* + + Kconfig file or list of Kconfig files to be sourced in the + ``kconfigs.in`` file. Can be used multiple times. If not provided, the + Kconfig files stored in the ``__KCONFIGS`` build property are used. + + *KCONFIGS_PROJBUILD[in,opt]* + + Kconfig file or list of Kconfig files to be sourced in the + ``kconfigs_projbuild.in`` file. Can be used multiple times. If not + provided, the Kconfig files stored in the ``__KCONFIGS`` build property + are used. + + *KCONFIGS_EXCLUDED[in,opt]* + + Kconfig file or list of Kconfig files to be sourced in the + ``kconfigs_excluded.in`` file. Can be used multiple times. + + *KCONFIGS_PROJBUILD_EXCLUDED[in,opt]* + + Kconfig file or list of Kconfig files to be sourced in the + ``kconfigs_projbuild_excluded.in`` file. Can be used multiple times. + + Generate a configuration file for the ``prepare_kconfig_files.py`` script + at ``config_env_path``. This file includes, among other details, the paths + where ``prepare_kconfig_files.py`` should generate the ``kconfigs*.in`` + files. These are Kconfig files, sourced in the main Kconfig file through + environmental variables, and contain a list of component configurations + that should be sourced. The ``kconfigs*.in`` file paths in the generated + configuration file are located in the same directory as the configuration + file itself. This means that ``prepare_kconfig_files.py`` will produce the + ``kconfigs*.in`` files in the same directory where the configuration file + created by this function is generated. +#]] +function(__create_config_env_file config_env_path) + set(options) + set(one_value) + set(multi_value KCONFIGS KCONFIGS_PROJBUILD KCONFIGS_EXCLUDED KCONFIGS_PROJBUILD_EXCLUDED) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + # Get all necessary build properties + idf_build_get_property(idf_path IDF_PATH) + idf_build_get_property(build_dir BUILD_DIR) + idf_build_get_property(kconfigs __KCONFIGS) + idf_build_get_property(projbuilds __KCONFIG_PROJBUILDS) + idf_build_get_property(renames __SDKCONFIG_RENAMES) + idf_build_get_property(target IDF_TARGET) + idf_build_get_property(toolchain IDF_TOOLCHAIN) + idf_build_get_property(sdkconfig SDKCONFIG) + + if(ARG_KCONFIGS) + set(kconfigs "${ARG_KCONFIGS}") + endif() + + if(ARG_KCONFIGS_PROJBUILD) + set(projbuilds "${ARG_KCONFIGS_PROJBUILD}") + endif() + + set(kconfigs_excluded "") + if(ARG_KCONFIGS_EXCLUDED) + set(kconfigs_excluded "${ARG_KCONFIGS_EXCLUDED}") + endif() + + set(kconfigs_projbuild_excluded "") + if(ARG_KCONFIGS_PROJBUILD_EXCLUDED) + set(kconfigs_projbuild_excluded "${ARG_KCONFIGS_PROJBUILD_EXCLUDED}") + endif() + + # Get IDF version info + __get_sdkconfig_option(OPTION CONFIG_IDF_INIT_VERSION + SDKCONFIG "${sdkconfig}" + OUTPUT idf_init_version) + if(NOT idf_init_version) + set(idf_init_version "$ENV{IDF_VERSION}") + endif() + set(ENV{IDF_INIT_VERSION} "${idf_init_version}") + idf_build_set_property(__IDF_INIT_VERSION "${idf_init_version}") + + # Get IDF_ENV_FPGA from the environment + set(idf_env_fpga "$ENV{IDF_ENV_FPGA}") + if(NOT idf_env_fpga) + set(idf_env_fpga "") + endif() + idf_build_set_property(__IDF_ENV_FPGA "${idf_env_fpga}") + + # Get the config.env.in template path + set(template_path "${idf_path}/tools/kconfig_new/config_buildv2.env.in") + if(NOT EXISTS "${template_path}") + idf_die("Kconfig environment template file not found at ${template_path}") + endif() + + get_filename_component(config_env_dir "${config_env_path}" DIRECTORY) + + # Set up variables for the config.env.in template + set(kconfigs "${kconfigs}") + set(kconfig_projbuilds "${projbuilds}") + set(kconfigs_excluded "${kconfigs_excluded}") + set(kconfigs_projbuild_excluded "${kconfigs_projbuild_excluded}") + set(sdkconfig_renames "${renames}") + set(idf_target "${target}") + set(idf_toolchain "${toolchain}") + set(idf_path "${idf_path}") + set(kconfigs_path "${config_env_dir}/kconfigs.in") + set(kconfigs_projbuild_path "${config_env_dir}/kconfigs_projbuild.in") + set(kconfigs_excluded_path "${config_env_dir}/kconfigs_excluded.in") + set(kconfigs_projbuild_excluded_path "${config_env_dir}/kconfigs_projbuild_excluded.in") + + # Generate the config.env file from the config.env.in template + configure_file("${template_path}" "${config_env_path}") + + idf_dbg("Created config environment file: ${config_env_path}") +endfunction() + +# ============================================================================= +# KCONFIG OUTPUT GENERATION FUNCTIONS +# ============================================================================= + +#[[ + __generate_kconfig_outputs() + + Generate all Kconfig output files. + Generates: sdkconfig.h, sdkconfig.cmake, sdkconfig.json, kconfig_menus.json + + Must be called after __setup_kconfig_environment(). +#]] +function(__generate_kconfig_outputs) + # Get inputs from build properties + idf_build_get_property(sdkconfig SDKCONFIG) + idf_build_get_property(sdkconfig_defaults SDKCONFIG_DEFAULTS) + idf_msg("Project sdkconfig file ${sdkconfig}") + + # Set up output paths + idf_build_get_property(config_dir CONFIG_DIR) + if(NOT config_dir) + idf_die("Kconfig directory not created. Call __init_kconfig() first.") + endif() + + set(sdkconfig_header "${config_dir}/sdkconfig.h") + set(sdkconfig_cmake "${config_dir}/sdkconfig.cmake") + set(sdkconfig_json "${config_dir}/sdkconfig.json") + + # Create and store base kconfgen command for this generation and target reuse + __create_base_kconfgen_command("${sdkconfig}" "${sdkconfig_defaults}") + + # Create and store common kconfgen outputs command + set(kconfgen_outputs_cmd + --output header "${sdkconfig_header}" + --output cmake "${sdkconfig_cmake}" + --output json "${sdkconfig_json}" + --output config "${sdkconfig}" + ) + + idf_build_set_property(__KCONFGEN_OUTPUTS_CMD "${kconfgen_outputs_cmd}") + + # Generate Kconfig outputs using kconfgen + __run_kconfgen() + + # Add the generated config header to build specifications + idf_build_set_property(INCLUDE_DIRECTORIES "${config_dir}" APPEND) + + # Set up file dependencies for CMake reconfiguration + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${sdkconfig}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${sdkconfig_header}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${sdkconfig_cmake}") + + # Add dependency on kconfgen tool + idf_build_get_property(idf_path IDF_PATH) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${idf_path}/tools/kconfig_new/confgen.py") + + # Set up clean files + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY + ADDITIONAL_CLEAN_FILES "${sdkconfig_header}" "${sdkconfig_cmake}") + + # Store output paths in build properties + 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() + +#[[ + __create_base_kconfgen_command(sdkconfig sdkconfig_defaults) + + *sdkconfig[in]* + + Path to sdkconfig file. + + *sdkconfig_defaults[in]* + + List of sdkconfig defaults files. + + Create the base kconfgen command and store it as a build property for + reuse. +#]] +function(__create_base_kconfgen_command sdkconfig sdkconfig_defaults) + # Get all necessary properties for base command + idf_build_get_property(python PYTHON) + idf_build_get_property(root_kconfig __ROOT_KCONFIG) + idf_build_get_property(root_sdkconfig_rename __ROOT_SDKCONFIG_RENAME) + idf_build_get_property(target IDF_TARGET) + + # Set up defaults arguments + set(defaults_args "") + if(sdkconfig_defaults) + foreach(default_file IN LISTS sdkconfig_defaults) + list(APPEND defaults_args "--defaults" "${default_file}") + if(EXISTS "${default_file}.${target}") + list(APPEND defaults_args "--defaults" "${default_file}.${target}") + endif() + endforeach() + 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}" + ${defaults_args} + --env "IDF_BUILD_V2=y") + + # Store base command as a build property + idf_build_set_property(__BASE_KCONFGEN_CMD "${base_kconfgen_cmd}") +endfunction() + +#[[ + __run_kconfgen() + + Run kconfgen and generate all output files using the stored base command. + Assumes that the base command is already created by + __create_base_kconfgen_command(). +#]] +function(__run_kconfgen) + idf_build_get_property(base_kconfgen_cmd __BASE_KCONFGEN_CMD) + idf_build_get_property(kconfgen_outputs_cmd __KCONFGEN_OUTPUTS_CMD) + idf_build_get_property(sdkconfig SDKCONFIG) + idf_build_get_property(config_env_path __CONFIG_ENV_PATH) + + # Create full command with output file paths + set(kconfgen_cmd ${base_kconfgen_cmd} ${kconfgen_outputs_cmd}) + + idf_dbg("Running kconfgen: ${kconfgen_cmd}") + execute_process( + COMMAND ${kconfgen_cmd} + --env-file "${config_env_path}" + RESULT_VARIABLE kconfgen_result + ) + + if(kconfgen_result) + idf_die("Failed to run kconfgen: ${kconfgen_result}") + endif() +endfunction() + +# ============================================================================= +# KCONFIG TARGETS FUNCTIONS +# ============================================================================= + +#[[ +.. cmakev2:function:: idf_create_menuconfig + + .. code-block:: cmake + + idf_create_menuconfig( + TARGET ) + + *executable[in]* + + Executable target for which to create the menuconfig target. + + *TARGET[in]* + + Name of the menuconfig target to be created. + + Create a menuconfig target with the name specified by the ``TARGET`` + option for an ``executable``. +#]] +function(idf_create_menuconfig executable) + set(options) + set(one_value TARGET) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_TARGET) + idf_die("TARGET option is required") + endif() + + if(TARGET "${ARG_TARGET}") + idf_die("TARGET '${ARG_TARGET}' for menuconfig already exists") + endif() + + idf_build_get_property(python PYTHON) + idf_build_get_property(idf_path IDF_PATH) + idf_build_get_property(kconfgen_cmd __BASE_KCONFGEN_CMD) + idf_build_get_property(sdkconfig SDKCONFIG) + idf_build_get_property(root_kconfig __ROOT_KCONFIG) + idf_build_get_property(build_dir BUILD_DIR) + idf_build_get_property(target IDF_TARGET) + idf_build_get_property(toolchain IDF_TOOLCHAIN) + idf_build_get_property(idf_init_version __IDF_INIT_VERSION) + idf_build_get_property(idf_env_fpga __IDF_ENV_FPGA) + idf_build_get_property(kconfgen_outputs_cmd __KCONFGEN_OUTPUTS_CMD) + + # Newer versions of esp-idf-kconfig renamed menuconfig to esp_menuconfig + # Order matters here, we want to use esp_menuconfig if it is available + execute_process( + COMMAND "${python}" -c "import esp_menuconfig" + RESULT_VARIABLE ESP_MENUCONFIG_AVAILABLE + OUTPUT_QUIET ERROR_QUIET + ) + if(ESP_MENUCONFIG_AVAILABLE EQUAL 0) + set(MENUCONFIG_CMD "${python}" -m esp_menuconfig) + else() + set(MENUCONFIG_CMD "${python}" -m menuconfig) + endif() + + __create_executable_config_env_file("${executable}") + get_target_property(config_env_dir "${executable}" CONFIG_ENV_DIR) + + add_custom_target("${ARG_TARGET}" + # Prepare Kconfig source files + COMMAND ${python} "${idf_path}/tools/kconfig_new/prepare_kconfig_files.py" + --list-separator=semicolon + --env-file "${config_env_dir}/config.env" + # Generate config with current settings + COMMAND ${kconfgen_cmd} + --env "IDF_TARGET=${target}" + --env "IDF_TOOLCHAIN=${toolchain}" + --env "IDF_ENV_FPGA=${idf_env_fpga}" + --env "IDF_INIT_VERSION=${idf_init_version}" + --dont-write-deprecated + ${kconfgen_outputs_cmd} + --env-file "${config_env_dir}/config.env" + # Check terminal capabilities + COMMAND ${python} "${idf_path}/tools/check_term.py" + # Run menuconfig + COMMAND ${CMAKE_COMMAND} -E env + "COMPONENT_KCONFIGS_SOURCE_FILE=${config_env_dir}/kconfigs.in" + "COMPONENT_KCONFIGS_PROJBUILD_SOURCE_FILE=${config_env_dir}/kconfigs_projbuild.in" + "COMPONENT_KCONFIGS_EXCLUDED_SOURCE_FILE=${config_env_dir}/kconfigs_excluded.in" + "COMPONENT_KCONFIGS_PROJBUILD_EXCLUDED_SOURCE_FILE=${config_env_dir}/kconfigs_projbuild_excluded.in" + "KCONFIG_CONFIG=${sdkconfig}" + "IDF_TARGET=${target}" + "IDF_TOOLCHAIN=${toolchain}" + "IDF_ENV_FPGA=${idf_env_fpga}" + "IDF_INIT_VERSION=${idf_init_version}" + "IDF_BUILD_V2=y" + ${MENUCONFIG_CMD} "${root_kconfig}" + # Post-menuconfig: insert deprecated options for backward compatibility + COMMAND ${kconfgen_cmd} + --env "IDF_TARGET=${target}" + --env "IDF_TOOLCHAIN=${toolchain}" + --env "IDF_ENV_FPGA=${idf_env_fpga}" + --env "IDF_INIT_VERSION=${idf_init_version}" + ${kconfgen_outputs_cmd} + --env-file "${config_env_dir}/config.env" + USES_TERMINAL + COMMENT "Running menuconfig..." + ) +endfunction() + +#[[ +.. cmakev2:function:: idf_create_confserver + + .. code-block:: cmake + + idf_create_confserver( + TARGET ) + + *executable[in]* + + Executable target for which to create the confserver target. + + *TARGET[in]* + + Name of the confserver target to be created. + + Create a confserver target with the name specified by the ``TARGET`` + option for an ``executable``. +#]] +function(idf_create_confserver executable) + set(options) + set(one_value TARGET) + set(multi_value) + cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN}) + + if(NOT DEFINED ARG_TARGET) + idf_die("TARGET option is required") + endif() + + idf_build_get_property(python PYTHON) + idf_build_get_property(sdkconfig SDKCONFIG) + idf_build_get_property(root_kconfig __ROOT_KCONFIG) + idf_build_get_property(root_sdkconfig_rename __ROOT_SDKCONFIG_RENAME) + idf_build_get_property(base_kconfgen_cmd __BASE_KCONFGEN_CMD) + idf_build_get_property(config_dir CONFIG_DIR) + + __create_executable_config_env_file("${executable}") + get_target_property(config_env_dir "${executable}" CONFIG_ENV_DIR) + + add_custom_target("${ARG_TARGET}" + # Prepare Kconfig source files + COMMAND ${python} "${idf_path}/tools/kconfig_new/prepare_kconfig_files.py" + --list-separator=semicolon + --env-file "${config_env_dir}/config.env" + # Generate kconfig_menus.json + COMMAND ${base_kconfgen_cmd} + --env-file "${config_env_dir}/config.env" + --output json_menus "${config_dir}/kconfig_menus.json" + # Run confserver + COMMAND ${python} -m kconfserver + --env-file "${config_env_dir}/config.env" + --kconfig "${root_kconfig}" + --sdkconfig-rename "${root_sdkconfig_rename}" + --config "${sdkconfig}" + VERBATIM + USES_TERMINAL + COMMENT "Running confserver..." + ) +endfunction() + +#[[ +.. cmakev2:function:: idf_create_save_defconfig + + .. code-block:: cmake + + idf_create_save_defconfig() + + Create the save-defconfig target. +#]] +function(idf_create_save_defconfig) + idf_build_get_property(prepare_cmd __PREPARE_KCONFIG_CMD) + idf_build_get_property(config_env_path __CONFIG_ENV_PATH) + idf_build_get_property(kconfgen_cmd __BASE_KCONFGEN_CMD) + + add_custom_target(save-defconfig + # Prepare Kconfig source files + COMMAND ${prepare_cmd} + # Generate save-defconfig + COMMAND ${kconfgen_cmd} + --dont-write-deprecated + --output savedefconfig "${CMAKE_SOURCE_DIR}/sdkconfig.defaults" + --env-file "${config_env_path}" + USES_TERMINAL + COMMENT "Saving defconfig..." + VERBATIM + ) +endfunction() diff --git a/tools/cmakev2/ldgen.cmake b/tools/cmakev2/ldgen.cmake new file mode 100644 index 0000000000..4e30ce5b13 --- /dev/null +++ b/tools/cmakev2/ldgen.cmake @@ -0,0 +1,102 @@ +include_guard(GLOBAL) + +#[[ + __ldgen_process_template(LIBRARY + TEMPLATE