feat(tools): Added create-project argument to allow cpp project creation

Closes https://github.com/espressif/esp-idf/issues/16121
This commit is contained in:
Jakub Kocka
2026-04-08 12:28:37 +02:00
parent b1d68624e1
commit fb02854ad6
8 changed files with 58 additions and 10 deletions

View File

@@ -25,7 +25,7 @@ Start a New Project: ``create-project``
idf.py create-project <project name>
This command creates a new ESP-IDF project. Additionally, the folder where the project will be created in can be specified by the ``--path`` option.
This command creates a new ESP-IDF project. Additionally, the folder where the project will be created in can be specified by the ``--path`` option. Pass ``--cpp`` to create a C++ source file (``<project name>.cpp``) with C linkage for ``app_main`` instead of a ``.c`` file.
Create a New Component: ``create-component``
--------------------------------------------

View File

@@ -340,6 +340,7 @@ if __name__ == '__main__':
_exclude_dirs = [
os.path.join(IDF_PATH, 'tools', 'test_build_system'),
os.path.join(IDF_PATH, 'tools', 'templates', 'sample_project'),
os.path.join(IDF_PATH, 'tools', 'templates', 'sample_project_cpp'),
os.path.join(IDF_PATH, 'tools', 'cmakev2', 'test'),
]

View File

@@ -55,3 +55,6 @@ tools/templates/sample_component/main.c
tools/templates/sample_project/CMakeLists.txt
tools/templates/sample_project/main/CMakeLists.txt
tools/templates/sample_project/main/main.c
tools/templates/sample_project_cpp/CMakeLists.txt
tools/templates/sample_project_cpp/main/CMakeLists.txt
tools/templates/sample_project_cpp/main/main.cpp

View File

@@ -1,12 +1,12 @@
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
import re
import stat
import sys
from collections.abc import Callable
from shutil import copyfile
from shutil import copytree
from typing import Dict
import click
@@ -65,9 +65,11 @@ def make_directory_permissions_writable(root_path: str) -> None:
continue
def create_project(target_path: str, name: str) -> None:
def create_project(target_path: str, name: str, *, use_cpp: bool = False) -> None:
template = 'sample_project_cpp' if use_cpp else 'sample_project'
main_ext = 'cpp' if use_cpp else 'c'
copytree(
os.path.join(os.environ['IDF_PATH'], 'tools', 'templates', 'sample_project'),
os.path.join(os.environ['IDF_PATH'], 'tools', 'templates', template),
target_path,
# 'copyfile' ensures only data are copied, without any metadata (file permissions) - for files only
copy_function=copyfile,
@@ -76,7 +78,10 @@ def create_project(target_path: str, name: str) -> None:
# since 'copyfile' does preserve directory metadata, we need to make sure the directories are writable
make_directory_permissions_writable(target_path)
main_folder = os.path.join(target_path, 'main')
os.rename(os.path.join(main_folder, 'main.c'), os.path.join(main_folder, '.'.join((name, 'c'))))
os.rename(
os.path.join(main_folder, '.'.join(('main', main_ext))),
os.path.join(main_folder, '.'.join((name, main_ext))),
)
replace_in_file(os.path.join(main_folder, 'CMakeLists.txt'), 'main', name)
replace_in_file(os.path.join(target_path, 'CMakeLists.txt'), 'main', name)
@@ -100,14 +105,17 @@ def create_component(target_path: str, name: str) -> None:
replace_in_file(os.path.join(target_path, 'CMakeLists.txt'), 'main', name)
def action_extensions(base_actions: Dict, project_path: str) -> Dict:
def create_new(action: str, ctx: click.core.Context, global_args: PropertyDict, **action_args: str) -> Dict:
def action_extensions(base_actions: dict, project_path: str) -> dict:
def create_new(action: str, ctx: click.core.Context, global_args: PropertyDict, **action_args: str) -> dict:
target_path = action_args.get('path') or os.path.join(project_path, action_args['name'])
is_empty_and_create(target_path, action)
func_action_map = {'create-project': create_project, 'create-component': create_component}
func_action_map[action](target_path, action_args['name'])
func_action_map: dict[str, Callable[..., None]] = {
'create-project': lambda tp, n, aa: create_project(tp, n, use_cpp=bool(aa.get('cpp'))),
'create-component': lambda tp, n, aa: create_component(tp, n),
}
func_action_map[action](target_path, action_args['name'], action_args)
print('The', get_type(action), 'was created in', os.path.abspath(target_path))
@@ -125,6 +133,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'`idf.py create-project new_proj` '
'will create a new project in subdirectory called `new_proj` '
'of the current working directory. '
'Use `--cpp` to generate a C++ source file (`NAME.cpp`) with `extern "C" void app_main(void)`. '
"For specifying the new project's path, use either the option --path for specifying the "
'destination directory, or the global option -C if the project should be created as a '
'subdirectory of the specified directory. '
@@ -143,6 +152,12 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'will be created directly in the given folder if it does not contain anything'
),
},
{
'names': ['--cpp'],
'is_flag': True,
'default': False,
'help': 'Create a C++ main source file instead of C.',
},
],
},
'create-component': {

View File

@@ -0,0 +1,6 @@
# 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/cmake/project.cmake)
project(main)

View File

@@ -0,0 +1,2 @@
idf_component_register(SRCS "main.cpp"
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,6 @@
#include <cstdio>
extern "C" void app_main(void)
{
}

View File

@@ -220,6 +220,21 @@ def test_create_component_project(idf_copy: Path) -> None:
run_idf_py('build', workdir=(idf_copy / 'projects' / 'temp_test_project'))
def test_create_project_cpp(idf_copy: Path) -> None:
logging.info('Create C++ project with idf.py create-project --cpp')
proj_dir = idf_copy / 'projects' / 'cpp_test_project'
run_idf_py('-C', 'projects', 'create-project', '--cpp', 'cpp_test_project', workdir=idf_copy)
main_cpp = proj_dir / 'main' / 'cpp_test_project.cpp'
assert main_cpp.is_file()
text = main_cpp.read_text(encoding='utf-8')
assert 'extern "C" void app_main(void)' in text
cmake = (proj_dir / 'main' / 'CMakeLists.txt').read_text(encoding='utf-8')
assert 'cpp_test_project.cpp' in cmake
# Avoid `assert 'cpp_test_project.c' not in cmake`: that string is a substring of `cpp_test_project.cpp`.
assert 'SRCS "cpp_test_project.c"' not in cmake
run_idf_py('build', workdir=str(proj_dir))
# In this test function, there are actually two logical tests in one test function.
# It would be better to have every check in a separate
# test case, but that would mean doing idf_copy each time, and copying takes most of the time