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