twister: add a --flash-command flag

Currently twister requires west to flash a board, either directly using
west-flash or indirectly through the cmake flash target.

Add an option to specify a custom flash script, this is passed a
build-dir and board-id flags so it can be used to implement custom
flashing scripts in a system that does not use west.

Signed-off-by: Fabio Baltieri <fabiobaltieri@google.com>
This commit is contained in:
Fabio Baltieri
2025-07-31 15:55:22 +01:00
committed by Benjamin Cabé
parent 5857aee4ab
commit a31f784e95
4 changed files with 86 additions and 4 deletions

View File

@@ -1443,6 +1443,32 @@ work. It is equivalent to following west and twister commands.
manually according to above example. This is because the serial port
of the PTY is not fixed and being allocated in the system at runtime.
If west is not available or does not know how to flash your system, a custom
flash command can be specified using the ``flash-command`` flag. The script is
called with a ``--build-dir`` with the path of the current build, as well as a
``--board-id`` flag to identify the specific device when multiple are available
in a hardware map.
.. tabs::
.. group-tab:: Linux
.. code-block:: bash
twister -p npcx9m6f_evb --device-testing --device-serial /dev/ttyACM0
--flash-command './custom_flash_script.py,--flag,"complex,argument"'
.. group-tab:: Windows
.. note::
python .\scripts\twister -p npcx9m6f_evb --device-testing
--device-serial COM1
--flash-command 'custom_flash_script.py,--flag,"complex,argument"'
Would result in calling ``./custom_flash_script.py --flag complex,argument --build-dir
<build directory> --board-id <board identification>``.
Fixtures
+++++++++

View File

@@ -836,6 +836,15 @@ structure in the main Zephyr tree: boards/<vendor>/<board_name>/""")
will translate to "west flash --runner pyocd"
"""
)
parser.add_argument(
"--flash-command",
help="""Instead of 'west flash', uses a custom flash command to flash
when running with --device-testing. Supports comma-separated
argument list, the script is also passed a --build-dir flag with
the build directory as an argument, and a --board-id flag with the
board or probe id if available.
"""
)
parser.add_argument(
"-X", "--fixture", action="append", default=[],

View File

@@ -9,6 +9,7 @@
import argparse
import contextlib
import csv
import logging
import math
import os
@@ -564,7 +565,24 @@ class DeviceHandler(Handler):
proc.communicate()
logger.error(f"{script} timed out")
def _create_flash_command(self, hardware):
flash_command = next(csv.reader([self.options.flash_command]))
command = [flash_command[0]]
command.extend(['--build-dir', self.build_dir])
board_id = hardware.probe_id or hardware.id
if board_id:
command.extend(['--board-id', board_id])
command.extend(flash_command[1:])
return command
def _create_command(self, runner, hardware):
if self.options.flash_command:
return self._create_flash_command(hardware)
command = ["west", "flash", "--skip-rebuild", "-d", self.build_dir]
command_extra_args = []

View File

@@ -1049,18 +1049,21 @@ TESTDATA_13 = [
None,
None,
None,
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir']
),
(
[],
None,
None,
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir']
),
(
'--dummy',
None,
None,
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--', '--dummy']
),
@@ -1068,6 +1071,7 @@ TESTDATA_13 = [
'--dummy1,--dummy2',
None,
None,
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--', '--dummy1', '--dummy2']
),
@@ -1076,6 +1080,7 @@ TESTDATA_13 = [
None,
'runner',
'product',
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--runner', 'runner', 'param1', 'param2']
),
@@ -1084,6 +1089,7 @@ TESTDATA_13 = [
None,
'pyocd',
'product',
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--runner', 'pyocd', 'param1', 'param2', '--', '--dev-id', 12345]
),
@@ -1091,6 +1097,7 @@ TESTDATA_13 = [
None,
'nrfjprog',
'product',
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--runner', 'nrfjprog', 'param1', 'param2', '--', '--dev-id', 12345]
),
@@ -1098,6 +1105,7 @@ TESTDATA_13 = [
None,
'openocd',
'STM32 STLink',
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--runner', 'openocd', 'param1', 'param2',
'--', '--cmd-pre-init', 'hla_serial 12345']
@@ -1106,6 +1114,7 @@ TESTDATA_13 = [
None,
'openocd',
'STLINK-V3',
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--runner', 'openocd', 'param1', 'param2',
'--', '--cmd-pre-init', 'hla_serial 12345']
@@ -1114,6 +1123,7 @@ TESTDATA_13 = [
None,
'openocd',
'EDBG CMSIS-DAP',
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--runner', 'openocd', 'param1', 'param2',
'--', '--cmd-pre-init', 'cmsis_dap_serial 12345']
@@ -1122,6 +1132,7 @@ TESTDATA_13 = [
None,
'jlink',
'product',
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--runner', 'jlink', '--dev-id', 12345,
'param1', 'param2']
@@ -1130,23 +1141,39 @@ TESTDATA_13 = [
None,
'stm32cubeprogrammer',
'product',
None,
['west', 'flash', '--skip-rebuild', '-d', '$build_dir',
'--runner', 'stm32cubeprogrammer', '--tool-opt=sn=12345',
'param1', 'param2']
),
(
None,
None,
None,
'flash_command',
['flash_command', '--build-dir', '$build_dir', '--board-id', 12345]
),
(
None,
None,
None,
'path to/flash_command,with,args,"1,2,3",4',
['path to/flash_command', '--build-dir', '$build_dir', '--board-id',
12345, 'with', 'args', '1,2,3', '4']
),
]
TESTDATA_13_2 = [(True), (False)]
@pytest.mark.parametrize(
'self_west_flash, runner,' \
' hardware_product_name, expected',
' hardware_product_name, self_flash_command, expected',
TESTDATA_13,
ids=['default', '--west-flash', 'one west flash value',
'multiple west flash values', 'generic runner', 'pyocd',
'nrfjprog', 'openocd, STM32 STLink', 'openocd, STLINK-v3',
'openocd, EDBG CMSIS-DAP', 'jlink', 'stm32cubeprogrammer']
'openocd, EDBG CMSIS-DAP', 'jlink', 'stm32cubeprogrammer',
'flash_command', 'flash_command with args']
)
@pytest.mark.parametrize('hardware_probe', TESTDATA_13_2, ids=['probe', 'id'])
def test_devicehandler_create_command(
@@ -1155,10 +1182,12 @@ def test_devicehandler_create_command(
runner,
hardware_probe,
hardware_product_name,
self_flash_command,
expected
):
handler = DeviceHandler(mocked_instance, 'build', mock.Mock())
handler.options = mock.Mock(west_flash=self_west_flash)
handler.options = mock.Mock(west_flash=self_west_flash,
flash_command=self_flash_command)
handler.generator_cmd = 'generator_cmd'
expected = [handler.build_dir if val == '$build_dir' else \