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