mirror of
https://github.com/espressif/esptool.git
synced 2025-10-14 19:28:40 +08:00
247 lines
9.0 KiB
Python
247 lines
9.0 KiB
Python
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
from typing import Any
|
|
|
|
import rich_click as click
|
|
from click.parser import OptionParser, ParsingState, _unpack_args
|
|
from esptool.cli_util import Group as EsptoolGroup
|
|
from esptool.logger import log
|
|
|
|
from espefuse.efuse_interface import (
|
|
DEPRECATED_COMMANDS,
|
|
init_commands,
|
|
SUPPORTED_BURN_COMMANDS,
|
|
SUPPORTED_READ_COMMANDS,
|
|
SUPPORTED_COMMANDS,
|
|
)
|
|
|
|
|
|
click.rich_click.USE_CLICK_SHORT_HELP = True
|
|
click.rich_click.COMMAND_GROUPS = {
|
|
"*": [
|
|
{
|
|
"name": "Burn commands",
|
|
"commands": SUPPORTED_BURN_COMMANDS,
|
|
},
|
|
{
|
|
"name": "Read commands",
|
|
"commands": SUPPORTED_READ_COMMANDS,
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
class ChainParser(OptionParser):
|
|
"""
|
|
This is a modified version of the OptionParser class from click.parser.
|
|
It allows for the processing of arguments and options in interspersed order
|
|
together with chaining commands.
|
|
"""
|
|
|
|
def _process_args_for_options(self, state: ParsingState) -> None:
|
|
while state.rargs:
|
|
arg = state.rargs.pop(0)
|
|
arglen = len(arg)
|
|
# Double dashes always handled explicitly regardless of what
|
|
# prefixes are valid.
|
|
if arg == "--":
|
|
return
|
|
# if the argument is a command, stop parsing options
|
|
elif arg.replace("_", "-") in SUPPORTED_COMMANDS:
|
|
state.largs.append(arg)
|
|
return
|
|
elif arg[:1] in self._opt_prefixes and arglen > 1:
|
|
self._process_opts(arg, state)
|
|
elif self.allow_interspersed_args:
|
|
state.largs.append(arg)
|
|
else:
|
|
state.rargs.insert(0, arg)
|
|
return
|
|
|
|
def _process_args_for_args(self, state: ParsingState) -> None:
|
|
pargs, args = _unpack_args(
|
|
state.largs + state.rargs, [x.nargs for x in self._args]
|
|
)
|
|
|
|
# This check is required because of the way we modify nargs in ChainingCommand
|
|
if len(pargs) > 0:
|
|
for idx, arg in enumerate(self._args):
|
|
arg.process(pargs[idx], state)
|
|
|
|
state.largs = args
|
|
state.rargs = []
|
|
|
|
|
|
class EfuseContext(click.RichContext):
|
|
@property
|
|
def show_sensitive_info(self) -> bool:
|
|
self.ensure_object(dict)
|
|
value: bool = self.obj.get("show_sensitive_info", False)
|
|
if not value:
|
|
log.print("Sensitive data will be hidden (see --show-sensitive-info)")
|
|
return value
|
|
|
|
|
|
class ChainingCommand(click.RichCommand, click.Command):
|
|
context_class = EfuseContext
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def _is_option(self, arg: str) -> bool:
|
|
return arg.startswith("--") or arg.startswith("-")
|
|
|
|
def invoke(self, ctx: click.Context) -> Any:
|
|
log.print(f'\n=== Run "{self.name}" command ===')
|
|
return super().invoke(ctx)
|
|
|
|
def parse_args(self, ctx: click.Context, args: list[str]):
|
|
# This is a hack to set nargs of the last argument to the number of arguments
|
|
# that will be processed separately
|
|
param_changed = None
|
|
for idx, arg in enumerate(args):
|
|
# command found in args or option found after argument
|
|
if arg.replace("_", "-") in SUPPORTED_COMMANDS or (
|
|
self._is_option(arg) and idx > 0
|
|
):
|
|
arguments_count = sum(
|
|
isinstance(param, click.Argument) for param in self.params
|
|
)
|
|
for param in self.params:
|
|
if param.nargs != -1:
|
|
continue
|
|
# set nargs of parameter to actual count of arguments and deduct
|
|
# arguments_count as each argument will be processed separately,
|
|
# we only care about the last one with nargs=-1
|
|
# at the end we add 1 to account for the processedargument itself
|
|
# e.g. if we have burn-bit BLOCK2 1 2 3, we want to set nargs to 3,
|
|
# so we need to account for BLOCK2 being processed separately
|
|
param.nargs = args.index(arg) - arguments_count + 1
|
|
param_changed = param
|
|
if (
|
|
param.nargs == 0
|
|
and param.required
|
|
and not ctx.resilient_parsing
|
|
):
|
|
raise click.UsageError(
|
|
f"Command `{self.name}` requires the `{param.name}` "
|
|
"argument."
|
|
)
|
|
break
|
|
break
|
|
ret = super().parse_args(ctx, args)
|
|
# restore nargs of the last argument to -1, in case it is going to be used again
|
|
if param_changed is not None:
|
|
param.nargs = -1
|
|
return ret
|
|
|
|
def make_parser(self, ctx: click.Context) -> OptionParser:
|
|
"""Creates the underlying option parser for this command."""
|
|
parser = ChainParser(ctx)
|
|
parser.allow_interspersed_args = True
|
|
for param in self.get_params(ctx):
|
|
param.add_to_parser(parser, ctx)
|
|
return parser
|
|
|
|
|
|
class Group(EsptoolGroup):
|
|
DEPRECATED_OPTIONS = {
|
|
"--file_name": "--file-name",
|
|
}
|
|
|
|
command_class = ChainingCommand
|
|
context_class = EfuseContext
|
|
|
|
@staticmethod
|
|
def _split_to_groups(args: list[str]) -> tuple[list[list[str]], list[str]]:
|
|
"""
|
|
This function splits the args list into groups,
|
|
where each item is a cmd with all its args.
|
|
|
|
Example:
|
|
all_args:
|
|
['burn-key-digest', 'secure_images/ecdsa256_secure_boot_signing_key_v2.pem',
|
|
'burn-key', 'BLOCK_KEY0', 'images/efuse/128bit_key',
|
|
'XTS_AES_128_KEY_DERIVED_FROM_128_EFUSE_BITS']
|
|
|
|
used_cmds: ['burn-key-digest', 'burn-key']
|
|
groups:
|
|
[['burn-key-digest', 'secure_images/ecdsa256_secure_boot_signing_key_v2.pem'],
|
|
['burn-key', 'BLOCK_KEY0', 'images/efuse/128bit_key',
|
|
'XTS_AES_128_KEY_DERIVED_FROM_128_EFUSE_BITS']]
|
|
"""
|
|
groups: list[list[str]] = []
|
|
args_group: list[str] = []
|
|
used_cmds: list[str] = []
|
|
for arg in args:
|
|
if arg.replace("_", "-") in SUPPORTED_COMMANDS:
|
|
groups.append(args_group)
|
|
used_cmds.append(arg)
|
|
args_group = [arg]
|
|
else:
|
|
args_group.append(arg)
|
|
groups.append(args_group)
|
|
return groups, used_cmds
|
|
|
|
@staticmethod
|
|
def repeat_read_commands(
|
|
used_cmds: list[str], groups: list[list[str]]
|
|
) -> list[list[str]]:
|
|
if (
|
|
sum(cmd in SUPPORTED_BURN_COMMANDS for cmd in used_cmds) > 0
|
|
and sum(cmd in SUPPORTED_READ_COMMANDS for cmd in used_cmds) > 0
|
|
):
|
|
# append all read commands at the end of group
|
|
read_commands = []
|
|
for group in groups:
|
|
if group[0] in SUPPORTED_READ_COMMANDS:
|
|
read_commands.append(group)
|
|
groups.extend(read_commands)
|
|
return groups
|
|
|
|
def parse_args(self, ctx: click.Context, args: list[str]):
|
|
ctx.ensure_object(dict)
|
|
ctx.obj["is_help"] = any(help_arg in args for help_arg in ctx.help_option_names)
|
|
idx = (
|
|
args.index("--chip")
|
|
if "--chip" in args
|
|
else (args.index("-c") if "-c" in args else -1)
|
|
)
|
|
ctx.obj["chip"] = args[idx + 1] if idx != -1 and idx + 1 < len(args) else "auto"
|
|
# override the default behavior of EsptoolGroup, because we don't need
|
|
# support for parameters with nargs=-1
|
|
args = self._replace_deprecated_args(args)
|
|
cmd_groups, used_cmds = self._split_to_groups(args)
|
|
|
|
# Add commands for shell completion
|
|
if ctx.resilient_parsing:
|
|
commands = init_commands(port=None, chip=ctx.obj["chip"], skip_connect=True)
|
|
commands.add_cli_commands(self)
|
|
elif len(used_cmds) == 0:
|
|
log.print(self.get_help(ctx))
|
|
ctx.exit()
|
|
|
|
cmd_groups = self.repeat_read_commands(used_cmds, cmd_groups)
|
|
args = [arg for group in cmd_groups for arg in group]
|
|
|
|
ctx.obj["used_cmds"] = used_cmds
|
|
ctx.obj["args"] = args
|
|
return super(click.RichGroup, self).parse_args(ctx, args)
|
|
|
|
def get_help(self, ctx: click.Context) -> str:
|
|
# help was called without any commands, so we need to add the commands for the
|
|
# default chip
|
|
if not (set(self.list_commands(ctx)) - set(DEPRECATED_COMMANDS)):
|
|
chip = ctx.obj["chip"]
|
|
if chip == "auto":
|
|
log.note(
|
|
"Chip not specified, showing commands for ESP32 by default. "
|
|
"Specify the --chip option to get chip-specific help."
|
|
)
|
|
chip = "esp32"
|
|
commands = init_commands(port=None, chip=chip, skip_connect=True)
|
|
commands.add_cli_commands(self)
|
|
return super().get_help(ctx) # type: ignore
|