zephyr/scripts/west_commands/runners/nrfutil.py
Grzegorz Chwierut db3c344af9 west: runners: Add ncs-provision to west flash command
Added automatic KMU key provisioning, when keyfile.json
file exists in the build directory.
This enables automated key provisioning during the
flashing process to enable testing nRF54L aplications using Twister.
Only applicable on nrfutil runner.

Signed-off-by: Grzegorz Chwierut <grzegorz.chwierut@nordicsemi.no>
2025-06-11 16:09:05 -07:00

193 lines
6.8 KiB
Python

# Copyright (c) 2023 Nordic Semiconductor ASA.
#
# SPDX-License-Identifier: Apache-2.0
'''Runner for flashing with nrfutil.'''
import json
import subprocess
import sys
from pathlib import Path
from runners.core import _DRY_RUN
from runners.nrf_common import NrfBinaryRunner
class NrfUtilBinaryRunner(NrfBinaryRunner):
'''Runner front-end for nrfutil.'''
def __init__(self, cfg, family, softreset, pinreset, dev_id, erase=False,
erase_mode=None, ext_erase_mode=None, reset=True, tool_opt=None,
force=False, recover=False, suit_starter=False,
ext_mem_config_file=None):
super().__init__(cfg, family, softreset, pinreset, dev_id, erase,
erase_mode, ext_erase_mode, reset, tool_opt, force,
recover)
self.suit_starter = suit_starter
self.ext_mem_config_file = ext_mem_config_file
self._ops = []
self._op_id = 1
@classmethod
def name(cls):
return 'nrfutil'
@classmethod
def capabilities(cls):
return NrfBinaryRunner._capabilities(mult_dev_ids=True)
@classmethod
def dev_id_help(cls) -> str:
return NrfBinaryRunner._dev_id_help() + \
'''.\n This option can be specified multiple times'''
@classmethod
def tool_opt_help(cls) -> str:
return 'Additional options for nrfutil, e.g. "--log-level"'
@classmethod
def do_create(cls, cfg, args):
return NrfUtilBinaryRunner(cfg, args.nrf_family, args.softreset,
args.pinreset, args.dev_id, erase=args.erase,
erase_mode=args.erase_mode,
ext_erase_mode=args.ext_erase_mode,
reset=args.reset, tool_opt=args.tool_opt,
force=args.force, recover=args.recover,
suit_starter=args.suit_manifest_starter,
ext_mem_config_file=args.ext_mem_config_file)
@classmethod
def do_add_parser(cls, parser):
super().do_add_parser(parser)
parser.add_argument('--suit-manifest-starter', required=False,
action='store_true',
help='Use the SUIT manifest starter file')
parser.add_argument('--ext-mem-config-file', required=False,
dest='ext_mem_config_file',
help='path to an JSON file with external memory configuration')
def _exec(self, args):
jout_all = []
cmd = ['nrfutil', '--json', 'device'] + args
self._log_cmd(cmd)
if _DRY_RUN:
return {}
with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p:
for line in iter(p.stdout.readline, b''):
# https://github.com/ndjson/ndjson-spec
jout = json.loads(line.decode(sys.getdefaultencoding()))
jout_all.append(jout)
if 'x-execute-batch' in args:
if jout['type'] == 'batch_update':
pld = jout['data']['data']
if (
pld['type'] == 'task_progress' and
pld['data']['progress']['progressPercentage'] == 0
):
self.logger.info(pld['data']['progress']['description'])
elif jout['type'] == 'batch_end' and jout['data']['error']:
raise subprocess.CalledProcessError(
jout['data']['error']['code'], cmd
)
if p.returncode != 0:
raise subprocess.CalledProcessError(p.returncode, cmd)
return jout_all
def do_get_boards(self):
out = self._exec(['list'])
devs = []
for o in out:
if o['type'] == 'task_end':
devs = o['data']['data']['devices']
snrs = [dev['serialNumber'] for dev in devs if dev['traits']['jlink']]
self.logger.debug(f'Found boards: {snrs}')
return snrs
def do_require(self):
self.require('nrfutil')
def _insert_op(self, op):
op['operationId'] = f'{self._op_id}'
self._op_id += 1
self._ops.append(op)
def _format_dev_ids(self):
if isinstance(self.dev_id, list):
return ','.join(self.dev_id)
else:
return self.dev_id
def _append_batch(self, op, json_file):
_op = op['operation']
op_type = _op['type']
cmd = [f'{op_type}']
if op_type == 'program':
cmd += ['--firmware', _op['firmware']['file']]
opts = _op['options']
# populate the options
cmd.append('--options')
cli_opts = f"chip_erase_mode={opts['chip_erase_mode']}"
if opts.get('ext_mem_erase_mode'):
cli_opts += f",ext_mem_erase_mode={opts['ext_mem_erase_mode']}"
if opts.get('verify'):
cli_opts += f",verify={opts['verify']}"
cmd.append(cli_opts)
elif op_type == 'reset':
cmd += ['--reset-kind', _op['kind']]
elif op_type == 'erase':
cmd.append(f'--{_op["kind"]}')
elif op_type == 'x-provision-keys':
cmd += ['--key-file', _op['keyfile']]
cmd += ['--core', op['core']] if op.get('core') else []
cmd += ['--x-family', f'{self.family}']
cmd += ['--x-append-batch', f'{json_file}']
self._exec(cmd)
def _exec_batch(self):
# Use x-append-batch to get the JSON from nrfutil itself
json_file = Path(self.hex_).parent / 'generated_nrfutil_batch.json'
json_file.unlink(missing_ok=True)
for op in self._ops:
self._append_batch(op, json_file)
# reset first in case an exception is thrown
self._ops = []
self._op_id = 1
self.logger.debug(f'Executing batch in: {json_file}')
precmd = []
if self.ext_mem_config_file:
# This needs to be prepended, as it's a global option
precmd = ['--x-ext-mem-config-file', self.ext_mem_config_file]
self._exec(precmd + ['x-execute-batch', '--batch-path', f'{json_file}',
'--serial-number', self._format_dev_ids()])
def do_exec_op(self, op, force=False):
self.logger.debug(f'Executing op: {op}')
if force:
if len(self._ops) != 0:
raise RuntimeError(f'Forced exec with {len(self._ops)} ops')
self._insert_op(op)
self._exec_batch()
return True
# Defer by default
return False
def flush_ops(self, force=True):
if not force:
return
while self.ops:
self._insert_op(self.ops.popleft())
self._exec_batch()