scripts: runner: add infrastructure for DT-based flashing

Add the necessary infrastructure to the runner core to support
computing flash addresses based on the devicetree. Specifically, add:

- a new RunnerCaps capability, flash_addr, which lets runners declare
  when they support flashing to an arbitrary address

- a common --dt-flash option to all runner command line parsers which
  support this capability, which lets users request flash addresses to
  be computed from device tree

- a ZephyrBinaryRunner helper method, get_flash_address(), which is
  the common code needed to compute a flash address from device
  tree (or return a default value if non-DT based flashing is
  requested). This relies on the BuildConfiguration parser introduced
  in an earlier patch.

Subsequent patches will use this functionality in individual runners.

Signed-off-by: Marti Bolivar <marti@opensourcefoundries.com>
diff --git a/scripts/support/runner/core.py b/scripts/support/runner/core.py
index 1aafc23..a38f7b0 100644
--- a/scripts/support/runner/core.py
+++ b/scripts/support/runner/core.py
@@ -12,6 +12,7 @@
 """
 
 import abc
+import argparse
 import os
 import platform
 import shlex
@@ -147,12 +148,36 @@
 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.'''
+    Each capability is represented as an attribute with the same
+    name. Flag attributes are True or False.
 
-    def __init__(self, commands={'flash', 'debug', 'debugserver'}):
+    Available capabilities:
+
+    - commands: set of supported commands; default is {'flash',
+      'debug', 'debugserver'}.
+
+    - flash_addr: whether the runner supports flashing to an
+      arbitrary address. Default is False. If true, the runner
+      must honor the --dt-flash option.
+    '''
+
+    def __init__(self,
+                 commands={'flash', 'debug', 'debugserver'},
+                 flash_addr=False):
         self.commands = commands
+        self.flash_addr = bool(flash_addr)
+
+
+_YN_CHOICES = ['Y', 'y', 'N', 'n', 'yes', 'no', 'YES', 'NO']
+
+
+class _DTFlashAction(argparse.Action):
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        if values.lower().startswith('y'):
+            namespace.dt_flash = True
+        else:
+            namespace.dt_flash = False
 
 
 class ZephyrBinaryRunner(abc.ABC):
@@ -238,9 +263,7 @@
     def capabilities(cls):
         '''Returns a RunnerCaps representing this runner's capabilities.
 
-        This implementation returns the default capabilities, which
-        includes support for all three commands, but no other special
-        powers.
+        This implementation returns the default capabilities.
 
         Subclasses should override appropriately if needed.'''
         return RunnerCaps()
@@ -262,6 +285,7 @@
 
         * --gdb
         * --openocd, --openocd-search
+        * --dt-flash (if the runner capabilities includes flash_addr)
 
         Runner-specific options are added through the do_add_parser()
         hook.
@@ -279,6 +303,12 @@
                             help='path to kernel binary in .bin format')
 
         # Optional options.
+        if cls.capabilities().flash_addr:
+            parser.add_argument('--dt-flash', default='n', choices=_YN_CHOICES,
+                                action=_DTFlashAction,
+                                help='''If 'yes', use configuration
+                                generated by device tree (DT) to compute flash
+                                addresses.''')
         parser.add_argument('--gdb', default=None,
                             help='GDB compatible with the target')
         parser.add_argument('--openocd', default='openocd',
@@ -317,6 +347,27 @@
         These will have been parsed from the command line according to
         the specification defined by add_parser().'''
 
+    @classmethod
+    def get_flash_address(cls, args, build_conf, default=0x0):
+        '''Helper method for extracting a flash address.
+
+        If args.dt_flash is true, get the address from the
+        BoardConfiguration, build_conf. (If
+        CONFIG_HAS_FLASH_LOAD_OFFSET is n in that configuration, it
+        returns CONFIG_FLASH_BASE_ADDRESS. Otherwise, it returns
+        CONFIG_FLASH_BASE_ADDRESS + CONFIG_FLASH_LOAD_OFFSET.)
+
+        Otherwise (when args.dt_flash is False), the default value is
+        returned.'''
+        if args.dt_flash:
+            if build_conf['CONFIG_HAS_FLASH_LOAD_OFFSET']:
+                return (build_conf['CONFIG_FLASH_BASE_ADDRESS'] +
+                        build_conf['CONFIG_FLASH_LOAD_OFFSET'])
+            else:
+                return build_conf['CONFIG_FLASH_BASE_ADDRESS']
+        else:
+            return default
+
     def run(self, command, **kwargs):
         '''Runs command ('flash', 'debug', 'debugserver').