scripts: zephyr_flash_debug: refactor into new "runner" package

Factor the classes which do the work into a new "runner" package. This
package has a core module where ZephyrBinaryRunner and common helpers
will live, and one file per subclass / runner front-end.

The top-level script, zephyr_flash_debug.py, still exists, but just
delegates its work to the core.

Signed-off-by: Marti Bolivar <marti.bolivar@linaro.org>
diff --git a/scripts/support/runner/__init__.py b/scripts/support/runner/__init__.py
new file mode 100644
index 0000000..d14f6a4
--- /dev/null
+++ b/scripts/support/runner/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from .core import ZephyrBinaryRunner
+
+# We import these here to ensure the ZephyrBinaryRunner subclasses are
+# defined; otherwise, ZephyrBinaryRunner.create_for_shell_script()
+# won't work.
+
+# Explicitly silence the unused import warning.
+# flake8: noqa: F401
+from . import arc
+from . import bossac
+from . import dfu
+from . import esp32
+from . import nios2
+from . import nrfjprog
+from . import openocd
+from . import pyocd
+
+__all__ = ['ZephyrBinaryRunner']
diff --git a/scripts/support/runner/arc.py b/scripts/support/runner/arc.py
new file mode 100644
index 0000000..670f534
--- /dev/null
+++ b/scripts/support/runner/arc.py
@@ -0,0 +1,155 @@
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''ARC architecture-specific runner.'''
+
+from os import path
+import os
+import shlex
+
+from .core import ZephyrBinaryRunner, get_env_or_bail
+
+DEFAULT_ARC_TCL_PORT = 6333
+DEFAULT_ARC_TELNET_PORT = 4444
+DEFAULT_ARC_GDB_PORT = 3333
+
+
+class ArcBinaryRunner(ZephyrBinaryRunner):
+    '''Runner front-end for the ARC architecture, using openocd.'''
+
+    # This unusual 'flash' implementation matches the original shell script.
+    #
+    # It works by starting a GDB server in a separate session, connecting a
+    # client to it to load the program, and running 'continue' within the
+    # client to execute the application.
+    #
+    # TODO: exit immediately when flashing is done, leaving Zephyr running.
+
+    def __init__(self, elf, zephyr_base, arch, board_name, python,
+                 gdb, openocd='openocd', extra_init=None, default_path=None,
+                 tui=None, tcl_port=DEFAULT_ARC_TCL_PORT,
+                 telnet_port=DEFAULT_ARC_TELNET_PORT,
+                 gdb_port=DEFAULT_ARC_GDB_PORT, debug=False):
+        super(ArcBinaryRunner, self).__init__(debug=debug)
+        self.elf = elf
+        self.zephyr_base = zephyr_base
+        self.arch = arch
+        self.board_name = board_name
+        self.python = python
+        self.gdb = gdb
+        search_args = []
+        if default_path is not None:
+            search_args = ['-s', default_path]
+        self.openocd_cmd = [openocd] + search_args
+        self.extra_init = extra_init if extra_init is not None else []
+        self.tui = tui
+        self.tcl_port = tcl_port
+        self.telnet_port = telnet_port
+        self.gdb_port = gdb_port
+
+    def replaces_shell_script(shell_script, command):
+        return (command in {'flash', 'debug', 'debugserver'} and
+                shell_script == 'arc_debugger.sh')
+
+    def create_from_env(command, debug):
+        '''Create runner from environment.
+
+        Required:
+
+        - O: build output directory
+        - KERNEL_ELF_NAME: zephyr kernel binary in ELF format
+        - ZEPHYR_BASE: zephyr Git repository base directory
+        - ARCH: board architecture
+        - BOARD_NAME: zephyr name of board
+        - PYTHON: python executable
+        - GDB: gdb executable
+
+        Optional:
+
+        - OPENOCD: path to openocd, defaults to openocd
+        - OPENOCD_EXTRA_INIT: initialization command for GDB server
+        - OPENOCD_DEFAULT_PATH: openocd search path to use
+        - TUI: if present, passed to gdb server used to flash
+        - TCL_PORT: openocd TCL port, defaults to 6333
+        - TELNET_PORT: openocd telnet port, defaults to 4444
+        - GDB_PORT: openocd gdb port, defaults to 3333
+        '''
+        elf = path.join(get_env_or_bail('O'),
+                        get_env_or_bail('KERNEL_ELF_NAME'))
+        zephyr_base = get_env_or_bail('ZEPHYR_BASE')
+        arch = get_env_or_bail('ARCH')
+        board_name = get_env_or_bail('BOARD_NAME')
+        python = get_env_or_bail('PYTHON')
+        gdb = get_env_or_bail('GDB')
+
+        openocd = os.environ.get('OPENOCD', 'openocd')
+        extra_init = os.environ.get('OPENOCD_EXTRA_INIT', None)
+        if extra_init is not None:
+            extra_init = shlex.split(extra_init)
+        default_path = os.environ.get('OPENOCD_DEFAULT_PATH', None)
+        tui = os.environ.get('TUI', None)
+        tcl_port = int(os.environ.get('TCL_PORT',
+                                      str(DEFAULT_ARC_TCL_PORT)))
+        telnet_port = int(os.environ.get('TELNET_PORT',
+                                         str(DEFAULT_ARC_TELNET_PORT)))
+        gdb_port = int(os.environ.get('GDB_PORT',
+                                      str(DEFAULT_ARC_GDB_PORT)))
+
+        return ArcBinaryRunner(elf, zephyr_base, arch, board_name, python,
+                               gdb, openocd=openocd, extra_init=extra_init,
+                               default_path=default_path, tui=tui,
+                               tcl_port=tcl_port, telnet_port=telnet_port,
+                               gdb_port=gdb_port, debug=debug)
+
+    def run(self, command, **kwargs):
+        if command not in {'flash', 'debug', 'debugserver'}:
+            raise ValueError('{} is not supported'.format(command))
+
+        kwargs['openocd-cfg'] = path.join(self.zephyr_base, 'boards',
+                                          self.arch, self.board_name,
+                                          'support', 'openocd.cfg')
+
+        if command in {'flash', 'debug'}:
+            self.flash_debug(command, **kwargs)
+        else:
+            self.debugserver(**kwargs)
+
+    def flash_debug(self, command, **kwargs):
+        config = kwargs['openocd-cfg']
+
+        server_cmd = (self.openocd_cmd +
+                      ['-f', config] +
+                      self.extra_init +
+                      ['-c', 'tcl_port {}'.format(self.tcl_port),
+                       '-c', 'telnet_port {}'.format(self.telnet_port),
+                       '-c', 'gdb_port {}'.format(self.gdb_port),
+                       '-c', 'init',
+                       '-c', 'targets',
+                       '-c', 'halt'])
+
+        tui_arg = []
+        if self.tui is not None:
+            tui_arg = [self.tui]
+
+        continue_arg = []
+        if command == 'flash':
+            continue_arg = ['-ex', 'c']
+
+        gdb_cmd = ([self.gdb] +
+                   tui_arg +
+                   ['-ex', 'target remote :{}'.format(self.gdb_port),
+                    '-ex', 'load'] +
+                   continue_arg +
+                   [self.elf])
+
+        self.run_server_and_client(server_cmd, gdb_cmd)
+
+    def debugserver(self, **kwargs):
+        config = kwargs['openocd-cfg']
+        cmd = (self.openocd_cmd +
+               ['-f', config,
+                '-c', 'init',
+                '-c', 'targets',
+                '-c', 'reset halt'])
+        self.check_call(cmd)
diff --git a/scripts/support/runner/bossac.py b/scripts/support/runner/bossac.py
new file mode 100644
index 0000000..394a9c38
--- /dev/null
+++ b/scripts/support/runner/bossac.py
@@ -0,0 +1,64 @@
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''bossac-specific runner (flash only) for Atmel SAM microcontrollers.'''
+
+from os import path
+import os
+import platform
+
+from .core import ZephyrBinaryRunner, get_env_or_bail
+
+DEFAULT_BOSSAC_PORT = '/dev/ttyACM0'
+
+
+class BossacBinaryRunner(ZephyrBinaryRunner):
+    '''Runner front-end for bossac.'''
+
+    def __init__(self, bin_name, bossac='bossac',
+                 port=DEFAULT_BOSSAC_PORT, debug=False):
+        super(BossacBinaryRunner, self).__init__(debug=debug)
+        self.bin_name = bin_name
+        self.bossac = bossac
+        self.port = port
+
+    def replaces_shell_script(shell_script, command):
+        return command == 'flash' and shell_script == 'bossa-flash.sh'
+
+    def create_from_env(command, debug):
+        '''Create flasher from environment.
+
+        Required:
+
+        - O: build output directory
+        - KERNEL_BIN_NAME: name of kernel binary
+
+        Optional:
+
+        - BOSSAC: path to bossac, default is bossac
+        - BOSSAC_PORT: serial port to use, default is /dev/ttyACM0
+        '''
+        bin_name = path.join(get_env_or_bail('O'),
+                             get_env_or_bail('KERNEL_BIN_NAME'))
+        bossac = os.environ.get('BOSSAC', 'bossac')
+        port = os.environ.get('BOSSAC_PORT', DEFAULT_BOSSAC_PORT)
+        return BossacBinaryRunner(bin_name, bossac=bossac, port=port,
+                                  debug=debug)
+
+    def run(self, command, **kwargs):
+        if command != 'flash':
+            raise ValueError('only flash is supported')
+
+        if platform.system() != 'Linux':
+            msg = 'CAUTION: No flash tool for your host system found!'
+            raise NotImplementedError(msg)
+
+        cmd_stty = ['stty', '-F', self.port, 'raw', 'ispeed', '1200',
+                    'ospeed', '1200', 'cs8', '-cstopb', 'ignpar', 'eol', '255',
+                    'eof', '255']
+        cmd_flash = [self.bossac, '-p', self.port, '-R', '-e', '-w', '-v',
+                     '-b', self.bin_name]
+
+        self.check_call(cmd_stty)
+        self.check_call(cmd_flash)
diff --git a/scripts/support/runner/core.py b/scripts/support/runner/core.py
new file mode 100644
index 0000000..e451d76
--- /dev/null
+++ b/scripts/support/runner/core.py
@@ -0,0 +1,219 @@
+#! /usr/bin/env python3
+
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Zephyr binary runner core interfaces
+
+This provides the core ZephyrBinaryRunner class meant for public use,
+as well as some other helpers for concrete runner classes.
+"""
+
+import abc
+import os
+import platform
+import pprint
+import shlex
+import signal
+import subprocess
+import sys
+
+
+def get_env_or_bail(env_var):
+    '''Get an environment variable, or raise an error.
+
+    In case of KeyError, an error message is printed, along with the
+    environment, and the exception is re-raised.
+    '''
+    try:
+        return os.environ[env_var]
+    except KeyError:
+        print('Variable {} not in environment:'.format(
+                  env_var), file=sys.stderr)
+        pprint.pprint(dict(os.environ), stream=sys.stderr)
+        raise
+
+
+def get_env_bool_or(env_var, default_value):
+    '''Get an environment variable as a boolean, or return a default value.
+
+    Get an environment variable, interpret it as a base ten
+    integer, and convert that to a boolean.
+
+    In case the environment variable is not defined, return default_value.
+    '''
+    try:
+        return bool(int(os.environ[env_var]))
+    except KeyError:
+        return default_value
+
+
+def get_env_strip_or(env_var, to_strip, default_value):
+    '''Get and clean up an environment variable, or return a default value.
+
+    Get the value of env_var from the environment. If it is
+    defined, return that value with to_strip stripped off. If it
+    is undefined, return default_value (without any stripping).
+    '''
+    value = os.environ.get(env_var, None)
+    if value is not None:
+        return value.strip(to_strip)
+    else:
+        return default_value
+
+
+def quote_sh_list(cmd):
+    '''Transform a command from list into shell string form.'''
+    fmt = ' '.join('{}' for _ in cmd)
+    args = [shlex.quote(s) for s in cmd]
+    return fmt.format(*args)
+
+
+class ZephyrBinaryRunner(abc.ABC):
+    '''Abstract superclass for binary runners (flashers, debuggers).
+
+    With some exceptions, boards supported by Zephyr must provide
+    generic means to be flashed (have a Zephyr firmware binary
+    permanently installed on the device for running) and debugged
+    (have a breakpoint debugger and program loader on a host
+    workstation attached to a running target). This is supported by
+    three top-level commands managed by the Zephyr build system:
+
+    - 'flash': flash a previously configured binary to the board,
+      start execution on the target, then return.
+
+    - 'debug': connect to the board via a debugging protocol, then
+      drop the user into a debugger interface with symbol tables
+      loaded from the current binary, and block until it exits.
+
+    - 'debugserver': connect via a board-specific debugging protocol,
+      then reset and halt the target. Ensure the user is now able to
+      connect to a debug server with symbol tables loaded from the
+      binary.
+
+    Runner functionality relies on a variety of target-specific tools
+    and configuration values, the user interface to which is
+    abstracted by this class. Each runner subclass should take any
+    values it needs to execute one of these commands in its
+    constructor.  The actual command execution is handled in the run()
+    method.
+
+    This functionality is also replacing the legacy Zephyr runners,
+    which are shell scripts.
+
+    At present, the Zephyr build system uses a variety of
+    tool-specific environment variables to control runner behavior.
+    To support a transition to ZephyrBinaryRunner and subclasses, this
+    class provides a create_for_shell_script() static factory method.
+    This method iterates over ZephyrBinaryRUnner subclasses,
+    determines which (if any) can provide equivalent functionality to
+    the shell-based runner, and returns a subclass instance with its
+    configuration determined from the environment.
+
+    To support this, subclasess currently must provide a pair of
+    static methods, replaces_shell_script() and create_from_env(). The
+    first allows the runner subclass to declare which commands and
+    scripts it can replace. The second is called by
+    create_for_shell_script() to create a concrete runner instance.
+
+    The environment-based factories are for legacy use *only*; the
+    user must be able to construct and use a runner using only the
+    constructor and run() method.'''
+
+    def __init__(self, debug=False):
+        self.debug = debug
+
+    @staticmethod
+    def create_for_shell_script(shell_script, command, debug):
+        '''Factory for using as a drop-in replacement to a shell script.
+
+        Command is one of 'flash', 'debug', 'debugserver'.
+
+        Get runner instance to use in place of shell_script, deriving
+        configuration from the environment.'''
+        for sub_cls in ZephyrBinaryRunner.__subclasses__():
+            if sub_cls.replaces_shell_script(shell_script, command):
+                return sub_cls.create_from_env(command, debug)
+        raise ValueError('cannot implement script {} command {}'.format(
+                             shell_script, command))
+
+    @staticmethod
+    @abc.abstractmethod
+    def replaces_shell_script(shell_script, command):
+        '''Check if this class replaces shell_script for the given command.'''
+
+    @staticmethod
+    @abc.abstractmethod
+    def create_from_env(command, debug):
+        '''Create new flasher instance from environment variables.
+
+        This class must be able to replace the current shell script
+        (FLASH_SCRIPT or DEBUG_SCRIPT, depending on command). The
+        environment variables expected by that script are used to build
+        the flasher in a backwards-compatible manner.'''
+
+    @abc.abstractmethod
+    def run(self, command, **kwargs):
+        '''Run a command ('flash', 'debug', 'debugserver').
+
+        In case of an unsupported command, raise a ValueError.'''
+
+    def run_server_and_client(self, server, client):
+        '''Run a server that ignores SIGINT, and a client that handles it.
+
+        This routine portably:
+
+        - creates a Popen object for the `server' command which ignores SIGINT
+        - runs `client' in a subprocess while temporarily ignoring SIGINT
+        - cleans up the server after the client exits.
+
+        It's useful to e.g. open a GDB server and client.'''
+        server_proc = self.popen_ignore_int(server)
+        previous = signal.signal(signal.SIGINT, signal.SIG_IGN)
+        try:
+            self.check_call(client)
+        finally:
+            signal.signal(signal.SIGINT, previous)
+            server_proc.terminate()
+            server_proc.wait()
+
+    def check_call(self, cmd):
+        '''Subclass subprocess.check_call() wrapper.
+
+        Subclasses should use this command to run command in a
+        subprocess and check that it executed correctly, rather than
+        using subprocess directly, to keep accurate debug logs.
+        '''
+        if self.debug:
+            print(quote_sh_list(cmd))
+        subprocess.check_call(cmd)
+
+    def check_output(self, cmd):
+        '''Subclass subprocess.check_output() wrapper.
+
+        Subclasses should use this command to run command in a
+        subprocess and check that it executed correctly, rather than
+        using subprocess directly, to keep accurate debug logs.
+        '''
+        if self.debug:
+            print(quote_sh_list(cmd))
+        return subprocess.check_output(cmd)
+
+    def popen_ignore_int(self, cmd):
+        '''Spawn a child command, ensuring it ignores SIGINT.
+
+        The returned subprocess.Popen object must be manually terminated.'''
+        cflags = 0
+        preexec = None
+        system = platform.system()
+
+        if system == 'Windows':
+            cflags |= subprocess.CREATE_NEW_PROCESS_GROUP
+        elif system in {'Linux', 'Darwin'}:
+            preexec = os.setsid
+
+        if self.debug:
+            print(quote_sh_list(cmd))
+
+        return subprocess.Popen(cmd, creationflags=cflags, preexec_fn=preexec)
diff --git a/scripts/support/runner/dfu.py b/scripts/support/runner/dfu.py
new file mode 100644
index 0000000..5dda9d8
--- /dev/null
+++ b/scripts/support/runner/dfu.py
@@ -0,0 +1,78 @@
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''Runner for flashing with dfu-util.'''
+
+import os
+import sys
+import time
+
+from .core import ZephyrBinaryRunner, get_env_or_bail
+
+
+class DfuUtilBinaryRunner(ZephyrBinaryRunner):
+    '''Runner front-end for dfu-util.'''
+
+    def __init__(self, pid, alt, img, dfuse=None, exe='dfu-util', debug=False):
+        super(DfuUtilBinaryRunner, self).__init__(debug=debug)
+        self.alt = alt
+        self.img = img
+        self.dfuse = dfuse
+        self.cmd = [exe, '-d,{}'.format(pid)]
+        try:
+            self.list_pattern = ', alt={},'.format(int(self.alt))
+        except ValueError:
+            self.list_pattern = ', name="{}",'.format(self.alt)
+
+    def replaces_shell_script(shell_script, command):
+        return command == 'flash' and shell_script == 'dfuutil.sh'
+
+    def create_from_env(command, debug):
+        '''Create flasher from environment.
+
+        Required:
+
+        - DFUUTIL_PID: USB VID:PID of the board
+        - DFUUTIL_ALT: interface alternate setting number or name
+        - DFUUTIL_IMG: binary to flash
+
+        Optional:
+
+        - DFUUTIL_DFUSE_ADDR: target address if the board is a
+          DfuSe device. Ignored if not present.
+        - DFUUTIL: dfu-util executable, defaults to dfu-util.
+        '''
+        pid = get_env_or_bail('DFUUTIL_PID')
+        alt = get_env_or_bail('DFUUTIL_ALT')
+        img = get_env_or_bail('DFUUTIL_IMG')
+        dfuse = os.environ.get('DFUUTIL_DFUSE_ADDR', None)
+        exe = os.environ.get('DFUUTIL', 'dfu-util')
+
+        return DfuUtilBinaryRunner(pid, alt, img, dfuse=dfuse, exe=exe,
+                                   debug=debug)
+
+    def find_device(self):
+        cmd = list(self.cmd) + ['-l']
+        output = self.check_output(cmd)
+        output = output.decode(sys.getdefaultencoding())
+        return self.list_pattern in output
+
+    def run(self, command, **kwargs):
+        if command != 'flash':
+            raise ValueError('only flash is supported')
+
+        reset = 0
+        if not self.find_device():
+            reset = 1
+            print('Please reset your board to switch to DFU mode...')
+            while not self.find_device():
+                time.sleep(0.1)
+
+        cmd = list(self.cmd)
+        if self.dfuse is not None:
+            cmd.extend(['-s', '{}:leave'.format(self.dfuse)])
+        cmd.extend(['-a', self.alt, '-D', self.img])
+        self.check_call(cmd)
+        if reset:
+            print('Now reset your board again to switch back to runtime mode.')
diff --git a/scripts/support/runner/esp32.py b/scripts/support/runner/esp32.py
new file mode 100644
index 0000000..0dcacae
--- /dev/null
+++ b/scripts/support/runner/esp32.py
@@ -0,0 +1,88 @@
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''Runner for flashing ESP32 devices with esptool/espidf.'''
+
+from os import path
+import os
+
+from .core import ZephyrBinaryRunner, get_env_or_bail
+
+
+class Esp32BinaryRunner(ZephyrBinaryRunner):
+    '''Runner front-end for espidf.'''
+
+    def __init__(self, elf, device, baud=921600, flash_size='detect',
+                 flash_freq='40m', flash_mode='dio', espidf='espidf',
+                 debug=False):
+        super(Esp32BinaryRunner, self).__init__(debug=debug)
+        self.elf = elf
+        self.device = device
+        self.baud = baud
+        self.flash_size = flash_size
+        self.flash_freq = flash_freq
+        self.flash_mode = flash_mode
+        self.espidf = espidf
+
+    def replaces_shell_script(shell_script, command):
+        return command == 'flash' and shell_script == 'esp32.sh'
+
+    def create_from_env(command, debug):
+        '''Create flasher from environment.
+
+        Required:
+
+        - O: build output directory
+        - KERNEL_ELF_NAME: name of kernel binary in ELF format
+
+        Optional:
+
+        - ESP_DEVICE: serial port to flash, default /dev/ttyUSB0
+        - ESP_BAUD_RATE: serial baud rate, default 921600
+        - ESP_FLASH_SIZE: flash size, default 'detect'
+        - ESP_FLASH_FREQ: flash frequency, default '40m'
+        - ESP_FLASH_MODE: flash mode, default 'dio'
+        - ESP_TOOL: complete path to espidf, or set to 'espidf' to look for it
+          in $ESP_IDF_PATH/components/esptool_py/esptool/esptool.py
+        '''
+        elf = path.join(get_env_or_bail('O'),
+                        get_env_or_bail('KERNEL_ELF_NAME'))
+
+        # TODO add sane device defaults on other platforms than Linux.
+        device = os.environ.get('ESP_DEVICE', '/dev/ttyUSB0')
+        baud = os.environ.get('ESP_BAUD_RATE', '921600')
+        flash_size = os.environ.get('ESP_FLASH_SIZE', 'detect')
+        flash_freq = os.environ.get('ESP_FLASH_FREQ', '40m')
+        flash_mode = os.environ.get('ESP_FLASH_MODE', 'dio')
+        espidf = os.environ.get('ESP_TOOL', 'espidf')
+
+        if espidf == 'espidf':
+            idf_path = get_env_or_bail('ESP_IDF_PATH')
+            espidf = path.join(idf_path, 'components', 'esptool_py', 'esptool',
+                               'esptool.py')
+
+        return Esp32BinaryRunner(elf, device, baud=baud,
+                                 flash_size=flash_size, flash_freq=flash_freq,
+                                 flash_mode=flash_mode, espidf=espidf,
+                                 debug=debug)
+
+    def run(self, command, **kwargs):
+        if command != 'flash':
+            raise ValueError('only flash is supported')
+
+        bin_name = path.splitext(self.elf)[0] + path.extsep + 'bin'
+        cmd_convert = [self.espidf, '--chip', 'esp32', 'elf2image', self.elf]
+        cmd_flash = [self.espidf, '--chip', 'esp32', '--port', self.device,
+                     '--baud', self.baud, '--before', 'default_reset',
+                     '--after', 'hard_reset', 'write_flash', '-u',
+                     '--flash_mode', self.flash_mode,
+                     '--flash_freq', self.flash_freq,
+                     '--flash_size', self.flash_size,
+                     '0x1000', bin_name]
+
+        print("Converting ELF to BIN")
+        self.check_call(cmd_convert)
+
+        print("Flashing ESP32 on {} ({}bps)".format(self.device, self.baud))
+        self.check_call(cmd_flash)
diff --git a/scripts/support/runner/nios2.py b/scripts/support/runner/nios2.py
new file mode 100644
index 0000000..b18145a
--- /dev/null
+++ b/scripts/support/runner/nios2.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''Runner for NIOS II.'''
+
+from os import path
+
+from .core import ZephyrBinaryRunner, get_env_or_bail
+
+
+class Nios2BinaryRunner(ZephyrBinaryRunner):
+    '''Runner front-end for NIOS II.'''
+
+    # From the original shell script:
+    #
+    #     "XXX [flash] only support[s] cases where the .elf is sent
+    #      over the JTAG and the CPU directly boots from __start. CONFIG_XIP
+    #      and CONFIG_INCLUDE_RESET_VECTOR must be disabled."
+
+    def __init__(self, hex_, cpu_sof, zephyr_base, debug=False):
+        super(Nios2BinaryRunner, self).__init__(debug=debug)
+        self.hex_ = hex_
+        self.cpu_sof = cpu_sof
+        self.zephyr_base = zephyr_base
+
+    def replaces_shell_script(shell_script, command):
+        return command == 'flash' and shell_script == 'nios2.sh'
+
+    def create_from_env(command, debug):
+        '''Create runner from environment.
+
+        Required:
+
+        - O: build output directory
+        - KERNEL_HEX_NAME: name of kernel binary in HEX format
+        - NIOS2_CPU_SOF: location of the CPU .sof data
+        - ZEPHYR_BASE: zephyr Git repository base directory
+        '''
+        hex_ = path.join(get_env_or_bail('O'),
+                         get_env_or_bail('KERNEL_HEX_NAME'))
+        cpu_sof = get_env_or_bail('NIOS2_CPU_SOF')
+        zephyr_base = get_env_or_bail('ZEPHYR_BASE')
+
+        return Nios2BinaryRunner(hex_, cpu_sof, zephyr_base, debug=debug)
+
+    def run(self, command, **kwargs):
+        if command not in {'flash', 'debug', 'debugserver'}:
+            raise ValueError('{} is not supported'.format(command))
+
+        if command == 'flash':
+            self.flash(**kwargs)
+        else:
+            self.debug_debugserver(command, **kwargs)
+
+    def flash(self, **kwargs):
+        cmd = [path.join(self.zephyr_base, 'scripts', 'support',
+                         'quartus-flash.py'),
+               '--sof', self.cpu_sof,
+               '--kernel', self.hex_]
+
+        self.check_call(cmd)
+
+    def debug_debugserver(command, **kwargs):
+        raise NotImplementedError()
diff --git a/scripts/support/runner/nrfjprog.py b/scripts/support/runner/nrfjprog.py
new file mode 100644
index 0000000..d326a73
--- /dev/null
+++ b/scripts/support/runner/nrfjprog.py
@@ -0,0 +1,96 @@
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''Runner for flashing with nrfjprog.'''
+
+from os import path
+import sys
+
+from .core import ZephyrBinaryRunner, get_env_or_bail
+
+
+class NrfJprogBinaryRunner(ZephyrBinaryRunner):
+    '''Runner front-end for nrfjprog.'''
+
+    def __init__(self, hex_, family, board, debug=False):
+        super(NrfJprogBinaryRunner, self).__init__(debug=debug)
+        self.hex_ = hex_
+        self.family = family
+        self.board = board
+
+    def replaces_shell_script(shell_script, command):
+        return command == 'flash' and shell_script == 'nrf_flash.sh'
+
+    def create_from_env(command, debug):
+        '''Create flasher from environment.
+
+        Required:
+
+        - O: build output directory
+        - KERNEL_HEX_NAME: name of kernel binary in ELF format
+        - NRF_FAMILY: e.g. NRF51 or NRF52
+        - BOARD: Zephyr board name
+        '''
+        hex_ = path.join(get_env_or_bail('O'),
+                         get_env_or_bail('KERNEL_HEX_NAME'))
+        family = get_env_or_bail('NRF_FAMILY')
+        board = get_env_or_bail('BOARD')
+
+        return NrfJprogBinaryRunner(hex_, family, board, debug=debug)
+
+    def get_board_snr_from_user(self):
+        snrs = self.check_output(['nrfjprog', '--ids'])
+        snrs = snrs.decode(sys.getdefaultencoding()).strip().splitlines()
+
+        if len(snrs) == 1:
+            return snrs[0]
+
+        print('There are multiple boards connected.')
+        for i, snr in enumerate(snrs, 1):
+            print('{}. {}'.format(i, snr))
+
+        p = 'Please select one with desired serial number (1-{}): '.format(
+                len(snrs))
+        while True:
+            value = input(p)
+            try:
+                value = int(value)
+            except ValueError:
+                continue
+            if 1 <= value <= len(snrs):
+                break
+
+        return snrs[value - 1]
+
+    def run(self, command, **kwargs):
+        if command != 'flash':
+            raise ValueError('only flash is supported')
+
+        board_snr = self.get_board_snr_from_user()
+
+        print('Flashing file: {}'.format(self.hex_))
+        commands = [
+            ['nrfjprog', '--eraseall', '-f', self.family, '--snr', board_snr],
+            ['nrfjprog', '--program', self.hex_, '-f', self.family, '--snr',
+             board_snr],
+        ]
+        if self.family == 'NRF52':
+            commands.extend([
+                # Set reset pin
+                ['nrfjprog', '--memwr', '0x10001200', '--val', '0x00000015',
+                 '-f', self.family, '--snr', board_snr],
+                ['nrfjprog', '--memwr', '0x10001204', '--val', '0x00000015',
+                 '-f', self.family, '--snr', board_snr],
+                ['nrfjprog', '--reset', '-f', self.family, '--snr', board_snr],
+            ])
+        commands.append(['nrfjprog',
+                         '--pinreset',
+                         '-f', self.family,
+                         '--snr', board_snr])
+
+        for cmd in commands:
+            self.check_call(cmd)
+
+        print('{} Serial Number {} flashed with success.'.format(
+                  self.board, board_snr))
diff --git a/scripts/support/runner/openocd.py b/scripts/support/runner/openocd.py
new file mode 100644
index 0000000..d657a78
--- /dev/null
+++ b/scripts/support/runner/openocd.py
@@ -0,0 +1,209 @@
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''Runner for openocd.'''
+
+from os import path
+import os
+import shlex
+
+from .core import ZephyrBinaryRunner, get_env_or_bail, get_env_strip_or
+
+DEFAULT_OPENOCD_TCL_PORT = 6333
+DEFAULT_OPENOCD_TELNET_PORT = 4444
+DEFAULT_OPENOCD_GDB_PORT = 3333
+
+
+class OpenOcdBinaryRunner(ZephyrBinaryRunner):
+    '''Runner front-end for openocd.'''
+
+    def __init__(self, openocd_config,
+                 openocd='openocd', default_path=None,
+                 bin_name=None, elf_name=None,
+                 load_cmd=None, verify_cmd=None, pre_cmd=None, post_cmd=None,
+                 extra_init=None,
+                 tcl_port=DEFAULT_OPENOCD_TCL_PORT,
+                 telnet_port=DEFAULT_OPENOCD_TELNET_PORT,
+                 gdb_port=DEFAULT_OPENOCD_GDB_PORT,
+                 gdb=None, tui=None, debug=False):
+        super(OpenOcdBinaryRunner, self).__init__(debug=debug)
+        self.openocd_config = openocd_config
+
+        search_args = []
+        if default_path is not None:
+            search_args = ['-s', default_path]
+        self.openocd_cmd = [openocd] + search_args
+        self.bin_name = bin_name
+        self.elf_name = elf_name
+        self.load_cmd = load_cmd
+        self.verify_cmd = verify_cmd
+        self.pre_cmd = pre_cmd
+        self.post_cmd = post_cmd
+        self.extra_init = extra_init if extra_init is not None else []
+        self.tcl_port = tcl_port
+        self.telnet_port = telnet_port
+        self.gdb_port = gdb_port
+        self.gdb_cmd = [gdb] if gdb is not None else None
+        self.tui_arg = [tui] if tui is not None else []
+
+    def replaces_shell_script(shell_script, command):
+        return (command in {'flash', 'debug', 'debugserver'} and
+                shell_script == 'openocd.sh')
+
+    def create_from_env(command, debug):
+        '''Create runner from environment.
+
+        Required:
+
+        - ZEPHYR_BASE: zephyr Git repository base directory
+        - ARCH: board architecture
+        - BOARD_NAME: zephyr name of board
+
+        Optional:
+
+        - OPENOCD: path to openocd, defaults to openocd
+        - OPENOCD_DEFAULT_PATH: openocd search path to use
+
+        Required for 'flash':
+
+        - O: build output directory
+        - KERNEL_BIN_NAME: zephyr kernel binary
+        - OPENOCD_LOAD_CMD: command to load binary into flash
+        - OPENOCD_VERIFY_CMD: command to verify flash executed correctly
+
+        Optional for 'flash':
+
+        - OPENOCD_PRE_CMD: command to run before any others
+        - OPENOCD_POST_CMD: command to run after verifying flash write
+
+        Required for 'debug':
+
+        - GDB: GDB executable
+        - O: build output directory
+        - KERNEL_ELF_NAME: zephyr kernel binary, ELF format
+
+        Optional for 'debug':
+
+        - TUI: one additional argument to GDB (e.g. -tui)
+        - OPENOCD_EXTRA_INIT: additional arguments to pass to openocd
+        - TCL_PORT: openocd TCL port, defaults to 6333
+        - TELNET_PORT: openocd telnet port, defaults to 4444
+        - GDB_PORT: openocd gdb port, defaults to 3333
+        '''
+        zephyr_base = get_env_or_bail('ZEPHYR_BASE')
+        arch = get_env_or_bail('ARCH')
+        board_name = get_env_or_bail('BOARD_NAME')
+        openocd_config = path.join(zephyr_base, 'boards', arch,
+                                   board_name, 'support', 'openocd.cfg')
+
+        openocd = os.environ.get('OPENOCD', 'openocd')
+        default_path = os.environ.get('OPENOCD_DEFAULT_PATH', None)
+
+        o = os.environ.get('O', None)
+        bin_ = os.environ.get('KERNEL_BIN_NAME', None)
+        elf = os.environ.get('KERNEL_ELF_NAME', None)
+        bin_name = None
+        elf_name = None
+        if o is not None:
+            if bin_ is not None:
+                bin_name = path.join(o, bin_)
+            if elf is not None:
+                elf_name = path.join(o, elf)
+
+        load_cmd = get_env_strip_or('OPENOCD_LOAD_CMD', '"', None)
+        verify_cmd = get_env_strip_or('OPENOCD_VERIFY_CMD', '"', None)
+        pre_cmd = get_env_strip_or('OPENOCD_PRE_CMD', '"', None)
+        post_cmd = get_env_strip_or('OPENOCD_POST_CMD', '"', None)
+
+        gdb = os.environ.get('GDB', None)
+        tui = os.environ.get('TUI', None)
+        extra_init = os.environ.get('OPENOCD_EXTRA_INIT', None)
+        if extra_init is not None:
+            extra_init = shlex.split(extra_init)
+        tcl_port = int(os.environ.get('TCL_PORT',
+                                      str(DEFAULT_OPENOCD_TCL_PORT)))
+        telnet_port = int(os.environ.get('TELNET_PORT',
+                                         str(DEFAULT_OPENOCD_TELNET_PORT)))
+        gdb_port = int(os.environ.get('GDB_PORT',
+                                      str(DEFAULT_OPENOCD_GDB_PORT)))
+
+        return OpenOcdBinaryRunner(openocd_config,
+                                   openocd=openocd, default_path=default_path,
+                                   bin_name=bin_name, elf_name=elf_name,
+                                   load_cmd=load_cmd, verify_cmd=verify_cmd,
+                                   pre_cmd=pre_cmd, post_cmd=post_cmd,
+                                   extra_init=extra_init, tcl_port=tcl_port,
+                                   telnet_port=telnet_port, gdb_port=gdb_port,
+                                   gdb=gdb, tui=tui, debug=debug)
+
+    def run(self, command, **kwargs):
+        if command not in {'flash', 'debug', 'debugserver'}:
+            raise ValueError('{} is not supported'.format(command))
+
+        if command == 'flash':
+            self.do_flash(**kwargs)
+        elif command == 'debug':
+            self.do_debug(**kwargs)
+        else:
+            self.do_debugserver(**kwargs)
+
+    def do_flash(self, **kwargs):
+        if self.bin_name is None:
+            raise ValueError('Cannot flash; binary name is missing')
+        if self.load_cmd is None:
+            raise ValueError('Cannot flash; load command is missing')
+        if self.verify_cmd is None:
+            raise ValueError('Cannot flash; verify command is missing')
+
+        pre_cmd = []
+        if self.pre_cmd is not None:
+            pre_cmd = ['-c', self.pre_cmd]
+
+        post_cmd = []
+        if self.post_cmd is not None:
+            post_cmd = ['-c', self.post_cmd]
+
+        cmd = (self.openocd_cmd +
+               ['-f', self.openocd_config,
+                '-c', 'init',
+                '-c', 'targets'] +
+               pre_cmd +
+               ['-c', 'reset halt',
+                '-c', self.load_cmd,
+                '-c', 'reset halt',
+                '-c', self.verify_cmd] +
+               post_cmd +
+               ['-c', 'reset run',
+                '-c', 'shutdown'])
+        self.check_call(cmd)
+
+    def do_debug(self, **kwargs):
+        if self.gdb_cmd is None:
+            raise ValueError('Cannot debug; no gdb specified')
+        if self.elf_name is None:
+            raise ValueError('Cannot debug; no .elf specified')
+
+        server_cmd = (self.openocd_cmd +
+                      ['-f', self.openocd_config] +
+                      self.extra_init +
+                      ['-c', 'tcl_port {}'.format(self.tcl_port),
+                       '-c', 'telnet_port {}'.format(self.telnet_port),
+                       '-c', 'gdb_port {}'.format(self.gdb_port),
+                       '-c', 'init',
+                       '-c', 'targets',
+                       '-c', 'halt'])
+
+        gdb_cmd = (self.gdb_cmd + self.tui_arg +
+                   ['-ex', 'target remote :{}'.format(self.gdb_port),
+                    self.elf_name])
+
+        self.run_server_and_client(server_cmd, gdb_cmd)
+
+    def do_debugserver(self, **kwargs):
+        cmd = (self.openocd_cmd +
+               ['-f', self.openocd_config,
+                '-c', 'init',
+                '-c', 'targets',
+                '-c', 'reset halt'])
+        self.check_call(cmd)
diff --git a/scripts/support/runner/pyocd.py b/scripts/support/runner/pyocd.py
new file mode 100644
index 0000000..d98579e
--- /dev/null
+++ b/scripts/support/runner/pyocd.py
@@ -0,0 +1,157 @@
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''Runner for pyOCD .'''
+
+from os import path
+import os
+
+from .core import ZephyrBinaryRunner, get_env_or_bail
+
+DEFAULT_PYOCD_GDB_PORT = 3333
+
+
+class PyOcdBinaryRunner(ZephyrBinaryRunner):
+    '''Runner front-end for pyocd-flashtool.'''
+
+    def __init__(self, target, flashtool='pyocd-flashtool',
+                 gdb=None, gdbserver='pyocd-gdbserver',
+                 gdb_port=DEFAULT_PYOCD_GDB_PORT, tui=None,
+                 bin_name=None, elf_name=None,
+                 board_id=None, daparg=None, debug=False):
+        super(PyOcdBinaryRunner, self).__init__(debug=debug)
+
+        self.target_args = ['-t', target]
+        self.flashtool = flashtool
+        self.gdb_cmd = [gdb] if gdb is not None else None
+        self.gdbserver = gdbserver
+        self.gdb_port = gdb_port
+        self.tui_args = [tui] if tui is not None else []
+        self.bin_name = bin_name
+        self.elf_name = elf_name
+
+        board_args = []
+        if board_id is not None:
+            board_args = ['-b', board_id]
+        self.board_args = board_args
+
+        daparg_args = []
+        if daparg is not None:
+            daparg_args = ['-da', daparg]
+        self.daparg_args = daparg_args
+
+    def replaces_shell_script(shell_script, command):
+        return (command in {'flash', 'debug', 'debugserver'} and
+                shell_script == 'pyocd.sh')
+
+    def port_args(self):
+        return ['-p', str(self.gdb_port)]
+
+    def create_from_env(command, debug):
+        '''Create runner from environment.
+
+        Required:
+
+        - PYOCD_TARGET: target override
+
+        Optional:
+
+        - PYOCD_DAPARG: arguments to pass to pyocd tool, default is none
+        - PYOCD_BOARD_ID: ID of board to flash, default is to prompt
+
+        Required for 'flash':
+
+        - O: build output directory
+        - KERNEL_BIN_NAME: name of kernel binary
+
+        Optional for 'flash':
+
+        - PYOCD_FLASHTOOL: flash tool path, defaults to pyocd-flashtool
+
+        Required for 'debug':
+
+        - O: build output directory
+        - KERNEL_ELF_NAME
+        - GDB: gdb executable
+
+        Optional for 'debug', 'debugserver':
+
+        - TUI: one additional argument to GDB (e.g. -tui)
+        - GDB_PORT: pyocd gdb port, defaults to 3333
+        - PYOCD_GDBSERVER: gdb server executable, defaults to pyocd-gdbserver
+        '''
+        target = get_env_or_bail('PYOCD_TARGET')
+
+        o = os.environ.get('O', None)
+        bin_ = os.environ.get('KERNEL_BIN_NAME', None)
+        elf = os.environ.get('KERNEL_ELF_NAME', None)
+        bin_name = None
+        elf_name = None
+        if o is not None:
+            if bin_ is not None:
+                bin_name = path.join(o, bin_)
+            if elf is not None:
+                elf_name = path.join(o, elf)
+
+        flashtool = os.environ.get('PYOCD_FLASHTOOL', 'pyocd-flashtool')
+        board_id = os.environ.get('PYOCD_BOARD_ID', None)
+        daparg = os.environ.get('PYOCD_DAPARG', None)
+        gdb = os.environ.get('GDB', None)
+        gdbserver = os.environ.get('PYOCD_GDBSERVER', 'pyocd-gdbserver')
+        gdb_port = os.environ.get('GDB_PORT', DEFAULT_PYOCD_GDB_PORT)
+        tui = os.environ.get('TUI', None)
+
+        return PyOcdBinaryRunner(target, flashtool=flashtool, gdb=gdb,
+                                 gdbserver=gdbserver, gdb_port=gdb_port,
+                                 tui=tui, bin_name=bin_name, elf_name=elf_name,
+                                 board_id=board_id, daparg=daparg, debug=debug)
+
+    def run(self, command, **kwargs):
+        if command not in {'flash', 'debug', 'debugserver'}:
+            raise ValueError('{} is not supported'.format(command))
+
+        if command == 'flash':
+            self.flash(**kwargs)
+        else:
+            self.debug_debugserver(command, **kwargs)
+
+    def flash(self, **kwargs):
+        if self.bin_name is None:
+            raise ValueError('Cannot flash; bin_name is missing')
+
+        cmd = ([self.flashtool] +
+               self.daparg_args +
+               self.target_args +
+               self.board_args +
+               [self.bin_name])
+
+        print('Flashing Target Device')
+        self.check_call(cmd)
+
+    def print_gdbserver_message(self):
+        print('pyOCD GDB server running on port {}'.format(self.gdb_port))
+
+    def debug_debugserver(self, command, **kwargs):
+        server_cmd = ([self.gdbserver] +
+                      self.daparg_args +
+                      self.port_args() +
+                      self.target_args +
+                      self.board_args)
+
+        if command == 'debugserver':
+            self.print_gdbserver_message()
+            self.check_call(server_cmd)
+        else:
+            if self.gdb_cmd is None:
+                raise ValueError('Cannot debug; gdb is missing')
+            if self.elf_name is None:
+                raise ValueError('Cannot debug; elf is missing')
+            client_cmd = (self.gdb_cmd +
+                          self.tui_args +
+                          [self.elf_name] +
+                          ['-ex', 'target remote :{}'.format(self.gdb_port),
+                           '-ex', 'load',
+                           '-ex', 'monitor reset halt'])
+            self.print_gdbserver_message()
+            self.run_server_and_client(server_cmd, client_cmd)
diff --git a/scripts/support/zephyr_flash_debug.py b/scripts/support/zephyr_flash_debug.py
index 69b1b72..ee8bf3f 100755
--- a/scripts/support/zephyr_flash_debug.py
+++ b/scripts/support/zephyr_flash_debug.py
@@ -11,1021 +11,11 @@
 emulated hardware. If it can invoke the relevant tools natively, it will do so;
 otherwise, it delegates to the shell script."""
 
-import abc
 from os import path
-import os
-import pprint
-import platform
-import shlex
-import signal
-import sys
 import subprocess
-import time
+import sys
 
-
-def get_env_or_bail(env_var):
-    try:
-        return os.environ[env_var]
-    except KeyError:
-        print('Variable {} not in environment:'.format(
-                  env_var), file=sys.stderr)
-        pprint.pprint(dict(os.environ), stream=sys.stderr)
-        raise
-
-
-def get_env_bool_or(env_var, default_value):
-    try:
-        return bool(int(os.environ[env_var]))
-    except KeyError:
-        return default_value
-
-
-def get_env_strip_or(env_var, to_strip, default_value):
-    value = os.environ.get(env_var, None)
-    if value is not None:
-        return value.strip(to_strip)
-    else:
-        return default_value
-
-
-def quote_sh_list(cmd):
-    '''Transform a command from list into shell string form.'''
-    fmt = ' '.join('{}' for _ in cmd)
-    args = [shlex.quote(s) for s in cmd]
-    return fmt.format(*args)
-
-
-def check_call(cmd, debug):
-    if debug:
-        print(quote_sh_list(cmd))
-    subprocess.check_call(cmd)
-
-
-def check_output(cmd, debug):
-    if debug:
-        print(quote_sh_list(cmd))
-    return subprocess.check_output(cmd)
-
-
-def popen_ignore_int(cmd, debug):
-    '''Spawn a child command, ensuring it ignores SIGINT.
-
-    The returned subprocess.Popen object must be manually terminated.'''
-    cflags = 0
-    preexec = None
-    system = platform.system()
-
-    if system == 'Windows':
-        cflags |= subprocess.CREATE_NEW_PROCESS_GROUP
-    elif system in {'Linux', 'Darwin'}:
-        preexec = os.setsid
-
-    if debug:
-        print(quote_sh_list(cmd))
-
-    return subprocess.Popen(cmd, creationflags=cflags, preexec_fn=preexec)
-
-
-class ZephyrBinaryRunner(abc.ABC):
-    '''Abstract superclass for binary runners (flashers, debuggers).
-
-    With some exceptions, boards supported by Zephyr must provide
-    generic means to be flashed (have a Zephyr firmware binary
-    permanently installed on the device for running) and debugged
-    (have a breakpoint debugger and program loader on a host
-    workstation attached to a running target). This is supported by
-    three top-level commands managed by the Zephyr build system:
-
-    - 'flash': flash a previously configured binary to the board,
-      start execution on the target, then return.
-
-    - 'debug': connect to the board via a debugging protocol, then
-      drop the user into a debugger interface with symbol tables
-      loaded from the current binary, and block until it exits.
-
-    - 'debugserver': connect via a board-specific debugging protocol,
-      then reset and halt the target. Ensure the user is now able to
-      connect to a debug server with symbol tables loaded from the
-      binary.
-
-    Runner functionality relies on a variety of target-specific tools
-    and configuration values, the user interface to which is
-    abstracted by this class. Each runner subclass should take any
-    values it needs to execute one of these commands in its
-    constructor.  The actual command execution is handled in the run()
-    method.
-
-    This functionality is also replacing the legacy Zephyr runners,
-    which are shell scripts.
-
-    At present, the Zephyr build system uses a variety of
-    tool-specific environment variables to control runner behavior.
-    To support a transition to ZephyrBinaryRunner and subclasses, this
-    class provides a create_for_shell_script() static factory method.
-    This method iterates over ZephyrBinaryRUnner subclasses,
-    determines which (if any) can provide equivalent functionality to
-    the shell-based runner, and returns a subclass instance with its
-    configuration determined from the environment.
-
-    To support this, subclasess currently must provide a pair of
-    static methods, replaces_shell_script() and create_from_env(). The
-    first allows the runner subclass to declare which commands and
-    scripts it can replace. The second is called by
-    create_for_shell_script() to create a concrete runner instance.
-
-    The environment-based factories are for legacy use *only*; the
-    user must be able to construct and use a runner using only the
-    constructor and run() method.'''
-
-    def __init__(self, debug=False):
-        self.debug = debug
-
-    @staticmethod
-    def create_for_shell_script(shell_script, command, debug):
-        '''Factory for using as a drop-in replacement to a shell script.
-
-        Command is one of 'flash', 'debug', 'debugserver'.
-
-        Get runner instance to use in place of shell_script, deriving
-        configuration from the environment.'''
-        for sub_cls in ZephyrBinaryRunner.__subclasses__():
-            if sub_cls.replaces_shell_script(shell_script, command):
-                return sub_cls.create_from_env(command, debug)
-        raise ValueError('cannot implement script {} command {}'.format(
-                             shell_script, command))
-
-    @staticmethod
-    @abc.abstractmethod
-    def replaces_shell_script(shell_script, command):
-        '''Check if this class replaces shell_script for the given command.'''
-
-    @staticmethod
-    @abc.abstractmethod
-    def create_from_env(command, debug):
-        '''Create new flasher instance from environment variables.
-
-        This class must be able to replace the current shell script
-        (FLASH_SCRIPT or DEBUG_SCRIPT, depending on command). The
-        environment variables expected by that script are used to build
-        the flasher in a backwards-compatible manner.'''
-
-    @abc.abstractmethod
-    def run(self, command, **kwargs):
-        '''Run a command ('flash', 'debug', 'debugserver').
-
-        In case of an unsupported command, raise a ValueError.'''
-
-    def run_server_and_client(self, server, client):
-        '''Run a server that ignores SIGINT, and a client that handles it.
-
-        This routine portably:
-
-        - creates a Popen object for the `server' command which ignores SIGINT
-        - runs `client' in a subprocess while temporarily ignoring SIGINT
-        - cleans up the server after the client exits.
-
-        It's useful to e.g. open a GDB server and client.'''
-        server_proc = popen_ignore_int(server, self.debug)
-        previous = signal.signal(signal.SIGINT, signal.SIG_IGN)
-        try:
-            check_call(client, self.debug)
-        finally:
-            signal.signal(signal.SIGINT, previous)
-            server_proc.terminate()
-            server_proc.wait()
-
-
-DEFAULT_ARC_TCL_PORT = 6333
-DEFAULT_ARC_TELNET_PORT = 4444
-DEFAULT_ARC_GDB_PORT = 3333
-
-
-class ArcBinaryRunner(ZephyrBinaryRunner):
-    '''Runner front-end for the ARC architecture, using openocd.'''
-
-    # This unusual 'flash' implementation matches the original shell script.
-    #
-    # It works by starting a GDB server in a separate session, connecting a
-    # client to it to load the program, and running 'continue' within the
-    # client to execute the application.
-    #
-    # TODO: exit immediately when flashing is done, leaving Zephyr running.
-
-    def __init__(self, elf, zephyr_base, arch, board_name, python,
-                 gdb, openocd='openocd', extra_init=None, default_path=None,
-                 tui=None, tcl_port=DEFAULT_ARC_TCL_PORT,
-                 telnet_port=DEFAULT_ARC_TELNET_PORT,
-                 gdb_port=DEFAULT_ARC_GDB_PORT, debug=False):
-        super(ArcBinaryRunner, self).__init__(debug=debug)
-        self.elf = elf
-        self.zephyr_base = zephyr_base
-        self.arch = arch
-        self.board_name = board_name
-        self.python = python
-        self.gdb = gdb
-        search_args = []
-        if default_path is not None:
-            search_args = ['-s', default_path]
-        self.openocd_cmd = [openocd] + search_args
-        self.extra_init = extra_init if extra_init is not None else []
-        self.tui = tui
-        self.tcl_port = tcl_port
-        self.telnet_port = telnet_port
-        self.gdb_port = gdb_port
-
-    def replaces_shell_script(shell_script, command):
-        return (command in {'flash', 'debug', 'debugserver'} and
-                shell_script == 'arc_debugger.sh')
-
-    def create_from_env(command, debug):
-        '''Create runner from environment.
-
-        Required:
-
-        - O: build output directory
-        - KERNEL_ELF_NAME: zephyr kernel binary in ELF format
-        - ZEPHYR_BASE: zephyr Git repository base directory
-        - ARCH: board architecture
-        - BOARD_NAME: zephyr name of board
-        - PYTHON: python executable
-        - GDB: gdb executable
-
-        Optional:
-
-        - OPENOCD: path to openocd, defaults to openocd
-        - OPENOCD_EXTRA_INIT: initialization command for GDB server
-        - OPENOCD_DEFAULT_PATH: openocd search path to use
-        - TUI: if present, passed to gdb server used to flash
-        - TCL_PORT: openocd TCL port, defaults to 6333
-        - TELNET_PORT: openocd telnet port, defaults to 4444
-        - GDB_PORT: openocd gdb port, defaults to 3333
-        '''
-        elf = path.join(get_env_or_bail('O'),
-                        get_env_or_bail('KERNEL_ELF_NAME'))
-        zephyr_base = get_env_or_bail('ZEPHYR_BASE')
-        arch = get_env_or_bail('ARCH')
-        board_name = get_env_or_bail('BOARD_NAME')
-        python = get_env_or_bail('PYTHON')
-        gdb = get_env_or_bail('GDB')
-
-        openocd = os.environ.get('OPENOCD', 'openocd')
-        extra_init = os.environ.get('OPENOCD_EXTRA_INIT', None)
-        if extra_init is not None:
-            extra_init = shlex.split(extra_init)
-        default_path = os.environ.get('OPENOCD_DEFAULT_PATH', None)
-        tui = os.environ.get('TUI', None)
-        tcl_port = int(os.environ.get('TCL_PORT',
-                                      str(DEFAULT_ARC_TCL_PORT)))
-        telnet_port = int(os.environ.get('TELNET_PORT',
-                                         str(DEFAULT_ARC_TELNET_PORT)))
-        gdb_port = int(os.environ.get('GDB_PORT',
-                                      str(DEFAULT_ARC_GDB_PORT)))
-
-        return ArcBinaryRunner(elf, zephyr_base, arch, board_name, python,
-                               gdb, openocd=openocd, extra_init=extra_init,
-                               default_path=default_path, tui=tui,
-                               tcl_port=tcl_port, telnet_port=telnet_port,
-                               gdb_port=gdb_port, debug=debug)
-
-    def run(self, command, **kwargs):
-        if command not in {'flash', 'debug', 'debugserver'}:
-            raise ValueError('{} is not supported'.format(command))
-
-        kwargs['openocd-cfg'] = path.join(self.zephyr_base, 'boards',
-                                          self.arch, self.board_name,
-                                          'support', 'openocd.cfg')
-
-        if command in {'flash', 'debug'}:
-            self.flash_debug(command, **kwargs)
-        else:
-            self.debugserver(**kwargs)
-
-    def flash_debug(self, command, **kwargs):
-        config = kwargs['openocd-cfg']
-
-        server_cmd = (self.openocd_cmd +
-                      ['-f', config] +
-                      self.extra_init +
-                      ['-c', 'tcl_port {}'.format(self.tcl_port),
-                       '-c', 'telnet_port {}'.format(self.telnet_port),
-                       '-c', 'gdb_port {}'.format(self.gdb_port),
-                       '-c', 'init',
-                       '-c', 'targets',
-                       '-c', 'halt'])
-
-        tui_arg = []
-        if self.tui is not None:
-            tui_arg = [self.tui]
-
-        continue_arg = []
-        if command == 'flash':
-            continue_arg = ['-ex', 'c']
-
-        gdb_cmd = ([self.gdb] +
-                   tui_arg +
-                   ['-ex', 'target remote :{}'.format(self.gdb_port),
-                    '-ex', 'load'] +
-                   continue_arg +
-                   [self.elf])
-
-        self.run_server_and_client(server_cmd, gdb_cmd)
-
-    def debugserver(self, **kwargs):
-        config = kwargs['openocd-cfg']
-        cmd = (self.openocd_cmd +
-               ['-f', config,
-                '-c', 'init',
-                '-c', 'targets',
-                '-c', 'reset halt'])
-        check_call(cmd, self.debug)
-
-
-DEFAULT_BOSSAC_PORT = '/dev/ttyACM0'
-
-
-class BossacBinaryRunner(ZephyrBinaryRunner):
-    '''Runner front-end for bossac.'''
-
-    def __init__(self, bin_name, bossac='bossac',
-                 port=DEFAULT_BOSSAC_PORT, debug=False):
-        super(BossacBinaryRunner, self).__init__(debug=debug)
-        self.bin_name = bin_name
-        self.bossac = bossac
-        self.port = port
-
-    def replaces_shell_script(shell_script, command):
-        return command == 'flash' and shell_script == 'bossa-flash.sh'
-
-    def create_from_env(command, debug):
-        '''Create flasher from environment.
-
-        Required:
-
-        - O: build output directory
-        - KERNEL_BIN_NAME: name of kernel binary
-
-        Optional:
-
-        - BOSSAC: path to bossac, default is bossac
-        - BOSSAC_PORT: serial port to use, default is /dev/ttyACM0
-        '''
-        bin_name = path.join(get_env_or_bail('O'),
-                             get_env_or_bail('KERNEL_BIN_NAME'))
-        bossac = os.environ.get('BOSSAC', 'bossac')
-        port = os.environ.get('BOSSAC_PORT', DEFAULT_BOSSAC_PORT)
-        return BossacBinaryRunner(bin_name, bossac=bossac, port=port,
-                                  debug=debug)
-
-    def run(self, command, **kwargs):
-        if command != 'flash':
-            raise ValueError('only flash is supported')
-
-        if platform.system() != 'Linux':
-            msg = 'CAUTION: No flash tool for your host system found!'
-            raise NotImplementedError(msg)
-
-        cmd_stty = ['stty', '-F', self.port, 'raw', 'ispeed', '1200',
-                    'ospeed', '1200', 'cs8', '-cstopb', 'ignpar', 'eol', '255',
-                    'eof', '255']
-        cmd_flash = [self.bossac, '-p', self.port, '-R', '-e', '-w', '-v',
-                     '-b', self.bin_name]
-
-        check_call(cmd_stty, self.debug)
-        check_call(cmd_flash, self.debug)
-
-
-class DfuUtilBinaryRunner(ZephyrBinaryRunner):
-    '''Runner front-end for dfu-util.'''
-
-    def __init__(self, pid, alt, img, dfuse=None, exe='dfu-util', debug=False):
-        super(DfuUtilBinaryRunner, self).__init__(debug=debug)
-        self.alt = alt
-        self.img = img
-        self.dfuse = dfuse
-        self.cmd = [exe, '-d,{}'.format(pid)]
-        try:
-            self.list_pattern = ', alt={},'.format(int(self.alt))
-        except ValueError:
-            self.list_pattern = ', name="{}",'.format(self.alt)
-
-    def replaces_shell_script(shell_script, command):
-        return command == 'flash' and shell_script == 'dfuutil.sh'
-
-    def create_from_env(command, debug):
-        '''Create flasher from environment.
-
-        Required:
-
-        - DFUUTIL_PID: USB VID:PID of the board
-        - DFUUTIL_ALT: interface alternate setting number or name
-        - DFUUTIL_IMG: binary to flash
-
-        Optional:
-
-        - DFUUTIL_DFUSE_ADDR: target address if the board is a
-          DfuSe device. Ignored if not present.
-        - DFUUTIL: dfu-util executable, defaults to dfu-util.
-        '''
-        pid = get_env_or_bail('DFUUTIL_PID')
-        alt = get_env_or_bail('DFUUTIL_ALT')
-        img = get_env_or_bail('DFUUTIL_IMG')
-        dfuse = os.environ.get('DFUUTIL_DFUSE_ADDR', None)
-        exe = os.environ.get('DFUUTIL', 'dfu-util')
-
-        return DfuUtilBinaryRunner(pid, alt, img, dfuse=dfuse, exe=exe,
-                                   debug=debug)
-
-    def find_device(self):
-        cmd = list(self.cmd) + ['-l']
-        output = check_output(cmd, self.debug)
-        output = output.decode(sys.getdefaultencoding())
-        return self.list_pattern in output
-
-    def run(self, command, **kwargs):
-        if command != 'flash':
-            raise ValueError('only flash is supported')
-
-        reset = 0
-        if not self.find_device():
-            reset = 1
-            print('Please reset your board to switch to DFU mode...')
-            while not self.find_device():
-                time.sleep(0.1)
-
-        cmd = list(self.cmd)
-        if self.dfuse is not None:
-            cmd.extend(['-s', '{}:leave'.format(self.dfuse)])
-        cmd.extend(['-a', self.alt, '-D', self.img])
-        check_call(cmd, self.debug)
-        if reset:
-            print('Now reset your board again to switch back to runtime mode.')
-
-
-class Esp32BinaryRunner(ZephyrBinaryRunner):
-    '''Runner front-end for espidf.'''
-
-    def __init__(self, elf, device, baud=921600, flash_size='detect',
-                 flash_freq='40m', flash_mode='dio', espidf='espidf',
-                 debug=False):
-        super(Esp32BinaryRunner, self).__init__(debug=debug)
-        self.elf = elf
-        self.device = device
-        self.baud = baud
-        self.flash_size = flash_size
-        self.flash_freq = flash_freq
-        self.flash_mode = flash_mode
-        self.espidf = espidf
-
-    def replaces_shell_script(shell_script, command):
-        return command == 'flash' and shell_script == 'esp32.sh'
-
-    def create_from_env(command, debug):
-        '''Create flasher from environment.
-
-        Required:
-
-        - O: build output directory
-        - KERNEL_ELF_NAME: name of kernel binary in ELF format
-
-        Optional:
-
-        - ESP_DEVICE: serial port to flash, default /dev/ttyUSB0
-        - ESP_BAUD_RATE: serial baud rate, default 921600
-        - ESP_FLASH_SIZE: flash size, default 'detect'
-        - ESP_FLASH_FREQ: flash frequency, default '40m'
-        - ESP_FLASH_MODE: flash mode, default 'dio'
-        - ESP_TOOL: complete path to espidf, or set to 'espidf' to look for it
-          in $ESP_IDF_PATH/components/esptool_py/esptool/esptool.py
-        '''
-        elf = path.join(get_env_or_bail('O'),
-                        get_env_or_bail('KERNEL_ELF_NAME'))
-
-        # TODO add sane device defaults on other platforms than Linux.
-        device = os.environ.get('ESP_DEVICE', '/dev/ttyUSB0')
-        baud = os.environ.get('ESP_BAUD_RATE', '921600')
-        flash_size = os.environ.get('ESP_FLASH_SIZE', 'detect')
-        flash_freq = os.environ.get('ESP_FLASH_FREQ', '40m')
-        flash_mode = os.environ.get('ESP_FLASH_MODE', 'dio')
-        espidf = os.environ.get('ESP_TOOL', 'espidf')
-
-        if espidf == 'espidf':
-            idf_path = get_env_or_bail('ESP_IDF_PATH')
-            espidf = path.join(idf_path, 'components', 'esptool_py', 'esptool',
-                               'esptool.py')
-
-        return Esp32BinaryRunner(elf, device, baud=baud,
-                                 flash_size=flash_size, flash_freq=flash_freq,
-                                 flash_mode=flash_mode, espidf=espidf,
-                                 debug=debug)
-
-    def run(self, command, **kwargs):
-        if command != 'flash':
-            raise ValueError('only flash is supported')
-
-        bin_name = path.splitext(self.elf)[0] + path.extsep + 'bin'
-        cmd_convert = [self.espidf, '--chip', 'esp32', 'elf2image', self.elf]
-        cmd_flash = [self.espidf, '--chip', 'esp32', '--port', self.device,
-                     '--baud', self.baud, '--before', 'default_reset',
-                     '--after', 'hard_reset', 'write_flash', '-u',
-                     '--flash_mode', self.flash_mode,
-                     '--flash_freq', self.flash_freq,
-                     '--flash_size', self.flash_size,
-                     '0x1000', bin_name]
-
-        print("Converting ELF to BIN")
-        check_call(cmd_convert, self.debug)
-
-        print("Flashing ESP32 on {} ({}bps)".format(self.device, self.baud))
-        check_call(cmd_flash, self.debug)
-
-
-class Nios2BinaryRunner(ZephyrBinaryRunner):
-    '''Runner front-end for NIOS II.'''
-
-    # From the original shell script:
-    #
-    #     "XXX [flash] only support[s] cases where the .elf is sent
-    #      over the JTAG and the CPU directly boots from __start. CONFIG_XIP
-    #      and CONFIG_INCLUDE_RESET_VECTOR must be disabled."
-
-    def __init__(self, hex_, cpu_sof, zephyr_base, debug=False):
-        super(Nios2BinaryRunner, self).__init__(debug=debug)
-        self.hex_ = hex_
-        self.cpu_sof = cpu_sof
-        self.zephyr_base = zephyr_base
-
-    def replaces_shell_script(shell_script, command):
-        return command == 'flash' and shell_script == 'nios2.sh'
-
-    def create_from_env(command, debug):
-        '''Create runner from environment.
-
-        Required:
-
-        - O: build output directory
-        - KERNEL_HEX_NAME: name of kernel binary in HEX format
-        - NIOS2_CPU_SOF: location of the CPU .sof data
-        - ZEPHYR_BASE: zephyr Git repository base directory
-        '''
-        hex_ = path.join(get_env_or_bail('O'),
-                         get_env_or_bail('KERNEL_HEX_NAME'))
-        cpu_sof = get_env_or_bail('NIOS2_CPU_SOF')
-        zephyr_base = get_env_or_bail('ZEPHYR_BASE')
-
-        return Nios2BinaryRunner(hex_, cpu_sof, zephyr_base, debug=debug)
-
-    def run(self, command, **kwargs):
-        if command not in {'flash', 'debug', 'debugserver'}:
-            raise ValueError('{} is not supported'.format(command))
-
-        if command == 'flash':
-            self.flash(**kwargs)
-        else:
-            self.debug_debugserver(command, **kwargs)
-
-    def flash(self, **kwargs):
-        cmd = [path.join(self.zephyr_base, 'scripts', 'support',
-                         'quartus-flash.py'),
-               '--sof', self.cpu_sof,
-               '--kernel', self.hex_]
-
-        check_call(cmd, self.debug)
-
-    def debug_debugserver(command, **kwargs):
-        raise NotImplementedError()
-
-
-class NrfJprogBinaryRunner(ZephyrBinaryRunner):
-    '''Runner front-end for nrfjprog.'''
-
-    def __init__(self, hex_, family, board, debug=False):
-        super(NrfJprogBinaryRunner, self).__init__(debug=debug)
-        self.hex_ = hex_
-        self.family = family
-        self.board = board
-
-    def replaces_shell_script(shell_script, command):
-        return command == 'flash' and shell_script == 'nrf_flash.sh'
-
-    def create_from_env(command, debug):
-        '''Create flasher from environment.
-
-        Required:
-
-        - O: build output directory
-        - KERNEL_HEX_NAME: name of kernel binary in ELF format
-        - NRF_FAMILY: e.g. NRF51 or NRF52
-        - BOARD: Zephyr board name
-        '''
-        hex_ = path.join(get_env_or_bail('O'),
-                         get_env_or_bail('KERNEL_HEX_NAME'))
-        family = get_env_or_bail('NRF_FAMILY')
-        board = get_env_or_bail('BOARD')
-
-        return NrfJprogBinaryRunner(hex_, family, board, debug=debug)
-
-    def get_board_snr_from_user(self):
-        snrs = check_output(['nrfjprog', '--ids'], self.debug)
-        snrs = snrs.decode(sys.getdefaultencoding()).strip().splitlines()
-
-        if len(snrs) == 1:
-            return snrs[0]
-
-        print('There are multiple boards connected.')
-        for i, snr in enumerate(snrs, 1):
-            print('{}. {}'.format(i, snr))
-
-        p = 'Please select one with desired serial number (1-{}): '.format(
-                len(snrs))
-        while True:
-            value = input(p)
-            try:
-                value = int(value)
-            except ValueError:
-                continue
-            if 1 <= value <= len(snrs):
-                break
-
-        return snrs[value - 1]
-
-    def run(self, command, **kwargs):
-        if command != 'flash':
-            raise ValueError('only flash is supported')
-
-        board_snr = self.get_board_snr_from_user()
-
-        print('Flashing file: {}'.format(self.hex_))
-        commands = [
-            ['nrfjprog', '--eraseall', '-f', self.family, '--snr', board_snr],
-            ['nrfjprog', '--program', self.hex_, '-f', self.family, '--snr',
-             board_snr],
-        ]
-        if self.family == 'NRF52':
-            commands.extend([
-                # Set reset pin
-                ['nrfjprog', '--memwr', '0x10001200', '--val', '0x00000015',
-                 '-f', self.family, '--snr', board_snr],
-                ['nrfjprog', '--memwr', '0x10001204', '--val', '0x00000015',
-                 '-f', self.family, '--snr', board_snr],
-                ['nrfjprog', '--reset', '-f', self.family, '--snr', board_snr],
-            ])
-        commands.append(['nrfjprog',
-                         '--pinreset',
-                         '-f', self.family,
-                         '--snr', board_snr])
-
-        for cmd in commands:
-            check_call(cmd, self.debug)
-
-        print('{} Serial Number {} flashed with success.'.format(
-                  self.board, board_snr))
-
-
-DEFAULT_OPENOCD_TCL_PORT = 6333
-DEFAULT_OPENOCD_TELNET_PORT = 4444
-DEFAULT_OPENOCD_GDB_PORT = 3333
-
-
-class OpenOcdBinaryRunner(ZephyrBinaryRunner):
-    '''Runner front-end for openocd.'''
-
-    def __init__(self, openocd_config,
-                 openocd='openocd', default_path=None,
-                 bin_name=None, elf_name=None,
-                 load_cmd=None, verify_cmd=None, pre_cmd=None, post_cmd=None,
-                 extra_init=None,
-                 tcl_port=DEFAULT_OPENOCD_TCL_PORT,
-                 telnet_port=DEFAULT_OPENOCD_TELNET_PORT,
-                 gdb_port=DEFAULT_OPENOCD_GDB_PORT,
-                 gdb=None, tui=None, debug=False):
-        super(OpenOcdBinaryRunner, self).__init__(debug=debug)
-        self.openocd_config = openocd_config
-
-        search_args = []
-        if default_path is not None:
-            search_args = ['-s', default_path]
-        self.openocd_cmd = [openocd] + search_args
-        self.bin_name = bin_name
-        self.elf_name = elf_name
-        self.load_cmd = load_cmd
-        self.verify_cmd = verify_cmd
-        self.pre_cmd = pre_cmd
-        self.post_cmd = post_cmd
-        self.extra_init = extra_init if extra_init is not None else []
-        self.tcl_port = tcl_port
-        self.telnet_port = telnet_port
-        self.gdb_port = gdb_port
-        self.gdb_cmd = [gdb] if gdb is not None else None
-        self.tui_arg = [tui] if tui is not None else []
-
-    def replaces_shell_script(shell_script, command):
-        return (command in {'flash', 'debug', 'debugserver'} and
-                shell_script == 'openocd.sh')
-
-    def create_from_env(command, debug):
-        '''Create runner from environment.
-
-        Required:
-
-        - ZEPHYR_BASE: zephyr Git repository base directory
-        - ARCH: board architecture
-        - BOARD_NAME: zephyr name of board
-
-        Optional:
-
-        - OPENOCD: path to openocd, defaults to openocd
-        - OPENOCD_DEFAULT_PATH: openocd search path to use
-
-        Required for 'flash':
-
-        - O: build output directory
-        - KERNEL_BIN_NAME: zephyr kernel binary
-        - OPENOCD_LOAD_CMD: command to load binary into flash
-        - OPENOCD_VERIFY_CMD: command to verify flash executed correctly
-
-        Optional for 'flash':
-
-        - OPENOCD_PRE_CMD: command to run before any others
-        - OPENOCD_POST_CMD: command to run after verifying flash write
-
-        Required for 'debug':
-
-        - GDB: GDB executable
-        - O: build output directory
-        - KERNEL_ELF_NAME: zephyr kernel binary, ELF format
-
-        Optional for 'debug':
-
-        - TUI: one additional argument to GDB (e.g. -tui)
-        - OPENOCD_EXTRA_INIT: additional arguments to pass to openocd
-        - TCL_PORT: openocd TCL port, defaults to 6333
-        - TELNET_PORT: openocd telnet port, defaults to 4444
-        - GDB_PORT: openocd gdb port, defaults to 3333
-        '''
-        zephyr_base = get_env_or_bail('ZEPHYR_BASE')
-        arch = get_env_or_bail('ARCH')
-        board_name = get_env_or_bail('BOARD_NAME')
-        openocd_config = path.join(zephyr_base, 'boards', arch,
-                                   board_name, 'support', 'openocd.cfg')
-
-        openocd = os.environ.get('OPENOCD', 'openocd')
-        default_path = os.environ.get('OPENOCD_DEFAULT_PATH', None)
-
-        o = os.environ.get('O', None)
-        bin_ = os.environ.get('KERNEL_BIN_NAME', None)
-        elf = os.environ.get('KERNEL_ELF_NAME', None)
-        bin_name = None
-        elf_name = None
-        if o is not None:
-            if bin_ is not None:
-                bin_name = path.join(o, bin_)
-            if elf is not None:
-                elf_name = path.join(o, elf)
-
-        load_cmd = get_env_strip_or('OPENOCD_LOAD_CMD', '"', None)
-        verify_cmd = get_env_strip_or('OPENOCD_VERIFY_CMD', '"', None)
-        pre_cmd = get_env_strip_or('OPENOCD_PRE_CMD', '"', None)
-        post_cmd = get_env_strip_or('OPENOCD_POST_CMD', '"', None)
-
-        gdb = os.environ.get('GDB', None)
-        tui = os.environ.get('TUI', None)
-        extra_init = os.environ.get('OPENOCD_EXTRA_INIT', None)
-        if extra_init is not None:
-            extra_init = shlex.split(extra_init)
-        tcl_port = int(os.environ.get('TCL_PORT',
-                                      str(DEFAULT_OPENOCD_TCL_PORT)))
-        telnet_port = int(os.environ.get('TELNET_PORT',
-                                         str(DEFAULT_OPENOCD_TELNET_PORT)))
-        gdb_port = int(os.environ.get('GDB_PORT',
-                                      str(DEFAULT_OPENOCD_GDB_PORT)))
-
-        return OpenOcdBinaryRunner(openocd_config,
-                                   openocd=openocd, default_path=default_path,
-                                   bin_name=bin_name, elf_name=elf_name,
-                                   load_cmd=load_cmd, verify_cmd=verify_cmd,
-                                   pre_cmd=pre_cmd, post_cmd=post_cmd,
-                                   extra_init=extra_init, tcl_port=tcl_port,
-                                   telnet_port=telnet_port, gdb_port=gdb_port,
-                                   gdb=gdb, tui=tui, debug=debug)
-
-    def run(self, command, **kwargs):
-        if command not in {'flash', 'debug', 'debugserver'}:
-            raise ValueError('{} is not supported'.format(command))
-
-        if command == 'flash':
-            self.do_flash(**kwargs)
-        elif command == 'debug':
-            self.do_debug(**kwargs)
-        else:
-            self.do_debugserver(**kwargs)
-
-    def do_flash(self, **kwargs):
-        if self.bin_name is None:
-            raise ValueError('Cannot flash; binary name is missing')
-        if self.load_cmd is None:
-            raise ValueError('Cannot flash; load command is missing')
-        if self.verify_cmd is None:
-            raise ValueError('Cannot flash; verify command is missing')
-
-        pre_cmd = []
-        if self.pre_cmd is not None:
-            pre_cmd = ['-c', self.pre_cmd]
-
-        post_cmd = []
-        if self.post_cmd is not None:
-            post_cmd = ['-c', self.post_cmd]
-
-        cmd = (self.openocd_cmd +
-               ['-f', self.openocd_config,
-                '-c', 'init',
-                '-c', 'targets'] +
-               pre_cmd +
-               ['-c', 'reset halt',
-                '-c', self.load_cmd,
-                '-c', 'reset halt',
-                '-c', self.verify_cmd] +
-               post_cmd +
-               ['-c', 'reset run',
-                '-c', 'shutdown'])
-        check_call(cmd, self.debug)
-
-    def do_debug(self, **kwargs):
-        if self.gdb_cmd is None:
-            raise ValueError('Cannot debug; no gdb specified')
-        if self.elf_name is None:
-            raise ValueError('Cannot debug; no .elf specified')
-
-        server_cmd = (self.openocd_cmd +
-                      ['-f', self.openocd_config] +
-                      self.extra_init +
-                      ['-c', 'tcl_port {}'.format(self.tcl_port),
-                       '-c', 'telnet_port {}'.format(self.telnet_port),
-                       '-c', 'gdb_port {}'.format(self.gdb_port),
-                       '-c', 'init',
-                       '-c', 'targets',
-                       '-c', 'halt'])
-
-        gdb_cmd = (self.gdb_cmd + self.tui_arg +
-                   ['-ex', 'target remote :{}'.format(self.gdb_port),
-                    self.elf_name])
-
-        self.run_server_and_client(server_cmd, gdb_cmd)
-
-    def do_debugserver(self, **kwargs):
-        cmd = (self.openocd_cmd +
-               ['-f', self.openocd_config,
-                '-c', 'init',
-                '-c', 'targets',
-                '-c', 'reset halt'])
-        check_call(cmd, self.debug)
-
-
-DEFAULT_PYOCD_GDB_PORT = 3333
-
-
-class PyOcdBinaryRunner(ZephyrBinaryRunner):
-    '''Runner front-end for pyocd-flashtool.'''
-
-    def __init__(self, target, flashtool='pyocd-flashtool',
-                 gdb=None, gdbserver='pyocd-gdbserver',
-                 gdb_port=DEFAULT_PYOCD_GDB_PORT, tui=None,
-                 bin_name=None, elf_name=None,
-                 board_id=None, daparg=None, debug=False):
-        super(PyOcdBinaryRunner, self).__init__(debug=debug)
-
-        self.target_args = ['-t', target]
-        self.flashtool = flashtool
-        self.gdb_cmd = [gdb] if gdb is not None else None
-        self.gdbserver = gdbserver
-        self.gdb_port = gdb_port
-        self.tui_args = [tui] if tui is not None else []
-        self.bin_name = bin_name
-        self.elf_name = elf_name
-
-        board_args = []
-        if board_id is not None:
-            board_args = ['-b', board_id]
-        self.board_args = board_args
-
-        daparg_args = []
-        if daparg is not None:
-            daparg_args = ['-da', daparg]
-        self.daparg_args = daparg_args
-
-    def replaces_shell_script(shell_script, command):
-        return (command in {'flash', 'debug', 'debugserver'} and
-                shell_script == 'pyocd.sh')
-
-    def port_args(self):
-        return ['-p', str(self.gdb_port)]
-
-    def create_from_env(command, debug):
-        '''Create runner from environment.
-
-        Required:
-
-        - PYOCD_TARGET: target override
-
-        Optional:
-
-        - PYOCD_DAPARG: arguments to pass to pyocd tool, default is none
-        - PYOCD_BOARD_ID: ID of board to flash, default is to prompt
-
-        Required for 'flash':
-
-        - O: build output directory
-        - KERNEL_BIN_NAME: name of kernel binary
-
-        Optional for 'flash':
-
-        - PYOCD_FLASHTOOL: flash tool path, defaults to pyocd-flashtool
-
-        Required for 'debug':
-
-        - O: build output directory
-        - KERNEL_ELF_NAME
-        - GDB: gdb executable
-
-        Optional for 'debug', 'debugserver':
-
-        - TUI: one additional argument to GDB (e.g. -tui)
-        - GDB_PORT: pyocd gdb port, defaults to 3333
-        - PYOCD_GDBSERVER: gdb server executable, defaults to pyocd-gdbserver
-        '''
-        target = get_env_or_bail('PYOCD_TARGET')
-
-        o = os.environ.get('O', None)
-        bin_ = os.environ.get('KERNEL_BIN_NAME', None)
-        elf = os.environ.get('KERNEL_ELF_NAME', None)
-        bin_name = None
-        elf_name = None
-        if o is not None:
-            if bin_ is not None:
-                bin_name = path.join(o, bin_)
-            if elf is not None:
-                elf_name = path.join(o, elf)
-
-        flashtool = os.environ.get('PYOCD_FLASHTOOL', 'pyocd-flashtool')
-        board_id = os.environ.get('PYOCD_BOARD_ID', None)
-        daparg = os.environ.get('PYOCD_DAPARG', None)
-        gdb = os.environ.get('GDB', None)
-        gdbserver = os.environ.get('PYOCD_GDBSERVER', 'pyocd-gdbserver')
-        gdb_port = os.environ.get('GDB_PORT', DEFAULT_PYOCD_GDB_PORT)
-        tui = os.environ.get('TUI', None)
-
-        return PyOcdBinaryRunner(target, flashtool=flashtool, gdb=gdb,
-                                 gdbserver=gdbserver, gdb_port=gdb_port,
-                                 tui=tui, bin_name=bin_name, elf_name=elf_name,
-                                 board_id=board_id, daparg=daparg, debug=debug)
-
-    def run(self, command, **kwargs):
-        if command not in {'flash', 'debug', 'debugserver'}:
-            raise ValueError('{} is not supported'.format(command))
-
-        if command == 'flash':
-            self.flash(**kwargs)
-        else:
-            self.debug_debugserver(command, **kwargs)
-
-    def flash(self, **kwargs):
-        if self.bin_name is None:
-            raise ValueError('Cannot flash; bin_name is missing')
-
-        cmd = ([self.flashtool] +
-               self.daparg_args +
-               self.target_args +
-               self.board_args +
-               [self.bin_name])
-
-        print('Flashing Target Device')
-        check_call(cmd, self.debug)
-
-    def print_gdbserver_message(self):
-        print('pyOCD GDB server running on port {}'.format(self.gdb_port))
-
-    def debug_debugserver(self, command, **kwargs):
-        server_cmd = ([self.gdbserver] +
-                      self.daparg_args +
-                      self.port_args() +
-                      self.target_args +
-                      self.board_args)
-
-        if command == 'debugserver':
-            self.print_gdbserver_message()
-            check_call(server_cmd, self.debug)
-        else:
-            if self.gdb_cmd is None:
-                raise ValueError('Cannot debug; gdb is missing')
-            if self.elf_name is None:
-                raise ValueError('Cannot debug; elf is missing')
-            client_cmd = (self.gdb_cmd +
-                          self.tui_args +
-                          [self.elf_name] +
-                          ['-ex', 'target remote :{}'.format(self.gdb_port),
-                           '-ex', 'load',
-                           '-ex', 'monitor reset halt'])
-            self.print_gdbserver_message()
-            self.run_server_and_client(server_cmd, client_cmd)
+from runner.core import ZephyrBinaryRunner, get_env_bool_or
 
 
 # TODO: Stop using environment variables.
@@ -1044,7 +34,9 @@
                                                             debug)
     except ValueError:
         # Unsupported; fall back on shell script.
-        check_call([shell_script_full, command], debug)
+        print('Unsupported, falling back on shell script',
+              file=sys.stderr)
+        subprocess.check_call([shell_script_full, command])
         return
 
     runner.run(command)