scripts: runner: generalize commands to "capabilities"

Some configuration options or device tree nodes affect the way that
runners ought to behave, but there's no good way for them to report
whether they can handle them.

One motivating example is CONFIG_FLASH_LOAD_OFFSET, as influenced by
the zephyr,code-partition chosen node in the DT for architectures
where CONFIG_HAS_FLASH_LOAD_OFFSET=y.

If CONFIG_FLASH_LOAD_OFFSET is nonzero, the 'flash' command ought to
place the kernel at that address offset from the device flash's start
address. Runners don't support this right now, which should be
fixed. However, we don't want to mandate support for this feature,
since not all targets need it.

We need to let runners declare what their capabilities are. Make it so
by adding a RunnerCaps class to the runner core. This currently just
states which commands a runner can handle, but can be generalized to
implement the above use case.

Signed-off-by: Marti Bolivar <marti@opensourcefoundries.com>
diff --git a/scripts/support/runner/bossac.py b/scripts/support/runner/bossac.py
index 36ada99..e22b0dc 100644
--- a/scripts/support/runner/bossac.py
+++ b/scripts/support/runner/bossac.py
@@ -8,7 +8,7 @@
 import os
 import platform
 
-from .core import ZephyrBinaryRunner, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
 
 DEFAULT_BOSSAC_PORT = '/dev/ttyACM0'
 
@@ -28,8 +28,8 @@
         return 'bossac'
 
     @classmethod
-    def handles_command(cls, command):
-        return command == 'flash'
+    def capabilities(cls):
+        return RunnerCaps(commands={'flash'})
 
     def create_from_env(command, debug):
         '''Create flasher from environment.
diff --git a/scripts/support/runner/core.py b/scripts/support/runner/core.py
index ad7113b..fcc0d80 100644
--- a/scripts/support/runner/core.py
+++ b/scripts/support/runner/core.py
@@ -143,6 +143,17 @@
         return {int(b) for b in used_bytes}
 
 
+class RunnerCaps:
+    '''This class represents a runner class's capabilities.
+
+    The most basic capability is the set of supported commands,
+    available in the commands field. This defaults to all three
+    commands.'''
+
+    def __init__(self, commands={'flash', 'debug', 'debugserver'}):
+        self.commands = commands
+
+
 class ZephyrBinaryRunner(abc.ABC):
     '''Abstract superclass for binary runners (flashers, debuggers).
 
@@ -184,7 +195,7 @@
 
     1. Define a ZephyrBinaryRunner subclass, and implement its
        abstract methods. Override any methods you need to, especially
-       handles_command().
+       capabilities().
 
     2. Make sure the Python module defining your runner class is
        imported by this package's __init__.py (otherwise,
@@ -229,7 +240,8 @@
         else:
             raise ValueError('no runner named {} is known'.format(runner_name))
 
-        if not cls.handles_command(command):
+        caps = cls.capabilities()
+        if command not in caps.commands:
             raise ValueError('runner {} does not implement command {}'.format(
                 runner_name, command))
 
@@ -246,13 +258,15 @@
         etc.).'''
 
     @classmethod
-    def handles_command(cls, command):
-        '''Return True iff this class can run the given command.
+    def capabilities(cls):
+        '''Returns a RunnerCaps representing this runner's capabilities.
 
-        The default implementation returns True if the command is
-        valid (i.e. is one of "flash", "debug", and "debugserver").
-        Subclasses should override if they only provide a subset.'''
-        return command in {'flash', 'debug', 'debugserver'}
+        This implementation returns the default capabilities, which
+        includes support for all three commands, but no other special
+        powers.
+
+        Subclasses should override appropriately if needed.'''
+        return RunnerCaps()
 
     @staticmethod
     @abc.abstractmethod
@@ -263,7 +277,8 @@
         '''Runs command ('flash', 'debug', 'debugserver').
 
         This is the main entry point to this runner.'''
-        if not self.handles_command(command):
+        caps = self.capabilities()
+        if command not in caps.commands:
             raise ValueError('runner {} does not implement command {}'.format(
                 self.name(), command))
         self.do_run(command, **kwargs)
diff --git a/scripts/support/runner/dfu.py b/scripts/support/runner/dfu.py
index 36399f9..a0703ce 100644
--- a/scripts/support/runner/dfu.py
+++ b/scripts/support/runner/dfu.py
@@ -8,7 +8,7 @@
 import sys
 import time
 
-from .core import ZephyrBinaryRunner, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
 
 
 class DfuUtilBinaryRunner(ZephyrBinaryRunner):
@@ -30,8 +30,8 @@
         return 'dfu-util'
 
     @classmethod
-    def handles_command(cls, command):
-        return command == 'flash'
+    def capabilities(cls):
+        return RunnerCaps(commands={'flash'})
 
     def create_from_env(command, debug):
         '''Create flasher from environment.
diff --git a/scripts/support/runner/esp32.py b/scripts/support/runner/esp32.py
index e666f9b..a7836b5 100644
--- a/scripts/support/runner/esp32.py
+++ b/scripts/support/runner/esp32.py
@@ -7,7 +7,7 @@
 from os import path
 import os
 
-from .core import ZephyrBinaryRunner, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
 
 
 class Esp32BinaryRunner(ZephyrBinaryRunner):
@@ -30,8 +30,8 @@
         return 'esp32'
 
     @classmethod
-    def handles_command(cls, command):
-        return command == 'flash'
+    def capabilities(cls):
+        return RunnerCaps(commands={'flash'})
 
     def create_from_env(command, debug):
         '''Create flasher from environment.
diff --git a/scripts/support/runner/jlink.py b/scripts/support/runner/jlink.py
index 2ad673e..37a0c44 100644
--- a/scripts/support/runner/jlink.py
+++ b/scripts/support/runner/jlink.py
@@ -7,7 +7,7 @@
 from os import path
 import os
 
-from .core import ZephyrBinaryRunner, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
 
 DEFAULT_JLINK_GDB_PORT = 2331
 
@@ -33,8 +33,8 @@
         return 'jlink'
 
     @classmethod
-    def handles_command(cls, command):
-        return command in {'debug', 'debugserver'}
+    def capabilities(cls):
+        return RunnerCaps(commands={'debug', 'debugserver'})
 
     def create_from_env(command, debug):
         '''Create runner from environment.
diff --git a/scripts/support/runner/nrfjprog.py b/scripts/support/runner/nrfjprog.py
index b52eba8..8b92b33 100644
--- a/scripts/support/runner/nrfjprog.py
+++ b/scripts/support/runner/nrfjprog.py
@@ -7,7 +7,7 @@
 from os import path
 import sys
 
-from .core import ZephyrBinaryRunner, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
 
 
 class NrfJprogBinaryRunner(ZephyrBinaryRunner):
@@ -23,8 +23,8 @@
         return 'nrfjprog'
 
     @classmethod
-    def handles_command(cls, command):
-        return command == 'flash'
+    def capabilities(cls):
+        return RunnerCaps(commands={'flash'})
 
     def create_from_env(command, debug):
         '''Create flasher from environment.
diff --git a/scripts/support/runner/xtensa.py b/scripts/support/runner/xtensa.py
index a73ba36..3098e6b 100644
--- a/scripts/support/runner/xtensa.py
+++ b/scripts/support/runner/xtensa.py
@@ -6,7 +6,7 @@
 
 from os import path
 
-from .core import ZephyrBinaryRunner, get_env_or_bail
+from .core import ZephyrBinaryRunner, RunnerCaps, get_env_or_bail
 
 
 class XtensaBinaryRunner(ZephyrBinaryRunner):
@@ -22,8 +22,8 @@
         return 'xtensa'
 
     @classmethod
-    def handles_command(cls, command):
-        return command == 'debug'
+    def capabilities(cls):
+        return RunnerCaps(commands={'debug'})
 
     def create_from_env(command, debug):
         '''Create runner from environment.