Breaking Changes

Most components written for Build System v1 build under v2 without modification. There are, however, design differences between v1 and v2 that may require changes to a v1 component. This page is a catalog of those differences and how to adapt to each. To keep a single component building under both v1 and v2, see Managing Component Backward Compatibility. The concepts behind these changes are described in Design and Architecture.

idf_build_add_post_elf_dependency and idf_build_get_post_elf_dependencies are Unavailable

In v1, components that need to run a step after the executable is linked but before the binary image is generated use idf_build_add_post_elf_dependency to register a dependency and idf_build_get_post_elf_dependencies to retrieve the list of such dependencies (see the Build System v1 API). These functions are not available in v2.

In v2, use a build event callback instead. Register a POST_ELF callback with idf_component_register_build_event_callback in the component's project_include.cmake. The callback receives the executable target name; use it to attach a POST_BUILD command (for example with add_custom_command(TARGET ... POST_BUILD ...)) or to add custom targets that depend on the executable. This achieves the same ordering, run after the ELF and before the binary, without relying on internal build properties. See Using Build Event Callbacks.

The BUILD_COMPONENTS Build Property is Unavailable

In v1, the build system collects the components built by the project in the BUILD_COMPONENTS build property. This is achieved by evaluating components early using CMake script mode and collecting them based on the dependencies provided in idf_component_register. The BUILD_COMPONENTS are later evaluated again using CMake's add_subdirectory. This approach imposes several restrictions. First, components cannot express dependencies based on Kconfig variables, because Kconfig variables are not known during the early evaluation. Second, CMake script mode does not allow 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.

v2 removes this two-stage component evaluation, so the BUILD_COMPONENTS build property does not exist. Any attempt to obtain it with idf_build_get_property results in an error. BUILD_COMPONENTS was primarily used in v1 by components to discover which other components were being built and to adjust their behavior, for example by adding source files. In v2, this can be replaced with the $<TARGET_EXISTS:tgt> generator expression.

For example, in v1 the esp_eth component is compiled with the additional esp_eth_netif_glue.c file when the esp_netif component is also being built:

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 work with v2, the use of BUILD_COMPONENTS has to be replaced with:

if(IDF_BUILD_V2)
    target_sources(${COMPONENT_TARGET} PRIVATE "$<$<TARGET_EXISTS:idf::esp_netif>: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 $<TARGET_EXISTS:tgt> generator expression works for both v1 and v2. IDF_BUILD_V2 is used here only to show the two different approaches.

The CMAKE_BUILD_EARLY_EXPANSION Variable is Never Set

In v1, each component is evaluated twice: an early pass in CMake script mode that collects the component's requirements, followed by the regular pass that evaluates the component with add_subdirectory and defines its targets. v1 sets the CMAKE_BUILD_EARLY_EXPANSION variable during the early pass, and components use it to guard work that must run only in the regular pass, such as defining targets or reading build-time state:

if(NOT CMAKE_BUILD_EARLY_EXPANSION)
    # In v1, this runs only in the regular pass, not the early one.
    # ...
endif()

v2 uses single-pass evaluation: each component is evaluated once, as ordinary CMake code, so there is no early pass and CMAKE_BUILD_EARLY_EXPANSION is never set. This affects the two ways the variable is used:

  • if(NOT CMAKE_BUILD_EARLY_EXPANSION) (the usual form) is always true, so its body runs once, exactly as in v1's regular pass. This form needs no change and remains valid under both v1 and v2.

  • if(CMAKE_BUILD_EARLY_EXPANSION) runs only during v1's early pass and never runs in v2. Code that relied on it must be reworked, as v2 has no early pass.

Standardized Kconfig and Kconfig.projbuild File Names

v1 allows the use of Kconfig and Kconfig.projbuild with custom file names in idf_component_register. This is possible because, in v1, the components participating in the build are collected during early component evaluation, which allows custom names for the Kconfig files to be specified. 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 convention, or by including them from Kconfig files that do.

Component Configuration Visibility

In v1, the sdkconfig is generated from the Kconfig files of the components that are part of the build. This means that if a component is not included in the build, for instance because it is not required as a dependency of another component, its configuration is not visible or available when components are evaluated. In v2, the sdkconfig is generated from the Kconfig files of all available components. This means that the presence of a configuration option does not guarantee that the component providing it is participating in the build, unlike in v1.

For example, a component might link vfs only when CONFIG_VFS_SUPPORT_IO is set. This works in v1 because the option is visible only when vfs is part of the build:

if(CONFIG_VFS_SUPPORT_IO)
    target_link_libraries(${COMPONENT_LIB} PUBLIC idf::vfs)
endif()

In v2 the option is visible even when vfs is not in the build, so the condition can be true while the idf::vfs target does not exist. Because a configuration option no longer implies that its component is present, a component that needs vfs when the option is set must bring it into the build explicitly with idf_component_include (see Configuring Component Dependencies):

if(CONFIG_VFS_SUPPORT_IO)
    idf_component_include(vfs)
    target_link_libraries(${COMPONENT_LIB} PUBLIC idf::vfs)
endif()

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 another component's functionality is available simply because its Kconfig variables are set. For example, if CONFIG_VFS_SUPPORT_IO 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 build and that the component declares a dependency on vfs in its CMakeLists.txt.

Recursive Evaluation of Components

In v1, the components participating in the build are collected in the BUILD_COMPONENTS build property during the early evaluation phase, prior to their evaluation with add_subdirectory. This means v1 is aware of all components participating in the build and their dependencies before CMake evaluates them, which allows v1 to evaluate components non-recursively 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. It is therefore important to ensure that all variables used by a component are properly initialized before use. A typical problem involves CMake lists and the APPEND operation.

# 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 component's project_include.cmake is included only after the project_include.cmake files of its dependencies, 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 build, and in the order in which the components are discovered. 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

v2 strictly adheres to the component precedence for components with the same name, as described in Multiple Components with the Same Name. While v1 allows components discovered in directories specified with the EXTRA_COMPONENT_DIRS variable to be overridden by Local Directory Dependencies specified in the idf_component.yml manifest file, this is no longer possible in v2.

The Behavior of idf_component_optional_requires has Changed

In v1, the idf_component_optional_requires function adds a dependency on a specified component only if that component is already included in the build, for instance because it is already required by another component. To achieve this, v1 examines the BUILD_COMPONENTS build property, which is generated during the early evaluation phase and lists all components involved in the build.

In v2, there is no early collection phase and BUILD_COMPONENTS does not exist. The build system discovers components as it evaluates dependencies, so v2 cannot use the same "only if already in the build" check and has to choose a different rule.

The build system supports two behaviors, controlled by the IDF_COMPONENT_OPTIONAL_REQUIRES_MODE build property:

  • IMMEDIATE (default): When a component calls idf_component_optional_requires(type req_component), the build system includes req_component and links it to the caller if it is recognized (discovered). No check is made whether the rest of the project actually needs that component. This is safe for multi-binary projects (multiple executables or binaries), but it can pull in more components than necessary and increase build time.

  • DEFERRED: The build system does not include or link immediately. It records the request and resolves it later in idf_build_library: the optional component is linked only if it ends up in that library's dependency graph. This matches v1 semantics and keeps the number of linked components minimal. It must not be used when building more than one library (see below).

A multi-binary project is one that creates more than one executable or binary, for example several application executables built from the same tree (see Building Multiple Binaries). Such a project calls idf_build_library or idf_build_executable more than once. In v2, component targets are shared globally across all libraries. If IDF_COMPONENT_OPTIONAL_REQUIRES_MODE is set to DEFERRED, the build system resolves optional requirements when it processes each library. When it processes the second or a later library, it may add new links to component targets that are already used by the first library. The first library's metadata (such as the list of linker fragments or linked components) was already computed when that library was processed and is not updated. As a result, linker script generation and section placement for the first library can be incorrect or stale. For this reason, DEFERRED mode is not allowed when more than one library is built; the build fails with an error in that case. IMMEDIATE mode does not have this problem, because optional requirements are applied during component evaluation, before any per-library metadata is computed. Its side effect is that it can pull in more components than necessary and increase build time.

idf_project_default (the usual entry point for a single-executable project) sets IDF_COMPONENT_OPTIONAL_REQUIRES_MODE to DEFERRED before building the default executable when no libraries have been created yet. So if your project uses idf_project_default() and builds only one executable, you get DEFERRED behavior automatically and do not need to do anything.

If you do not use idf_project_default and instead call idf_project_init and then the lower-level API (idf_build_executable, idf_build_library) yourself, the default mode is IMMEDIATE. If you build only one library or executable and want the same efficient, v1-like behavior as idf_project_default, set the mode to DEFERRED yourself after project init:

idf_project_init()
idf_build_set_property(IDF_COMPONENT_OPTIONAL_REQUIRES_MODE DEFERRED)
idf_build_executable(my_app COMPONENTS main ...)
# ... rest of your project ...

Do not set IDF_COMPONENT_OPTIONAL_REQUIRES_MODE to DEFERRED if you build multiple libraries; the build will error. Keep the default IMMEDIATE in that case.


Was this page helpful?