1- # SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
1+ # SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
22# SPDX-License-Identifier: Apache-2.0
33import os
44import re
5+ import stat
56import sys
67from shutil import copyfile
78from shutil import copytree
89from typing import Dict
910
1011import click
12+
1113from idf_py_actions .tools import PropertyDict
1214
1315
@@ -29,23 +31,50 @@ def is_empty_and_create(path: str, action: str) -> None:
2931 if not os .path .exists (abspath ):
3032 os .makedirs (abspath )
3133 elif not os .path .isdir (abspath ):
32- print ('Your target path is not a directory. Please remove the' , os .path .abspath (abspath ),
33- 'or use different target path.' )
34+ print (
35+ f'Your target path is not a directory.'
36+ f'Please remove the { os .path .abspath (abspath )} or use different target path.'
37+ )
3438 sys .exit (4 )
3539 elif len (os .listdir (path )) > 0 :
36- print ('The directory' , abspath , 'is not empty. To create a' , get_type (action ),
37- 'you must empty the directory or choose a different path.' )
40+ print (
41+ f'The directory { abspath } is not empty. To create a { get_type (action )} you must '
42+ f'empty the directory or choose a different path.'
43+ )
3844 sys .exit (3 )
3945
4046
47+ def make_directory_permissions_writable (root_path : str ) -> None :
48+ """
49+ Ensures all directories under `root_path` have write permission for the owner.
50+ Skips files and doesn't override existing permissions unnecessarily.
51+ Only applies to POSIX systems (Linux/macOS).
52+ """
53+ if sys .platform == 'win32' :
54+ return
55+
56+ for current_root , dirs , _ in os .walk (root_path ):
57+ for dirname in dirs :
58+ dir_path = os .path .join (current_root , dirname )
59+ try :
60+ current_perm = stat .S_IMODE (os .stat (dir_path ).st_mode )
61+ new_perm = current_perm | stat .S_IWUSR # mask permission for owner (write)
62+ if new_perm != current_perm :
63+ os .chmod (dir_path , new_perm )
64+ except PermissionError :
65+ continue
66+
67+
4168def create_project (target_path : str , name : str ) -> None :
4269 copytree (
4370 os .path .join (os .environ ['IDF_PATH' ], 'tools' , 'templates' , 'sample_project' ),
4471 target_path ,
45- # 'copyfile' ensures only data are copied, without any metadata (file permissions)
72+ # 'copyfile' ensures only data are copied, without any metadata (file permissions) - for files only
4673 copy_function = copyfile ,
4774 dirs_exist_ok = True ,
4875 )
76+ # since 'copyfile' does preserve directory metadata, we need to make sure the directories are writable
77+ make_directory_permissions_writable (target_path )
4978 main_folder = os .path .join (target_path , 'main' )
5079 os .rename (os .path .join (main_folder , 'main.c' ), os .path .join (main_folder , '.' .join ((name , 'c' ))))
5180 replace_in_file (os .path .join (main_folder , 'CMakeLists.txt' ), 'main' , name )
@@ -56,13 +85,16 @@ def create_component(target_path: str, name: str) -> None:
5685 copytree (
5786 os .path .join (os .environ ['IDF_PATH' ], 'tools' , 'templates' , 'sample_component' ),
5887 target_path ,
59- # 'copyfile' ensures only data are copied, without any metadata (file permissions)
88+ # 'copyfile' ensures only data are copied, without any metadata (file permissions) - for files only
6089 copy_function = copyfile ,
6190 dirs_exist_ok = True ,
6291 )
92+ # since 'copyfile' does preserve directory metadata, we need to make sure the directories are writable
93+ make_directory_permissions_writable (target_path )
6394 os .rename (os .path .join (target_path , 'main.c' ), os .path .join (target_path , '.' .join ((name , 'c' ))))
64- os .rename (os .path .join (target_path , 'include' , 'main.h' ),
65- os .path .join (target_path , 'include' , '.' .join ((name , 'h' ))))
95+ os .rename (
96+ os .path .join (target_path , 'include' , 'main.h' ), os .path .join (target_path , 'include' , '.' .join ((name , 'h' )))
97+ )
6698
6799 replace_in_file (os .path .join (target_path , '.' .join ((name , 'c' ))), 'main' , name )
68100 replace_in_file (os .path .join (target_path , 'CMakeLists.txt' ), 'main' , name )
@@ -87,45 +119,50 @@ def create_new(action: str, ctx: click.core.Context, global_args: PropertyDict,
87119 'create-project' : {
88120 'callback' : create_new ,
89121 'short_help' : 'Create a new project.' ,
90- 'help' : ('Create a new project with the name NAME specified as argument. '
91- 'For example: '
92- '`idf.py create-project new_proj` '
93- 'will create a new project in subdirectory called `new_proj` '
94- 'of the current working directory. '
95- "For specifying the new project's path, use either the option --path for specifying the "
96- 'destination directory, or the global option -C if the project should be created as a '
97- 'subdirectory of the specified directory. '
98- 'If the target path does not exist it will be created. If the target folder is not empty '
99- 'then the operation will fail with return code 3. '
100- 'If the target path is not a folder, the script will fail with return code 4. '
101- 'After the execution idf.py terminates '
102- 'so this operation should be used alone.' ),
122+ 'help' : (
123+ 'Create a new project with the name NAME specified as argument. '
124+ 'For example: '
125+ '`idf.py create-project new_proj` '
126+ 'will create a new project in subdirectory called `new_proj` '
127+ 'of the current working directory. '
128+ "For specifying the new project's path, use either the option --path for specifying the "
129+ 'destination directory, or the global option -C if the project should be created as a '
130+ 'subdirectory of the specified directory. '
131+ 'If the target path does not exist it will be created. If the target folder is not empty '
132+ 'then the operation will fail with return code 3. '
133+ 'If the target path is not a folder, the script will fail with return code 4. '
134+ 'After the execution idf.py terminates '
135+ 'so this operation should be used alone.'
136+ ),
103137 'arguments' : [{'names' : ['name' ]}],
104138 'options' : [
105139 {
106140 'names' : ['-p' , '--path' ],
107- 'help' : ('Set the path for the new project. The project '
108- 'will be created directly in the given folder if it does not contain anything' ),
141+ 'help' : (
142+ 'Set the path for the new project. The project '
143+ 'will be created directly in the given folder if it does not contain anything'
144+ ),
109145 },
110146 ],
111-
112147 },
113148 'create-component' : {
114149 'callback' : create_new ,
115150 'short_help' : 'Create a new component.' ,
116- 'help' : ('Create a new component with the name NAME specified as argument. '
117- 'For example: '
118- '`idf.py create-component new_comp` '
119- 'will create a new component in subdirectory called `new_comp` '
120- 'of the current working directory. '
121- "For specifying the new component's path use the option -C. "
122- 'If the target path does not exist then it will be created. '
123- 'If the target folder is not empty '
124- 'then the operation will fail with return code 3. '
125- 'If the target path is not a folder, the script will fail with return code 4. '
126- 'After the execution idf.py terminates '
127- 'so this operation should be used alone.' ),
151+ 'help' : (
152+ 'Create a new component with the name NAME specified as argument. '
153+ 'For example: '
154+ '`idf.py create-component new_comp` '
155+ 'will create a new component in subdirectory called `new_comp` '
156+ 'of the current working directory. '
157+ "For specifying the new component's path use the option -C. "
158+ 'If the target path does not exist then it will be created. '
159+ 'If the target folder is not empty '
160+ 'then the operation will fail with return code 3. '
161+ 'If the target path is not a folder, the script will fail with return code 4. '
162+ 'After the execution idf.py terminates '
163+ 'so this operation should be used alone.'
164+ ),
128165 'arguments' : [{'names' : ['name' ]}],
129- }
166+ },
130167 }
131168 }
0 commit comments