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