Files
esp-idf/components/partition_table/parttool.py

544 lines
18 KiB
Python
Executable File

#!/usr/bin/env python
#
# parttool is used to perform partition level operations - reading,
# writing, erasing and getting info about the partition.
#
# SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
import re
import subprocess
import sys
import tempfile
import gen_esp32part as gen
import rich_click as click
from esp_pylib.cli_options import EspRichGroup
from esp_pylib.cli_options import MutuallyExclusiveOption
from esp_pylib.cli_options import OptionEatAll
from esp_pylib.cli_types import AnyIntType
from esp_pylib.logger import log
__version__ = '2.2'
COMPONENTS_PATH = os.path.expandvars(os.path.join('$IDF_PATH', 'components'))
PARTITION_TABLE_OFFSET = 0x8000
class _StringListParamType(click.ParamType):
"""ParamType for `OptionEatAll` without ``multiple=True`` (argparse ``nargs='+'``)."""
name = 'text_list'
def convert(self, value, param, ctx):
if not value:
return ()
if isinstance(value, (list, tuple)): # noqa: UP038
return tuple(value)
return (value,)
_ESPTOOL_ARGS_TYPE = _StringListParamType()
quiet = False
def status(msg):
if not quiet:
log.print(msg, file=sys.stderr)
class _PartitionId:
def __init__(self, name=None, p_type=None, subtype=None, part_list=None):
self.name = name
self.type = p_type
self.subtype = subtype
self.part_list = part_list
class PartitionName(_PartitionId):
def __init__(self, name):
_PartitionId.__init__(self, name=name)
class PartitionType(_PartitionId):
def __init__(self, p_type, subtype, part_list=None):
_PartitionId.__init__(self, p_type=p_type, subtype=subtype, part_list=part_list)
PARTITION_BOOT_DEFAULT = _PartitionId()
class ParttoolTarget:
def __init__(
self,
port=None,
baud=None,
partition_table_offset=PARTITION_TABLE_OFFSET,
primary_bootloader_offset=None,
recovery_bootloader_offset=None,
partition_table_file=None,
esptool_args=[],
esptool_write_args=[],
esptool_read_args=[],
esptool_erase_args=[],
):
self.port = port
self.baud = baud
gen.offset_part_table = partition_table_offset
gen.primary_bootloader_offset = primary_bootloader_offset
gen.recovery_bootloader_offset = recovery_bootloader_offset
gen.quiet = True
def parse_esptool_args(esptool_args):
results = list()
for arg in esptool_args:
pattern = re.compile(r'(.+)=(.+)')
result = pattern.match(arg)
try:
key = result.group(1)
value = result.group(2)
results.extend(['--' + key, value])
except AttributeError:
results.extend(['--' + arg])
return results
self.esptool_args = parse_esptool_args(esptool_args)
self.esptool_write_args = parse_esptool_args(esptool_write_args)
self.esptool_read_args = parse_esptool_args(esptool_read_args)
self.esptool_erase_args = parse_esptool_args(esptool_erase_args)
if partition_table_file:
with open(partition_table_file, 'rb') as f:
partition_table, _ = gen.PartitionTable.from_file(f)
else:
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
try:
self._call_esptool(
['read-flash', str(partition_table_offset), str(gen.MAX_PARTITION_LENGTH), temp_file.name]
)
with open(temp_file.name, 'rb') as f:
partition_table = gen.PartitionTable.from_binary(f.read())
finally:
os.unlink(temp_file.name)
self.partition_table = partition_table
# set `out` to None to redirect the output to the STDOUT
# otherwise set `out` to file descriptor
# beware that the method does not close the file descriptor
def _call_esptool(self, args, out=None):
esptool_args = [sys.executable, '-m', 'esptool'] + self.esptool_args
if self.port:
esptool_args += ['--port', self.port]
if self.baud:
esptool_args += ['--baud', str(self.baud)]
esptool_args += args
log.print(f'Running {" ".join(esptool_args)}...', file=sys.stderr)
try:
subprocess.check_call(esptool_args, stdout=out, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
log.print(f'An exception: ** {e} ** occurred in _call_esptool.', file=out or sys.stderr)
raise e
def get_partition_info(self, partition_id):
partition = None
if partition_id.name:
partition = self.partition_table.find_by_name(partition_id.name)
elif partition_id.type and partition_id.subtype:
partition = list(self.partition_table.find_by_type(partition_id.type, partition_id.subtype))
if not partition_id.part_list:
partition = partition[0]
else: # default boot partition
search = ['factory'] + [f'ota_{d}' for d in range(16)]
for subtype in search:
partition = next(self.partition_table.find_by_type('app', subtype), None)
if partition:
break
if not partition:
raise Exception('Partition does not exist')
return partition
def erase_partition(self, partition_id):
partition = self.get_partition_info(partition_id)
self._call_esptool(['erase-region', str(partition.offset), str(partition.size)] + self.esptool_erase_args)
def read_partition(self, partition_id, output):
partition = self.get_partition_info(partition_id)
self._call_esptool(['read-flash', str(partition.offset), str(partition.size), output] + self.esptool_read_args)
def write_partition(self, partition_id, input, ignore_readonly=False): # noqa: A002
partition = self.get_partition_info(partition_id)
if partition.readonly and not ignore_readonly:
log.die(f'"{partition.name}" partition is read-only, (use the --ignore-readonly flag to skip it)')
self.erase_partition(partition_id)
with open(input, 'rb') as f:
content_len = len(f.read())
if content_len > partition.size:
raise Exception('Input file size exceeds partition size')
self._call_esptool(['write-flash', str(partition.offset), input] + self.esptool_write_args)
def _write_partition(target, partition_id, input, ignore_readonly=False): # noqa: A002
target.write_partition(partition_id, input, ignore_readonly)
partition = target.get_partition_info(partition_id)
status(f"Written contents of file '{input}' at offset 0x{partition.offset:x}")
def _read_partition(target, partition_id, output):
target.read_partition(partition_id, output)
partition = target.get_partition_info(partition_id)
status(
f"Read partition '{partition.name}' contents from device at offset 0x{partition.offset:x} to file '{output}'"
)
def _erase_partition(target, partition_id):
target.erase_partition(partition_id)
partition = target.get_partition_info(partition_id)
status(f"Erased partition '{partition.name}' at offset 0x{partition.offset:x}")
def _get_partition_info(target, partition_id, info):
try:
partitions = target.get_partition_info(partition_id)
if not isinstance(partitions, list):
partitions = [partitions]
except Exception:
return
infos = []
try:
for p in partitions:
info_dict = {
'name': f'{p.name}',
'type': f'{p.type}',
'subtype': f'{p.subtype}',
'offset': f'0x{p.offset:x}',
'size': f'0x{p.size:x}',
'encrypted': f'{p.encrypted}',
'readonly': f'{p.readonly}',
}
for i in info:
infos += [info_dict[i]]
except KeyError:
log.die(f'Request for unknown partition info {i}')
print(' '.join(infos))
def _partition_selection_options(func):
decorators = [
click.option(
'--partition-name',
'-n',
cls=MutuallyExclusiveOption,
exclusive_with=['partition_type', 'partition_boot_default'],
help='name of the partition',
),
click.option(
'--partition-type',
'-t',
cls=MutuallyExclusiveOption,
exclusive_with=['partition_name', 'partition_boot_default'],
help='type of the partition',
),
click.option(
'--partition-boot-default',
'-d',
is_flag=True,
cls=MutuallyExclusiveOption,
exclusive_with=['partition_name', 'partition_type'],
help='select the default boot partition using the same fallback logic as the IDF bootloader',
),
click.option('--partition-subtype', '-s', help='subtype of the partition'),
# Use OptionEatAll to keep argparse nargs='*' semantics: a single
# --extra-partition-subtypes flag consumes all following space-separated
# entries (CMake project_include.cmake passes them this way).
click.option(
'--extra-partition-subtypes',
multiple=True,
cls=OptionEatAll,
type=str,
help='Extra partition subtype entries',
),
]
for decorator in reversed(decorators):
func = decorator(func)
return func
def _build_partition_id(partition_name, partition_type, partition_subtype, partition_boot_default, part_list=False):
if partition_name:
return PartitionName(partition_name)
if partition_type:
if not partition_subtype:
log.die('--partition-subtype should be defined when --partition-type is defined')
return PartitionType(partition_type, partition_subtype, part_list)
if partition_boot_default:
return PARTITION_BOOT_DEFAULT
log.die(
'Partition to operate on should be defined using --partition-name OR '
'partition-type,--partition-subtype OR partition-boot-default'
)
def _target_kwargs_from_ctx(ctx_obj, extra_partition_subtypes=None):
kwargs = {}
for key, value in (
('port', ctx_obj.get('port')),
('baud', ctx_obj.get('baud')),
('partition_table_offset', ctx_obj.get('partition_table_offset')),
('primary_bootloader_offset', ctx_obj.get('primary_bootloader_offset')),
('recovery_bootloader_offset', ctx_obj.get('recovery_bootloader_offset')),
('partition_table_file', ctx_obj.get('partition_table_file')),
('esptool_args', ctx_obj.get('esptool_args')),
('esptool_write_args', ctx_obj.get('esptool_write_args')),
('esptool_read_args', ctx_obj.get('esptool_read_args')),
('esptool_erase_args', ctx_obj.get('esptool_erase_args')),
):
if value is not None and value != ():
kwargs[key] = value
if extra_partition_subtypes:
gen.add_extra_subtypes(extra_partition_subtypes)
return kwargs
def _run_operation(operation, quiet_flag, target, partition_id, **op_kwargs):
global quiet
quiet = quiet_flag
parttool_ops = {
'erase_partition': (_erase_partition, []),
'read_partition': (_read_partition, ['output']),
'write_partition': (_write_partition, ['input', 'ignore_readonly']),
'get_partition_info': (_get_partition_info, ['info']),
}
op, op_arg_names = parttool_ops[operation]
common_args = {'target': target, 'partition_id': partition_id}
for op_arg in op_arg_names:
common_args[op_arg] = op_kwargs[op_arg]
if quiet:
try:
op(**common_args)
except Exception:
sys.exit(2)
else:
try:
op(**common_args)
except gen.InputError as e:
log.print(str(e), file=sys.stderr)
sys.exit(2)
@click.group(
cls=EspRichGroup,
context_settings={'help_option_names': ['-h', '--help']},
help='ESP-IDF Partitions Tool',
)
@click.option('--quiet', '-q', is_flag=True, help='suppress stderr messages')
@click.option(
'--esptool-args',
cls=OptionEatAll,
type=_ESPTOOL_ARGS_TYPE,
default=(),
help='additional main arguments for esptool',
)
@click.option(
'--esptool-write-args',
cls=OptionEatAll,
type=_ESPTOOL_ARGS_TYPE,
default=(),
help='additional subcommand arguments when writing to flash',
)
@click.option(
'--esptool-read-args',
cls=OptionEatAll,
type=_ESPTOOL_ARGS_TYPE,
default=(),
help='additional subcommand arguments when reading flash',
)
@click.option(
'--esptool-erase-args',
cls=OptionEatAll,
type=_ESPTOOL_ARGS_TYPE,
default=(),
help='additional subcommand arguments when erasing regions of flash',
)
@click.option(
'--port',
'-p',
help='port where the target device of the command is connected to; the partition table is sourced from '
'this device when the partition table file is not defined',
)
@click.option('--baud', '-b', type=int, help='baudrate to use')
@click.option('--partition-table-offset', '-o', type=AnyIntType(), help='offset to read the partition table from')
@click.option('--primary-bootloader-offset', type=AnyIntType(), help='offset for primary bootloader')
@click.option('--recovery-bootloader-offset', type=AnyIntType(), help='offset for recovery bootloader')
@click.option(
'--partition-table-file',
'-f',
type=click.Path(),
help='file (CSV/binary) to read the partition table from; '
'overrides device attached to specified port as the partition table source when defined',
)
@click.pass_context
def cli(
ctx,
quiet,
esptool_args,
esptool_write_args,
esptool_read_args,
esptool_erase_args,
port,
baud,
partition_table_offset,
primary_bootloader_offset,
recovery_bootloader_offset,
partition_table_file,
):
ctx.ensure_object(dict)
ctx.obj.update(
{
'quiet': quiet,
'esptool_args': esptool_args,
'esptool_write_args': esptool_write_args,
'esptool_read_args': esptool_read_args,
'esptool_erase_args': esptool_erase_args,
'port': port,
'baud': baud,
'partition_table_offset': partition_table_offset,
'primary_bootloader_offset': primary_bootloader_offset,
'recovery_bootloader_offset': recovery_bootloader_offset,
'partition_table_file': partition_table_file,
}
)
def _shared_subcommand_options(func):
func = click.pass_context(func)
return func
@cli.command('read_partition', help='read partition from device and dump contents into a file')
@_partition_selection_options
@click.option('--output', help='file to dump the read partition contents to')
@_shared_subcommand_options
def read_partition_cmd(
ctx,
partition_name,
partition_type,
partition_boot_default,
partition_subtype,
extra_partition_subtypes,
output,
):
partition_id = _build_partition_id(partition_name, partition_type, partition_subtype, partition_boot_default)
target = ParttoolTarget(**_target_kwargs_from_ctx(ctx.obj, extra_partition_subtypes))
_run_operation('read_partition', ctx.obj['quiet'], target, partition_id, output=output)
@cli.command('write_partition', help='write contents of a binary file to partition on device')
@_partition_selection_options
@click.option('--input', 'input_file', help='file whose contents are to be written to the partition offset')
@click.option('--ignore-readonly', is_flag=True, help='Ignore read-only attribute')
@_shared_subcommand_options
def write_partition_cmd(
ctx,
partition_name,
partition_type,
partition_boot_default,
partition_subtype,
extra_partition_subtypes,
input_file,
ignore_readonly,
):
partition_id = _build_partition_id(partition_name, partition_type, partition_subtype, partition_boot_default)
target = ParttoolTarget(**_target_kwargs_from_ctx(ctx.obj, extra_partition_subtypes))
_run_operation(
'write_partition',
ctx.obj['quiet'],
target,
partition_id,
input=input_file,
ignore_readonly=ignore_readonly,
)
@cli.command('erase_partition', help='erase the contents of a partition on the device')
@_partition_selection_options
@_shared_subcommand_options
def erase_partition_cmd(
ctx,
partition_name,
partition_type,
partition_boot_default,
partition_subtype,
extra_partition_subtypes,
):
partition_id = _build_partition_id(partition_name, partition_type, partition_subtype, partition_boot_default)
target = ParttoolTarget(**_target_kwargs_from_ctx(ctx.obj, extra_partition_subtypes))
_run_operation('erase_partition', ctx.obj['quiet'], target, partition_id)
@cli.command('get_partition_info', help='get partition information')
@_partition_selection_options
@click.option(
'--info',
type=click.Choice(['name', 'type', 'subtype', 'offset', 'size', 'encrypted', 'readonly']),
multiple=True,
cls=OptionEatAll,
default=['offset', 'size'],
show_default=True,
help='type of partition information to get',
)
@click.option('--part_list', 'part_list', is_flag=True, help='Get a list of partitions suitable for a given type')
@_shared_subcommand_options
def get_partition_info_cmd(
ctx,
partition_name,
partition_type,
partition_boot_default,
partition_subtype,
extra_partition_subtypes,
info,
part_list,
):
partition_id = _build_partition_id(
partition_name, partition_type, partition_subtype, partition_boot_default, part_list=part_list
)
target = ParttoolTarget(**_target_kwargs_from_ctx(ctx.obj, extra_partition_subtypes))
_run_operation('get_partition_info', ctx.obj['quiet'], target, partition_id, info=info)
def main():
cli()
if __name__ == '__main__':
from esp_pylib.excepthook import install_exception_reporting
install_exception_reporting()
main()