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