1#!/usr/bin/env python 2# 3# SPDX-FileCopyrightText: 2019-2021 Espressif Systems (Shanghai) CO LTD 4# 5# SPDX-License-Identifier: Apache-2.0 6# 7# 'idf.py' is a top-level config/build command line tool for ESP-IDF 8# 9# You don't have to use idf.py, you can use cmake directly 10# (or use cmake in an IDE) 11 12# WARNING: we don't check for Python build-time dependencies until 13# check_environment() function below. If possible, avoid importing 14# any external libraries here - put in external script, or import in 15# their specific function instead. 16from __future__ import print_function 17 18import codecs 19import json 20import locale 21import os 22import os.path 23import signal 24import subprocess 25import sys 26from collections import Counter, OrderedDict 27from importlib import import_module 28from pkgutil import iter_modules 29 30# pyc files remain in the filesystem when switching between branches which might raise errors for incompatible 31# idf.py extensions. Therefore, pyc file generation is turned off: 32sys.dont_write_bytecode = True 33 34import python_version_checker # noqa: E402 35from idf_py_actions.errors import FatalError # noqa: E402 36from idf_py_actions.tools import executable_exists, idf_version, merge_action_lists, realpath # noqa: E402 37 38# Use this Python interpreter for any subprocesses we launch 39PYTHON = sys.executable 40 41# note: os.environ changes don't automatically propagate to child processes, 42# you have to pass env=os.environ explicitly anywhere that we create a process 43os.environ['PYTHON'] = sys.executable 44 45# Name of the program, normally 'idf.py'. 46# Can be overridden from idf.bat using IDF_PY_PROGRAM_NAME 47PROG = os.getenv('IDF_PY_PROGRAM_NAME', 'idf.py') 48 49 50# function prints warning when autocompletion is not being performed 51# set argument stream to sys.stderr for errors and exceptions 52def print_warning(message, stream=None): 53 stream = stream or sys.stderr 54 if not os.getenv('_IDF.PY_COMPLETE'): 55 print(message, file=stream) 56 57 58def check_environment(): 59 """ 60 Verify the environment contains the top-level tools we need to operate 61 62 (cmake will check a lot of other things) 63 """ 64 checks_output = [] 65 66 if not executable_exists(['cmake', '--version']): 67 debug_print_idf_version() 68 raise FatalError("'cmake' must be available on the PATH to use %s" % PROG) 69 70 # verify that IDF_PATH env variable is set 71 # find the directory idf.py is in, then the parent directory of this, and assume this is IDF_PATH 72 detected_idf_path = realpath(os.path.join(os.path.dirname(__file__), '..')) 73 if 'IDF_PATH' in os.environ: 74 set_idf_path = realpath(os.environ['IDF_PATH']) 75 if set_idf_path != detected_idf_path: 76 print_warning( 77 'WARNING: IDF_PATH environment variable is set to %s but %s path indicates IDF directory %s. ' 78 'Using the environment variable directory, but results may be unexpected...' % 79 (set_idf_path, PROG, detected_idf_path)) 80 else: 81 print_warning('Setting IDF_PATH environment variable: %s' % detected_idf_path) 82 os.environ['IDF_PATH'] = detected_idf_path 83 84 try: 85 # The Python compatibility check could have been done earlier (tools/detect_python.{sh,fish}) but PATH is 86 # not set for import at that time. Even if the check would be done before, the same check needs to be done 87 # here as well (for example one can call idf.py from a not properly set-up environment). 88 python_version_checker.check() 89 except RuntimeError as e: 90 raise FatalError(e) 91 92 # check Python dependencies 93 checks_output.append('Checking Python dependencies...') 94 try: 95 out = subprocess.check_output( 96 [ 97 os.environ['PYTHON'], 98 os.path.join(os.environ['IDF_PATH'], 'tools', 'check_python_dependencies.py'), 99 ], 100 env=os.environ, 101 ) 102 103 checks_output.append(out.decode('utf-8', 'ignore').strip()) 104 except subprocess.CalledProcessError as e: 105 print_warning(e.output.decode('utf-8', 'ignore'), stream=sys.stderr) 106 debug_print_idf_version() 107 raise SystemExit(1) 108 109 return checks_output 110 111 112def _safe_relpath(path, start=None): 113 """ Return a relative path, same as os.path.relpath, but only if this is possible. 114 115 It is not possible on Windows, if the start directory and the path are on different drives. 116 """ 117 try: 118 return os.path.relpath(path, os.curdir if start is None else start) 119 except ValueError: 120 return os.path.abspath(path) 121 122 123def debug_print_idf_version(): 124 version = idf_version() 125 if version: 126 print_warning('ESP-IDF %s' % version) 127 else: 128 print_warning('ESP-IDF version unknown') 129 130 131class PropertyDict(dict): 132 def __getattr__(self, name): 133 if name in self: 134 return self[name] 135 else: 136 raise AttributeError("'PropertyDict' object has no attribute '%s'" % name) 137 138 def __setattr__(self, name, value): 139 self[name] = value 140 141 def __delattr__(self, name): 142 if name in self: 143 del self[name] 144 else: 145 raise AttributeError("'PropertyDict' object has no attribute '%s'" % name) 146 147 148def init_cli(verbose_output=None): 149 # Click is imported here to run it after check_environment() 150 import click 151 152 class Deprecation(object): 153 """Construct deprecation notice for help messages""" 154 def __init__(self, deprecated=False): 155 self.deprecated = deprecated 156 self.since = None 157 self.removed = None 158 self.exit_with_error = None 159 self.custom_message = '' 160 161 if isinstance(deprecated, dict): 162 self.custom_message = deprecated.get('message', '') 163 self.since = deprecated.get('since', None) 164 self.removed = deprecated.get('removed', None) 165 self.exit_with_error = deprecated.get('exit_with_error', None) 166 elif isinstance(deprecated, str): 167 self.custom_message = deprecated 168 169 def full_message(self, type='Option'): 170 if self.exit_with_error: 171 return '%s is deprecated %sand was removed%s.%s' % ( 172 type, 173 'since %s ' % self.since if self.since else '', 174 ' in %s' % self.removed if self.removed else '', 175 ' %s' % self.custom_message if self.custom_message else '', 176 ) 177 else: 178 return '%s is deprecated %sand will be removed in%s.%s' % ( 179 type, 180 'since %s ' % self.since if self.since else '', 181 ' %s' % self.removed if self.removed else ' future versions', 182 ' %s' % self.custom_message if self.custom_message else '', 183 ) 184 185 def help(self, text, type='Option', separator=' '): 186 text = text or '' 187 return self.full_message(type) + separator + text if self.deprecated else text 188 189 def short_help(self, text): 190 text = text or '' 191 return ('Deprecated! ' + text) if self.deprecated else text 192 193 def check_deprecation(ctx): 194 """Prints deprecation warnings for arguments in given context""" 195 for option in ctx.command.params: 196 default = () if option.multiple else option.default 197 if isinstance(option, Option) and option.deprecated and ctx.params[option.name] != default: 198 deprecation = Deprecation(option.deprecated) 199 if deprecation.exit_with_error: 200 raise FatalError('Error: %s' % deprecation.full_message('Option "%s"' % option.name)) 201 else: 202 print_warning('Warning: %s' % deprecation.full_message('Option "%s"' % option.name)) 203 204 class Task(object): 205 def __init__(self, callback, name, aliases, dependencies, order_dependencies, action_args): 206 self.callback = callback 207 self.name = name 208 self.dependencies = dependencies 209 self.order_dependencies = order_dependencies 210 self.action_args = action_args 211 self.aliases = aliases 212 213 def __call__(self, context, global_args, action_args=None): 214 if action_args is None: 215 action_args = self.action_args 216 217 self.callback(self.name, context, global_args, **action_args) 218 219 class Action(click.Command): 220 def __init__( 221 self, 222 name=None, 223 aliases=None, 224 deprecated=False, 225 dependencies=None, 226 order_dependencies=None, 227 hidden=False, 228 **kwargs): 229 super(Action, self).__init__(name, **kwargs) 230 231 self.name = self.name or self.callback.__name__ 232 self.deprecated = deprecated 233 self.hidden = hidden 234 235 if aliases is None: 236 aliases = [] 237 self.aliases = aliases 238 239 self.help = self.help or self.callback.__doc__ 240 if self.help is None: 241 self.help = '' 242 243 if dependencies is None: 244 dependencies = [] 245 246 if order_dependencies is None: 247 order_dependencies = [] 248 249 # Show first line of help if short help is missing 250 self.short_help = self.short_help or self.help.split('\n')[0] 251 252 if deprecated: 253 deprecation = Deprecation(deprecated) 254 self.short_help = deprecation.short_help(self.short_help) 255 self.help = deprecation.help(self.help, type='Command', separator='\n') 256 257 # Add aliases to help string 258 if aliases: 259 aliases_help = 'Aliases: %s.' % ', '.join(aliases) 260 261 self.help = '\n'.join([self.help, aliases_help]) 262 self.short_help = ' '.join([aliases_help, self.short_help]) 263 264 self.unwrapped_callback = self.callback 265 if self.callback is not None: 266 267 def wrapped_callback(**action_args): 268 return Task( 269 callback=self.unwrapped_callback, 270 name=self.name, 271 dependencies=dependencies, 272 order_dependencies=order_dependencies, 273 action_args=action_args, 274 aliases=self.aliases, 275 ) 276 277 self.callback = wrapped_callback 278 279 def invoke(self, ctx): 280 if self.deprecated: 281 deprecation = Deprecation(self.deprecated) 282 message = deprecation.full_message('Command "%s"' % self.name) 283 284 if deprecation.exit_with_error: 285 raise FatalError('Error: %s' % message) 286 else: 287 print_warning('Warning: %s' % message) 288 289 self.deprecated = False # disable Click's built-in deprecation handling 290 291 # Print warnings for options 292 check_deprecation(ctx) 293 return super(Action, self).invoke(ctx) 294 295 class Argument(click.Argument): 296 """ 297 Positional argument 298 299 names - alias of 'param_decls' 300 """ 301 def __init__(self, **kwargs): 302 names = kwargs.pop('names') 303 super(Argument, self).__init__(names, **kwargs) 304 305 class Scope(object): 306 """ 307 Scope for sub-command option. 308 possible values: 309 - default - only available on defined level (global/action) 310 - global - When defined for action, also available as global 311 - shared - Opposite to 'global': when defined in global scope, also available for all actions 312 """ 313 314 SCOPES = ('default', 'global', 'shared') 315 316 def __init__(self, scope=None): 317 if scope is None: 318 self._scope = 'default' 319 elif isinstance(scope, str) and scope in self.SCOPES: 320 self._scope = scope 321 elif isinstance(scope, Scope): 322 self._scope = str(scope) 323 else: 324 raise FatalError('Unknown scope for option: %s' % scope) 325 326 @property 327 def is_global(self): 328 return self._scope == 'global' 329 330 @property 331 def is_shared(self): 332 return self._scope == 'shared' 333 334 def __str__(self): 335 return self._scope 336 337 class Option(click.Option): 338 """Option that knows whether it should be global""" 339 def __init__(self, scope=None, deprecated=False, hidden=False, **kwargs): 340 """ 341 Keyword arguments additional to Click's Option class: 342 343 names - alias of 'param_decls' 344 deprecated - marks option as deprecated. May be boolean, string (with custom deprecation message) 345 or dict with optional keys: 346 since: version of deprecation 347 removed: version when option will be removed 348 custom_message: Additional text to deprecation warning 349 """ 350 351 kwargs['param_decls'] = kwargs.pop('names') 352 super(Option, self).__init__(**kwargs) 353 354 self.deprecated = deprecated 355 self.scope = Scope(scope) 356 self.hidden = hidden 357 358 if deprecated: 359 deprecation = Deprecation(deprecated) 360 self.help = deprecation.help(self.help) 361 362 if self.envvar: 363 self.help += ' The default value can be set with the %s environment variable.' % self.envvar 364 365 if self.scope.is_global: 366 self.help += ' This option can be used at most once either globally, or for one subcommand.' 367 368 def get_help_record(self, ctx): 369 # Backport "hidden" parameter to click 5.0 370 if self.hidden: 371 return 372 373 return super(Option, self).get_help_record(ctx) 374 375 class CLI(click.MultiCommand): 376 """Action list contains all actions with options available for CLI""" 377 def __init__(self, all_actions=None, verbose_output=None, help=None): 378 super(CLI, self).__init__( 379 chain=True, 380 invoke_without_command=True, 381 result_callback=self.execute_tasks, 382 context_settings={'max_content_width': 140}, 383 help=help, 384 ) 385 self._actions = {} 386 self.global_action_callbacks = [] 387 self.commands_with_aliases = {} 388 389 if verbose_output is None: 390 verbose_output = [] 391 392 self.verbose_output = verbose_output 393 394 if all_actions is None: 395 all_actions = {} 396 397 shared_options = [] 398 399 # Global options 400 for option_args in all_actions.get('global_options', []): 401 option = Option(**option_args) 402 self.params.append(option) 403 404 if option.scope.is_shared: 405 shared_options.append(option) 406 407 # Global options validators 408 self.global_action_callbacks = all_actions.get('global_action_callbacks', []) 409 410 # Actions 411 for name, action in all_actions.get('actions', {}).items(): 412 arguments = action.pop('arguments', []) 413 options = action.pop('options', []) 414 415 if arguments is None: 416 arguments = [] 417 418 if options is None: 419 options = [] 420 421 self._actions[name] = Action(name=name, **action) 422 for alias in [name] + action.get('aliases', []): 423 self.commands_with_aliases[alias] = name 424 425 for argument_args in arguments: 426 self._actions[name].params.append(Argument(**argument_args)) 427 428 # Add all shared options 429 for option in shared_options: 430 self._actions[name].params.append(option) 431 432 for option_args in options: 433 option = Option(**option_args) 434 435 if option.scope.is_shared: 436 raise FatalError( 437 '"%s" is defined for action "%s". ' 438 ' "shared" options can be declared only on global level' % (option.name, name)) 439 440 # Promote options to global if see for the first time 441 if option.scope.is_global and option.name not in [o.name for o in self.params]: 442 self.params.append(option) 443 444 self._actions[name].params.append(option) 445 446 def list_commands(self, ctx): 447 return sorted(filter(lambda name: not self._actions[name].hidden, self._actions)) 448 449 def get_command(self, ctx, name): 450 if name in self.commands_with_aliases: 451 return self._actions.get(self.commands_with_aliases.get(name)) 452 453 # Trying fallback to build target (from "all" action) if command is not known 454 else: 455 return Action(name=name, callback=self._actions.get('fallback').unwrapped_callback) 456 457 def _print_closing_message(self, args, actions): 458 # print a closing message of some kind 459 # 460 if any(t in str(actions) for t in ('flash', 'dfu', 'uf2', 'uf2-app')): 461 print('Done') 462 return 463 464 if not os.path.exists(os.path.join(args.build_dir, 'flasher_args.json')): 465 print('Done') 466 return 467 468 # Otherwise, if we built any binaries print a message about 469 # how to flash them 470 def print_flashing_message(title, key): 471 with open(os.path.join(args.build_dir, 'flasher_args.json')) as f: 472 flasher_args = json.load(f) 473 474 def flasher_path(f): 475 return _safe_relpath(os.path.join(args.build_dir, f)) 476 477 if key != 'project': # flashing a single item 478 if key not in flasher_args: 479 # This is the case for 'idf.py bootloader' if Secure Boot is on, need to follow manual flashing steps 480 print('\n%s build complete.' % title) 481 return 482 cmd = '' 483 if (key == 'bootloader'): # bootloader needs --flash-mode, etc to be passed in 484 cmd = ' '.join(flasher_args['write_flash_args']) + ' ' 485 486 cmd += flasher_args[key]['offset'] + ' ' 487 cmd += flasher_path(flasher_args[key]['file']) 488 else: # flashing the whole project 489 cmd = ' '.join(flasher_args['write_flash_args']) + ' ' 490 flash_items = sorted( 491 ((o, f) for (o, f) in flasher_args['flash_files'].items() if len(o) > 0), 492 key=lambda x: int(x[0], 0), 493 ) 494 for o, f in flash_items: 495 cmd += o + ' ' + flasher_path(f) + ' ' 496 497 print('\n%s build complete. To flash, run this command:' % title) 498 499 print( 500 '%s %s -p %s -b %s --before %s --after %s --chip %s %s write_flash %s' % ( 501 PYTHON, 502 _safe_relpath('%s/components/esptool_py/esptool/esptool.py' % os.environ['IDF_PATH']), 503 args.port or '(PORT)', 504 args.baud, 505 flasher_args['extra_esptool_args']['before'], 506 flasher_args['extra_esptool_args']['after'], 507 flasher_args['extra_esptool_args']['chip'], 508 '--no-stub' if not flasher_args['extra_esptool_args']['stub'] else '', 509 cmd.strip(), 510 )) 511 print( 512 "or run 'idf.py -p %s %s'" % ( 513 args.port or '(PORT)', 514 key + '-flash' if key != 'project' else 'flash', 515 )) 516 517 if 'all' in actions or 'build' in actions: 518 print_flashing_message('Project', 'project') 519 else: 520 if 'app' in actions: 521 print_flashing_message('App', 'app') 522 if 'partition-table' in actions: 523 print_flashing_message('Partition Table', 'partition-table') 524 if 'bootloader' in actions: 525 print_flashing_message('Bootloader', 'bootloader') 526 527 def execute_tasks(self, tasks, **kwargs): 528 ctx = click.get_current_context() 529 global_args = PropertyDict(kwargs) 530 531 def _help_and_exit(): 532 print(ctx.get_help()) 533 ctx.exit() 534 535 # Show warning if some tasks are present several times in the list 536 dupplicated_tasks = sorted( 537 [item for item, count in Counter(task.name for task in tasks).items() if count > 1]) 538 if dupplicated_tasks: 539 dupes = ', '.join('"%s"' % t for t in dupplicated_tasks) 540 541 print_warning( 542 'WARNING: Command%s found in the list of commands more than once. ' % 543 ('s %s are' % dupes if len(dupplicated_tasks) > 1 else ' %s is' % dupes) + 544 'Only first occurrence will be executed.') 545 546 for task in tasks: 547 # Show help and exit if help is in the list of commands 548 if task.name == 'help': 549 _help_and_exit() 550 551 # Set propagated global options. 552 # These options may be set on one subcommand, but available in the list of global arguments 553 for key in list(task.action_args): 554 option = next((o for o in ctx.command.params if o.name == key), None) 555 556 if option and (option.scope.is_global or option.scope.is_shared): 557 local_value = task.action_args.pop(key) 558 global_value = global_args[key] 559 default = () if option.multiple else option.default 560 561 if global_value != default and local_value != default and global_value != local_value: 562 raise FatalError( 563 'Option "%s" provided for "%s" is already defined to a different value. ' 564 'This option can appear at most once in the command line.' % (key, task.name)) 565 if local_value != default: 566 global_args[key] = local_value 567 568 # Show warnings about global arguments 569 check_deprecation(ctx) 570 571 # Make sure that define_cache_entry is mutable list and can be modified in callbacks 572 global_args.define_cache_entry = list(global_args.define_cache_entry) 573 574 # Execute all global action callback - first from idf.py itself, then from extensions 575 for action_callback in ctx.command.global_action_callbacks: 576 action_callback(ctx, global_args, tasks) 577 578 # Always show help when command is not provided 579 if not tasks: 580 _help_and_exit() 581 582 # Build full list of tasks to and deal with dependencies and order dependencies 583 tasks_to_run = OrderedDict() 584 while tasks: 585 task = tasks[0] 586 tasks_dict = dict([(t.name, t) for t in tasks]) 587 588 dependecies_processed = True 589 590 # If task have some dependecies they have to be executed before the task. 591 for dep in task.dependencies: 592 if dep not in tasks_to_run.keys(): 593 # If dependent task is in the list of unprocessed tasks move to the front of the list 594 if dep in tasks_dict.keys(): 595 dep_task = tasks.pop(tasks.index(tasks_dict[dep])) 596 # Otherwise invoke it with default set of options 597 # and put to the front of the list of unprocessed tasks 598 else: 599 print( 600 'Adding "%s"\'s dependency "%s" to list of commands with default set of options.' % 601 (task.name, dep)) 602 dep_task = ctx.invoke(ctx.command.get_command(ctx, dep)) 603 604 # Remove options with global scope from invoke tasks because they are already in global_args 605 for key in list(dep_task.action_args): 606 option = next((o for o in ctx.command.params if o.name == key), None) 607 if option and (option.scope.is_global or option.scope.is_shared): 608 dep_task.action_args.pop(key) 609 610 tasks.insert(0, dep_task) 611 dependecies_processed = False 612 613 # Order only dependencies are moved to the front of the queue if they present in command list 614 for dep in task.order_dependencies: 615 if dep in tasks_dict.keys() and dep not in tasks_to_run.keys(): 616 tasks.insert(0, tasks.pop(tasks.index(tasks_dict[dep]))) 617 dependecies_processed = False 618 619 if dependecies_processed: 620 # Remove task from list of unprocessed tasks 621 tasks.pop(0) 622 623 # And add to the queue 624 if task.name not in tasks_to_run.keys(): 625 tasks_to_run.update([(task.name, task)]) 626 627 # Run all tasks in the queue 628 # when global_args.dry_run is true idf.py works in idle mode and skips actual task execution 629 if not global_args.dry_run: 630 for task in tasks_to_run.values(): 631 name_with_aliases = task.name 632 if task.aliases: 633 name_with_aliases += ' (aliases: %s)' % ', '.join(task.aliases) 634 635 print('Executing action: %s' % name_with_aliases) 636 task(ctx, global_args, task.action_args) 637 638 self._print_closing_message(global_args, tasks_to_run.keys()) 639 640 return tasks_to_run 641 642 # That's a tiny parser that parse project-dir even before constructing 643 # fully featured click parser to be sure that extensions are loaded from the right place 644 @click.command( 645 add_help_option=False, 646 context_settings={ 647 'allow_extra_args': True, 648 'ignore_unknown_options': True 649 }, 650 ) 651 @click.option('-C', '--project-dir', default=os.getcwd(), type=click.Path()) 652 def parse_project_dir(project_dir): 653 return realpath(project_dir) 654 # Set `complete_var` to not existing environment variable name to prevent early cmd completion 655 project_dir = parse_project_dir(standalone_mode=False, complete_var='_IDF.PY_COMPLETE_NOT_EXISTING') 656 657 all_actions = {} 658 # Load extensions from components dir 659 idf_py_extensions_path = os.path.join(os.environ['IDF_PATH'], 'tools', 'idf_py_actions') 660 extension_dirs = [realpath(idf_py_extensions_path)] 661 extra_paths = os.environ.get('IDF_EXTRA_ACTIONS_PATH') 662 if extra_paths is not None: 663 for path in extra_paths.split(';'): 664 path = realpath(path) 665 if path not in extension_dirs: 666 extension_dirs.append(path) 667 668 extensions = [] 669 for directory in extension_dirs: 670 if directory and not os.path.exists(directory): 671 print_warning('WARNING: Directory with idf.py extensions doesn\'t exist:\n %s' % directory) 672 continue 673 674 sys.path.append(directory) 675 for _finder, name, _ispkg in sorted(iter_modules([directory])): 676 if name.endswith('_ext'): 677 extensions.append((name, import_module(name))) 678 679 # Load component manager if available and not explicitly disabled 680 if os.getenv('IDF_COMPONENT_MANAGER', None) != '0': 681 try: 682 from idf_component_manager import idf_extensions 683 684 extensions.append(('component_manager_ext', idf_extensions)) 685 os.environ['IDF_COMPONENT_MANAGER'] = '1' 686 687 except ImportError: 688 pass 689 690 for name, extension in extensions: 691 try: 692 all_actions = merge_action_lists(all_actions, extension.action_extensions(all_actions, project_dir)) 693 except AttributeError: 694 print_warning('WARNING: Cannot load idf.py extension "%s"' % name) 695 696 # Load extensions from project dir 697 if os.path.exists(os.path.join(project_dir, 'idf_ext.py')): 698 sys.path.append(project_dir) 699 try: 700 from idf_ext import action_extensions 701 except ImportError: 702 print_warning('Error importing extension file idf_ext.py. Skipping.') 703 print_warning("Please make sure that it contains implementation (even if it's empty) of add_action_extensions") 704 705 try: 706 all_actions = merge_action_lists(all_actions, action_extensions(all_actions, project_dir)) 707 except NameError: 708 pass 709 710 cli_help = ( 711 'ESP-IDF CLI build management tool. ' 712 'For commands that are not known to idf.py an attempt to execute it as a build system target will be made.') 713 714 return CLI(help=cli_help, verbose_output=verbose_output, all_actions=all_actions) 715 716 717def signal_handler(_signal, _frame): 718 # The Ctrl+C processed by other threads inside 719 pass 720 721 722def main(): 723 724 # Processing of Ctrl+C event for all threads made by main() 725 signal.signal(signal.SIGINT, signal_handler) 726 727 checks_output = check_environment() 728 cli = init_cli(verbose_output=checks_output) 729 # the argument `prog_name` must contain name of the file - not the absolute path to it! 730 cli(sys.argv[1:], prog_name=PROG, complete_var='_IDF.PY_COMPLETE') 731 732 733def _valid_unicode_config(): 734 # Python 2 is always good 735 if sys.version_info[0] == 2: 736 return True 737 738 # With python 3 unicode environment is required 739 try: 740 return codecs.lookup(locale.getpreferredencoding()).name != 'ascii' 741 except Exception: 742 return False 743 744 745def _find_usable_locale(): 746 try: 747 locales = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0] 748 except OSError: 749 locales = '' 750 if isinstance(locales, bytes): 751 locales = locales.decode('ascii', 'replace') 752 753 usable_locales = [] 754 for line in locales.splitlines(): 755 locale = line.strip() 756 locale_name = locale.lower().replace('-', '') 757 758 # C.UTF-8 is the best option, if supported 759 if locale_name == 'c.utf8': 760 return locale 761 762 if locale_name.endswith('.utf8'): 763 # Make a preference of english locales 764 if locale.startswith('en_'): 765 usable_locales.insert(0, locale) 766 else: 767 usable_locales.append(locale) 768 769 if not usable_locales: 770 raise FatalError( 771 'Support for Unicode filenames is required, but no suitable UTF-8 locale was found on your system.' 772 ' Please refer to the manual for your operating system for details on locale reconfiguration.') 773 774 return usable_locales[0] 775 776 777if __name__ == '__main__': 778 try: 779 # On MSYS2 we need to run idf.py with "winpty" in order to be able to cancel the subprocesses properly on 780 # keyboard interrupt (CTRL+C). 781 # Using an own global variable for indicating that we are running with "winpty" seems to be the most suitable 782 # option as os.environment['_'] contains "winpty" only when it is run manually from console. 783 WINPTY_VAR = 'WINPTY' 784 WINPTY_EXE = 'winpty' 785 if ('MSYSTEM' in os.environ) and (not os.environ.get('_', '').endswith(WINPTY_EXE) 786 and WINPTY_VAR not in os.environ): 787 788 if 'menuconfig' in sys.argv: 789 # don't use winpty for menuconfig because it will print weird characters 790 main() 791 else: 792 os.environ[WINPTY_VAR] = '1' # the value is of no interest to us 793 # idf.py calls itself with "winpty" and WINPTY global variable set 794 ret = subprocess.call([WINPTY_EXE, sys.executable] + sys.argv, env=os.environ) 795 if ret: 796 raise SystemExit(ret) 797 798 elif os.name == 'posix' and not _valid_unicode_config(): 799 # Trying to find best utf-8 locale available on the system and restart python with it 800 best_locale = _find_usable_locale() 801 802 print_warning( 803 'Your environment is not configured to handle unicode filenames outside of ASCII range.' 804 ' Environment variable LC_ALL is temporary set to %s for unicode support.' % best_locale) 805 806 os.environ['LC_ALL'] = best_locale 807 ret = subprocess.call([sys.executable] + sys.argv, env=os.environ) 808 if ret: 809 raise SystemExit(ret) 810 811 else: 812 main() 813 814 except FatalError as e: 815 print(e, file=sys.stderr) 816 sys.exit(2) 817