def init_cli(verbose_output=None): # Click is imported here to run it after check_environment() import click class Deprecation(object): """Construct deprecation notice for help messages""" def __init__(self, deprecated=False): self.deprecated = deprecated self.since = None self.removed = None self.exit_with_error = None self.custom_message = '' if isinstance(deprecated, dict): self.custom_message = deprecated.get('message', '') self.since = deprecated.get('since', None) self.removed = deprecated.get('removed', None) self.exit_with_error = deprecated.get('exit_with_error', None) elif isinstance(deprecated, str): self.custom_message = deprecated def full_message(self, type='Option'): if self.exit_with_error: return '%s is deprecated %sand was removed%s.%s' % ( type, 'since %s ' % self.since if self.since else '', ' in %s' % self.removed if self.removed else '', ' %s' % self.custom_message if self.custom_message else '', ) else: return '%s is deprecated %sand will be removed in%s.%s' % ( type, 'since %s ' % self.since if self.since else '', ' %s' % self.removed if self.removed else ' future versions', ' %s' % self.custom_message if self.custom_message else '', ) def help(self, text, type='Option', separator=' '): text = text or '' return self.full_message(type) + separator + text if self.deprecated else text def short_help(self, text): text = text or '' return ('Deprecated! ' + text) if self.deprecated else text def check_deprecation(ctx): """Prints deprecation warnings for arguments in given context""" for option in ctx.command.params: default = () if option.multiple else option.default if isinstance(option, Option) and option.deprecated and ctx.params[option.name] != default: deprecation = Deprecation(option.deprecated) if deprecation.exit_with_error: raise FatalError('Error: %s' % deprecation.full_message('Option "%s"' % option.name)) else: print_warning('Warning: %s' % deprecation.full_message('Option "%s"' % option.name)) class Task(object): def __init__(self, callback, name, aliases, dependencies, order_dependencies, action_args): self.callback = callback self.name = name self.dependencies = dependencies self.order_dependencies = order_dependencies self.action_args = action_args self.aliases = aliases def __call__(self, context, global_args, action_args=None): if action_args is None: action_args = self.action_args self.callback(self.name, context, global_args, **action_args) class Action(click.Command): def __init__( self, name=None, aliases=None, deprecated=False, dependencies=None, order_dependencies=None, hidden=False, **kwargs): super(Action, self).__init__(name, **kwargs) self.name = self.name or self.callback.__name__ self.deprecated = deprecated self.hidden = hidden if aliases is None: aliases = [] self.aliases = aliases self.help = self.help or self.callback.__doc__ if self.help is None: self.help = '' if dependencies is None: dependencies = [] if order_dependencies is None: order_dependencies = [] # Show first line of help if short help is missing self.short_help = self.short_help or self.help.split('\n')[0] if deprecated: deprecation = Deprecation(deprecated) self.short_help = deprecation.short_help(self.short_help) self.help = deprecation.help(self.help, type='Command', separator='\n') # Add aliases to help string if aliases: aliases_help = 'Aliases: %s.' % ', '.join(aliases) self.help = '\n'.join([self.help, aliases_help]) self.short_help = ' '.join([aliases_help, self.short_help]) self.unwrapped_callback = self.callback if self.callback is not None: def wrapped_callback(**action_args): return Task( callback=self.unwrapped_callback, name=self.name, dependencies=dependencies, order_dependencies=order_dependencies, action_args=action_args, aliases=self.aliases, ) self.callback = wrapped_callback def invoke(self, ctx): if self.deprecated: deprecation = Deprecation(self.deprecated) message = deprecation.full_message('Command "%s"' % self.name) if deprecation.exit_with_error: raise FatalError('Error: %s' % message) else: print_warning('Warning: %s' % message) self.deprecated = False # disable Click's built-in deprecation handling # Print warnings for options check_deprecation(ctx) return super(Action, self).invoke(ctx) class Argument(click.Argument): """ Positional argument names - alias of 'param_decls' """ def __init__(self, **kwargs): names = kwargs.pop('names') super(Argument, self).__init__(names, **kwargs) class Scope(object): """ Scope for sub-command option. possible values: - default - only available on defined level (global/action) - global - When defined for action, also available as global - shared - Opposite to 'global': when defined in global scope, also available for all actions """ SCOPES = ('default', 'global', 'shared') def __init__(self, scope=None): if scope is None: self._scope = 'default' elif isinstance(scope, str) and scope in self.SCOPES: self._scope = scope elif isinstance(scope, Scope): self._scope = str(scope) else: raise FatalError('Unknown scope for option: %s' % scope) @property def is_global(self): return self._scope == 'global' @property def is_shared(self): return self._scope == 'shared' def __str__(self): return self._scope class Option(click.Option): """Option that knows whether it should be global""" def __init__(self, scope=None, deprecated=False, hidden=False, **kwargs): """ Keyword arguments additional to Click's Option class: names - alias of 'param_decls' deprecated - marks option as deprecated. May be boolean, string (with custom deprecation message) or dict with optional keys: since: version of deprecation removed: version when option will be removed custom_message: Additional text to deprecation warning """ kwargs['param_decls'] = kwargs.pop('names') super(Option, self).__init__(**kwargs) self.deprecated = deprecated self.scope = Scope(scope) self.hidden = hidden if deprecated: deprecation = Deprecation(deprecated) self.help = deprecation.help(self.help) if self.envvar: self.help += ' The default value can be set with the %s environment variable.' % self.envvar if self.scope.is_global: self.help += ' This option can be used at most once either globally, or for one subcommand.' def get_help_record(self, ctx): # Backport "hidden" parameter to click 5.0 if self.hidden: return return super(Option, self).get_help_record(ctx) class CLI(click.MultiCommand): """Action list contains all actions with options available for CLI""" def __init__(self, all_actions=None, verbose_output=None, help=None): super(CLI, self).__init__( chain=True, invoke_without_command=True, result_callback=self.execute_tasks, context_settings={'max_content_width': 140}, help=help, ) self._actions = {} self.global_action_callbacks = [] self.commands_with_aliases = {} if verbose_output is None: verbose_output = [] self.verbose_output = verbose_output if all_actions is None: all_actions = {} shared_options = [] # Global options for option_args in all_actions.get('global_options', []): option = Option(**option_args) self.params.append(option) if option.scope.is_shared: shared_options.append(option) # Global options validators self.global_action_callbacks = all_actions.get('global_action_callbacks', []) # Actions for name, action in all_actions.get('actions', {}).items(): arguments = action.pop('arguments', []) options = action.pop('options', []) if arguments is None: arguments = [] if options is None: options = [] self._actions[name] = Action(name=name, **action) for alias in [name] + action.get('aliases', []): self.commands_with_aliases[alias] = name for argument_args in arguments: self._actions[name].params.append(Argument(**argument_args)) # Add all shared options for option in shared_options: self._actions[name].params.append(option) for option_args in options: option = Option(**option_args) if option.scope.is_shared: raise FatalError( '"%s" is defined for action "%s". ' ' "shared" options can be declared only on global level' % (option.name, name)) # Promote options to global if see for the first time if option.scope.is_global and option.name not in [o.name for o in self.params]: self.params.append(option) self._actions[name].params.append(option) def list_commands(self, ctx): return sorted(filter(lambda name: not self._actions[name].hidden, self._actions)) def get_command(self, ctx, name): if name in self.commands_with_aliases: return self._actions.get(self.commands_with_aliases.get(name)) # Trying fallback to build target (from "all" action) if command is not known else: return Action(name=name, callback=self._actions.get('fallback').unwrapped_callback) def _print_closing_message(self, args, actions): # print a closing message of some kind # if any(t in str(actions) for t in ('flash', 'dfu', 'uf2', 'uf2-app')): print('Done') return if not os.path.exists(os.path.join(args.build_dir, 'flasher_args.json')): print('Done') return # Otherwise, if we built any binaries print a message about # how to flash them def print_flashing_message(title, key): with open(os.path.join(args.build_dir, 'flasher_args.json')) as f: flasher_args = json.load(f) def flasher_path(f): return _safe_relpath(os.path.join(args.build_dir, f)) if key != 'project': # flashing a single item if key not in flasher_args: # This is the case for 'idf.py bootloader' if Secure Boot is on, need to follow manual flashing steps print('\n%s build complete.' % title) return cmd = '' if (key == 'bootloader'): # bootloader needs --flash-mode, etc to be passed in cmd = ' '.join(flasher_args['write_flash_args']) + ' ' cmd += flasher_args[key]['offset'] + ' ' cmd += flasher_path(flasher_args[key]['file']) else: # flashing the whole project cmd = ' '.join(flasher_args['write_flash_args']) + ' ' flash_items = sorted( ((o, f) for (o, f) in flasher_args['flash_files'].items() if len(o) > 0), key=lambda x: int(x[0], 0), ) for o, f in flash_items: cmd += o + ' ' + flasher_path(f) + ' ' print('\n%s build complete. To flash, run this command:' % title) print( '%s %s -p %s -b %s --before %s --after %s --chip %s %s write_flash %s' % ( PYTHON, _safe_relpath('%s/components/esptool_py/esptool/esptool.py' % os.environ['IDF_PATH']), args.port or '(PORT)', args.baud, flasher_args['extra_esptool_args']['before'], flasher_args['extra_esptool_args']['after'], flasher_args['extra_esptool_args']['chip'], '--no-stub' if not flasher_args['extra_esptool_args']['stub'] else '', cmd.strip(), )) print( "or run 'idf.py -p %s %s'" % ( args.port or '(PORT)', key + '-flash' if key != 'project' else 'flash', )) if 'all' in actions or 'build' in actions: print_flashing_message('Project', 'project') else: if 'app' in actions: print_flashing_message('App', 'app') if 'partition_table' in actions: print_flashing_message('Partition Table', 'partition_table') if 'bootloader' in actions: print_flashing_message('Bootloader', 'bootloader') def execute_tasks(self, tasks, **kwargs): ctx = click.get_current_context() global_args = PropertyDict(kwargs) def _help_and_exit(): print(ctx.get_help()) ctx.exit() # Show warning if some tasks are present several times in the list dupplicated_tasks = sorted( [item for item, count in Counter(task.name for task in tasks).items() if count > 1]) if dupplicated_tasks: dupes = ', '.join('"%s"' % t for t in dupplicated_tasks) print_warning( 'WARNING: Command%s found in the list of commands more than once. ' % ('s %s are' % dupes if len(dupplicated_tasks) > 1 else ' %s is' % dupes) + 'Only first occurrence will be executed.') for task in tasks: # Show help and exit if help is in the list of commands if task.name == 'help': _help_and_exit() # Set propagated global options. # These options may be set on one subcommand, but available in the list of global arguments for key in list(task.action_args): option = next((o for o in ctx.command.params if o.name == key), None) if option and (option.scope.is_global or option.scope.is_shared): local_value = task.action_args.pop(key) global_value = global_args[key] default = () if option.multiple else option.default if global_value != default and local_value != default and global_value != local_value: raise FatalError( 'Option "%s" provided for "%s" is already defined to a different value. ' 'This option can appear at most once in the command line.' % (key, task.name)) if local_value != default: global_args[key] = local_value # Show warnings about global arguments check_deprecation(ctx) # Make sure that define_cache_entry is mutable list and can be modified in callbacks global_args.define_cache_entry = list(global_args.define_cache_entry) # Execute all global action callback - first from idf.py itself, then from extensions for action_callback in ctx.command.global_action_callbacks: action_callback(ctx, global_args, tasks) # Always show help when command is not provided if not tasks: _help_and_exit() # Build full list of tasks to and deal with dependencies and order dependencies tasks_to_run = OrderedDict() while tasks: task = tasks[0] tasks_dict = dict([(t.name, t) for t in tasks]) dependecies_processed = True # If task have some dependecies they have to be executed before the task. for dep in task.dependencies: if dep not in tasks_to_run.keys(): # If dependent task is in the list of unprocessed tasks move to the front of the list if dep in tasks_dict.keys(): dep_task = tasks.pop(tasks.index(tasks_dict[dep])) # Otherwise invoke it with default set of options # and put to the front of the list of unprocessed tasks else: print( 'Adding "%s"\'s dependency "%s" to list of commands with default set of options.' % (task.name, dep)) dep_task = ctx.invoke(ctx.command.get_command(ctx, dep)) # Remove options with global scope from invoke tasks because they are already in global_args for key in list(dep_task.action_args): option = next((o for o in ctx.command.params if o.name == key), None) if option and (option.scope.is_global or option.scope.is_shared): dep_task.action_args.pop(key) tasks.insert(0, dep_task) dependecies_processed = False # Order only dependencies are moved to the front of the queue if they present in command list for dep in task.order_dependencies: if dep in tasks_dict.keys() and dep not in tasks_to_run.keys(): tasks.insert(0, tasks.pop(tasks.index(tasks_dict[dep]))) dependecies_processed = False if dependecies_processed: # Remove task from list of unprocessed tasks tasks.pop(0) # And add to the queue if task.name not in tasks_to_run.keys(): tasks_to_run.update([(task.name, task)]) # Run all tasks in the queue # when global_args.dry_run is true idf.py works in idle mode and skips actual task execution if not global_args.dry_run: for task in tasks_to_run.values(): name_with_aliases = task.name if task.aliases: name_with_aliases += ' (aliases: %s)' % ', '.join(task.aliases) print('Executing action: %s' % name_with_aliases) task(ctx, global_args, task.action_args) self._print_closing_message(global_args, tasks_to_run.keys()) return tasks_to_run # That's a tiny parser that parse project-dir even before constructing # fully featured click parser to be sure that extensions are loaded from the right place @click.command( add_help_option=False, context_settings={ 'allow_extra_args': True, 'ignore_unknown_options': True }, ) @click.option('-C', '--project-dir', default=os.getcwd(), type=click.Path()) def parse_project_dir(project_dir): return realpath(project_dir) # Set `complete_var` to not existing environment variable name to prevent early cmd completion project_dir = parse_project_dir(standalone_mode=False, complete_var='_IDF.PY_COMPLETE_NOT_EXISTING') all_actions = {} # Load extensions from components dir idf_py_extensions_path = os.path.join(os.environ['IDF_PATH'], 'tools', 'idf_py_actions') extension_dirs = [realpath(idf_py_extensions_path)] extra_paths = os.environ.get('IDF_EXTRA_ACTIONS_PATH') if extra_paths is not None: for path in extra_paths.split(';'): path = realpath(path) if path not in extension_dirs: extension_dirs.append(path) extensions = {} for directory in extension_dirs: if directory and not os.path.exists(directory): print_warning('WARNING: Directory with idf.py extensions doesn\'t exist:\n %s' % directory) continue sys.path.append(directory) for _finder, name, _ispkg in sorted(iter_modules([directory])): if name.endswith('_ext'): extensions[name] = import_module(name) # Load component manager if available and not explicitly disabled if os.getenv('IDF_COMPONENT_MANAGER', None) != '0': try: from idf_component_manager import idf_extensions extensions['component_manager_ext'] = idf_extensions os.environ['IDF_COMPONENT_MANAGER'] = '1' except ImportError: pass for name, extension in extensions.items(): try: all_actions = merge_action_lists(all_actions, extension.action_extensions(all_actions, project_dir)) except AttributeError: print_warning('WARNING: Cannot load idf.py extension "%s"' % name) # Load extensions from project dir if os.path.exists(os.path.join(project_dir, 'idf_ext.py')): sys.path.append(project_dir) try: from idf_ext import action_extensions except ImportError: print_warning('Error importing extension file idf_ext.py. Skipping.') print_warning("Please make sure that it contains implementation (even if it's empty) of add_action_extensions") try: all_actions = merge_action_lists(all_actions, action_extensions(all_actions, project_dir)) except NameError: pass cli_help = ( 'ESP-IDF CLI build management tool. ' 'For commands that are not known to idf.py an attempt to execute it as a build system target will be made.') return CLI(help=cli_help, verbose_output=verbose_output, all_actions=all_actions)
def action_extensions(base_actions: Dict, project_path: str) -> Any: def build_target(target_name: str, ctx: Context, args: PropertyDict) -> None: """ Execute the target build system to build target 'target_name' Calls ensure_build_directory() which will run cmake to generate a build directory (with the specified generator) as needed. """ hints = not args.no_hints ensure_build_directory(args, ctx.info_name) run_target(target_name, args, force_progression=GENERATORS[args.generator].get('force_progression', False), hints=hints) def size_target(target_name: str, ctx: Context, args: PropertyDict) -> None: """ Builds the app and then executes a size-related target passed in 'target_name'. `tool_error_handler` handler is used to suppress errors during the build, so size action can run even in case of overflow. """ def tool_error_handler(e: int, stdout: str, stderr: str) -> None: print_hints(stdout, stderr) hints = not args.no_hints ensure_build_directory(args, ctx.info_name) run_target('all', args, force_progression=GENERATORS[args.generator].get('force_progression', False), custom_error_handler=tool_error_handler, hints=hints) run_target(target_name, args) def list_build_system_targets(target_name: str, ctx: Context, args: PropertyDict) -> None: """Shows list of targets known to build sytem (make/ninja)""" build_target('help', ctx, args) def menuconfig(target_name: str, ctx: Context, args: PropertyDict, style: str) -> None: """ Menuconfig target is build_target extended with the style argument for setting the value for the environment variable. """ if sys.version_info[0] < 3: # The subprocess lib cannot accept environment variables as "unicode". # This encoding step is required only in Python 2. style = style.encode(sys.getfilesystemencoding() or 'utf-8') os.environ['MENUCONFIG_STYLE'] = style args.no_hints = True build_target(target_name, ctx, args) def fallback_target(target_name: str, ctx: Context, args: PropertyDict) -> None: """ Execute targets that are not explicitly known to idf.py """ ensure_build_directory(args, ctx.info_name) try: subprocess.check_output(GENERATORS[args.generator]['dry_run'] + [target_name], cwd=args.build_dir) except Exception: if target_name in ['clang-check', 'clang-html-report']: raise FatalError('command "{}" requires an additional plugin "pyclang". ' 'Please install it via "pip install --upgrade pyclang"'.format(target_name)) raise FatalError( 'command "%s" is not known to idf.py and is not a %s target' % (target_name, args.generator)) run_target(target_name, args) def verbose_callback(ctx: Context, param: List, value: str) -> Optional[str]: if not value or ctx.resilient_parsing: return None for line in ctx.command.verbose_output: print(line) return value def clean(action: str, ctx: Context, args: PropertyDict) -> None: if not os.path.isdir(args.build_dir): print("Build directory '%s' not found. Nothing to clean." % args.build_dir) return build_target('clean', ctx, args) def fullclean(action: str, ctx: Context, args: PropertyDict) -> None: build_dir = args.build_dir if not os.path.isdir(build_dir): print("Build directory '%s' not found. Nothing to clean." % build_dir) return if len(os.listdir(build_dir)) == 0: print("Build directory '%s' is empty. Nothing to clean." % build_dir) return if not os.path.exists(os.path.join(build_dir, 'CMakeCache.txt')): raise FatalError( "Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically " "delete files in this directory. Delete the directory manually to 'clean' it." % build_dir) red_flags = ['CMakeLists.txt', '.git', '.svn'] for red in red_flags: red = os.path.join(build_dir, red) if os.path.exists(red): raise FatalError( "Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." % red) if args.verbose and len(build_dir) > 1: print('The following symlinks were identified and removed:\n%s' % '\n'.join(build_dir)) for f in os.listdir(build_dir): # TODO: once we are Python 3 only, this can be os.scandir() f = os.path.join(build_dir, f) if args.verbose: print('Removing: %s' % f) if os.path.isdir(f): shutil.rmtree(f) else: os.remove(f) def python_clean(action: str, ctx: Context, args: PropertyDict) -> None: for root, dirnames, filenames in os.walk(os.environ['IDF_PATH']): for d in dirnames: if d == '__pycache__': dir_to_delete = os.path.join(root, d) if args.verbose: print('Removing: %s' % dir_to_delete) shutil.rmtree(dir_to_delete) for filename in fnmatch.filter(filenames, '*.py[co]'): file_to_delete = os.path.join(root, filename) if args.verbose: print('Removing: %s' % file_to_delete) os.remove(file_to_delete) def set_target(action: str, ctx: Context, args: PropertyDict, idf_target: str) -> None: if (not args['preview'] and idf_target in PREVIEW_TARGETS): raise FatalError( "%s is still in preview. You have to append '--preview' option after idf.py to use any preview feature." % idf_target) args.define_cache_entry.append('IDF_TARGET=' + idf_target) sdkconfig_path = os.path.join(args.project_dir, 'sdkconfig') sdkconfig_old = sdkconfig_path + '.old' if os.path.exists(sdkconfig_old): os.remove(sdkconfig_old) if os.path.exists(sdkconfig_path): os.rename(sdkconfig_path, sdkconfig_old) print('Set Target to: %s, new sdkconfig created. Existing sdkconfig renamed to sdkconfig.old.' % idf_target) ensure_build_directory(args, ctx.info_name, True) def reconfigure(action: str, ctx: Context, args: PropertyDict) -> None: ensure_build_directory(args, ctx.info_name, True) def validate_root_options(ctx: Context, args: PropertyDict, tasks: List) -> None: args.project_dir = realpath(args.project_dir) if args.build_dir is not None and args.project_dir == realpath(args.build_dir): raise FatalError( 'Setting the build directory to the project directory is not supported. Suggest dropping ' "--build-dir option, the default is a 'build' subdirectory inside the project directory.") if args.build_dir is None: args.build_dir = os.path.join(args.project_dir, 'build') args.build_dir = realpath(args.build_dir) def idf_version_callback(ctx: Context, param: str, value: str) -> None: if not value or ctx.resilient_parsing: return version = idf_version() if not version: raise FatalError('ESP-IDF version cannot be determined') print('ESP-IDF %s' % version) sys.exit(0) def list_targets_callback(ctx: Context, param: List, value: int) -> None: if not value or ctx.resilient_parsing: return for target in SUPPORTED_TARGETS: print(target) if 'preview' in ctx.params: for target in PREVIEW_TARGETS: print(target) sys.exit(0) def show_docs(action: str, ctx: Context, args: PropertyDict, no_browser: bool, language: str, starting_page: str, version: str, target: str) -> None: if language == 'cn': language = 'zh_CN' if not version: # '0.0-dev' here because if 'dev' in version it will transform in to 'latest' version_search = re.search(r'v\d+\.\d+\.?\d*(-dev|-beta\d|-rc)?', idf_version() or '0.0-dev') version = version_search.group() if version_search else 'latest' if 'dev' in version: version = 'latest' elif version[0] != 'v': version = 'v' + version target = target or get_target(args.project_dir) or 'esp32' link = '/'.join([URL_TO_DOC, language, version, target, starting_page or '']) redirect_link = False try: req = Request(link) webpage = urlopen(req) redirect_link = webpage.geturl().endswith('404.html') except URLError: print("We can't check the link's functionality because you don't have an internet connection") if redirect_link: print('Target', target, 'doesn\'t exist for version', version) link = '/'.join([URL_TO_DOC, language, version, starting_page or '']) if not no_browser: print('Opening documentation in the default browser:') print(link) open_new_tab(link) else: print('Please open the documentation link in the browser:') print(link) sys.exit(0) def get_default_language() -> str: try: language = 'zh_CN' if locale.getdefaultlocale()[0] == 'zh_CN' else 'en' except ValueError: language = 'en' return language root_options = { 'global_options': [ { 'names': ['--version'], 'help': 'Show IDF version and exit.', 'is_flag': True, 'expose_value': False, 'callback': idf_version_callback, }, { 'names': ['--list-targets'], 'help': 'Print list of supported targets and exit.', 'is_flag': True, 'expose_value': False, 'callback': list_targets_callback, }, { 'names': ['-C', '--project-dir'], 'scope': 'shared', 'help': 'Project directory.', 'type': click.Path(), 'default': os.getcwd(), }, { 'names': ['-B', '--build-dir'], 'help': 'Build directory.', 'type': click.Path(), 'default': None, }, { 'names': ['-w/-n', '--cmake-warn-uninitialized/--no-warnings'], 'help': ('Enable CMake uninitialized variable warnings for CMake files inside the project directory. ' "(--no-warnings is now the default, and doesn't need to be specified.)"), 'envvar': 'IDF_CMAKE_WARN_UNINITIALIZED', 'is_flag': True, 'default': False, }, { 'names': ['-v', '--verbose'], 'help': 'Verbose build output.', 'is_flag': True, 'is_eager': True, 'default': False, 'callback': verbose_callback, }, { 'names': ['--preview'], 'help': 'Enable IDF features that are still in preview.', 'is_flag': True, 'default': False, }, { 'names': ['--ccache/--no-ccache'], 'help': 'Use ccache in build. Disabled by default.', 'is_flag': True, 'envvar': 'IDF_CCACHE_ENABLE', 'default': False, }, { 'names': ['-G', '--generator'], 'help': 'CMake generator.', 'type': click.Choice(GENERATORS.keys()), }, { 'names': ['--dry-run'], 'help': "Only process arguments, but don't execute actions.", 'is_flag': True, 'hidden': True, 'default': False, }, { 'names': ['--no-hints'], 'help': 'Disable hints on how to resolve errors and logging.', 'is_flag': True, 'default': False } ], 'global_action_callbacks': [validate_root_options], } build_actions = { 'actions': { 'all': { 'aliases': ['build'], 'callback': build_target, 'short_help': 'Build the project.', 'help': ( 'Build the project. This can involve multiple steps:\n\n' '1. Create the build directory if needed. ' "The sub-directory 'build' is used to hold build output, " 'although this can be changed with the -B option.\n\n' '2. Run CMake as necessary to configure the project ' 'and generate build files for the main build tool.\n\n' '3. Run the main build tool (Ninja or GNU Make). ' 'By default, the build tool is automatically detected ' 'but it can be explicitly set by passing the -G option to idf.py.\n\n'), 'options': global_options, 'order_dependencies': [ 'reconfigure', 'menuconfig', 'clean', 'fullclean', ], }, 'menuconfig': { 'callback': menuconfig, 'help': 'Run "menuconfig" project configuration tool.', 'options': global_options + [ { 'names': ['--style', '--color-scheme', 'style'], 'help': ( 'Menuconfig style.\n' 'The built-in styles include:\n\n' '- default - a yellowish theme,\n\n' '- monochrome - a black and white theme, or\n\n' '- aquatic - a blue theme.\n\n' 'It is possible to customize these themes further' ' as it is described in the Color schemes section of the kconfiglib documentation.\n' 'The default value is \"aquatic\".'), 'envvar': 'MENUCONFIG_STYLE', 'default': 'aquatic', } ], }, 'confserver': { 'callback': build_target, 'help': 'Run JSON configuration server.', 'options': global_options, }, 'size': { 'callback': size_target, 'help': 'Print basic size information about the app.', 'options': global_options, }, 'size-components': { 'callback': size_target, 'help': 'Print per-component size information.', 'options': global_options, }, 'size-files': { 'callback': size_target, 'help': 'Print per-source-file size information.', 'options': global_options, }, 'bootloader': { 'callback': build_target, 'help': 'Build only bootloader.', 'options': global_options, }, 'app': { 'callback': build_target, 'help': 'Build only the app.', 'order_dependencies': ['clean', 'fullclean', 'reconfigure'], 'options': global_options, }, 'efuse-common-table': { 'callback': build_target, 'help': 'Generate C-source for IDF\'s eFuse fields.', 'order_dependencies': ['reconfigure'], 'options': global_options, }, 'efuse-custom-table': { 'callback': build_target, 'help': 'Generate C-source for user\'s eFuse fields.', 'order_dependencies': ['reconfigure'], 'options': global_options, }, 'show-efuse-table': { 'callback': build_target, 'help': 'Print eFuse table.', 'order_dependencies': ['reconfigure'], 'options': global_options, }, 'partition-table': { 'callback': build_target, 'help': 'Build only partition table.', 'order_dependencies': ['reconfigure'], 'options': global_options, }, 'build-system-targets': { 'callback': list_build_system_targets, 'help': 'Print list of build system targets.', }, 'fallback': { 'callback': fallback_target, 'help': 'Handle for targets not known for idf.py.', 'hidden': True, }, 'docs': { 'callback': show_docs, 'help': 'Open web browser with documentation for ESP-IDF', 'options': [ { 'names': ['--no-browser', '-nb'], 'is_flag': True, 'help': 'Don\'t open browser.' }, { 'names': ['--language', '-l'], 'default': get_default_language(), 'type': click.Choice(['en', 'zh_CN', 'cn']), 'help': 'Documentation language. Your system language by default (en or cn)' }, { 'names': ['--starting-page', '-sp'], 'help': 'Documentation page (get-started, api-reference etc).' }, { 'names': ['--version', '-v'], 'help': 'Version of ESP-IDF.' }, { 'names': ['--target', '-t'], 'type': TargetChoice(SUPPORTED_TARGETS + PREVIEW_TARGETS + ['']), 'help': 'Chip target.' } ] }, 'save-defconfig': { 'callback': build_target, 'help': 'Generate a sdkconfig.defaults with options different from the default ones', 'options': global_options } } } clean_actions = { 'actions': { 'reconfigure': { 'callback': reconfigure, 'short_help': 'Re-run CMake.', 'help': ( "Re-run CMake even if it doesn't seem to need re-running. " "This isn't necessary during normal usage, " 'but can be useful after adding/removing files from the source tree, ' 'or when modifying CMake cache variables. ' "For example, \"idf.py -DNAME='VALUE' reconfigure\" " 'can be used to set variable "NAME" in CMake cache to value "VALUE".'), 'options': global_options, 'order_dependencies': ['menuconfig', 'fullclean'], }, 'set-target': { 'callback': set_target, 'short_help': 'Set the chip target to build.', 'help': ( 'Set the chip target to build. This will remove the ' 'existing sdkconfig file and corresponding CMakeCache and ' 'create new ones according to the new target.\nFor example, ' "\"idf.py set-target esp32\" will select esp32 as the new chip " 'target.'), 'arguments': [ { 'names': ['idf-target'], 'nargs': 1, 'type': TargetChoice(SUPPORTED_TARGETS + PREVIEW_TARGETS), }, ], 'dependencies': ['fullclean'], }, 'clean': { 'callback': clean, 'short_help': 'Delete build output files from the build directory.', 'help': ( 'Delete build output files from the build directory, ' "forcing a 'full rebuild' the next time " "the project is built. Cleaning doesn't delete " 'CMake configuration output and some other files'), 'order_dependencies': ['fullclean'], }, 'fullclean': { 'callback': fullclean, 'short_help': 'Delete the entire build directory contents.', 'help': ( 'Delete the entire build directory contents. ' 'This includes all CMake configuration output.' 'The next time the project is built, ' 'CMake will configure it from scratch. ' 'Note that this option recursively deletes all files ' 'in the build directory, so use with care.' 'Project configuration is not deleted.') }, 'python-clean': { 'callback': python_clean, 'short_help': 'Delete generated Python byte code from the IDF directory', 'help': ( 'Delete generated Python byte code from the IDF directory ' 'which may cause issues when switching between IDF and Python versions. ' 'It is advised to run this target after switching versions.') }, } } return merge_action_lists(root_options, build_actions, clean_actions)
def action_extensions(base_actions, project_path): def build_target(target_name, ctx, args): """ Execute the target build system to build target 'target_name' Calls ensure_build_directory() which will run cmake to generate a build directory (with the specified generator) as needed. """ ensure_build_directory(args, ctx.info_name) run_target(target_name, args) def menuconfig(target_name, ctx, args, style): """ Menuconfig target is build_target extended with the style argument for setting the value for the environment variable. """ if sys.version_info[0] < 3: # The subprocess lib cannot accept environment variables as "unicode". # This encoding step is required only in Python 2. style = style.encode(sys.getfilesystemencoding() or 'utf-8') os.environ['MENUCONFIG_STYLE'] = style build_target(target_name, ctx, args) def fallback_target(target_name, ctx, args): """ Execute targets that are not explicitly known to idf.py """ ensure_build_directory(args, ctx.info_name) try: subprocess.check_output(GENERATORS[args.generator]["dry_run"] + [target_name], cwd=args.build_dir) except Exception: raise FatalError( 'command "%s" is not known to idf.py and is not a %s target' % (target_name, args.generator)) run_target(target_name, args) def verbose_callback(ctx, param, value): if not value or ctx.resilient_parsing: return for line in ctx.command.verbose_output: print(line) return value def clean(action, ctx, args): if not os.path.isdir(args.build_dir): print("Build directory '%s' not found. Nothing to clean." % args.build_dir) return build_target("clean", ctx, args) def _delete_windows_symlinks(directory): """ It deletes symlinks recursively on Windows. It is useful for Python 2 which doesn't detect symlinks on Windows. """ deleted_paths = [] if os.name == "nt": import ctypes for root, dirnames, _filenames in os.walk(directory): for d in dirnames: full_path = os.path.join(root, d) try: full_path = full_path.decode("utf-8") except Exception: pass if ctypes.windll.kernel32.GetFileAttributesW( full_path) & 0x0400: os.rmdir(full_path) deleted_paths.append(full_path) return deleted_paths def fullclean(action, ctx, args): build_dir = args.build_dir if not os.path.isdir(build_dir): print("Build directory '%s' not found. Nothing to clean." % build_dir) return if len(os.listdir(build_dir)) == 0: print("Build directory '%s' is empty. Nothing to clean." % build_dir) return if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")): raise FatalError( "Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically " "delete files in this directory. Delete the directory manually to 'clean' it." % build_dir) red_flags = ["CMakeLists.txt", ".git", ".svn"] for red in red_flags: red = os.path.join(build_dir, red) if os.path.exists(red): raise FatalError( "Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." % red) # OK, delete everything in the build directory... # Note: Python 2.7 doesn't detect symlinks on Windows (it is supported form 3.2). Tools promising to not # follow symlinks will actually follow them. Deleting the build directory with symlinks deletes also items # outside of this directory. deleted_symlinks = _delete_windows_symlinks(build_dir) if args.verbose and len(deleted_symlinks) > 1: print("The following symlinks were identified and removed:\n%s" % "\n".join(deleted_symlinks)) for f in os.listdir( build_dir ): # TODO: once we are Python 3 only, this can be os.scandir() f = os.path.join(build_dir, f) if args.verbose: print("Removing: %s" % f) if os.path.isdir(f): shutil.rmtree(f) else: os.remove(f) def python_clean(action, ctx, args): for root, dirnames, filenames in os.walk(os.environ["IDF_PATH"]): for d in dirnames: if d == "__pycache__": dir_to_delete = os.path.join(root, d) if args.verbose: print("Removing: %s" % dir_to_delete) shutil.rmtree(dir_to_delete) for filename in fnmatch.filter(filenames, '*.py[co]'): file_to_delete = os.path.join(root, filename) if args.verbose: print("Removing: %s" % file_to_delete) os.remove(file_to_delete) def set_target(action, ctx, args, idf_target): if (not args["preview"] and idf_target in PREVIEW_TARGETS): raise FatalError( "%s is still in preview. You have to append '--preview' option after idf.py to use any preview feature." % idf_target) args.define_cache_entry.append("IDF_TARGET=" + idf_target) sdkconfig_path = os.path.join(args.project_dir, 'sdkconfig') sdkconfig_old = sdkconfig_path + ".old" if os.path.exists(sdkconfig_old): os.remove(sdkconfig_old) if os.path.exists(sdkconfig_path): os.rename(sdkconfig_path, sdkconfig_old) print( "Set Target to: %s, new sdkconfig created. Existing sdkconfig renamed to sdkconfig.old." % idf_target) ensure_build_directory(args, ctx.info_name, True) def reconfigure(action, ctx, args): ensure_build_directory(args, ctx.info_name, True) def validate_root_options(ctx, args, tasks): args.project_dir = realpath(args.project_dir) if args.build_dir is not None and args.project_dir == realpath( args.build_dir): raise FatalError( "Setting the build directory to the project directory is not supported. Suggest dropping " "--build-dir option, the default is a 'build' subdirectory inside the project directory." ) if args.build_dir is None: args.build_dir = os.path.join(args.project_dir, "build") args.build_dir = realpath(args.build_dir) def idf_version_callback(ctx, param, value): if not value or ctx.resilient_parsing: return version = idf_version() if not version: raise FatalError("ESP-IDF version cannot be determined") print("ESP-IDF %s" % version) sys.exit(0) def list_targets_callback(ctx, param, value): if not value or ctx.resilient_parsing: return for target in SUPPORTED_TARGETS: print(target) if "preview" in ctx.params: for target in PREVIEW_TARGETS: print(target) sys.exit(0) root_options = { "global_options": [ { "names": ["--version"], "help": "Show IDF version and exit.", "is_flag": True, "expose_value": False, "callback": idf_version_callback }, { "names": ["--list-targets"], "help": "Print list of supported targets and exit.", "is_flag": True, "expose_value": False, "callback": list_targets_callback }, { "names": ["-C", "--project-dir"], "help": "Project directory.", "type": click.Path(), "default": os.getcwd(), }, { "names": ["-B", "--build-dir"], "help": "Build directory.", "type": click.Path(), "default": None, }, { "names": ["-n", "--no-warnings"], "help": "Disable Cmake warnings.", "is_flag": True, "default": False, }, { "names": ["-v", "--verbose"], "help": "Verbose build output.", "is_flag": True, "is_eager": True, "default": False, "callback": verbose_callback }, { "names": ["--preview"], "help": "Enable IDF features that are still in preview.", "is_flag": True, "default": False, }, { "names": ["--ccache/--no-ccache"], "help": ("Use ccache in build. Disabled by default, unless " "IDF_CCACHE_ENABLE environment variable is set to a non-zero value." ), "is_flag": True, "default": os.getenv("IDF_CCACHE_ENABLE") not in [None, "", "0"], }, { "names": ["-G", "--generator"], "help": "CMake generator.", "type": click.Choice(GENERATORS.keys()), }, { "names": ["--dry-run"], "help": "Only process arguments, but don't execute actions.", "is_flag": True, "hidden": True, "default": False }, ], "global_action_callbacks": [validate_root_options], } build_actions = { "actions": { "all": { "aliases": ["build"], "callback": build_target, "short_help": "Build the project.", "help": ("Build the project. This can involve multiple steps:\n\n" "1. Create the build directory if needed. " "The sub-directory 'build' is used to hold build output, " "although this can be changed with the -B option.\n\n" "2. Run CMake as necessary to configure the project " "and generate build files for the main build tool.\n\n" "3. Run the main build tool (Ninja or GNU Make). " "By default, the build tool is automatically detected " "but it can be explicitly set by passing the -G option to idf.py.\n\n" ), "options": global_options, "order_dependencies": [ "reconfigure", "menuconfig", "clean", "fullclean", ], }, "menuconfig": { "callback": menuconfig, "help": 'Run "menuconfig" project configuration tool.', "options": global_options + [{ "names": ["--style", "--color-scheme", "style"], "help": ("Menuconfig style.\n" "Is it possible to customize the menuconfig style by either setting the MENUCONFIG_STYLE " "environment variable or through this option. The built-in styles include:\n\n" "- default - a yellowish theme,\n\n" "- monochrome - a black and white theme, or\n" "- aquatic - a blue theme.\n\n" "The default value is \"aquatic\". It is possible to customize these themes further " "as it is described in the Color schemes section of the kconfiglib documentation." ), "default": os.environ.get('MENUCONFIG_STYLE', 'aquatic'), }], }, "confserver": { "callback": build_target, "help": "Run JSON configuration server.", "options": global_options, }, "size": { "callback": build_target, "help": "Print basic size information about the app.", "options": global_options, "dependencies": ["app"], }, "size-components": { "callback": build_target, "help": "Print per-component size information.", "options": global_options, "dependencies": ["app"], }, "size-files": { "callback": build_target, "help": "Print per-source-file size information.", "options": global_options, "dependencies": ["app"], }, "bootloader": { "callback": build_target, "help": "Build only bootloader.", "options": global_options, }, "app": { "callback": build_target, "help": "Build only the app.", "order_dependencies": ["clean", "fullclean", "reconfigure"], "options": global_options, }, "efuse_common_table": { "callback": build_target, "help": "Generate C-source for IDF's eFuse fields.", "order_dependencies": ["reconfigure"], "options": global_options, }, "efuse_custom_table": { "callback": build_target, "help": "Generate C-source for user's eFuse fields.", "order_dependencies": ["reconfigure"], "options": global_options, }, "show_efuse_table": { "callback": build_target, "help": "Print eFuse table.", "order_dependencies": ["reconfigure"], "options": global_options, }, "partition_table": { "callback": build_target, "help": "Build only partition table.", "order_dependencies": ["reconfigure"], "options": global_options, }, "erase_otadata": { "callback": build_target, "help": "Erase otadata partition.", "options": global_options, }, "read_otadata": { "callback": build_target, "help": "Read otadata partition.", "options": global_options, }, "fallback": { "callback": fallback_target, "help": "Handle for targets not known for idf.py.", "hidden": True } } } clean_actions = { "actions": { "reconfigure": { "callback": reconfigure, "short_help": "Re-run CMake.", "help": ("Re-run CMake even if it doesn't seem to need re-running. " "This isn't necessary during normal usage, " "but can be useful after adding/removing files from the source tree, " "or when modifying CMake cache variables. " "For example, \"idf.py -DNAME='VALUE' reconfigure\" " 'can be used to set variable "NAME" in CMake cache to value "VALUE".' ), "options": global_options, "order_dependencies": ["menuconfig", "fullclean"], }, "set-target": { "callback": set_target, "short_help": "Set the chip target to build.", "help": ("Set the chip target to build. This will remove the " "existing sdkconfig file and corresponding CMakeCache and " "create new ones according to the new target.\nFor example, " "\"idf.py set-target esp32\" will select esp32 as the new chip " "target."), "arguments": [ { "names": ["idf-target"], "nargs": 1, "type": TargetChoice(SUPPORTED_TARGETS + PREVIEW_TARGETS), }, ], "dependencies": ["fullclean"], }, "clean": { "callback": clean, "short_help": "Delete build output files from the build directory.", "help": ("Delete build output files from the build directory, " "forcing a 'full rebuild' the next time " "the project is built. Cleaning doesn't delete " "CMake configuration output and some other files"), "order_dependencies": ["fullclean"], }, "fullclean": { "callback": fullclean, "short_help": "Delete the entire build directory contents.", "help": ("Delete the entire build directory contents. " "This includes all CMake configuration output." "The next time the project is built, " "CMake will configure it from scratch. " "Note that this option recursively deletes all files " "in the build directory, so use with care." "Project configuration is not deleted.") }, "python-clean": { "callback": python_clean, "short_help": "Delete generated Python byte code from the IDF directory", "help": ("Delete generated Python byte code from the IDF directory " "which may cause issues when switching between IDF and Python versions. " "It is advised to run this target after switching versions.") }, } } return merge_action_lists(root_options, build_actions, clean_actions)
def init_cli(verbose_output=None): # Click is imported here to run it after check_environment() import click class DeprecationMessage(object): """Construct deprecation notice for help messages""" def __init__(self, deprecated=False): self.deprecated = deprecated self.since = None self.removed = None self.custom_message = "" if isinstance(deprecated, dict): self.custom_message = deprecated.get("message", "") self.since = deprecated.get("since", None) self.removed = deprecated.get("removed", None) elif isinstance(deprecated, str): self.custom_message = deprecated def full_message(self, type="Option"): return "%s is deprecated %sand will be removed in%s.%s" % ( type, "since %s " % self.since if self.since else "", " %s" % self.removed if self.removed else " future versions", " %s" % self.custom_message if self.custom_message else "") def help(self, text, type="Option", separator=" "): text = text or "" return self.full_message( type) + separator + text if self.deprecated else text def short_help(self, text): text = text or "" return ("Deprecated! " + text) if self.deprecated else text def print_deprecation_warning(ctx): """Prints deprectation warnings for arguments in given context""" for option in ctx.command.params: default = () if option.multiple else option.default if isinstance(option, Option) and option.deprecated and ctx.params[ option.name] != default: print("Warning: %s" % DeprecationMessage(option.deprecated).full_message( 'Option "%s"' % option.name)) class Task(object): def __init__(self, callback, name, aliases, dependencies, order_dependencies, action_args): self.callback = callback self.name = name self.dependencies = dependencies self.order_dependencies = order_dependencies self.action_args = action_args self.aliases = aliases def run(self, context, global_args, action_args=None): if action_args is None: action_args = self.action_args self.callback(self.name, context, global_args, **action_args) class Action(click.Command): def __init__(self, name=None, aliases=None, deprecated=False, dependencies=None, order_dependencies=None, **kwargs): super(Action, self).__init__(name, **kwargs) self.name = self.name or self.callback.__name__ self.deprecated = deprecated if aliases is None: aliases = [] self.aliases = aliases self.help = self.help or self.callback.__doc__ if self.help is None: self.help = "" if dependencies is None: dependencies = [] if order_dependencies is None: order_dependencies = [] # Show first line of help if short help is missing self.short_help = self.short_help or self.help.split("\n")[0] if deprecated: deprecation = DeprecationMessage(deprecated) self.short_help = deprecation.short_help(self.short_help) self.help = deprecation.help(self.help, type="Command", separator="\n") # Add aliases to help string if aliases: aliases_help = "Aliases: %s." % ", ".join(aliases) self.help = "\n".join([self.help, aliases_help]) self.short_help = " ".join([aliases_help, self.short_help]) if self.callback is not None: callback = self.callback def wrapped_callback(**action_args): return Task( callback=callback, name=self.name, dependencies=dependencies, order_dependencies=order_dependencies, action_args=action_args, aliases=self.aliases, ) self.callback = wrapped_callback def invoke(self, ctx): if self.deprecated: print("Warning: %s" % DeprecationMessage( self.deprecated).full_message('Command "%s"' % self.name)) self.deprecated = False # disable Click's built-in deprecation handling # Print warnings for options print_deprecation_warning(ctx) return super(Action, self).invoke(ctx) class Argument(click.Argument): """ Positional argument names - alias of 'param_decls' """ def __init__(self, **kwargs): names = kwargs.pop("names") super(Argument, self).__init__(names, **kwargs) class Scope(object): """ Scope for sub-command option. possible values: - default - only available on defined level (global/action) - global - When defined for action, also available as global - shared - Opposite to 'global': when defined in global scope, also available for all actions """ SCOPES = ("default", "global", "shared") def __init__(self, scope=None): if scope is None: self._scope = "default" elif isinstance(scope, str) and scope in self.SCOPES: self._scope = scope elif isinstance(scope, Scope): self._scope = str(scope) else: raise FatalError("Unknown scope for option: %s" % scope) @property def is_global(self): return self._scope == "global" @property def is_shared(self): return self._scope == "shared" def __str__(self): return self._scope class Option(click.Option): """Option that knows whether it should be global""" def __init__(self, scope=None, deprecated=False, hidden=False, **kwargs): """ Keyword arguments additional to Click's Option class: names - alias of 'param_decls' deprecated - marks option as deprecated. May be boolean, string (with custom deprecation message) or dict with optional keys: since: version of deprecation removed: version when option will be removed custom_message: Additional text to deprecation warning """ kwargs["param_decls"] = kwargs.pop("names") super(Option, self).__init__(**kwargs) self.deprecated = deprecated self.scope = Scope(scope) self.hidden = hidden if deprecated: deprecation = DeprecationMessage(deprecated) self.help = deprecation.help(self.help) if self.scope.is_global: self.help += " This option can be used at most once either globally, or for one subcommand." def get_help_record(self, ctx): # Backport "hidden" parameter to click 5.0 if self.hidden: return return super(Option, self).get_help_record(ctx) class CLI(click.MultiCommand): """Action list contains all actions with options available for CLI""" def __init__(self, all_actions=None, verbose_output=None, help=None): super(CLI, self).__init__( chain=True, invoke_without_command=True, result_callback=self.execute_tasks, context_settings={"max_content_width": 140}, help=help, ) self._actions = {} self.global_action_callbacks = [] self.commands_with_aliases = {} if verbose_output is None: verbose_output = [] self.verbose_output = verbose_output if all_actions is None: all_actions = {} shared_options = [] # Global options for option_args in all_actions.get("global_options", []): option = Option(**option_args) self.params.append(option) if option.scope.is_shared: shared_options.append(option) # Global options validators self.global_action_callbacks = all_actions.get( "global_action_callbacks", []) # Actions for name, action in all_actions.get("actions", {}).items(): arguments = action.pop("arguments", []) options = action.pop("options", []) if arguments is None: arguments = [] if options is None: options = [] self._actions[name] = Action(name=name, **action) for alias in [name] + action.get("aliases", []): self.commands_with_aliases[alias] = name for argument_args in arguments: self._actions[name].params.append( Argument(**argument_args)) # Add all shared options for option in shared_options: self._actions[name].params.append(option) for option_args in options: option = Option(**option_args) if option.scope.is_shared: raise FatalError( '"%s" is defined for action "%s". ' ' "shared" options can be declared only on global level' % (option.name, name)) # Promote options to global if see for the first time if option.scope.is_global and option.name not in [ o.name for o in self.params ]: self.params.append(option) self._actions[name].params.append(option) def list_commands(self, ctx): return sorted(self._actions) def get_command(self, ctx, name): return self._actions.get(self.commands_with_aliases.get(name)) def _print_closing_message(self, args, actions): # print a closing message of some kind # if "flash" in str(actions): print("Done") return if not os.path.exists( os.path.join(args.build_dir, "flasher_args.json")): print("Done") return # Otherwise, if we built any binaries print a message about # how to flash them def print_flashing_message(title, key): print("\n%s build complete. To flash, run this command:" % title) with open(os.path.join(args.build_dir, "flasher_args.json")) as f: flasher_args = json.load(f) def flasher_path(f): return _safe_relpath(os.path.join(args.build_dir, f)) if key != "project": # flashing a single item cmd = "" if ( key == "bootloader" ): # bootloader needs --flash-mode, etc to be passed in cmd = " ".join(flasher_args["write_flash_args"]) + " " cmd += flasher_args[key]["offset"] + " " cmd += flasher_path(flasher_args[key]["file"]) else: # flashing the whole project cmd = " ".join(flasher_args["write_flash_args"]) + " " flash_items = sorted( ((o, f) for (o, f) in flasher_args["flash_files"].items() if len(o) > 0), key=lambda x: int(x[0], 0), ) for o, f in flash_items: cmd += o + " " + flasher_path(f) + " " print("%s -p %s -b %s --after %s write_flash %s" % ( _safe_relpath("%s/components/esptool_py/esptool/esptool.py" % os.environ["IDF_PATH"]), args.port or "(PORT)", args.baud, flasher_args["extra_esptool_args"]["after"], cmd.strip(), )) print("or run 'idf.py -p %s %s'" % ( args.port or "(PORT)", key + "-flash" if key != "project" else "flash", )) if "all" in actions or "build" in actions: print_flashing_message("Project", "project") else: if "app" in actions: print_flashing_message("App", "app") if "partition_table" in actions: print_flashing_message("Partition Table", "partition_table") if "bootloader" in actions: print_flashing_message("Bootloader", "bootloader") def execute_tasks(self, tasks, **kwargs): ctx = click.get_current_context() global_args = PropertyDict(kwargs) # Show warning if some tasks are present several times in the list dupplicated_tasks = sorted([ item for item, count in Counter( task.name for task in tasks).items() if count > 1 ]) if dupplicated_tasks: dupes = ", ".join('"%s"' % t for t in dupplicated_tasks) print( "WARNING: Command%s found in the list of commands more than once. " % ("s %s are" % dupes if len(dupplicated_tasks) > 1 else " %s is" % dupes) + "Only first occurence will be executed.") # Set propagated global options. # These options may be set on one subcommand, but available in the list of global arguments for task in tasks: for key in list(task.action_args): option = next( (o for o in ctx.command.params if o.name == key), None) if option and (option.scope.is_global or option.scope.is_shared): local_value = task.action_args.pop(key) global_value = global_args[key] default = () if option.multiple else option.default if global_value != default and local_value != default and global_value != local_value: raise FatalError( 'Option "%s" provided for "%s" is already defined to a different value. ' "This option can appear at most once in the command line." % (key, task.name)) if local_value != default: global_args[key] = local_value # Show warnings about global arguments print_deprecation_warning(ctx) # Make sure that define_cache_entry is mutable list and can be modified in callbacks global_args.define_cache_entry = list( global_args.define_cache_entry) # Execute all global action callback - first from idf.py itself, then from extensions for action_callback in ctx.command.global_action_callbacks: action_callback(ctx, global_args, tasks) # Always show help when command is not provided if not tasks: print(ctx.get_help()) ctx.exit() # Build full list of tasks to and deal with dependencies and order dependencies tasks_to_run = OrderedDict() while tasks: task = tasks[0] tasks_dict = dict([(t.name, t) for t in tasks]) dependecies_processed = True # If task have some dependecies they have to be executed before the task. for dep in task.dependencies: if dep not in tasks_to_run.keys(): # If dependent task is in the list of unprocessed tasks move to the front of the list if dep in tasks_dict.keys(): dep_task = tasks.pop(tasks.index(tasks_dict[dep])) # Otherwise invoke it with default set of options # and put to the front of the list of unprocessed tasks else: print( 'Adding "%s"\'s dependency "%s" to list of commands with default set of options.' % (task.name, dep)) dep_task = ctx.invoke( ctx.command.get_command(ctx, dep)) # Remove options with global scope from invoke tasks because they are alread in global_args for key in list(dep_task.action_args): option = next((o for o in ctx.command.params if o.name == key), None) if option and (option.scope.is_global or option.scope.is_shared): dep_task.action_args.pop(key) tasks.insert(0, dep_task) dependecies_processed = False # Order only dependencies are moved to the front of the queue if they present in command list for dep in task.order_dependencies: if dep in tasks_dict.keys( ) and dep not in tasks_to_run.keys(): tasks.insert(0, tasks.pop(tasks.index(tasks_dict[dep]))) dependecies_processed = False if dependecies_processed: # Remove task from list of unprocessed tasks tasks.pop(0) # And add to the queue if task.name not in tasks_to_run.keys(): tasks_to_run.update([(task.name, task)]) # Run all tasks in the queue # when global_args.dry_run is true idf.py works in idle mode and skips actual task execution if not global_args.dry_run: for task in tasks_to_run.values(): name_with_aliases = task.name if task.aliases: name_with_aliases += " (aliases: %s)" % ", ".join( task.aliases) print("Executing action: %s" % name_with_aliases) task.run(ctx, global_args, task.action_args) self._print_closing_message(global_args, tasks_to_run.keys()) return tasks_to_run # That's a tiny parser that parse project-dir even before constructing # fully featured click parser to be sure that extensions are loaded from the right place @click.command( add_help_option=False, context_settings={ "allow_extra_args": True, "ignore_unknown_options": True }, ) @click.option("-C", "--project-dir", default=os.getcwd()) def parse_project_dir(project_dir): return realpath(project_dir) project_dir = parse_project_dir(standalone_mode=False) all_actions = {} # Load extensions from components dir idf_py_extensions_path = os.path.join(os.environ["IDF_PATH"], "tools", "idf_py_actions") extra_paths = os.environ.get("IDF_EXTRA_ACTIONS_PATH", "").split(';') extension_dirs = [idf_py_extensions_path] + extra_paths extensions = {} for directory in extension_dirs: if directory and not os.path.exists(directory): print( 'WARNING: Directroy with idf.py extensions doesn\'t exist:\n %s' % directory) continue sys.path.append(directory) for _finder, name, _ispkg in sorted(iter_modules([directory])): if name.endswith('_ext'): extensions[name] = import_module(name) for name, extension in extensions.items(): try: all_actions = merge_action_lists( all_actions, extension.action_extensions(all_actions, project_dir)) except AttributeError: print('WARNING: Cannot load idf.py extension "%s"' % name) # Load extensions from project dir if os.path.exists(os.path.join(project_dir, "idf_ext.py")): sys.path.append(project_dir) try: from idf_ext import action_extensions except ImportError: print("Error importing extension file idf_ext.py. Skipping.") print( "Please make sure that it contains implementation (even if it's empty) of add_action_extensions" ) try: all_actions = merge_action_lists( all_actions, action_extensions(all_actions, project_dir)) except NameError: pass return CLI(help="ESP-IDF build management", verbose_output=verbose_output, all_actions=all_actions)