Merge branch 'feat/buildv2_examples' into 'master'

feat(cmakev2): Added cmakev2 examples

Closes IDF-13070, IDF-14185, and IDF-15153

See merge request espressif/esp-idf!45400
This commit is contained in:
Sudeep Mohanty
2026-02-25 13:58:27 +01:00
95 changed files with 2421 additions and 18 deletions
+17 -1
View File
@@ -15,10 +15,26 @@ if( NOT CONFIG_ESP_WIFI_ENABLED
"src/wifi_netif.c"
"src/wifi_default_ap.c")
# In build system v2, idf_component_optional_requires() includes the target
# component into the build immediately, so these stub sources are compiled
# even on targets where Wi-Fi is not supported. The stubs need esp_event and
# esp_netif headers, so those dependencies must be declared here for v2.
#
# In build system v1, idf_component_optional_requires() only links a
# component that is already part of the build. On non-Wi-Fi targets, this
# component is never pulled in, so the stubs are never compiled and the
# dependencies are not needed. Additionally, the CMAKE_BUILD_EARLY_EXPANSION
# guard ensures that the stubs are not compiled for v1.
set(priv_reqs "")
if(IDF_BUILD_V2)
set(priv_reqs "esp_event" "esp_netif")
endif()
# This component provides "esp_wifi" "wifi_apps/nan_app" headers if WiFi not enabled
# (implementation supported optionally in a managed component esp_wifi_remote)
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include" "wifi_apps/nan_app/include")
INCLUDE_DIRS "include" "wifi_apps/nan_app/include"
PRIV_REQUIRES ${priv_reqs})
add_subdirectory(remote) # wifi-remote on esp32p4/h2 (no wifi)
return()
endif()
+2
View File
@@ -7,6 +7,8 @@ Build System v2
ESP-IDF CMake-based build system v2, referred to in this documentation simply as v2 or build system, is a successor to the original CMake-based :doc:`/api-guides/build-system`, referred to as v1. The v2 addresses limitations introduced in the previous version while trying to maintain backward compatibility for components written for v1. The most significant changes include the ability to use Kconfig variables to specify component dependencies, the removal of early component evaluation using CMake script mode, and support for writing components using the native CMake approach. While v2 aims to be as backward compatible with v1 as possible, meaning most components written for v1 should work without modification with v2, there are design differences between v1 and v2 that may require changes in v1 components to work with v2. The incompatibilities are described in :ref:`cmakev2-breaking-changes`.
Example applications for Build System v2 are described in the :idf_file:`Build System v2 examples README <examples/build_system/cmakev2/README.md>`.
Creating a New Project
======================
+155
View File
@@ -0,0 +1,155 @@
# Build System v2 Examples
This directory contains examples demonstrating **ESP-IDF Build System v2** (`cmakev2`). Build System v2 is the next-generation build system for ESP-IDF, offering improved architecture, better CMake integration, and enhanced features. Build System v2 attempts to be backward compatible with Build System v1 applications and components as much as possible. More information about Build System v2 can be found in the ESP-IDF Programming Guide under **API Guides****Build System v2**.
> **Note:** Build System v2 is currently a **Technical Preview**.
---
## Getting Started
### Migrating an Existing Project
Change your project's `CMakeLists.txt` from:
```cmake
# Build System v1
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_project)
```
To:
```cmake
# Build System v2
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(my_project C CXX ASM)
idf_project_default()
```
This update is sufficient for most ESP-IDF projects. Components and application code remain unchanged.
### Building Examples
Building any `cmakev2` example follows the standard workflow:
```bash
cd examples/build_system/cmakev2/get-started/<example_name>
idf.py set-target <target>
idf.py build
idf.py flash monitor
```
---
## Get-Started Examples
These examples mirror the ones in `examples/get-started/` but use Build System v2.
### hello_world
[get-started/hello_world/](./get-started/hello_world/)
The simplest ESP-IDF project with `cmakev2`. It prints "Hello World" and demonstrates the minimal project structure required: a `CMakeLists.txt` with `idf_project_default()` and a main component. Start here to understand how a basic v2 project is set up.
---
## Build System Features Examples
These examples demonstrate advanced build system capabilities unique to or enhanced in `cmakev2`. The components used in the examples in this section are registered with `idf_component_register`, which ensures that newly written components remain compatible with Build System v1.
### component_manager
[features/component_manager/](./features/component_manager/)
Shows how to use the ESP Component Registry with `cmakev2`. Declares dependencies in `idf_component.yml`, which are automatically downloads from ESP Component Registry during the build.
### import_lib
[features/import_lib/](./features/import_lib/)
Demonstrates importing third-party CMake libraries using CMake's `ExternalProject_Add()` module. The example downloads and builds [tinyxml2](https://github.com/leethomason/tinyxml2) from GitHub, wraps it as an IDF component, and uses it to parse XML data. This approach is recommended for C++ libraries that use exceptions, as it properly handles ESP-IDF's C++ runtime integration through `PRIV_REQUIRES cxx`. For simpler C libraries, see `import_lib_direct` which demonstrates direct integration without component wrappers.
### import_prebuilt
[features/import_prebuilt/](./features/import_prebuilt/)
Shows how to import pre-compiled static libraries into your project using `add_prebuilt_library()`. The example includes a `prebuilt/` directory containing a component that gets compiled separately, with its output (`libprebuilt.a` and headers) consumed by the main application. Useful for distributing proprietary libraries or speeding up builds with pre-compiled components.
### multi_config
[features/multi_config/](./features/multi_config/)
Demonstrates building multiple configurations of a single application using CMake presets. The example defines development and production presets, each with different `sdkconfig.defaults` files, separate build directories, and conditional source file compilation. Useful for building binaries for different product variants or deployment environments from one codebase.
Build with presets:
```bash
idf.py --preset default build # Development build
idf.py --preset prod1 build # Product 1 build
idf.py --preset prod2 build # Product 2 build
```
### plugins
[features/plugins/](./features/plugins/)
Demonstrates link-time plugin registration using the `WHOLE_ARCHIVE` component property. Plugins register themselves automatically at startup using `__attribute__((constructor))` functions, without requiring explicit function calls from the main application. The example shows both dynamic registration (constructor functions) and static registration (linker sections with `KEEP()`). Useful for building extensible applications where plugins can be added or removed by simply including or excluding components.
### idf_as_lib
[features/idf_as_lib/](./features/idf_as_lib/)
Shows how to use ESP-IDF components as a library in external CMake projects. The example showcases how a non-IDF project can invoke the ESP-IDF build system to create a library which the executable can link against.
---
## cmakev2-Specific Examples
The following examples demonstrate capabilities that are unique to Build System v2 and cannot be achieved with Build System v1.
### conditional_component
[features/conditional_component/](./features/conditional_component/)
Demonstrates conditional component inclusion driven by Kconfig options, with all components written as pure CMake static libraries (no `idf_component_register`).
### multi_binary
[features/multi_binary/](./features/multi_binary/)
Demonstrates building multiple independent firmware binaries from a single project in one build command. Each binary has its own entry point, component dependencies, and flash target.
The project defines two applications (`app1` and `app2`) with shared and distinct components:
Build and flash:
```bash
idf.py build # Creates both app1.bin and app2.bin
idf.py app1-flash monitor # Flash and monitor app1
idf.py app2-flash monitor # Flash and monitor app2
```
This is useful for creating firmware variants with different features, manufacturing test firmware alongside production firmware, or modular applications where different binaries serve different purposes.
### import_lib_direct
[features/import_lib_direct/](./features/import_lib_direct/)
Demonstrates a cmakev2-specific capability of importing external CMake libraries directly at the project level using `FetchContent` without wrapping them as IDF components. The example downloads [lwjson](https://github.com/MaJerle/lwjson), a lightweight JSON parser, and links it directly to the main component using standard CMake `target_link_libraries()`. This pattern works well for pure C libraries and simplifies third-party library integration.
---
## Further Reading
For comprehensive documentation on Build System v2 APIs, migration details, and advanced usage, see the Build System v2 guide in the ESP-IDF Programming Guide under **API Guides****Build System v2**.
---
## Contributing
When adding new examples:
1. Use `cmakev2` APIs where applicable
2. Include a README.md explaining the example
3. Test on multiple targets (esp32, esp32c3, esp32s3)
4. Document which `cmakev2` features are demonstrated
@@ -0,0 +1,7 @@
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(component_manager C CXX ASM)
idf_project_default()
@@ -0,0 +1,68 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Using the component manager for downloading dependencies
This example demonstrates how to use [IDF Component Manager](https://pypi.org/project/idf-component-manager/) for downloading dependencies from [ESP Component Registry](https://components.espressif.com). More details and use cases of IDF Component Manager can be found in the programming guide under `API Guides` -> `Tools` -> `IDF Component Manager`.
## How to use the example
### Hardware Required
This example is designed to work with any commonly available development kit.
### Build and Flash
Run `idf.py reconfigure` to configure this project. During CMake execution the component manager will process data from the manifest file `./main/idf_component.yml` where 2 dependencies are defined:
- `idf: ">=6.0"` - Specifies required version of ESP-IDF.
- `example/cmp: ">=3.3.3"` - Defines dependency on [example/cmp](https://components.espressif.com/component/example/cmp) component that is used by the main component.
CMake Output:
```
...
-- IDF: Component manager round 1...
NOTICE: Dependencies lock doesn't exist, solving dependencies.
...NOTICE: Updating lock file at /Users/sudeepmohanty/esp/esp-idf/examples/build_system/cmakev2/features/component_manager/dependencies.lock
NOTICE: Processing 2 dependencies:
NOTICE: [1/2] example/cmp (3.3.9~1)
NOTICE: [2/2] idf (6.1.0)
...
```
Content of the `./managed_components` directory after successful build:
```
▶ find ./managed_components
./managed_components
./managed_components/example__cmp
./managed_components/example__cmp/CMakeLists.txt
./managed_components/example__cmp/LICENSE
./managed_components/example__cmp/changelog.md
./managed_components/example__cmp/idf_component.yml
./managed_components/example__cmp/include
./managed_components/example__cmp/include/cmp.h
./managed_components/example__cmp/README.md
./managed_components/example__cmp/examples
./managed_components/example__cmp/examples/cmp_ex
./managed_components/example__cmp/examples/cmp_ex/CMakeLists.txt
./managed_components/example__cmp/examples/cmp_ex/README.md
./managed_components/example__cmp/examples/cmp_ex/main
./managed_components/example__cmp/examples/cmp_ex/main/CMakeLists.txt
./managed_components/example__cmp/examples/cmp_ex/main/idf_component.yml
./managed_components/example__cmp/examples/cmp_ex/main/cmp_ex.c
./managed_components/example__cmp/cmp.c
./managed_components/example__cmp/.component_hash
```
Flash the project and run the serial monitor to view the output:
```
idf.py -p PORT flash monitor
```
### Example Output
The example outputs a line from the `cmp_hello` function from the component downloaded by the component manager.
```
Hello from example component!
```
@@ -0,0 +1,2 @@
idf_component_register(SRCS "component_manager.c"
INCLUDE_DIRS ".")
@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include "cmp.h"
void app_main(void)
{
cmp_hello();
}
@@ -0,0 +1,36 @@
dependencies:
# Required IDF version
idf: ">=6.0"
# Defining a dependency from the ESP Component Registry:
# https://components.espressif.com/component/example/cmp
example/cmp: "^3.3.3"
# # Other ways to define dependencies
#
# # For components maintained by Espressif only name can be used.
# # Same as `espressif/cmp`
# component: "~1.0.0"
#
# # Or in a longer form with extra parameters
# component2:
# version: ">=2.0.0"
#
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect for the `main` component.
# # All dependencies of `main` are public by default.
# public: true
#
# # For components hosted on non-default registry:
# service_url: "https://componentregistry.company.com"
#
# # For components in git repository:
# test_component:
# path: test_component
# git: ssh://git@gitlab.com/user/components.git
#
# # For test projects during component development
# # components can be used from a local directory
# # with relative or absolute path
# some_local_component:
# path: ../../projects/component
@@ -0,0 +1,41 @@
# This example demonstrates conditional component inclusion with Build System v2
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(conditional_component C CXX ASM)
# Initialize the project
idf_project_init()
# Build the executable.
# main component provides the main function and conditionally
# includes logging_util and math_util. esptool_py is required
# for binary generation.
idf_build_executable(${CMAKE_PROJECT_NAME}.elf
COMPONENTS main esptool_py)
# Generate binary from executable
idf_build_binary(${CMAKE_PROJECT_NAME}.elf
OUTPUT_FILE "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin"
TARGET ${CMAKE_PROJECT_NAME}_binary)
# Create flash target
idf_flash_binary(${CMAKE_PROJECT_NAME}_binary
TARGET app-flash
NAME "app"
FLASH)
# Check binary size
idf_check_binary_size(${CMAKE_PROJECT_NAME}_binary)
# Create app target
add_custom_target(app ALL DEPENDS ${CMAKE_PROJECT_NAME}_binary)
# Generate metadata
idf_build_generate_metadata(BINARY ${CMAKE_PROJECT_NAME}_binary)
# Create utility targets for configuration
idf_create_menuconfig(${CMAKE_PROJECT_NAME}.elf TARGET menuconfig)
idf_create_confserver(${CMAKE_PROJECT_NAME}.elf TARGET confserver)
@@ -0,0 +1,41 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Conditional Component Inclusion Example
This example demonstrates how to conditionally include components at build time using Build System v2's pure CMake approach. Components are included or excluded based on Kconfig configuration options, and all components are written using standard CMake functions rather than IDF-specific abstractions.
## Overview
In Build System v2, components can be written as plain CMake static libraries using `add_library`, `target_link_libraries`, and `target_include_directories`. A component is brought into the build on demand by calling `idf_component_include()`, which sets up the component's target name and invokes its CMakeLists.txt. Dependencies on other components are expressed using the `idf::<name>` alias targets.
This example uses that mechanism to conditionally pull in two utility components — `logging_util` and `math_util` — from within the `main` component, based on Kconfig options.
## Project Structure
The project is initialized manually with `idf_project_init()` and `idf_build_executable()`, which gives precise control over which components are seeded into the build. The `main` component itself is a pure CMake static library. It conditionally includes `logging_util` and/or `math_util` at configure time by calling `idf_component_include()` inside `if(CONFIG_...)` guards and linking the resulting targets with `target_link_libraries`.
`logging_util` depends on IDF's `log` component for `ESP_LOG*` macros. It pulls `log` into the build with `idf_component_include(log)` and links it via the `idf::log` alias target. `math_util` has no IDF dependencies and is a self-contained static library.
## Configuration Options
Two Kconfig options control which components are included:
- `CONFIG_EXAMPLE_ENABLE_LOGGING` — includes and links the `logging_util` component.
- `CONFIG_EXAMPLE_ENABLE_MATH` — includes and links the `math_util` component.
Both default to enabled. Toggle them via menuconfig under **Conditional Component Example Configuration**.
## Building
```bash
cd examples/build_system/cmakev2/conditional_component
idf.py set-target <target>
idf.py build
```
To change which components are included, run `idf.py menuconfig` before rebuilding.
## Output
With both components enabled the serial output shows info and warning log messages from `logging_util` and the results of add, subtract, and multiply operations from `math_util`. Disabling either option removes the corresponding section and replaces it with a "DISABLED" message.
@@ -0,0 +1,8 @@
# This component provides the logging utility functions.
# Explicitly pull in the IDF log component before using it.
idf_component_include(log)
add_library(${COMPONENT_TARGET} STATIC logging_util.c)
target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${COMPONENT_TARGET} PRIVATE idf::log)
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Log an informational message
*
* @param message The message to log
*/
void log_info(const char *message);
/**
* @brief Log a warning message
*
* @param message The message to log
*/
void log_warn(const char *message);
/**
* @brief Log an error message
*
* @param message The message to log
*/
void log_error(const char *message);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "logging_util.h"
#include "esp_log.h"
static const char *TAG = "logging_util";
void log_info(const char *message)
{
ESP_LOGI(TAG, "%s", message);
}
void log_warn(const char *message)
{
ESP_LOGW(TAG, "%s", message);
}
void log_error(const char *message)
{
ESP_LOGE(TAG, "%s", message);
}
@@ -0,0 +1,3 @@
# This component provides the math utility functions.
add_library(${COMPONENT_TARGET} STATIC math_util.c)
target_include_directories(${COMPONENT_TARGET} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Add two integers
*
* @param a First operand
* @param b Second operand
* @return Sum of a and b
*/
int math_add(int a, int b);
/**
* @brief Subtract two integers
*
* @param a First operand
* @param b Second operand
* @return a minus b
*/
int math_subtract(int a, int b);
/**
* @brief Multiply two integers
*
* @param a First operand
* @param b Second operand
* @return Product of a and b
*/
int math_multiply(int a, int b);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "math_util.h"
int math_add(int a, int b)
{
return a + b;
}
int math_subtract(int a, int b)
{
return a - b;
}
int math_multiply(int a, int b)
{
return a * b;
}
@@ -0,0 +1,21 @@
# This component provides the main function and conditionally
# includes logging_util and math_util based on the Kconfig options.
# Provide the main component as a static library
add_library(${COMPONENT_TARGET} STATIC main.c)
if(CONFIG_EXAMPLE_ENABLE_LOGGING)
idf_component_include(logging_util)
target_link_libraries(${COMPONENT_TARGET} PRIVATE idf::logging_util)
idf_msg("Conditional component 'logging_util' is ENABLED")
else()
idf_msg("Conditional component 'logging_util' is DISABLED")
endif()
if(CONFIG_EXAMPLE_ENABLE_MATH)
idf_component_include(math_util)
target_link_libraries(${COMPONENT_TARGET} PRIVATE idf::math_util)
idf_msg("Conditional component 'math_util' is ENABLED")
else()
idf_msg("Conditional component 'math_util' is DISABLED")
endif()
@@ -0,0 +1,17 @@
menu "Conditional Component Example Configuration"
config EXAMPLE_ENABLE_LOGGING
bool "Enable Logging Utility Component"
default y
help
Enable the logging_util component to demonstrate conditional
component inclusion in Build System v2.
config EXAMPLE_ENABLE_MATH
bool "Enable Math Utility Component"
default y
help
Enable the math_util component to demonstrate conditional
component inclusion in Build System v2.
endmenu
@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include "sdkconfig.h"
#ifdef CONFIG_EXAMPLE_ENABLE_LOGGING
#include "logging_util.h"
#endif
#ifdef CONFIG_EXAMPLE_ENABLE_MATH
#include "math_util.h"
#endif
void app_main(void)
{
printf("Conditional Component Example\n");
printf("==============================\n\n");
printf("This example demonstrates Build System v2's idf_component_include()\n");
printf("function for conditional component inclusion.\n\n");
#ifdef CONFIG_EXAMPLE_ENABLE_LOGGING
printf("Logging component is ENABLED\n");
log_info("This is an info message from logging_util");
log_warn("This is a warning message from logging_util");
#else
printf("Logging component is DISABLED\n");
#endif
printf("\n");
#ifdef CONFIG_EXAMPLE_ENABLE_MATH
printf("Math component is ENABLED\n");
int a = 10, b = 5;
printf(" add(%d, %d) = %d\n", a, b, math_add(a, b));
printf(" subtract(%d, %d) = %d\n", a, b, math_subtract(a, b));
printf(" multiply(%d, %d) = %d\n", a, b, math_multiply(a, b));
#else
printf("Math component is DISABLED\n");
#endif
printf("\n");
printf("You can change the component configuration via menuconfig:\n");
printf(" idf.py menuconfig -> Conditional Component Example Configuration\n");
}
@@ -0,0 +1,55 @@
# A standard CMake project that uses ESP-IDF as a library.
# When ESP_PLATFORM is set (idf.py invocation), IDF components are bundled
# into a single library and linked into the executable. Without it the
# project builds a plain host executable with no IDF dependency.
cmake_minimum_required(VERSION 3.22)
if(ESP_PLATFORM)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
endif()
project(idf_as_lib C CXX ASM)
# Single source file for both builds. IDF code paths inside main.c are
# guarded by #ifdef ESP_PLATFORM.
add_executable(${CMAKE_PROJECT_NAME}.elf main.c)
if(ESP_PLATFORM)
# Initialise the IDF build system and bundle the required components
# into a single linkable library.
idf_project_init()
# Create ESP-IDF library with specified components.
idf_build_library(idf_components COMPONENTS spi_flash esp_system)
# Apply IDF build properties to the custom executable so it receives
# include paths, compile definitions, and compile options that IDF normally adds.
idf_build_get_property(include_directories INCLUDE_DIRECTORIES GENERATOR_EXPRESSION)
target_include_directories(${CMAKE_PROJECT_NAME}.elf PRIVATE "${include_directories}")
idf_build_get_property(compile_definitions COMPILE_DEFINITIONS GENERATOR_EXPRESSION)
target_compile_definitions(${CMAKE_PROJECT_NAME}.elf PRIVATE "${compile_definitions}")
idf_build_get_compile_options(compile_options)
target_compile_options(${CMAKE_PROJECT_NAME}.elf PRIVATE "${compile_options}")
# Link the bundled IDF library. This brings in component headers,
# sdkconfig includes, and all necessary linker options.
target_link_libraries(${CMAKE_PROJECT_NAME}.elf PRIVATE idf_components)
# Binary and flash targets
idf_build_binary(${CMAKE_PROJECT_NAME}.elf
OUTPUT_FILE "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin"
TARGET ${CMAKE_PROJECT_NAME}_binary)
idf_flash_binary(${CMAKE_PROJECT_NAME}_binary
TARGET app-flash
NAME "app"
FLASH)
idf_check_binary_size(${CMAKE_PROJECT_NAME}_binary)
idf_build_generate_metadata(BINARY ${CMAKE_PROJECT_NAME}_binary)
idf_build_generate_flasher_args()
add_custom_target(app ALL DEPENDS ${CMAKE_PROJECT_NAME}_binary)
endif()
@@ -0,0 +1,55 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# ESP-IDF as a Library in a Standard CMake Project
This example demonstrates how to use ESP-IDF as a library inside a standard CMake project. The same source files produce either a plain host executable or an ESP-IDF firmware image depending on how the project is invoked.
## How it works
When `idf.py` is used it sets the `ESP_PLATFORM` CMake variable before invoking CMake. A single project-level CMakeLists.txt checks for this variable: if set, it initialises the IDF build system, bundles the required IDF components into one linkable library via `idf_build_library`, compiles the application sources, links them against that library, and produces the final binary and flash targets. Without `ESP_PLATFORM` the same file produces a minimal host executable with no IDF dependency.
`main.c` is the only source file. All IDF API calls inside it are guarded by `#ifdef ESP_PLATFORM`, so the same file compiles cleanly for both targets. On ESP it defines `app_main`; on the host it defines a plain `main`.
## Project Layout
```
idf_as_lib/
├── CMakeLists.txt ← Single file: host + ESP build logic
├── main.c ← One file, two entry points (host & ESP)
└── sdkconfig ← ESP-IDF configuration
```
## How to build the host app
```
cmake -B build-host .
cmake --build build-host
./build-host/idf_as_lib.elf
```
### Expected output
```
Hello from host build
Run with idf.py set-target <target> && idf.py build for ESP build.
```
## How to build the ESP-IDF app
```
idf.py set-target <target>
idf.py build flash monitor
```
### Expected output
```
Hello from ESP-IDF build
idf_lib initialized (IDF as library)
This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, silicon revision v3.0, 2MB external flash
Minimum free heap size: 303764 bytes
Restarting in 10...
...
Restarting now.
```
@@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Single source file for both host and ESP-IDF builds.
* - Host: plain main(), no IDF dependency.
* - ESP-IDF: app_main(), uses IDF APIs guarded by ESP_PLATFORM.
*/
#include <stdio.h>
#include <inttypes.h>
#ifdef ESP_PLATFORM
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#endif
#ifdef ESP_PLATFORM
void app_main(void)
#else
int main(void)
#endif
{
printf("Hello from %s build\n",
#ifdef ESP_PLATFORM
"ESP-IDF"
#else
"host"
#endif
);
#ifdef ESP_PLATFORM
/* --- chip info --------------------------------------------------------- */
esp_chip_info_t chip_info;
uint32_t flash_size;
esp_chip_info(&chip_info);
printf("This is %s chip with %d CPU core(s), WiFi%s%s, ",
CONFIG_IDF_TARGET,
chip_info.cores,
(chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "");
unsigned major_rev = chip_info.revision / 100;
unsigned minor_rev = chip_info.revision % 100;
printf("silicon revision v%d.%d, ", major_rev, minor_rev);
if (esp_flash_get_size(NULL, &flash_size) == ESP_OK) {
printf("%" PRIu32 " MB %s flash\n", flash_size / (1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");
}
printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size());
/* --- countdown and restart -------------------------------------------- */
for (int i = 10; i >= 0; i--) {
printf("Restarting in %d...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
printf("Restarting now.\n");
fflush(stdout);
esp_restart();
#else
printf("Run with idf.py set-target <target> && idf.py build for ESP-IDF build.\n");
return 0;
#endif
}
@@ -0,0 +1,9 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(import_lib C CXX ASM)
idf_project_default()
@@ -0,0 +1,41 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Import Third-Party CMake Library Example
This example demonstrates how to import third-party CMake libraries.
## Example Flow
[tinyxml2](https://github.com/leethomason/tinyxml2) is a small C++ XML parser.
It is imported, without modification, into the [tinyxml2](components/tinyxml2/) component. Please refer to the component CMakeLists.txt file for the description of the process: [components/tinyxml2/CMakeLists.txt](components/tinyxml2/CMakeLists.txt).
To demonstrate the library being used, a sample XML is embedded into the project. This sample XML is then read and parsed using `tinyxml2`. Please refer to the [main](main/) component for details.
### Output
```
I (317) example: Setting up...
I (317) example: Copying sample XML to filesystem...
I (647) example: Reading XML file
I (657) example: Read XML data:
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
I (667) example: Parsed XML data:
To: Tove
From: Jani
Heading: Reminder
Body: Don't forget me this weekend!
I (677) example: Example end
```
---
There is a discussion on importing third-party CMake libraries in the programming guide under `API Guides` -> `Build System` -> `Using Third-Party CMake Projects with Components`
@@ -0,0 +1,67 @@
# This component demonstrates how to add an existing third-party library as a component
# to ESP-IDF build system.
#
# Since we are wrapping the library inside a component,
# the component has to be registered first:
idf_component_register()
# To build a third-party library, ExternalProject CMake module can be used.
# ExternalProject offers many features which are impossible to demonstrate
# in a single example. Please refer to its documentation for more info:
# https://cmake.org/cmake/help/latest/module/ExternalProject.html
include(ExternalProject)
# Define the location where tinyxml2 will be installed:
set(TINYXML2_INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/tinyxml2_install)
# This function downloads the project, calls CMake to configure it,
# builds the project and installs it to the specified location:
externalproject_add(tinyxml2_proj
# Download the source code of the third party project from the following URL.
# (Two URLs are provided, the 2nd one is the mirror for Chinese users)
URL https://github.com/leethomason/tinyxml2/archive/refs/tags/9.0.0.zip
https://dl.espressif.com/dl/tinyxml2/9.0.0.zip
# (Downloading is not the only option; the library can also be located in your source tree.
# Consult ExternalProject_Add function documentation for other options.)
# Specify arguments to be passed when running CMake for this subproject.
# Note that ExternalProject_Add also works with non-CMake projects, so this
# is just an example.
CMAKE_ARGS
# Use the same CMake toolchain file as for the main project.
-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
# tinyxml2-specific settings: disable building everything except for the static library
-Dtinyxml2_BUILD_TESTING=FALSE
-Dtinyxml2_SHARED_LIBS=FALSE
# Pass the install directory to the subproject.
-DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
# These options are set so that Ninja immediately outputs
# the subproject build to the terminal. Otherwise it looks like the
# build process "hangs" while the subproject is being built.
USES_TERMINAL_DOWNLOAD TRUE
USES_TERMINAL_CONFIGURE TRUE
USES_TERMINAL_BUILD TRUE
# Specify the installation directory for the subproject
INSTALL_DIR ${TINYXML2_INSTALL_DIR}
# Let CMake know that the library is generated by the subproject build step.
BUILD_BYPRODUCTS "${TINYXML2_INSTALL_DIR}/lib/libtinyxml2.a"
)
# Now that the subproject build is set up, we need to consume the results
# of the build: the header file and the static library.
# To do this, define an imported CMake library:
add_prebuilt_library(tinyxml2_lib "${TINYXML2_INSTALL_DIR}/lib/libtinyxml2.a"
# tinyxml calls certain C++ support library functions (_Unwind_Resume and similar)
# so a dependency on IDF's cxx component is added here:
PRIV_REQUIRES cxx)
target_include_directories(tinyxml2_lib INTERFACE "${TINYXML2_INSTALL_DIR}/include")
add_dependencies(tinyxml2_lib tinyxml2_proj)
# Link the imported library to the current component.
target_link_libraries(${COMPONENT_LIB} INTERFACE tinyxml2_lib)
# To use tinyxml2 in another component, add 'tinyxml2' (the name of this component)
# to PRIV_REQUIRES or REQUIRES list its idf_component_register call.
# See ../../main/CMakeLists.txt for an example.
@@ -0,0 +1,7 @@
idf_component_register(SRCS "import_lib_example_main.cpp"
INCLUDE_DIRS "."
PRIV_REQUIRES tinyxml2 fatfs)
# Create a FAT filesystem image from the contents of data/ subdirectory,
# The image will be flashed into the 'storage' partition when 'idf.py flash' is used.
fatfs_create_spiflash_image(storage data FLASH_IN_PROJECT)
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include "esp_err.h"
#include "esp_log.h"
#include "esp_vfs_fat.h"
#include "tinyxml2.h"
static const char *TAG = "example";
extern "C" void app_main(void)
{
ESP_LOGI(TAG, "Initializing the filesystem");
esp_vfs_fat_mount_config_t mount_config = {};
mount_config.max_files = 1;
wl_handle_t wl_handle = WL_INVALID_HANDLE;
esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl("/spiflash", "storage", &mount_config, &wl_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to mount FATFS (%s)", esp_err_to_name(err));
return;
}
// Load the XML file from the filesystem and parse it using tinyxml2
ESP_LOGI(TAG, "Reading XML file");
tinyxml2::XMLDocument data;
data.LoadFile("/spiflash/sample.xml");
tinyxml2::XMLPrinter printer;
data.Print(&printer);
ESP_LOGI(TAG, "Read XML data:\n%s", printer.CStr());
const char* to_data = data.FirstChildElement("note")->FirstChildElement("to")->GetText();
const char* from_data = data.FirstChildElement("note")->FirstChildElement("from")->GetText();
const char* heading_data = data.FirstChildElement("note")->FirstChildElement("heading")->GetText();
const char* body_data = data.FirstChildElement("note")->FirstChildElement("body")->GetText();
ESP_LOGI(TAG, "Parsed XML data:\n\nTo: %s\nFrom: %s\nHeading: %s\nBody: %s",
to_data, from_data, heading_data, body_data);
// Clean up
esp_vfs_fat_spiflash_unmount_rw_wl("/spiflash", wl_handle);
ESP_LOGI(TAG, "Example end");
}
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
storage, data, fat, , 528K,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 1M,
6 storage, data, fat, , 528K,
@@ -0,0 +1,3 @@
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_example.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_example.csv"
@@ -0,0 +1,11 @@
# This example demonstrates a cmakev2 capability of importing an external
# C library via FetchContent, built by its own CMake, and linked directly
# to an IDF component without any IDF component wrapper for the library.
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(import_lib_direct C CXX ASM)
idf_project_default()
@@ -0,0 +1,61 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Import External C Library Directly (cmakev2)
This example demonstrates importing an external C library that is a pure CMake project. It is downloaded at configure time, built by its own CMake, and linked directly to the IDF app without wrapping it as an IDF component.
## Overview
In Build System v1, integrating third-party CMake libraries required wrapping them as IDF components (see the `import_lib` example). With cmakev2, CMake's `FetchContent` module can be used to fetch and build an external CMake library (lwjson) and link the library to an IDF component using `target_link_libraries`.
This example uses [lwjson](https://github.com/MaJerle/lwjson), a lightweight JSON parser for embedded systems.
## Project Structure
```
import_lib_direct/
├── CMakeLists.txt # Initialize the IDF project
├── README.md
└── main/
├── CMakeLists.txt # FetchContent for lwjson, register component, link
└── main.c # Uses lwjson API
```
## How It Works
1. The **main component's CMakeLists.txt** uses `FetchContent_Declare()` and `FetchContent_MakeAvailable()` to download and build lwjson at configure time. The fetched sources land in `./build/_deps/`.
2. After registering the main omponent with `idf_component_register`, it links the fetched library:
```cmake
target_link_libraries(${COMPONENT_LIB} PUBLIC lwjson)
```
3. **main.c** uses the lwjson API (`lwjson_init`, `lwjson_parse`, `lwjson_find`, etc.) to parse a sample JSON string and print device name, cores, features, and specs.
## How to Use
Build and flash the example:
```bash
idf.py set-target <target>
idf.py build
idf.py flash monitor
```
## Expected Output
```
I (275) import_lib_direct: lwjson library imported directly (downloaded, built, linked) without IDF component wrapper
I (275) import_lib_direct: Parsing JSON string...
I (285) import_lib_direct: Device name: ESP32
I (285) import_lib_direct: Number of cores: 2
I (285) import_lib_direct: Features:
I (295) import_lib_direct: - WiFi
I (295) import_lib_direct: - Bluetooth
I (295) import_lib_direct: - GPIO
I (305) import_lib_direct: Specifications:
I (305) import_lib_direct: Flash: 4MB
I (305) import_lib_direct: RAM: 520KB
I (315) import_lib_direct: Example complete!
```
@@ -0,0 +1,15 @@
# Fetch the external lwjson library at configure time.
include(FetchContent)
fetchcontent_declare(
lwjson
GIT_REPOSITORY https://github.com/MaJerle/lwjson.git
GIT_TAG v1.8.1
GIT_SHALLOW TRUE
)
fetchcontent_makeavailable(lwjson)
idf_component_register(SRCS "main.c"
INCLUDE_DIRS ".")
# Link the fetched lwjson library to this component
target_link_libraries(${COMPONENT_LIB} PUBLIC lwjson)
@@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/*
* This example demonstrates importing an external C library (lwjson)
* via FetchContent, built by its own CMake, and linked directly to this IDF component
* without any IDF component wrapper.
*/
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "lwjson/lwjson.h"
static const char *TAG = "import_lib_direct";
#define LWJSON_TOKENS 64
/* Sample JSON string to parse */
static const char *sample_json =
"{"
" \"name\": \"ESP32\","
" \"cores\": 2,"
" \"features\": [\"WiFi\", \"Bluetooth\", \"GPIO\"],"
" \"specs\": {"
" \"flash\": \"4MB\","
" \"ram\": \"520KB\""
" }"
"}";
void app_main(void)
{
ESP_LOGI(TAG, "lwjson library imported directly (downloaded, built, linked) without IDF component wrapper");
static lwjson_token_t tokens[LWJSON_TOKENS];
lwjson_t lwobj;
if (lwjson_init(&lwobj, tokens, LWJSON_TOKENS) != lwjsonOK) {
ESP_LOGE(TAG, "lwjson_init failed");
return;
}
if (lwjson_parse(&lwobj, sample_json) != lwjsonOK) {
ESP_LOGE(TAG, "lwjson_parse failed");
lwjson_free(&lwobj);
return;
}
ESP_LOGI(TAG, "Parsing JSON string...");
const lwjson_token_t *name_t = lwjson_find(&lwobj, "name");
if (name_t != NULL) {
size_t len;
const char *s = lwjson_get_val_string(name_t, &len);
if (s != NULL) {
ESP_LOGI(TAG, "Device name: %.*s", (int)len, s);
}
}
const lwjson_token_t *cores_t = lwjson_find(&lwobj, "cores");
if (cores_t != NULL && cores_t->type == LWJSON_TYPE_NUM_INT) {
ESP_LOGI(TAG, "Number of cores: %lld", (long long)lwjson_get_val_int(cores_t));
}
const lwjson_token_t *features_t = lwjson_find(&lwobj, "features");
if (features_t != NULL && features_t->type == LWJSON_TYPE_ARRAY) {
ESP_LOGI(TAG, "Features:");
const lwjson_token_t *child = lwjson_get_first_child(features_t);
while (child != NULL) {
if (child->type == LWJSON_TYPE_STRING) {
size_t len;
const char *s = lwjson_get_val_string(child, &len);
if (s != NULL) {
ESP_LOGI(TAG, " - %.*s", (int)len, s);
}
}
child = child->next;
}
}
const lwjson_token_t *flash_t = lwjson_find(&lwobj, "specs.flash");
const lwjson_token_t *ram_t = lwjson_find(&lwobj, "specs.ram");
if (flash_t != NULL || ram_t != NULL) {
ESP_LOGI(TAG, "Specifications:");
if (flash_t != NULL) {
size_t len;
const char *s = lwjson_get_val_string(flash_t, &len);
if (s != NULL) {
ESP_LOGI(TAG, " Flash: %.*s", (int)len, s);
}
}
if (ram_t != NULL) {
size_t len;
const char *s = lwjson_get_val_string(ram_t, &len);
if (s != NULL) {
ESP_LOGI(TAG, " RAM: %.*s", (int)len, s);
}
}
}
lwjson_free(&lwobj);
ESP_LOGI(TAG, "Example complete!");
}
@@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(import_prebuilt C CXX ASM)
idf_project_default()
@@ -0,0 +1,40 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Import Prebuilt Library Example
This example illustrates how to import a prebuilt static library in the ESP-IDF build system.
## Example Flow
Users need to first build the project in the [prebuilt](prebuilt) subdirectory:
```
cd prebuilt
idf.py build
```
This builds a component named [prebuilt](prebuilt/components/prebuilt), which has private dependency on ESP-IDF components `spi_flash`, `log` and `app_update` (see [its CMakeLists.txt](prebuilt/components/prebuilt/CMakeLists.txt)). Once built, the archive file `libprebuilt.a`, along with the header file `prebuilt.h`, is automatically copied to the [`main` component](main) of this example project.
The [`main` component's CMakeLists.txt](main/CMakeLists.txt) demonstrates how to import `libprebuilt.a` and link it to `main` so that the definitions inside can be used.
It also demonstrates how to specify the same dependencies the original component had so as to properly resolve symbols used inside the prebuilt library.
Users can then return to this directory and build the main example:
```
cd ..
idf.py build
```
### Output
The example simply outputs the current running partition.
```
I (319) prebuilt: The running partition is 'factory'!
```
---
There is a discussion on importing prebuilt libraries in the programming guide under `API Guides` -> `Build System` -> `Using Prebuilt Libraries with Components`
@@ -0,0 +1,10 @@
idf_component_register(SRCS "main.c"
INCLUDE_DIRS ".")
# Import the library, specifying a target name and the library path.
# The private dependencies of the library is also specified.
add_prebuilt_library(prebuilt "libprebuilt.a"
PRIV_REQUIRES spi_flash app_update log)
# `main` calls a function from the library, so link it to `main`
target_link_libraries(${COMPONENT_LIB} PRIVATE prebuilt)
@@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
// Include the prebuilt library's header file so as to be able
// to reference `prebuilt_func` here.
#include "prebuilt.h"
void app_main(void)
{
prebuilt_func();
}
@@ -0,0 +1,16 @@
# For users checking this example, ignore the following code. This is so that
# the prebuilt project is built automatically in ESP-IDF CI.
if("$ENV{CI}")
# otherwise these file won't be rebuilt when switching the built target within the same job
file(REMOVE
${CMAKE_SOURCE_DIR}/prebuilt/sdkconfig
${CMAKE_SOURCE_DIR}/main/libprebuilt.a
${CMAKE_SOURCE_DIR}/main/prebuilt.h
)
file(REMOVE_RECURSE ${CMAKE_SOURCE_DIR}/prebuilt/build)
idf_build_get_property(python PYTHON)
idf_build_get_property(idf_path IDF_PATH)
execute_process(COMMAND ${python} "${idf_path}/tools/idf.py" build
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/prebuilt)
endif()
@@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(prebuilt C CXX ASM)
idf_project_default()
@@ -0,0 +1,10 @@
idf_component_register(SRCS prebuilt.c
INCLUDE_DIRS "."
PRIV_REQUIRES app_update spi_flash log)
# After build, copy the archive file and header file to parent example directory's main component
add_custom_command(TARGET ${COMPONENT_LIB}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:${COMPONENT_LIB}> ${CMAKE_SOURCE_DIR}/../main
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/prebuilt.h ${CMAKE_SOURCE_DIR}/../main
COMMENT "Copying built archive file and header to parent example directory...")
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include "esp_ota_ops.h"
#include "esp_partition.h"
#include "esp_log.h"
const char *TAG = "prebuilt";
void prebuilt_func(void)
{
const esp_partition_t* running_partition = esp_ota_get_running_partition();
ESP_LOGI(TAG, "The running partition is '%s'!", running_partition->label);
}
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#pragma once
void prebuilt_func(void);
@@ -0,0 +1,3 @@
idf_component_register(SRCS "main.c"
PRIV_REQUIRES prebuilt esptool_py
INCLUDE_DIRS "")
@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
void app_main(void)
{
printf("Hello World!\n");
}
@@ -0,0 +1,54 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(multi_binary C CXX ASM)
# Manual initialization for fine-grained control over component inclusion
idf_project_init()
# Build the first executable: app1
# Links: app1_main, component1, component2
idf_build_executable(app1.elf
COMPONENTS app1_main component1 component2)
# Build the second executable: app2
# Links: app2_main, component1, component2, component3
idf_build_executable(app2.elf
COMPONENTS app2_main component1 component2 component3)
# Generate binaries and flash targets
# App1 binary
idf_build_binary(app1.elf
OUTPUT_FILE "${CMAKE_BINARY_DIR}/app1.bin"
TARGET app1_binary)
idf_flash_binary(app1_binary
TARGET app1-flash
NAME "app1"
FLASH)
# Generate metadata only for the primary app1 binary
idf_build_generate_metadata(BINARY app1_binary)
# Create menuconfig and confserver targets for app1 binary
idf_create_menuconfig(app1.elf TARGET app1-menuconfig)
idf_create_confserver(app1.elf TARGET app1-confserver)
# App2 binary
idf_build_binary(app2.elf
OUTPUT_FILE "${CMAKE_BINARY_DIR}/app2.bin"
TARGET app2_binary)
idf_flash_binary(app2_binary
TARGET app2-flash
NAME "app2"
FLASH)
# Create menuconfig and confserver targets for app2 binary
idf_create_menuconfig(app2.elf TARGET app2-menuconfig)
idf_create_confserver(app2.elf TARGET app2-confserver)
# Make both binaries part of the default "app" target
add_custom_target(app ALL DEPENDS app1.bin app2.bin)
@@ -0,0 +1,92 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Multi-Binary Example: Multiple Apps with Different Component Sets
This example demonstrates **Build System v2's** capability to build multiple independent firmware binaries from a single project in one build command. Each binary has its own entry point component with different dependencies.
## Overview
Two independent executables are built simultaneously:
1. **`app1.bin`** - Links `app1_main`, `component1`, `component2`
2. **`app2.bin`** - Links `app2_main`, `component1`, `component2`, `component3`
Each app has its own dedicated entry point component that explicitly declares which components it needs. This avoids complex conditional logic and makes dependencies clear.
## Project Structure
All components live under `components/`. Each `idf_build_executable` call picks exactly the subset it requires.
```
multi_binary/
├── CMakeLists.txt
└── components/
├── app1_main/ # Entry point for app1
│ ├── CMakeLists.txt
│ └── app1_main.c
├── app2_main/ # Entry point for app2
│ ├── CMakeLists.txt
│ └── app2_main.c
├── component1/ # Shared component
│ ├── CMakeLists.txt
│ ├── component1.c
│ └── component1.h
├── component2/ # Shared component
│ ├── CMakeLists.txt
│ ├── component2.c
│ └── component2.h
└── component3/ # Only linked into app2
├── CMakeLists.txt
├── component3.c
└── component3.h
```
## Building
### Build both binaries in a single command:
```bash
cd examples/build_system/cmakev2/features/multi_binary
idf.py set-target <target>
idf.py build
```
This generates both `app1.bin` and `app2.bin` in the `build/` directory.
## Flashing
Flash app1:
```bash
idf.py app1-flash monitor
```
Flash app2:
```bash
idf.py app2-flash monitor
```
You can switch between apps without rebuilding - just select which one to flash.
### Configuration (menuconfig)
When invoking a custom menuconfig target via `idf.py`, you must pass `--no-hints`. For example, use `idf.py app1-menuconfig --no-hints` (plain `idf.py app1-menuconfig` may not work because `idf.py` redirects stdout by default, which breaks the curses-based menu).
> **Note:** Although multiple binaries can be produced from a single project, each component is evaluated only once using the current configuration. If a different configuration is needed for a component, a new project must be created. You cannot use the same component with different configurations within a single project.
## Expected Output
### app1 Output
```
I (xxx) component1: Hello from component1!
I (xxx) component2: Hello from component2!
```
### app2 Output
```
I (xxx) component1: Hello from component1!
I (xxx) component2: Hello from component2!
I (xxx) component3: Hello from component3!
```
@@ -0,0 +1,2 @@
idf_component_register(SRCS "app1_main.c"
PRIV_REQUIRES component1 component2)
@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "component1.h"
#include "component2.h"
void app_main(void)
{
component1_print_hello();
component2_print_hello();
while (1) {
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
@@ -0,0 +1,2 @@
idf_component_register(SRCS "app2_main.c"
PRIV_REQUIRES component1 component2 component3)
@@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "component1.h"
#include "component2.h"
#include "component3.h"
void app_main(void)
{
component1_print_hello();
component2_print_hello();
component3_print_hello();
while (1) {
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
@@ -0,0 +1,2 @@
idf_component_register(SRCS "component1.c"
INCLUDE_DIRS ".")
@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include "esp_log.h"
static const char *TAG = "component1";
void component1_print_hello(void)
{
ESP_LOGI(TAG, "Hello from component1!");
}
@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef COMPONENT1_H
#define COMPONENT1_H
void component1_print_hello(void);
#endif // COMPONENT1_H
@@ -0,0 +1,2 @@
idf_component_register(SRCS "component2.c"
INCLUDE_DIRS ".")
@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include "esp_log.h"
static const char *TAG = "component2";
void component2_print_hello(void)
{
ESP_LOGI(TAG, "Hello from component2!");
}
@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef COMPONENT2_H
#define COMPONENT2_H
void component2_print_hello(void);
#endif // COMPONENT2_H
@@ -0,0 +1,2 @@
idf_component_register(SRCS "component3.c"
INCLUDE_DIRS ".")
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include "esp_log.h"
#include "sdkconfig.h"
static const char *TAG = "component3";
void component3_print_hello(void)
{
ESP_LOGI(TAG, "Hello from component3!");
}
@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef COMPONENT3_H
#define COMPONENT3_H
void component3_print_hello(void);
#endif // COMPONENT3_H
@@ -0,0 +1,2 @@
build_prod1/
build_prod2/
@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.22)
# In this example, sdkconfig file is placed into the build directory.
# This allows building development and production configs side by side,
# without having them influence each other.
set(SDKCONFIG "${CMAKE_BINARY_DIR}/sdkconfig")
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(multi_config C CXX ASM)
idf_project_default()
@@ -0,0 +1,34 @@
{
"version": 3,
"configurePresets": [
{
"name": "default",
"binaryDir": "build/default",
"displayName": "Default (development)",
"description": "Development configuration",
"cacheVariables": {
"SDKCONFIG": "./build/default/sdkconfig"
}
},
{
"name": "prod1",
"binaryDir": "build/prod1",
"displayName": "Product 1",
"description": "Production configuration for product 1",
"cacheVariables": {
"SDKCONFIG_DEFAULTS": "sdkconfig.defaults.prod_common;sdkconfig.defaults.prod1",
"SDKCONFIG": "./build/prod1/sdkconfig"
}
},
{
"name": "prod2",
"binaryDir": "build/prod2",
"displayName": "Product 2",
"description": "Production configuration for product 2",
"cacheVariables": {
"SDKCONFIG_DEFAULTS": "sdkconfig.defaults.prod_common;sdkconfig.defaults.prod2",
"SDKCONFIG": "./build/prod2/sdkconfig"
}
}
]
}
@@ -0,0 +1,132 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Multiple Build Configurations Example
(See the README.md file in the upper level 'examples' directory for more information about examples.)
This example demonstrates how to build multiple configurations of a single application. This can be useful in the following cases:
* Building binaries for multiple similar products from single codebase
* Building the application for development or production hardware
* Optimizing the application differently for development and production
This example contains three build configurations:
* Development configuration, described by `sdkconfig.defaults` file. This configuration is used by default if the application is built using `idf.py build`.
* Production configuration for product 1 ("Blinky Smart Light"), described in `sdkconfig.prod1` file. This configuration is not built by default, however it can be built as shown in the next section. It is used together `sdkconfig.prod_common`, common configuration file for all products.
* Production configuration for product 2 ("Blinky Smart Switch"), described in `sdkconfig.prod2` file. Differs from `prod1` configuration only in product name.
For each configuration, a few configuration options are set:
* Project-specific Kconfig options, `CONFIG_EXAMPLE_PRODUCT_NAME` and `CONFIG_EXAMPLE_FUNC_IMPL`. These options are declared in [component Kconfig.projbuild](main/Kconfig.projbuild). These are used to demonstrate how to create and set project-specific options. These options are set differently in `sdkconfig.defaults` and `sdkconfig.prod_common` files.
- `CONFIG_EXAMPLE_PRODUCT_NAME` is a simple `string` option. It is used to set the product name.
- `CONFIG_EXAMPLE_FUNC_IMPL` is a `choice` option. It is used to select which of the two source files, [func_dev.c](main/func_dev.c) or [func_prod.c](main/func_prod.c), is compiled and linked. See [component CMakeLists.txt file](main/CMakeLists.txt) for related logic.
* ESP-IDF configuration options, `CONFIG_COMPILER_OPTIMIZATION_SIZE`, `CONFIG_BOOTLOADER_LOG_LEVEL_NONE`, `CONFIG_LOG_DEFAULT_LEVEL_NONE` are set in `sdkconfig.prod_common` to illustrate a typical production configuration where log messages are disabled and optimization for size is used.
## How to Use Example
### Development build
To build the development configuration (specified in `sdkconfig.defaults`), specify it using --preset argument:
```
idf.py --preset default build
```
To flash the project and see the output, run:
```
idf.py --preset default -p PORT flash monitor
```
(To exit the serial monitor, type ``Ctrl-]``.)
### Production build
To build one of the Production configurations, specify the name using idf.py --preset argument:
```
idf.py --preset prod1 build
```
To flash the project and see the output, run:
```
idf.py --preset prod1 -p PORT flash monitor
```
To build and run the app with `prod2` configuration, repeat the steps above, replacing `prod1` with `prod2`.
### Specifying the preset for multiple commands
To avoid having to specify `--preset` argument every time you run `idf.py`, you can set `IDF_PRESET` environment variable:
For UNIX-like systems (Linux, macOS):
```shell
export IDF_PRESET=prod1
```
For Windows (PowerShell):
```powershell
$ENV:IDF_PRESET='prod1'
```
For Windows (cmd.exe):
```shell
set IDF_PRESET=prod1
```
Then subsequent commands will work with `prod1` configuration:
```shell
idf.py build
idf.py flash monitor
```
### Combining multiple files in `SDKCONFIG_DEFAULTS`
`SDKCONFIG_DEFAULTS` build system variable selects the file which contains the default app configuration, used when no `sdkconfig` file is present. If not specified, `SDKCONFIG_DEFAULTS` is set to `"sdkconfig.defaults"`.
`SDKCONFIG_DEFAULTS` can be set to a different name from the command line, using `-D` flag of `idf.py`, as shown above. It can also be set from the project CMakeLists.txt file, before `project.cmake` is included.
It is possible to specify multiple files in this variable, separating them with semicolons. In the example given in the previous section, this is used to create a common config file for production builds and product-specific config files:
* product 1: `sdkconfig.prod_common;sdkconfig.prod1`
* product 2: `sdkconfig.prod_common;sdkconfig.prod2`
This way the common options do not need to be repeated in each of `sdkconfig.prodN` files.
### Generated `sdkconfig` file
In this example, `sdkconfig` file is placed into the build directory, instead of the project root directory as it is done by default. This allows development and production builds to exist side by side. The location of `sdkconfig` file is set using `SDKCONFIG` variable in [project CMakeLists.txt](CMakeLists.txt) file.
## Example Output
### Development build output
```
I (310) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
This app is built for running on: Blinky Development Board
func() from func_dev.c (Development) called.
See README.md for building and running other app configurations.
```
### Production build output
When building with `-DSDKCONFIG_DEFAULTS="sdkconfig.prod_common;sdkconfig.prod1"` option:
```
This app is built for running on: Blinky Smart Light
func() from func_prod.c (Production) called.
See README.md for building and running other app configurations.
```
When building with `-DSDKCONFIG_DEFAULTS="sdkconfig.prod_common;sdkconfig.prod2"` option:
```
This app is built for running on: Blinky Smart Switch
func() from func_prod.c (Production) called.
See README.md for building and running other app configurations.
```
@@ -0,0 +1,8 @@
idf_component_register(SRCS "multi_config_example_main.c"
INCLUDE_DIRS ".")
if(CONFIG_EXAMPLE_FUNC_IMPL_DEV)
target_sources(${COMPONENT_LIB} PRIVATE "func_dev.c")
elseif(CONFIG_EXAMPLE_FUNC_IMPL_PROD)
target_sources(${COMPONENT_LIB} PRIVATE "func_prod.c")
endif()
@@ -0,0 +1,20 @@
menu "Example Product Configuration"
config EXAMPLE_PRODUCT_NAME
string "Product name"
default "Not set"
help
Product name used in the example
choice EXAMPLE_FUNC_IMPL
prompt "Implementation of function 'func'"
help
Select one of the implementations of 'func' to be used in the app.
This setting is used in component CMakeLists.txt.
config EXAMPLE_FUNC_IMPL_DEV
bool "Development (func_dev.c)"
config EXAMPLE_FUNC_IMPL_PROD
bool "Production (func_prod.c)"
endchoice
endmenu
@@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief This function has different implementations depending on the product type.
*
* See func_dev.c and func_prod.c. Which of the files is compiled is determined in
* CMakeLists.txt,
*/
void func(void);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include "func.h"
void func(void)
{
printf("func() from func_dev.c (Development) called.\n");
}
@@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include "func.h"
void func(void)
{
printf("func() from func_prod.c (Production) called.\n");
}
@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include "sdkconfig.h"
#include "func.h"
void app_main(void)
{
printf("This app is built for running on: " CONFIG_EXAMPLE_PRODUCT_NAME "\n");
/* This will call func() either from func_dev.c or func_prod.c, depending on
* the build configuration.
*/
func();
printf("See README.md for building and running other app configurations.\n");
}
@@ -0,0 +1,6 @@
# In this example, the default build (obtained with 'idf.py build')
# targets a hypothetical development platform.
CONFIG_EXAMPLE_PRODUCT_NAME="Blinky Development Board"
# This selects 'func_dev.c' file to be compiled and linked, see CMakeLists.txt.
CONFIG_EXAMPLE_FUNC_IMPL_DEV=y
@@ -0,0 +1,2 @@
# This build configuration is for a specific product:
CONFIG_EXAMPLE_PRODUCT_NAME="Blinky Smart Light"
@@ -0,0 +1,2 @@
# This build configuration is for a specific product:
CONFIG_EXAMPLE_PRODUCT_NAME="Blinky Smart Switch"
@@ -0,0 +1,36 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(plugins C CXX ASM)
# Use project-level initialization instead of idf_project_default
idf_project_init()
# Create executable with explicit component specification
# This approach includes plugin components in the executable
idf_build_executable(${CMAKE_PROJECT_NAME}.elf
COMPONENTS main plugins plugin_hello plugin_nihao)
# Generate binary
idf_build_binary(${CMAKE_PROJECT_NAME}.elf
OUTPUT_FILE "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin"
TARGET ${CMAKE_PROJECT_NAME}_binary)
# Create flash target
idf_flash_binary(${CMAKE_PROJECT_NAME}_binary
TARGET app-flash
NAME "app"
FLASH)
# Check binary size
idf_check_binary_size(${CMAKE_PROJECT_NAME}_binary)
# Create app target
add_custom_target(app ALL DEPENDS ${CMAKE_PROJECT_NAME}_binary)
# Generate metadata
idf_build_generate_metadata(BINARY ${CMAKE_PROJECT_NAME}_binary)
# Create other utility targets
idf_create_menuconfig(${CMAKE_PROJECT_NAME}.elf
TARGET menuconfig)
@@ -0,0 +1,132 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Link Time Plugins Registration
(See the README.md file in the upper level 'examples' directory for more information about examples.)
This example demonstrates features of ESP-IDF build system related to link time registration of plugins. Link time registration of plugins is often used to add multiple implementations of a certain feature without having to make the application aware of all these implementations. With this approach, adding a new implementation is often as simple as adding a new source file or a new component. Aside from plugins, link time registration may be used for other purposes, such as automatic registration of test cases.
# Overview of link time registration
When using link time registration, there are typically two challenges: getting the plugin code linked into the application and enumerating the plugins at run time. The following sections explain these problems and the solutions to them.
## Ensuring that the plugin code is included into the executable
When GNU linker (ld) links a static library, it considers each object file in the library separately. The object file is ignored if it doesn't resolve any unresolved references known to the linker at that moment. With link-time plugin registration this is typically the case — the application doesn't explicitly reference any plugins, so the linker sees no reason to include the respective object files into the executable.
Aside from adding an explicit reference from the application to the plugin object file, there are two common ways to resolve this issue:
1. Link the object file of the plugin directly to the executable, and not via a static library.
2. Instruct the linker to include every object file of a library into the executable, even those which don't resolve any references from the rest of the application. For GNU ld this can be achieved by surrounding the library on the linker command line with `-Wl,--whole-archive` and `-Wl,--no-whole-archive` flags.
ESP-IDF build system implements the 2nd approach by providing a `WHOLE_ARCHIVE` component property. It can be set in component CMakeLists.txt in two ways. One is to add `WHOLE_ARCHIVE` option when calling `idf_component_register`:
```cmake
idf_component_register(SRCS file.c
WHOLE_ARCHIVE)
```
Another is to call `idf_component_set_property` after registering the component:
```cmake
idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE)
```
This will instruct the build system to surround the component library with whole-archive flags on the linker command line, ensuring that all object files from the library get included into the final application.
Note that the linker also performs "garbage collection" at the end of the linking process, eliminating all functions and global variables which are not referenced anywhere. This is addressed in the current example using `__attribute__((constructor))` function attribute (for dynamic registration) and `KEEP()` linker fragment flag (for static registration).
## Registering and enumerating the plugins
To make use of the plugins linked into the application, the application must somehow enumerate them. There are 2 common ways to register and enumerate the plugins: dynamic and static. This example demonstrates both approaches.
### Dynamic registration (or self-registration)
With this approach, each plugin module has a function with `__attribute__((constructor))` attribute (in C) or a static global object with a non-trivial constructor (in C++). Startup code calls all constructor functions before the application entry point (`app_main`) is executed. Plugin constructor functions then register themselves by calling a function defined in the application. As an example, this registration function can, add structures describing the plugins into a linked list.
### Static registration
This approach relies on plugin description structures being collected into an array at link time.
For each plugin, a structure describing the plugin (or a pointer to it) is placed into some special input section using `__attribute((section(".plugins_desc")))`. Using the linker script generator features in ESP-IDF, all entries from this input section can be gathered into a continuous array, surrounded by some symbols (e.g. `_plugins_array_start`, `_plugins_array_end`). At run time, the application casts the `&_plugins_array_start` pointer to the plugin description structure pointer and then iterates over structures collected from all plugins.
## Example code overview
Unlike `cmakev1` which used a global `COMPONENTS` variable, `cmakev2` uses an explicit dependency-driven model. For plugin architectures, this example demonstrates the use of `idf_build_executable()` with specific components to ensure plugins are properly included and linked.
```cmake
idf_project_init()
idf_build_executable(${CMAKE_PROJECT_NAME}.elf
COMPONENTS main plugins plugin_hello plugin_nihao)
```
This example contains 4 components:
* `main` — Only calls two sample functions defined in `plugins` component.
* `plugins` — The main part of the plugin system.
For dynamic registration, it provides an API which plugin components call to register themselves (`example_plugin_register`).
It also provides two sample functions for the application:
- `example_plugins_list`: prints the list of registered plugins. This function demonstrates static registration.
- `example_plugins_greet`: calls a function defined by each plugin with a given argument. This function demonstrates working with dynamically registered plugins.
* `plugin_hello` and `plugin_nihao` — two almost identical components, each provides one plugin.
Note that multiple plugins may also be defined in the same component.
Please refer to the comments in the example code for a more detailed description.
## How to use example
### Hardware Required
This example runs on any ESP development board, no special hardware is required.
### Build and Flash
Build the project and flash it to the board, then run monitor tool to view serial output:
```
idf.py -p PORT flash monitor
```
(Replace PORT with the name of the serial port to use.)
(To exit the serial monitor, type `Ctrl-]`.)
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
## Example Output
```
Nihao plugin performing self-registration...
Successfully registered plugin 'Nihao'
Hello plugin performing self-registration...
Successfully registered plugin 'Hello'
I (325) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
List of plugins:
- Plugin 'Hello', function greet=0x400d4f40
0x400d4f40: plugin_hello_greet at /home/user/esp/esp-idf/examples/build_system/cmake/plugins/build/../components/plugin_hello/plugin_hello.c:14
- Plugin 'Nihao', function greet=0x400d4f70
0x400d4f70: plugin_nihao_greet at /home/user/esp/esp-idf/examples/build_system/cmake/plugins/build/../components/plugin_nihao/plugin_nihao.c:14
Calling greet function of plugin 'Hello'...
Hello, World!
Done with greet function of plugin 'Hello'.
Calling greet function of plugin 'Nihao'...
你好 World!
Done with greet function of plugin 'Nihao'.
```
## Troubleshooting
When implementing the approaches described in this example, the following issues may occur:
* Plugin self-registration function (constructor function) is never called. To troubleshoot this, check the application .map file — it is generated in the `build` directory of the project. Look for the object file where the constructor function is defined. If the object file and the constructor function are missing, it means that the object file was discarded. Double-check that the `WHOLE_ARCHIVE` property of the component is set correctly. Verify that on the linker command line, the component library is surrounded by `-Wl,--whole-archive`, `-Wl,--no-whole-archive`. To see the linker command line, build the project with verbose (-v) flag.
* With static registration, the plugin description structure is missing from the link-time array. Same as in the case above, start by examining the map file.
- If the plugin object file is missing from the map file, double-check that the `WHOLE_ARCHIVE` property of the component is set correctly (see the instructions above).
- If the plugin object file is present, but the plugin description structure is missing, check that the linker fragment rule and `__attribute((section(...)))` use the same section name. Check that the linker fragment rule uses `KEEP()` flag.
- If the plugin description structure is in the map file but is not located inside the link-time array (is located in some other section), check the generated linker script found in the build directory (`build/esp-idf/esp_system/ld/sections.ld`). Check that the rules for placing the plugin description structure have correct precedence with respect to other rules in the linker script.
@@ -0,0 +1,3 @@
idf_component_register(SRCS plugin_hello.c
PRIV_REQUIRES plugins
WHOLE_ARCHIVE)
@@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <string.h>
#include "plugins_api.h"
/**
* This is an example function implemented by the plugin.
*/
static void plugin_hello_greet(const char* arg)
{
if (arg == NULL) {
return;
}
printf("Hello, %s!\n", arg);
}
/* The code below demonstrates both static and dynamic registration approaches. */
/**
* Static registration of this plugin can be achieved by defining the plugin description
* structure and placing it into .plugins_desc section.
* The name of the section and its placement is determined by linker.lf file in 'plugins' component.
*/
static const example_plugin_desc_t __attribute__((section(".plugins_desc"),used)) PLUGIN = {
.name = "Hello",
.greet = &plugin_hello_greet
};
/**
* Dynamic registration of this plugin can be achieved by calling plugin registration function
* ('example_plugin_register') from a "constructor" function. Constructor function is called automatically
* during application startup.
*/
static void __attribute__((constructor)) plugin_hello_self_register(void)
{
printf("Hello plugin performing self-registration...\n");
example_plugin_register(&(example_plugin_desc_t){
.name = "Hello",
.greet = &plugin_hello_greet
});
}
@@ -0,0 +1,5 @@
idf_component_register(SRCS plugin_nihao.c
PRIV_REQUIRES plugins)
# This is equivalent to adding WHOLE_ARCHIVE option to the idf_component_register call above:
idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE)
@@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <string.h>
#include "plugins_api.h"
/**
* This is an example function implemented by the plugin.
*/
static void plugin_nihao_greet(const char* arg)
{
if (arg == NULL) {
return;
}
printf("你好 %s!\n", arg);
}
/* The code below demonstrates both static and dynamic registration approaches. */
/**
* Static registration of this plugin can be achieved by defining the plugin description
* structure and placing it into .plugins_desc section.
* The name of the section and its placement is determined by linker.lf file in 'plugins' component.
*/
static const example_plugin_desc_t __attribute__((section(".plugins_desc"),used)) PLUGIN = {
.name = "Nihao",
.greet = &plugin_nihao_greet
};
/**
* Dynamic registration of this plugin can be achieved by calling plugin registration function
* ('example_plugin_register') from a "constructor" function. Constructor function is called automatically
* during application startup.
*/
static void __attribute__((constructor)) plugin_nihao_self_register(void)
{
printf("Nihao plugin performing self-registration...\n");
example_plugin_register(&(example_plugin_desc_t){
.name = "Nihao",
.greet = &plugin_nihao_greet
});
}
@@ -0,0 +1,3 @@
idf_component_register(SRCS plugins.c
INCLUDE_DIRS include
LDFRAGMENTS linker.lf)
@@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* This structure describes the plugin to the rest of the application */
typedef struct {
/* A pointer to the plugin name */
const char* name;
/* A function which the plugin provides to the application.
* In this example, this function prints something to the console
* depending on the value of the argument 'arg'.
*/
void (*greet)(const char* arg);
} example_plugin_desc_t;
/**
* @brief Register the plugin with the application
* This function is called from each plugin's "constructor" function.
* It adds the plugin to the list.
* @param plugin_desc Pointer to the structure which describes the given plugin.
*/
void example_plugin_register(const example_plugin_desc_t* plugin_desc);
/**
* @brief Print the list of registered plugins to the console.
* This function is called from the application.
*/
void example_plugins_list(void);
/**
* @brief Invoke 'greet' function of each registered plugin with the given argument.
* This function is called from the application.
* @param arg argument to pass to plugins' greet functions.
*/
void example_plugins_greet(const char* arg);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,13 @@
[sections:plugins_desc]
entries:
.plugins_desc
[scheme:plugins_desc_default]
entries:
plugins_desc -> flash_rodata
[mapping:plugins_desc]
archive: *
entries:
* (plugins_desc_default);
plugins_desc -> flash_rodata KEEP() SORT(name) SURROUND(plugins_array)
@@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>
#include "plugins_api.h"
/**
* Demonstration of dynamic registration (self-registration):
*
* - example_plugin_register function is called from "constructor" functions of each plugin.
* Information about the plugin is passed inside 'example_plugin_desc_t' structure.
* This function adds each plugin description into linked list (s_plugins_list).
*
* - example_plugins_greet function iterates over the linked list.
*/
struct plugin_record {
example_plugin_desc_t plugin_desc;
LIST_ENTRY(plugin_record) list_entry;
};
static LIST_HEAD(plugins_list, plugin_record) s_plugins_list = LIST_HEAD_INITIALIZER(s_plugins_list);
void example_plugin_register(const example_plugin_desc_t* plugin_desc)
{
struct plugin_record *record = (struct plugin_record *) malloc(sizeof(struct plugin_record));
if (record == NULL) {
abort();
}
memcpy(&record->plugin_desc, plugin_desc, sizeof(*plugin_desc));
static struct plugin_record *tail = NULL;
if (tail == NULL) {
LIST_INSERT_HEAD(&s_plugins_list, record, list_entry);
} else {
LIST_INSERT_AFTER(tail, record, list_entry);
}
tail = record;
printf("Successfully registered plugin '%s'\n", plugin_desc->name);
}
void example_plugins_greet(const char* arg)
{
struct plugin_record *it;
LIST_FOREACH(it, &s_plugins_list, list_entry) {
printf("Calling greet function of plugin '%s'...\n", it->plugin_desc.name);
(*it->plugin_desc.greet)(arg);
printf("Done with greet function of plugin '%s'.\n", it->plugin_desc.name);
}
}
/**
* Demonstration of static registration.
* Symbols '_plugins_array_start' and '_plugins_array_end' mark the beginning and end
* of the array where 'example_plugin_desc_t' structures are placed by the linker.
* The names of these variables are determined by linker.lf in 'plugins' component,
* look for 'SURROUND(plugins_array)'.
*/
void example_plugins_list(void)
{
printf("List of plugins:\n");
extern const example_plugin_desc_t _plugins_array_start;
extern const example_plugin_desc_t _plugins_array_end;
for (const example_plugin_desc_t* it = &_plugins_array_start; it != &_plugins_array_end; ++it) {
printf("- Plugin '%s', function greet=%p\n", it->name, it->greet);
}
}
@@ -0,0 +1,3 @@
idf_component_register(SRCS "plugins_example_main.c"
INCLUDE_DIRS "."
PRIV_REQUIRES plugins)
@@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include "plugins_api.h"
void app_main(void)
{
example_plugins_list();
example_plugins_greet("World");
}
@@ -0,0 +1,9 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake)
project(hello_world C CXX ASM)
idf_project_default()
@@ -0,0 +1,53 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Hello World Example
Starts a FreeRTOS task to print "Hello World".
(See the README.md file in the upper level 'examples' directory for more information about examples.)
## How to use example
Follow detailed instructions provided specifically for this example.
Select the instructions depending on Espressif chip installed on your development board:
- [ESP32 Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/stable/get-started/index.html)
- [ESP32-S2 Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/get-started/index.html)
## Example folder contents
The project **hello_world** contains one source file in C language [hello_world_main.c](main/hello_world_main.c). The file is located in folder [main](main).
ESP-IDF projects are built using CMake. The project build configuration is contained in `CMakeLists.txt` files that provide set of directives and instructions describing the project's source files and targets (executable, library, or both).
Below is short explanation of remaining files in the project folder.
```
├── CMakeLists.txt
├── pytest_hello_world.py Python script used for automated testing
├── main
│ ├── CMakeLists.txt
│ └── hello_world_main.c
└── README.md This is the file you are currently reading
```
For more information on structure and contents of ESP-IDF projects, please refer to Section [Build System v2](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/build-system-v2.html) of the ESP-IDF Programming Guide.
## Troubleshooting
* Program upload failure
* Hardware connection is not correct: run `idf.py -p PORT monitor`, and reboot your board to see if there are any output logs.
* The baud rate for downloading is too high: lower your baud rate in the `menuconfig` menu, and try again.
## Technical support and feedback
Please use the following feedback channels:
* For technical queries, go to the [esp32.com](https://esp32.com/) forum
* For a feature request or bug report, create a [GitHub issue](https://github.com/espressif/esp-idf/issues)
We will get back to you as soon as possible.
@@ -0,0 +1,3 @@
idf_component_register(SRCS "hello_world_main.c"
PRIV_REQUIRES spi_flash
INCLUDE_DIRS "")
@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void app_main(void)
{
printf("Hello world!\n");
/* Print chip information */
esp_chip_info_t chip_info;
uint32_t flash_size;
esp_chip_info(&chip_info);
printf("This is %s chip with %d CPU core(s), %s%s%s%s, ",
CONFIG_IDF_TARGET,
chip_info.cores,
(chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "",
(chip_info.features & CHIP_FEATURE_BT) ? "BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
(chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : "");
unsigned major_rev = chip_info.revision / 100;
unsigned minor_rev = chip_info.revision % 100;
printf("silicon revision v%d.%d, ", major_rev, minor_rev);
if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
printf("Get flash size failed");
return;
}
printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");
printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size());
for (int i = 10; i >= 0; i--) {
printf("Restarting in %d seconds...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
printf("Restarting now.\n");
fflush(stdout);
esp_restart();
}
+2
View File
@@ -597,6 +597,8 @@ function(idf_build_executable executable)
if(ARG_MAPFILE_TARGET AND "${linker_type}" STREQUAL "GNU")
set(mapfile "${CMAKE_BINARY_DIR}/${ARG_NAME}.map")
target_link_options(${executable} PRIVATE "LINKER:--Map=${mapfile}")
# Add cross-reference table to the map file
target_link_options(${executable} PRIVATE "LINKER:--cref")
add_custom_command(
OUTPUT "${mapfile}"
DEPENDS ${executable}
+1 -1
View File
@@ -427,7 +427,7 @@ function(idf_component_register)
idf_build_get_property(compile_definitions COMPILE_DEFINITIONS GENERATOR_EXPRESSION)
add_compile_definitions("${compile_definitions}")
__get_compile_options(OUTPUT compile_options)
idf_build_get_compile_options(compile_options)
add_compile_options("${compile_options}")
idf_build_get_property(common_component_interfaces __COMMON_COMPONENT_INTERFACES)
+1 -1
View File
@@ -1055,6 +1055,6 @@ function(idf_component_include name)
idf_build_get_property(compile_definitions COMPILE_DEFINITIONS GENERATOR_EXPRESSION)
target_compile_definitions("${component_real_target}" PRIVATE "${compile_definitions}")
__get_compile_options(OUTPUT compile_options)
idf_build_get_compile_options(compile_options)
target_compile_options("${component_real_target}" BEFORE PRIVATE "${compile_options}")
endfunction()
-2
View File
@@ -419,8 +419,6 @@ function(__init_project_configuration)
if("${linker_type}" STREQUAL "GNU")
set(target_upper "${idf_target}")
string(TOUPPER ${target_upper} target_upper)
# Add cross-reference table to the map file
list(APPEND link_options "-Wl,--cref")
# Add this symbol as a hint for esp_idf_size to guess the target name
list(APPEND link_options "-Wl,--defsym=IDF_TARGET_${target_upper}=0")
# Check if linker supports --no-warn-rwx-segments
+6 -13
View File
@@ -609,26 +609,19 @@ function(__split)
endfunction()
#[[
__get_compile_options(OUTPUT <variable>)
idf_build_get_compile_options(<variable>)
*OUTPUT[out]*
*variable*
List of generator expressions for C, CXX, and ASM compile options
Variable name in which the list of generator expressions for C, CXX,
and ASM compile options will be stored.
Gather the compilation options from COMPILE_OPTIONS, C_COMPILE_OPTIONS,
CXX_COMPILE_OPTIONS, and ASM_COMPILE_OPTIONS build properties into a single
list using generator expressions. This list can then be used with the
target_compile_options call.
#]]
function(__get_compile_options)
set(options)
set(one_value OUTPUT)
set(multi_value)
cmake_parse_arguments(ARG "${options}" "${one_value}" "${multi_value}" ${ARGN})
if(NOT DEFINED ARG_OUTPUT)
idf_die("OUTPUT option is required")
endif()
function(idf_build_get_compile_options output)
idf_build_get_property(compile_options COMPILE_OPTIONS GENERATOR_EXPRESSION)
idf_build_get_property(c_compile_options C_COMPILE_OPTIONS GENERATOR_EXPRESSION)
idf_build_get_property(cxx_compile_options CXX_COMPILE_OPTIONS GENERATOR_EXPRESSION)
@@ -643,7 +636,7 @@ function(__get_compile_options)
foreach(option IN LISTS asm_compile_options)
list(APPEND compile_options $<$<COMPILE_LANGUAGE:ASM>:${option}>)
endforeach()
set(${ARG_OUTPUT} "${compile_options}" PARENT_SCOPE)
set(${output} "${compile_options}" PARENT_SCOPE)
endfunction()
#[[