diff --git a/docs/en/api-guides/tools/idf-py.rst b/docs/en/api-guides/tools/idf-py.rst index 0c531d44b05..39f91b8ab2b 100644 --- a/docs/en/api-guides/tools/idf-py.rst +++ b/docs/en/api-guides/tools/idf-py.rst @@ -25,7 +25,7 @@ Start a New Project: ``create-project`` idf.py create-project -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 (``.cpp``) with C linkage for ``app_main`` instead of a ``.c`` file. Create a New Component: ``create-component`` -------------------------------------------- diff --git a/tools/ci/check_build_test_rules.py b/tools/ci/check_build_test_rules.py index 8c029c6aa49..1c323a5a7af 100755 --- a/tools/ci/check_build_test_rules.py +++ b/tools/ci/check_build_test_rules.py @@ -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'), ] diff --git a/tools/ci/exclude_check_tools_files.txt b/tools/ci/exclude_check_tools_files.txt index 4541238259f..7e333f4ecaf 100644 --- a/tools/ci/exclude_check_tools_files.txt +++ b/tools/ci/exclude_check_tools_files.txt @@ -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 diff --git a/tools/idf_py_actions/create_ext.py b/tools/idf_py_actions/create_ext.py index ef53d2d11f7..2feab908f07 100644 --- a/tools/idf_py_actions/create_ext.py +++ b/tools/idf_py_actions/create_ext.py @@ -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': { diff --git a/tools/templates/sample_project_cpp/CMakeLists.txt b/tools/templates/sample_project_cpp/CMakeLists.txt new file mode 100644 index 00000000000..718e83f01cf --- /dev/null +++ b/tools/templates/sample_project_cpp/CMakeLists.txt @@ -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) diff --git a/tools/templates/sample_project_cpp/main/CMakeLists.txt b/tools/templates/sample_project_cpp/main/CMakeLists.txt new file mode 100644 index 00000000000..9eb7ec47a07 --- /dev/null +++ b/tools/templates/sample_project_cpp/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "main.cpp" + INCLUDE_DIRS ".") diff --git a/tools/templates/sample_project_cpp/main/main.cpp b/tools/templates/sample_project_cpp/main/main.cpp new file mode 100644 index 00000000000..5b2137bcc17 --- /dev/null +++ b/tools/templates/sample_project_cpp/main/main.cpp @@ -0,0 +1,6 @@ +#include + +extern "C" void app_main(void) +{ + +} diff --git a/tools/test_build_system/test_common.py b/tools/test_build_system/test_common.py index 567cddbe5d7..6cb3cca2582 100644 --- a/tools/test_build_system/test_common.py +++ b/tools/test_build_system/test_common.py @@ -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