west: runners: add file argument

adds --file argument to west flash/debug as described in #52262.

Signed-off-by: Gerhard Jörges <joerges@metratec.com>
diff --git a/scripts/west_commands/completion/west-completion.bash b/scripts/west_commands/completion/west-completion.bash
index 0a6a3bf..48375cf 100644
--- a/scripts/west_commands/completion/west-completion.bash
+++ b/scripts/west_commands/completion/west-completion.bash
@@ -802,6 +802,8 @@
 	"
 
 	local file_opts="
+		--file -f
+		--file-type -t
 		--elf-file
 		--hex-file
 		--bin-file
diff --git a/scripts/west_commands/completion/west-completion.zsh b/scripts/west_commands/completion/west-completion.zsh
index 7ed9edd..8cca9b5 100644
--- a/scripts/west_commands/completion/west-completion.zsh
+++ b/scripts/west_commands/completion/west-completion.zsh
@@ -261,6 +261,8 @@
 typeset -a -g _west_runner_opts=(
   '(-H --context)'{-H,--context}'[print runner-specific options]'
   '--board-dir[board directory]:board dir:_directories'
+  '(-f --file)'{-f,--file}'[path to binary]:path to binary:_files'
+  '(-t --file-type)'{-t,--file-type}'[type of binary]:type of binary:(hex bin elf)'
   '--elf-file[path to zephyr.elf]:path to zephyr.elf:_files'
   '--hex-file[path to zephyr.hex]:path to zephyr.hex:_files'
   '--bin-file[path to zephyr.bin]:path to zephyr.bin:_files'
diff --git a/scripts/west_commands/run_common.py b/scripts/west_commands/run_common.py
index 06d3c34..ce529ad 100644
--- a/scripts/west_commands/run_common.py
+++ b/scripts/west_commands/run_common.py
@@ -20,6 +20,7 @@
     FIND_BUILD_DIR_DESCRIPTION
 from west.commands import CommandError
 from west.configuration import config
+from runners.core import FileType
 import yaml
 
 from zephyr_ext_common import ZEPHYR_SCRIPTS
@@ -132,11 +133,6 @@
     # Options used to override RunnerConfig values in runners.yaml.
     # TODO: is this actually useful?
     group.add_argument('--board-dir', metavar='DIR', help='board directory')
-    # FIXME: we should just have a single --file argument. The variation
-    # between runners is confusing people.
-    group.add_argument('--elf-file', metavar='FILE', help='path to zephyr.elf')
-    group.add_argument('--hex-file', metavar='FILE', help='path to zephyr.hex')
-    group.add_argument('--bin-file', metavar='FILE', help='path to zephyr.bin')
     # FIXME: these are runner-specific and should be moved to where --context
     # can find them instead.
     group.add_argument('--gdb', help='path to GDB')
@@ -385,11 +381,40 @@
     def config(attr, default=None):
         return getattr(args, attr, None) or yaml_config.get(attr, default)
 
+    def filetype(attr):
+        ftype = str(getattr(args, attr, None)).lower()
+        if ftype == "hex":
+            return FileType.HEX
+        elif ftype == "bin":
+            return FileType.BIN
+        elif ftype == "elf":
+            return FileType.ELF
+        elif getattr(args, attr, None) is not None:
+            err = 'unknown --file-type ({}). Please use hex, bin or elf'
+            raise ValueError(err.format(ftype))
+
+        # file-type not provided, try to get from filename
+        file = getattr(args, "file", None)
+        if file is not None:
+            ext = Path(file).suffix
+            if ext == ".hex":
+                return FileType.HEX
+            if ext == ".bin":
+                return FileType.BIN
+            if ext == ".elf":
+                return FileType.ELF
+
+        # we couldn't get the file-type, set to
+        # OTHER and let the runner deal with it
+        return FileType.OTHER
+
     return RunnerConfig(build_dir,
                         yaml_config['board_dir'],
                         output_file('elf'),
                         output_file('hex'),
                         output_file('bin'),
+                        config('file'),
+                        filetype('file_type'),
                         config('gdb'),
                         config('openocd'),
                         config('openocd_search', []))
diff --git a/scripts/west_commands/runners/core.py b/scripts/west_commands/runners/core.py
index eff3596..7ed95d5 100644
--- a/scripts/west_commands/runners/core.py
+++ b/scripts/west_commands/runners/core.py
@@ -22,6 +22,8 @@
 import signal
 import subprocess
 import re
+from functools import partial
+from enum import Enum
 from typing import Dict, List, NamedTuple, NoReturn, Optional, Set, Type, \
     Union
 
@@ -237,19 +239,22 @@
                  dev_id: bool = False,
                  flash_addr: bool = False,
                  erase: bool = False,
-                 tool_opt: bool = False):
+                 tool_opt: bool = False,
+                 file: bool = False):
         self.commands = commands
         self.dev_id = dev_id
         self.flash_addr = bool(flash_addr)
         self.erase = bool(erase)
         self.tool_opt = bool(tool_opt)
+        self.file = bool(file)
 
     def __str__(self):
         return (f'RunnerCaps(commands={self.commands}, '
                 f'dev_id={self.dev_id}, '
                 f'flash_addr={self.flash_addr}, '
                 f'erase={self.erase}, '
-                f'tool_opt={self.tool_opt}'
+                f'tool_opt={self.tool_opt}, '
+                f'file={self.file}'
                 ')')
 
 
@@ -261,6 +266,13 @@
     raise ValueError(f"{cls.name()} doesn't support {option} option")
 
 
+class FileType(Enum):
+    OTHER = 0
+    HEX = 1
+    BIN = 2
+    ELF = 3
+
+
 class RunnerConfig(NamedTuple):
     '''Runner execution-time configuration.
 
@@ -268,13 +280,15 @@
     can register specific configuration options using their
     do_add_parser() hooks.
     '''
-    build_dir: str              # application build directory
-    board_dir: str              # board definition directory
-    elf_file: Optional[str]     # zephyr.elf path, or None
-    hex_file: Optional[str]     # zephyr.hex path, or None
-    bin_file: Optional[str]     # zephyr.bin path, or None
-    gdb: Optional[str] = None   # path to a usable gdb
-    openocd: Optional[str] = None  # path to a usable openocd
+    build_dir: str                  # application build directory
+    board_dir: str                  # board definition directory
+    elf_file: Optional[str]         # zephyr.elf path, or None
+    hex_file: Optional[str]         # zephyr.hex path, or None
+    bin_file: Optional[str]         # zephyr.bin path, or None
+    file: Optional[str]             # binary file path (provided by the user), or None
+    file_type: Optional[FileType] = FileType.OTHER  # binary file type
+    gdb: Optional[str] = None       # path to a usable gdb
+    openocd: Optional[str] = None   # path to a usable openocd
     openocd_search: List[str] = []  # add these paths to the openocd search path
 
 
@@ -298,12 +312,14 @@
 class DeprecatedAction(argparse.Action):
 
     def __call__(self, parser, namespace, values, option_string=None):
-        _logger.warning(f'Argument {self.option_strings[0]} is deprecated, '
-                        f'use {self._replacement} instead.')
+        _logger.warning(f'Argument {self.option_strings[0]} is deprecated' +
+                        (f' for your runner {self._cls.name()}'  if self._cls is not None else '') +
+                        f', use {self._replacement} instead.')
         setattr(namespace, self.dest, values)
 
-def depr_action(*args, replacement=None, **kwargs):
+def depr_action(*args, cls=None, replacement=None, **kwargs):
     action = DeprecatedAction(*args, **kwargs)
+    setattr(action, '_cls', cls)
     setattr(action, '_replacement', replacement)
     return action
 
@@ -465,6 +481,30 @@
         else:
             parser.add_argument('--dt-flash', help=argparse.SUPPRESS)
 
+        if caps.file:
+            parser.add_argument('-f', '--file',
+                                dest='file',
+                                help="path to binary file")
+            parser.add_argument('-t', '--file-type',
+                                dest='file_type',
+                                help="type of binary file")
+        else:
+            parser.add_argument('-f', '--file', help=argparse.SUPPRESS)
+            parser.add_argument('-t', '--file-type', help=argparse.SUPPRESS)
+
+        parser.add_argument('--elf-file',
+                        metavar='FILE',
+                        action=(partial(depr_action, cls=cls, replacement='-f/--file') if caps.file else None),
+                        help='path to zephyr.elf' if not caps.file else 'Deprecated, use -f/--file instead.')
+        parser.add_argument('--hex-file',
+                        metavar='FILE',
+                        action=(partial(depr_action, cls=cls, replacement='-f/--file') if caps.file else None),
+                        help='path to zephyr.hex' if not caps.file else 'Deprecated, use -f/--file instead.')
+        parser.add_argument('--bin-file',
+                        metavar='FILE',
+                        action=(partial(depr_action, cls=cls, replacement='-f/--file') if caps.file else None),
+                        help='path to zephyr.bin' if not caps.file else 'Deprecated, use -f/--file instead.')
+
         parser.add_argument('--erase', '--no-erase', nargs=0,
                             action=_ToggleAction,
                             help=("mass erase flash before loading, or don't"
@@ -500,6 +540,12 @@
             _missing_cap(cls, '--erase')
         if args.tool_opt and not caps.tool_opt:
             _missing_cap(cls, '--tool-opt')
+        if args.file and not caps.file:
+            _missing_cap(cls, '--file')
+        if args.file_type and not args.file:
+            raise ValueError("--file-type requires --file")
+        if args.file_type and not caps.file:
+            _missing_cap(cls, '--file-type')
 
         ret = cls.do_create(cfg, args)
         if args.erase:
diff --git a/scripts/west_commands/runners/jlink.py b/scripts/west_commands/runners/jlink.py
index 63fb2f7..8d595f2 100644
--- a/scripts/west_commands/runners/jlink.py
+++ b/scripts/west_commands/runners/jlink.py
@@ -13,7 +13,7 @@
 import sys
 import tempfile
 
-from runners.core import ZephyrBinaryRunner, RunnerCaps
+from runners.core import ZephyrBinaryRunner, RunnerCaps, FileType
 
 try:
     import pylink
@@ -43,6 +43,8 @@
                  gdb_port=DEFAULT_JLINK_GDB_PORT,
                  tui=False, tool_opt=[]):
         super().__init__(cfg)
+        self.file = cfg.file
+        self.file_type = cfg.file_type
         self.hex_name = cfg.hex_file
         self.bin_name = cfg.bin_file
         self.elf_name = cfg.elf_file
@@ -73,7 +75,7 @@
     def capabilities(cls):
         return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach'},
                           dev_id=True, flash_addr=True, erase=True,
-                          tool_opt=True)
+                          tool_opt=True, file=True)
 
     @classmethod
     def dev_id_help(cls) -> str:
@@ -246,11 +248,17 @@
         else:
             if self.gdb_cmd is None:
                 raise ValueError('Cannot debug; gdb is missing')
-            if self.elf_name is None:
+            if self.file is not None:
+                if self.file_type != FileType.ELF:
+                    raise ValueError('Cannot debug; elf file required')
+                elf_name = self.file
+            elif self.elf_name is None:
                 raise ValueError('Cannot debug; elf is missing')
+            else:
+                elf_name = self.elf_name
             client_cmd = (self.gdb_cmd +
                           self.tui_arg +
-                          [self.elf_name] +
+                          [elf_name] +
                           ['-ex', 'target remote {}:{}'.format(self.gdb_host, self.gdb_port)])
             if command == 'debug':
                 client_cmd += ['-ex', 'monitor halt',
@@ -276,20 +284,42 @@
         if self.erase:
             lines.append('erase') # Erase all flash sectors
 
-        # Get the build artifact to flash, preferring .hex over .bin
-        if self.hex_name is not None and os.path.isfile(self.hex_name):
-            flash_file = self.hex_name
-            flash_cmd = f'loadfile "{self.hex_name}"'
-        elif self.bin_name is not None and os.path.isfile(self.bin_name):
-            if self.dt_flash:
-                flash_addr = self.flash_address_from_build_conf(self.build_conf)
+        # Get the build artifact to flash
+        if self.file is not None:
+            # use file provided by the user
+            if not os.path.isfile(self.file):
+                err = 'Cannot flash; file ({}) not found'
+                raise ValueError(err.format(self.file))
+
+            flash_file = self.file
+
+            if self.file_type == FileType.HEX:
+                flash_cmd = f'loadfile "{self.file}"'
+            elif self.file_type == FileType.BIN:
+                if self.dt_flash:
+                    flash_addr = self.flash_address_from_build_conf(self.build_conf)
+                else:
+                    flash_addr = 0
+                flash_cmd = f'loadfile "{self.file}" 0x{flash_addr:x}'
             else:
-                flash_addr = 0
-            flash_file = self.bin_name
-            flash_cmd = f'loadfile "{self.bin_name}" 0x{flash_addr:x}'
+                err = 'Cannot flash; jlink runner only supports hex and bin files'
+                raise ValueError(err)
+
         else:
-            err = 'Cannot flash; no hex ({}) or bin ({}) files found.'
-            raise ValueError(err.format(self.hex_name, self.bin_name))
+            # use hex or bin file provided by the buildsystem, preferring .hex over .bin
+            if self.hex_name is not None and os.path.isfile(self.hex_name):
+                flash_file = self.hex_name
+                flash_cmd = f'loadfile "{self.hex_name}"'
+            elif self.bin_name is not None and os.path.isfile(self.bin_name):
+                if self.dt_flash:
+                    flash_addr = self.flash_address_from_build_conf(self.build_conf)
+                else:
+                    flash_addr = 0
+                flash_file = self.bin_name
+                flash_cmd = f'loadfile "{self.bin_name}" 0x{flash_addr:x}'
+            else:
+                err = 'Cannot flash; no hex ({}) or bin ({}) files found.'
+                raise ValueError(err.format(self.hex_name, self.bin_name))
 
         # Flash the selected build artifact
         lines.append(flash_cmd)
diff --git a/scripts/west_commands/tests/conftest.py b/scripts/west_commands/tests/conftest.py
index a566e76..da89a2d 100644
--- a/scripts/west_commands/tests/conftest.py
+++ b/scripts/west_commands/tests/conftest.py
@@ -6,7 +6,7 @@
 
 import pytest
 
-from runners.core import RunnerConfig
+from runners.core import RunnerConfig, FileType
 
 RC_BUILD_DIR = '/test/build-dir'
 RC_BOARD_DIR = '/test/zephyr/boards/test-arch/test-board'
@@ -22,5 +22,6 @@
 def runner_config():
     '''Fixture which provides a runners.core.RunnerConfig.'''
     return RunnerConfig(RC_BUILD_DIR, RC_BOARD_DIR, RC_KERNEL_ELF,
-                        RC_KERNEL_HEX, RC_KERNEL_BIN, gdb=RC_GDB,
-                        openocd=RC_OPENOCD, openocd_search=RC_OPENOCD_SEARCH)
+                        RC_KERNEL_HEX, RC_KERNEL_BIN, None, FileType.OTHER,
+                        gdb=RC_GDB, openocd=RC_OPENOCD,
+                        openocd_search=RC_OPENOCD_SEARCH)