build: interpose zephyr_flash_debug.py in flash target

Zephyr board flashing and debugging is done via shell scripts. It
would improve the CMake transition to remove the shell dependency.

Add zephyr_flash_debug.py to allow phasing out the shell scripts.
This takes two arguments:

- a command (eventually flash, debug, and debugserver, but just flash
  for now)

- the path to the corresponding shell script

zephyr_flash_debug.py runs the command in pure Python if it
knows how. Otherwise, it falls back on the shell script. In
this patch, it always falls back. Subsequent patches add support
for existing flash backends.

Invoke zephyr_flash_debug.py from the Makefile flash target, but only
if USE_ZEPHYR_FLASH_DEBUG_SHELL is empty. This lets users keep existing
behavior in case of issues, and can be removed later once the Python
script is more widely tested.

Signed-off-by: Marti Bolivar <marti.bolivar@linaro.org>
diff --git a/Makefile b/Makefile
index 9a46330..1226310 100644
--- a/Makefile
+++ b/Makefile
@@ -1374,9 +1374,15 @@
 endif
 
 ifneq ($(FLASH_SCRIPT),)
+ifeq ($(USE_ZEPHYR_FLASH_DEBUG_SHELL),)
+flash: zephyr
+	@echo "Flashing $(BOARD_NAME)"
+	$(Q)$(srctree)/scripts/support/zephyr_flash_debug.py flash $(srctree)/scripts/support/$(FLASH_SCRIPT)
+else
 flash: zephyr
 	@echo "Flashing $(BOARD_NAME)"
 	$(Q)$(CONFIG_SHELL) $(srctree)/scripts/support/$(FLASH_SCRIPT) flash
+endif
 else
 flash: FORCE
 	@echo Flashing not supported with this board.
diff --git a/scripts/support/zephyr_flash_debug.py b/scripts/support/zephyr_flash_debug.py
new file mode 100755
index 0000000..113a1b7
--- /dev/null
+++ b/scripts/support/zephyr_flash_debug.py
@@ -0,0 +1,105 @@
+#! /usr/bin/env python3
+
+# Copyright (c) 2017 Linaro Limited.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+"""Zephyr board flashing script
+
+This script is a transparent replacement for an existing Zephyr flash
+script. If it can invoke the flashing tools natively, it will do so; otherwise,
+it delegates to the shell script passed as second argument."""
+
+import abc
+from os import path
+import os
+import sys
+import subprocess
+
+
+def get_env_bool_or(env_var, default_value):
+    try:
+        return bool(int(os.environ[env_var]))
+    except KeyError:
+        return default_value
+
+
+def check_call(cmd, debug):
+    if debug:
+        print(' '.join(cmd))
+    subprocess.check_call(cmd)
+
+
+class ZephyrBinaryFlasher(abc.ABC):
+    '''Abstract superclass for flasher objects.'''
+
+    def __init__(self, debug=False):
+        self.debug = debug
+
+    @staticmethod
+    def create_for_shell_script(shell_script, debug):
+        '''Factory for using as a drop-in replacement to a shell script.
+
+        Get flasher instance to use in place of shell_script, deriving
+        flasher configuration from the environment.'''
+        for sub_cls in ZephyrBinaryFlasher.__subclasses__():
+            if sub_cls.replaces_shell_script(shell_script):
+                return sub_cls.create_from_env(debug)
+        raise ValueError('no flasher replaces script {}'.format(shell_script))
+
+    @staticmethod
+    @abc.abstractmethod
+    def replaces_shell_script(shell_script):
+        '''Check if this flasher class replaces FLASH_SCRIPT=shell_script.'''
+
+    @staticmethod
+    @abc.abstractmethod
+    def create_from_env(debug):
+        '''Create new flasher instance from environment variables.
+
+        This class must be able to replace the current FLASH_SCRIPT. The
+        environment variables expected by that script are used to build
+        the flasher in a backwards-compatible manner.'''
+
+    @abc.abstractmethod
+    def flash(self, **kwargs):
+        '''Flash the board.'''
+
+
+# TODO: Stop using environment variables.
+#
+# Migrate the build system so we can use an argparse.ArgumentParser and
+# per-flasher subparsers, so invoking the script becomes something like:
+#
+#   python zephyr_flash_debug.py openocd --openocd-bin=/openocd/path ...
+#
+# For now, maintain compatibility.
+def flash(shell_script_full, debug):
+    shell_script = path.basename(shell_script_full)
+    try:
+        flasher = ZephyrBinaryFlasher.create_for_shell_script(shell_script,
+                                                              debug)
+    except ValueError:
+        # Can't create a flasher; fall back on shell script.
+        check_call([shell_script_full, 'flash'], debug)
+        return
+
+    flasher.flash()
+
+
+if __name__ == '__main__':
+    debug = True
+    try:
+        debug = get_env_bool_or('KBUILD_VERBOSE', False)
+        if len(sys.argv) != 3 or sys.argv[1] != 'flash':
+            raise ValueError('usage: {} flash path-to-script'.format(
+                sys.argv[0]))
+        flash(sys.argv[2], debug)
+    except Exception as e:
+        if debug:
+            raise
+        else:
+            print('Error: {}'.format(e), file=sys.stderr)
+            print('Re-run with KBUILD_VERBOSE=1 for a stack trace.',
+                  file=sys.stderr)
+            sys.exit(1)