1import fnmatch 2import os 3import shutil 4import subprocess 5import sys 6 7import click 8from idf_py_actions.constants import GENERATORS, PREVIEW_TARGETS, SUPPORTED_TARGETS 9from idf_py_actions.errors import FatalError 10from idf_py_actions.global_options import global_options 11from idf_py_actions.tools import (TargetChoice, ensure_build_directory, idf_version, merge_action_lists, realpath, 12 run_target) 13 14 15def action_extensions(base_actions, project_path): 16 def build_target(target_name, ctx, args): 17 """ 18 Execute the target build system to build target 'target_name' 19 20 Calls ensure_build_directory() which will run cmake to generate a build 21 directory (with the specified generator) as needed. 22 """ 23 ensure_build_directory(args, ctx.info_name) 24 run_target(target_name, args) 25 26 def list_build_system_targets(target_name, ctx, args): 27 """Shows list of targets known to build sytem (make/ninja)""" 28 build_target('help', ctx, args) 29 30 def menuconfig(target_name, ctx, args, style): 31 """ 32 Menuconfig target is build_target extended with the style argument for setting the value for the environment 33 variable. 34 """ 35 if sys.version_info[0] < 3: 36 # The subprocess lib cannot accept environment variables as "unicode". 37 # This encoding step is required only in Python 2. 38 style = style.encode(sys.getfilesystemencoding() or 'utf-8') 39 os.environ['MENUCONFIG_STYLE'] = style 40 build_target(target_name, ctx, args) 41 42 def fallback_target(target_name, ctx, args): 43 """ 44 Execute targets that are not explicitly known to idf.py 45 """ 46 ensure_build_directory(args, ctx.info_name) 47 48 try: 49 subprocess.check_output(GENERATORS[args.generator]['dry_run'] + [target_name], cwd=args.build_dir) 50 51 except Exception: 52 raise FatalError( 53 'command "%s" is not known to idf.py and is not a %s target' % (target_name, args.generator)) 54 55 run_target(target_name, args) 56 57 def verbose_callback(ctx, param, value): 58 if not value or ctx.resilient_parsing: 59 return 60 61 for line in ctx.command.verbose_output: 62 print(line) 63 64 return value 65 66 def clean(action, ctx, args): 67 if not os.path.isdir(args.build_dir): 68 print("Build directory '%s' not found. Nothing to clean." % args.build_dir) 69 return 70 build_target('clean', ctx, args) 71 72 def _delete_windows_symlinks(directory): 73 """ 74 It deletes symlinks recursively on Windows. It is useful for Python 2 which doesn't detect symlinks on Windows. 75 """ 76 deleted_paths = [] 77 if os.name == 'nt': 78 import ctypes 79 80 for root, dirnames, _filenames in os.walk(directory): 81 for d in dirnames: 82 full_path = os.path.join(root, d) 83 try: 84 full_path = full_path.decode('utf-8') 85 except Exception: 86 pass 87 if ctypes.windll.kernel32.GetFileAttributesW(full_path) & 0x0400: 88 os.rmdir(full_path) 89 deleted_paths.append(full_path) 90 return deleted_paths 91 92 def fullclean(action, ctx, args): 93 build_dir = args.build_dir 94 if not os.path.isdir(build_dir): 95 print("Build directory '%s' not found. Nothing to clean." % build_dir) 96 return 97 if len(os.listdir(build_dir)) == 0: 98 print("Build directory '%s' is empty. Nothing to clean." % build_dir) 99 return 100 101 if not os.path.exists(os.path.join(build_dir, 'CMakeCache.txt')): 102 raise FatalError( 103 "Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically " 104 "delete files in this directory. Delete the directory manually to 'clean' it." % build_dir) 105 red_flags = ['CMakeLists.txt', '.git', '.svn'] 106 for red in red_flags: 107 red = os.path.join(build_dir, red) 108 if os.path.exists(red): 109 raise FatalError( 110 "Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." 111 % red) 112 # OK, delete everything in the build directory... 113 # Note: Python 2.7 doesn't detect symlinks on Windows (it is supported form 3.2). Tools promising to not 114 # follow symlinks will actually follow them. Deleting the build directory with symlinks deletes also items 115 # outside of this directory. 116 deleted_symlinks = _delete_windows_symlinks(build_dir) 117 if args.verbose and len(deleted_symlinks) > 1: 118 print('The following symlinks were identified and removed:\n%s' % '\n'.join(deleted_symlinks)) 119 for f in os.listdir(build_dir): # TODO: once we are Python 3 only, this can be os.scandir() 120 f = os.path.join(build_dir, f) 121 if args.verbose: 122 print('Removing: %s' % f) 123 if os.path.isdir(f): 124 shutil.rmtree(f) 125 else: 126 os.remove(f) 127 128 def python_clean(action, ctx, args): 129 for root, dirnames, filenames in os.walk(os.environ['IDF_PATH']): 130 for d in dirnames: 131 if d == '__pycache__': 132 dir_to_delete = os.path.join(root, d) 133 if args.verbose: 134 print('Removing: %s' % dir_to_delete) 135 shutil.rmtree(dir_to_delete) 136 for filename in fnmatch.filter(filenames, '*.py[co]'): 137 file_to_delete = os.path.join(root, filename) 138 if args.verbose: 139 print('Removing: %s' % file_to_delete) 140 os.remove(file_to_delete) 141 142 def set_target(action, ctx, args, idf_target): 143 if (not args['preview'] and idf_target in PREVIEW_TARGETS): 144 raise FatalError( 145 "%s is still in preview. You have to append '--preview' option after idf.py to use any preview feature." 146 % idf_target) 147 args.define_cache_entry.append('IDF_TARGET=' + idf_target) 148 sdkconfig_path = os.path.join(args.project_dir, 'sdkconfig') 149 sdkconfig_old = sdkconfig_path + '.old' 150 if os.path.exists(sdkconfig_old): 151 os.remove(sdkconfig_old) 152 if os.path.exists(sdkconfig_path): 153 os.rename(sdkconfig_path, sdkconfig_old) 154 print('Set Target to: %s, new sdkconfig created. Existing sdkconfig renamed to sdkconfig.old.' % idf_target) 155 ensure_build_directory(args, ctx.info_name, True) 156 157 def reconfigure(action, ctx, args): 158 ensure_build_directory(args, ctx.info_name, True) 159 160 def validate_root_options(ctx, args, tasks): 161 args.project_dir = realpath(args.project_dir) 162 if args.build_dir is not None and args.project_dir == realpath(args.build_dir): 163 raise FatalError( 164 'Setting the build directory to the project directory is not supported. Suggest dropping ' 165 "--build-dir option, the default is a 'build' subdirectory inside the project directory.") 166 if args.build_dir is None: 167 args.build_dir = os.path.join(args.project_dir, 'build') 168 args.build_dir = realpath(args.build_dir) 169 170 def idf_version_callback(ctx, param, value): 171 if not value or ctx.resilient_parsing: 172 return 173 174 version = idf_version() 175 176 if not version: 177 raise FatalError('ESP-IDF version cannot be determined') 178 179 print('ESP-IDF %s' % version) 180 sys.exit(0) 181 182 def list_targets_callback(ctx, param, value): 183 if not value or ctx.resilient_parsing: 184 return 185 186 for target in SUPPORTED_TARGETS: 187 print(target) 188 189 if 'preview' in ctx.params: 190 for target in PREVIEW_TARGETS: 191 print(target) 192 193 sys.exit(0) 194 195 root_options = { 196 'global_options': [ 197 { 198 'names': ['--version'], 199 'help': 'Show IDF version and exit.', 200 'is_flag': True, 201 'expose_value': False, 202 'callback': idf_version_callback, 203 }, 204 { 205 'names': ['--list-targets'], 206 'help': 'Print list of supported targets and exit.', 207 'is_flag': True, 208 'expose_value': False, 209 'callback': list_targets_callback, 210 }, 211 { 212 'names': ['-C', '--project-dir'], 213 'scope': 'shared', 214 'help': 'Project directory.', 215 'type': click.Path(), 216 'default': os.getcwd(), 217 }, 218 { 219 'names': ['-B', '--build-dir'], 220 'help': 'Build directory.', 221 'type': click.Path(), 222 'default': None, 223 }, 224 { 225 'names': ['-w/-n', '--cmake-warn-uninitialized/--no-warnings'], 226 'help': ('Enable CMake uninitialized variable warnings for CMake files inside the project directory. ' 227 "(--no-warnings is now the default, and doesn't need to be specified.)"), 228 'envvar': 'IDF_CMAKE_WARN_UNINITIALIZED', 229 'is_flag': True, 230 'default': False, 231 }, 232 { 233 'names': ['-v', '--verbose'], 234 'help': 'Verbose build output.', 235 'is_flag': True, 236 'is_eager': True, 237 'default': False, 238 'callback': verbose_callback, 239 }, 240 { 241 'names': ['--preview'], 242 'help': 'Enable IDF features that are still in preview.', 243 'is_flag': True, 244 'default': False, 245 }, 246 { 247 'names': ['--ccache/--no-ccache'], 248 'help': 'Use ccache in build. Disabled by default.', 249 'is_flag': True, 250 'envvar': 'IDF_CCACHE_ENABLE', 251 'default': False, 252 }, 253 { 254 'names': ['-G', '--generator'], 255 'help': 'CMake generator.', 256 'type': click.Choice(GENERATORS.keys()), 257 }, 258 { 259 'names': ['--dry-run'], 260 'help': "Only process arguments, but don't execute actions.", 261 'is_flag': True, 262 'hidden': True, 263 'default': False, 264 }, 265 ], 266 'global_action_callbacks': [validate_root_options], 267 } 268 269 build_actions = { 270 'actions': { 271 'all': { 272 'aliases': ['build'], 273 'callback': build_target, 274 'short_help': 'Build the project.', 275 'help': ( 276 'Build the project. This can involve multiple steps:\n\n' 277 '1. Create the build directory if needed. ' 278 "The sub-directory 'build' is used to hold build output, " 279 'although this can be changed with the -B option.\n\n' 280 '2. Run CMake as necessary to configure the project ' 281 'and generate build files for the main build tool.\n\n' 282 '3. Run the main build tool (Ninja or GNU Make). ' 283 'By default, the build tool is automatically detected ' 284 'but it can be explicitly set by passing the -G option to idf.py.\n\n'), 285 'options': global_options, 286 'order_dependencies': [ 287 'reconfigure', 288 'menuconfig', 289 'clean', 290 'fullclean', 291 ], 292 }, 293 'menuconfig': { 294 'callback': menuconfig, 295 'help': 'Run "menuconfig" project configuration tool.', 296 'options': global_options + [ 297 { 298 'names': ['--style', '--color-scheme', 'style'], 299 'help': ( 300 'Menuconfig style.\n' 301 'The built-in styles include:\n\n' 302 '- default - a yellowish theme,\n\n' 303 '- monochrome - a black and white theme, or\n\n' 304 '- aquatic - a blue theme.\n\n' 305 'It is possible to customize these themes further' 306 ' as it is described in the Color schemes section of the kconfiglib documentation.\n' 307 'The default value is \"aquatic\".'), 308 'envvar': 'MENUCONFIG_STYLE', 309 'default': 'aquatic', 310 } 311 ], 312 }, 313 'confserver': { 314 'callback': build_target, 315 'help': 'Run JSON configuration server.', 316 'options': global_options, 317 }, 318 'size': { 319 'callback': build_target, 320 'help': 'Print basic size information about the app.', 321 'options': global_options, 322 'dependencies': ['app'], 323 }, 324 'size-components': { 325 'callback': build_target, 326 'help': 'Print per-component size information.', 327 'options': global_options, 328 'dependencies': ['app'], 329 }, 330 'size-files': { 331 'callback': build_target, 332 'help': 'Print per-source-file size information.', 333 'options': global_options, 334 'dependencies': ['app'], 335 }, 336 'bootloader': { 337 'callback': build_target, 338 'help': 'Build only bootloader.', 339 'options': global_options, 340 }, 341 'app': { 342 'callback': build_target, 343 'help': 'Build only the app.', 344 'order_dependencies': ['clean', 'fullclean', 'reconfigure'], 345 'options': global_options, 346 }, 347 'efuse_common_table': { 348 'callback': build_target, 349 'help': "Generate C-source for IDF's eFuse fields.", 350 'order_dependencies': ['reconfigure'], 351 'options': global_options, 352 }, 353 'efuse_custom_table': { 354 'callback': build_target, 355 'help': "Generate C-source for user's eFuse fields.", 356 'order_dependencies': ['reconfigure'], 357 'options': global_options, 358 }, 359 'show_efuse_table': { 360 'callback': build_target, 361 'help': 'Print eFuse table.', 362 'order_dependencies': ['reconfigure'], 363 'options': global_options, 364 }, 365 'partition_table': { 366 'callback': build_target, 367 'help': 'Build only partition table.', 368 'order_dependencies': ['reconfigure'], 369 'options': global_options, 370 }, 371 'erase_otadata': { 372 'callback': build_target, 373 'help': 'Erase otadata partition.', 374 'options': global_options, 375 }, 376 'read_otadata': { 377 'callback': build_target, 378 'help': 'Read otadata partition.', 379 'options': global_options, 380 }, 381 'build-system-targets': { 382 'callback': list_build_system_targets, 383 'help': 'Print list of build system targets.', 384 }, 385 'fallback': { 386 'callback': fallback_target, 387 'help': 'Handle for targets not known for idf.py.', 388 'hidden': True, 389 } 390 } 391 } 392 393 clean_actions = { 394 'actions': { 395 'reconfigure': { 396 'callback': reconfigure, 397 'short_help': 'Re-run CMake.', 398 'help': ( 399 "Re-run CMake even if it doesn't seem to need re-running. " 400 "This isn't necessary during normal usage, " 401 'but can be useful after adding/removing files from the source tree, ' 402 'or when modifying CMake cache variables. ' 403 "For example, \"idf.py -DNAME='VALUE' reconfigure\" " 404 'can be used to set variable "NAME" in CMake cache to value "VALUE".'), 405 'options': global_options, 406 'order_dependencies': ['menuconfig', 'fullclean'], 407 }, 408 'set-target': { 409 'callback': set_target, 410 'short_help': 'Set the chip target to build.', 411 'help': ( 412 'Set the chip target to build. This will remove the ' 413 'existing sdkconfig file and corresponding CMakeCache and ' 414 'create new ones according to the new target.\nFor example, ' 415 "\"idf.py set-target esp32\" will select esp32 as the new chip " 416 'target.'), 417 'arguments': [ 418 { 419 'names': ['idf-target'], 420 'nargs': 1, 421 'type': TargetChoice(SUPPORTED_TARGETS + PREVIEW_TARGETS), 422 }, 423 ], 424 'dependencies': ['fullclean'], 425 }, 426 'clean': { 427 'callback': clean, 428 'short_help': 'Delete build output files from the build directory.', 429 'help': ( 430 'Delete build output files from the build directory, ' 431 "forcing a 'full rebuild' the next time " 432 "the project is built. Cleaning doesn't delete " 433 'CMake configuration output and some other files'), 434 'order_dependencies': ['fullclean'], 435 }, 436 'fullclean': { 437 'callback': fullclean, 438 'short_help': 'Delete the entire build directory contents.', 439 'help': ( 440 'Delete the entire build directory contents. ' 441 'This includes all CMake configuration output.' 442 'The next time the project is built, ' 443 'CMake will configure it from scratch. ' 444 'Note that this option recursively deletes all files ' 445 'in the build directory, so use with care.' 446 'Project configuration is not deleted.') 447 }, 448 'python-clean': { 449 'callback': python_clean, 450 'short_help': 'Delete generated Python byte code from the IDF directory', 451 'help': ( 452 'Delete generated Python byte code from the IDF directory ' 453 'which may cause issues when switching between IDF and Python versions. ' 454 'It is advised to run this target after switching versions.') 455 }, 456 } 457 } 458 459 return merge_action_lists(root_options, build_actions, clean_actions) 460