scripts: west_commands: Support out-of-tree runners

Add runners entry to the module schema and import the file specified.

Every class that inherits from ZephyrBinaryRunner will be discovered and
added to runners list.

Signed-off-by: Pieter De Gendt <pieter.degendt@basalte.be>
diff --git a/scripts/west_commands/run_common.py b/scripts/west_commands/run_common.py
index adfd292..e5492ad 100644
--- a/scripts/west_commands/run_common.py
+++ b/scripts/west_commands/run_common.py
@@ -6,6 +6,7 @@
 '''Common code used by commands which execute runners.
 '''
 
+import importlib.util
 import re
 import argparse
 import logging
@@ -28,7 +29,8 @@
 from runners.core import BuildConfiguration
 import yaml
 
-from zephyr_ext_common import ZEPHYR_SCRIPTS
+import zephyr_module
+from zephyr_ext_common import ZEPHYR_BASE, ZEPHYR_SCRIPTS
 
 # Runners depend on edtlib. Make sure the copy in the tree is
 # available to them before trying to import any.
@@ -107,6 +109,13 @@
     priority: int = IGNORED_RUN_ONCE_PRIORITY
     yaml: object = None
 
+def import_from_path(module_name, file_path):
+    spec = importlib.util.spec_from_file_location(module_name, file_path)
+    module = importlib.util.module_from_spec(spec)
+    sys.modules[module_name] = module
+    spec.loader.exec_module(module)
+    return module
+
 def command_verb(command):
     return "flash" if command.name == "flash" else "debug"
 
@@ -197,6 +206,14 @@
         dump_context(command, user_args, user_runner_args)
         return
 
+    # Import external module runners
+    for module in zephyr_module.parse_modules(ZEPHYR_BASE, command.manifest):
+        runners_ext = module.meta.get("runners", [])
+        for runner in runners_ext:
+            import_from_path(
+                module.meta.get("name", "runners_ext"), Path(module.project) / runner["file"]
+            )
+
     build_dir = get_build_dir(user_args)
     if not user_args.skip_rebuild:
         rebuild(command, build_dir, user_args)
diff --git a/scripts/zephyr_module.py b/scripts/zephyr_module.py
index ae2c35c..22369aa 100755
--- a/scripts/zephyr_module.py
+++ b/scripts/zephyr_module.py
@@ -174,6 +174,15 @@
             type: seq
             sequence:
               - type: str
+  runners:
+    required: false
+    type: seq
+    sequence:
+      - type: map
+        mapping:
+          file:
+            required: true
+            type: str
 '''
 
 MODULE_YML_PATH = PurePath('zephyr/module.yml')