|
| 1 | +--- |
| 2 | +title: "Extending idf.py: Create custom commands for your ESP-IDF workflow" |
| 3 | +date: "2025-10-10" |
| 4 | +showAuthor: false |
| 5 | +authors: |
| 6 | + - "marek-fiala" |
| 7 | +tags: ["ESP-IDF", "idf.py", "CLI", "Extensions", "Development Tools"] |
| 8 | +summary: "Learn how to extend idf.py with custom commands for your development workflow. This guide covers both component-based extensions for project-specific tools and Python package extensions for reusable commands, with practical examples and best practices for seamless integration." |
| 9 | +--- |
| 10 | + |
| 11 | +<!-- TODO[Remove after ESP-IDF v6.0 release] |
| 12 | +Note: This article mentions ESP-IDF v6.0. Once it is released, remove the note below about development status. |
| 13 | +Context: Developer Portal's GitLab MR `88#note_2327272` |
| 14 | +ReviewDate: 2026-01-15 |
| 15 | +Tags: ESP-IDF v6.0, obsolete |
| 16 | +--> |
| 17 | + |
| 18 | +What if you could extend `idf.py` with your own custom commands tailored to your specific workflow? With the ESP-IDF v6.0 and newer (to be released soon), you can do exactly that through a powerful extension system that lets you add project-specific tools or distribute reusable commands across your projects. |
| 19 | + |
| 20 | +Before we dive into extensions, let’s recall what `idf.py` gives you out of the box. It’s the central command-line tool for ESP-IDF that allows you to: |
| 21 | + |
| 22 | +- Set the target chip using `idf.py set-target` (like esp32) |
| 23 | +- Tweak your project settings using `idf.py menuconfig` |
| 24 | +- Build your application using `idf.py build` |
| 25 | +- Flash it using `idf.py -p PORT flash` |
| 26 | +- Watch the logs in real time using `idf.py monitor` |
| 27 | + |
| 28 | +For most developers, the daily cycle is simply **build → flash → monitor** — all streamlined under one command. |
| 29 | + |
| 30 | +## Why extend idf.py? |
| 31 | + |
| 32 | +Sometimes, though, the built-in commands aren’t enough. Maybe you need a custom deployment command that packages your firmware with metadata, or perhaps you want to integrate with your CI/CD pipeline through specialized build targets. Rather than maintaining separate scripts, you can now integrate these directly into `idf.py`, giving you: |
| 33 | + |
| 34 | +- **Unified interface**: All your tools accessible through the familiar `idf.py` command |
| 35 | +- **Consistent help system**: Your commands appear in `idf.py --help` with proper documentation |
| 36 | +- **Shared options**: Leverage existing global options like `--port` and `--build-dir` |
| 37 | +- **Dependency management**: Ensure commands run in the right order automatically |
| 38 | + |
| 39 | +## Two ways to extend idf.py |
| 40 | + |
| 41 | +ESP-IDF supports two extension mechanisms, each suited for different use cases: |
| 42 | + |
| 43 | +- Component-based extensions |
| 44 | +- Python package extensions |
| 45 | + |
| 46 | +### Component-based extensions |
| 47 | + |
| 48 | +This is the case for project-specific commands that should only be available when working with a particular project or component. |
| 49 | + |
| 50 | +**How it works**: Place a file named `idf_ext.py` in your component directory. ESP-IDF automatically discovers and loads extensions from this file **after the project is configured** with `idf.py reconfigure` or `idf.py build`. Within the component-based extension, the name of the file is important, as `idf.py` searches exactly for `idf_ext.py`. |
| 51 | + |
| 52 | +**Note**: You may also place `idf_ext.py` in the project root instead of a component. This option has existed in earlier ESP-IDF versions and works the same way, but using a dedicated component is recommended for clarity and reusability. |
| 53 | + |
| 54 | +#### Step 1: Create the extension file |
| 55 | + |
| 56 | +- Create a new component (or use an existing one). |
| 57 | +- Inside the component, add a file named `idf_ext.py`. |
| 58 | +- This file must implement an `action_extensions` function returning a dictionary that describes your new commands, options, and callbacks. |
| 59 | + |
| 60 | +**Example (sensor manager)**: |
| 61 | +In this example, we’ll add a new command sensor-info that prints configuration details about sensors in your project. Start by creating a component called `sensor_manager`: |
| 62 | + |
| 63 | +```bash |
| 64 | +# Create the component using idf.py |
| 65 | +idf.py create-component -C components sensor_manager |
| 66 | +``` |
| 67 | + |
| 68 | +Then, inside your component directory `components/sensor_manager/`, create `idf_ext.py` Python file and place the following code: |
| 69 | + |
| 70 | +```python |
| 71 | +from typing import Any |
| 72 | +import click |
| 73 | + |
| 74 | +def action_extensions(base_actions: dict, project_path: str) -> dict: |
| 75 | + def sensor_info(subcommand_name: str, ctx: click.Context, global_args: dict, **action_args: Any) -> None: |
| 76 | + sensor_type = action_args.get('type', 'all') |
| 77 | + verbose = getattr(global_args, 'detail', False) |
| 78 | + |
| 79 | + print(f"Running {subcommand_name} for sensor type: {sensor_type}") |
| 80 | + if verbose: |
| 81 | + print(f"Project path: {project_path}") |
| 82 | + print("Detailed sensor configuration would be displayed here...") |
| 83 | + |
| 84 | + def global_callback_detail(ctx: click.Context, global_args: dict, tasks: list) -> None: |
| 85 | + if getattr(global_args, 'detail', False): |
| 86 | + print(f"About to execute {len(tasks)} task(s): {[t.name for t in tasks]}") |
| 87 | + |
| 88 | + return { |
| 89 | + "version": "1", |
| 90 | + "global_options": [ |
| 91 | + { |
| 92 | + "names": ["--detail", "-d"], |
| 93 | + "is_flag": True, |
| 94 | + "help": "Enable detailed output for all commands", |
| 95 | + } |
| 96 | + ], |
| 97 | + "global_action_callbacks": [global_callback_detail], |
| 98 | + "actions": { |
| 99 | + "sensor-info": { |
| 100 | + "callback": sensor_info, |
| 101 | + "short_help": "Display sensor configuration", |
| 102 | + "help": "Show detailed information about sensor configuration and status", |
| 103 | + "options": [ |
| 104 | + { |
| 105 | + "names": ["--type", "-t"], |
| 106 | + "help": "Sensor type to query (temperature, humidity, pressure, or all)", |
| 107 | + "default": "all", |
| 108 | + "type": click.Choice(['temperature', 'humidity', 'pressure', 'all']), |
| 109 | + } |
| 110 | + ] |
| 111 | + }, |
| 112 | + }, |
| 113 | + } |
| 114 | +``` |
| 115 | + |
| 116 | +#### Step 2: Register the component |
| 117 | + |
| 118 | +- Ensure the new component is registered in your project’s CMakeLists.txt. |
| 119 | +- Further information on how to register commponents can be found in [Espressif documentation](https://docs.espressif.com/projects/esp-idf/en/stable/api-guides/build-system.html#component-requirements). |
| 120 | + |
| 121 | +<!-- Now you need to make sure your component is registered in your project's main `CMakeLists.txt`: --> |
| 122 | + |
| 123 | +**Example (sensor manager)**: |
| 124 | +Update your project's main `CMakeLists.txt` |
| 125 | +```cmake |
| 126 | +idf_component_register( |
| 127 | + SRCS "main.c" |
| 128 | + INCLUDE_DIRS "." |
| 129 | + REQUIRES "sensor_manager" # This makes the extension available |
| 130 | +) |
| 131 | +``` |
| 132 | + |
| 133 | +#### Step 3: Load and test |
| 134 | +- Reconfigure or build the project to let ESP-IDF discover the extension. |
| 135 | +- Run idf.py help to check that your new command appears. |
| 136 | +- Test the new command with its options. |
| 137 | + |
| 138 | +**Example (sensor manager)**: In our case, the extension adds the `sensor-info` command: |
| 139 | + |
| 140 | +```bash |
| 141 | +# Configure the project to discover the extension |
| 142 | +idf.py reconfigure |
| 143 | + |
| 144 | +# Check that your command appears in help |
| 145 | +idf.py --help |
| 146 | + |
| 147 | +# Try your new command |
| 148 | +idf.py sensor-info --type temperature |
| 149 | +idf.py --detail sensor-info --type all |
| 150 | +``` |
| 151 | + |
| 152 | +### Python package extensions |
| 153 | + |
| 154 | +This is ideal for reusable tools that you want to share across multiple projects or distribute to your team. |
| 155 | + |
| 156 | +**How it works**: Create a Python package with an entry point in the `idf_extension` group. Once installed, the extension is available globally for all projects. |
| 157 | + |
| 158 | +#### Step 1: Create the package structure |
| 159 | + |
| 160 | +- Create a new folder for your tool. |
| 161 | +- Add a `pyproject.toml` file to describe the package. |
| 162 | +- Inside the folder, create a subfolder with the same name, which will contain your Python code. |
| 163 | +- Inside that subfolder, add `__init__.py` and a Python file for the extension (e.g., `esp_ext.py`). |
| 164 | + - Unlike the fixed `idf_ext.py` in component-based extensions, the filename here is flexible because it is explicitly referenced in `pyproject.toml`. |
| 165 | + - For clarity and consistency, it’s recommended to prefix it with your tool name and suffix it with `_ext.py`. |
| 166 | + |
| 167 | +The resulting structure should look like this: |
| 168 | + |
| 169 | +```bash |
| 170 | +my_sensor_tools/ |
| 171 | +├── pyproject.toml # describe the package here |
| 172 | +├── my_sensor_tools/ # place your Python code here |
| 173 | +│ ├── __init__.py |
| 174 | +│ └── esp_ext.py |
| 175 | +``` |
| 176 | + |
| 177 | +#### Step 2: Fill the extension file |
| 178 | + |
| 179 | +- Implement the `action_extensions` function inside your package’s Python file. |
| 180 | + |
| 181 | +**Example (sensor manager)**: |
| 182 | + |
| 183 | +Here we simply copy the `action_extensions` function from the component example into `my_sensor_tools/esp_ext.py`. |
| 184 | + |
| 185 | +#### Step 3: Configure and install |
| 186 | + |
| 187 | +- Define the Python entry-point in your `pyproject.toml` under `[project.entry-points.idf_extension]`. |
| 188 | + - Use the format name: `package.module:function`. |
| 189 | +- Install the package (for development, use `pip install -e .`). |
| 190 | +- The new command will now be globally available in any ESP-IDF project. |
| 191 | + |
| 192 | +<!-- On the final step, you will need to create the `pyproject.toml` file: --> |
| 193 | +**Example (sensor manager)**: |
| 194 | + |
| 195 | +The `pyproject.toml` file for our example could look like this: |
| 196 | + |
| 197 | +```toml |
| 198 | +[project] |
| 199 | +name = "my-sensor-tools" |
| 200 | +version = "1.0.0" |
| 201 | + |
| 202 | +# Register the extension under the `idf_extension` group, |
| 203 | +# so ESP-IDF can automatically discover it |
| 204 | +[project.entry-points.idf_extension] |
| 205 | +my_sensor_tools = "my_sensor_tools.esp_ext:action_extensions" |
| 206 | +``` |
| 207 | + |
| 208 | +Install and use: |
| 209 | + |
| 210 | +```bash |
| 211 | +# Install in development mode |
| 212 | +cd my_sensor_tools |
| 213 | +pip install -e . |
| 214 | + |
| 215 | +# Your command is now available in any ESP-IDF project |
| 216 | +cd my_sensor_tools/ |
| 217 | +idf.py sensor-info --type temperature |
| 218 | +idf.py --detail sensor-info --type all |
| 219 | +``` |
| 220 | + |
| 221 | +## Naming conventions |
| 222 | + |
| 223 | +- **Avoid conflicts**: Your commands cannot override built-in `idf.py` commands like `build`, `flash`, or `monitor` |
| 224 | +- **Use descriptive names**: Prefer `sensor-info` over `info` to avoid ambiguity |
| 225 | +- **Package prefixes**: For Python package extensions, consider prefixing commands with your tool name |
| 226 | + |
| 227 | +## Advanced features |
| 228 | + |
| 229 | +Do you need something extra? Beyond simple commands, the extension system also gives you ways to define global options, control execution order, and build richer command-line interfaces. These features let you create tools that feel fully integrated with the rest of `idf.py`. |
| 230 | + |
| 231 | +### Global options and callbacks |
| 232 | + |
| 233 | +The extension system supports sophisticated features for power users: |
| 234 | + |
| 235 | +**Global options**: Define options that work across all commands. Can be exposed under `global_args` parameter. |
| 236 | + |
| 237 | +**Global callbacks**: Functions that run before any tasks execute, perfect for validation, logging, or injecting additional tasks based on global options. |
| 238 | + |
| 239 | +### Dependencies and order management |
| 240 | + |
| 241 | +Ensure your commands run in the correct sequence: |
| 242 | + |
| 243 | +```python |
| 244 | +"actions": { |
| 245 | + "deploy": { |
| 246 | + "callback": deploy_firmware, |
| 247 | + "dependencies": ["all"], # Always build before deploying |
| 248 | + "order_dependencies": ["flash"], # If flash is requested, run it before deploy |
| 249 | + "help": "Deploy firmware to production servers" |
| 250 | + } |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +### Rich argument support |
| 255 | + |
| 256 | +Support complex command-line interfaces: |
| 257 | + |
| 258 | +```python |
| 259 | +"options": [ |
| 260 | + { |
| 261 | + "names": ["--config-file", "-c"], |
| 262 | + "type": click.Path(exists=True), |
| 263 | + "help": "Configuration file path" |
| 264 | + }, |
| 265 | + { |
| 266 | + "names": ["--verbose", "-v"], |
| 267 | + "count": True, # -v, -vv, -vvv for different verbosity levels |
| 268 | + "help": "Increase verbosity (use multiple times)" |
| 269 | + } |
| 270 | +], |
| 271 | +"arguments": [ |
| 272 | + { |
| 273 | + "names": ["targets"], |
| 274 | + "nargs": -1, # Accept multiple targets |
| 275 | + "required": True |
| 276 | + } |
| 277 | +] |
| 278 | +``` |
| 279 | + |
| 280 | +For more details on the extension API and additional features, see the [Click documentation](https://click.palletsprojects.com/) for argument types and the [ESP-IDF documentation](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/tools/idf-py.html#extending-idf-py) for the complete extension reference. |
| 281 | + |
| 282 | +## Conclusion |
| 283 | + |
| 284 | +The `idf.py` extension system opens up powerful possibilities for customizing your ESP-IDF development workflow. Whether you're adding simple project-specific helpers or building sophisticated development tools, extensions let you integrate seamlessly with the existing ESP-IDF ecosystem. |
| 285 | + |
| 286 | +Start small with a component-based extension for your current project, then graduate to distributable packages as your tools mature. |
| 287 | + |
| 288 | +## What's next? |
| 289 | + |
| 290 | +- Explore the [full extension API documentation](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/tools/idf-py.html#extending-idf-py) for advanced features |
| 291 | +- Check out existing extensions in the ESP-IDF codebase for inspiration |
| 292 | + |
| 293 | +Happy extending! |
0 commit comments