scripts: west commands to support --domain

This commit extends the west commands build, flash, and debug to support
--domain when having multiple domains (images) defined in a domains.yaml
build file.

The domains.yaml uses the following yaml format to specify the
build directory of each domain in the multi image build:
> default: <domain-n>
> domains:
>   <domain-1>:
>     build_dir: <build_dir-domain-1>
>   <domain-2>:
>     build_dir: <build_dir-domain-2>
>   ...

`west <build|flash|debug>` has been extended to support
`--domain <domain>`.

`west build` calls CMake to create the build system, and if `--domain`
is given, then the build tool will be invoked afterwards for the
specified domain.

`west flash` will default flash all domains, but `--domain <domain>`
argument can be used to select a specific domain to flash, for example:
> west flash --domain mcuboot

`west debug` only a single domain can be debugged at any given time.
If `--domain` is not specified, then the default domain specified in the
domains.yml file will be used.
Users can still select a different domain, for example with:
> west debug --domain mcuboot

Signed-off-by: Torsten Rasmussen <Torsten.Rasmussen@nordicsemi.no>
diff --git a/scripts/west_commands/domains.py b/scripts/west_commands/domains.py
new file mode 100644
index 0000000..9301c76
--- /dev/null
+++ b/scripts/west_commands/domains.py
@@ -0,0 +1,139 @@
+# Copyright (c) 2022 Nordic Semiconductor ASA
+#
+# SPDX-License-Identifier: Apache-2.0
+
+'''Domain handling for west extension commands.
+
+This provides parsing of domains yaml file and creation of objects of the
+Domain class.
+'''
+
+import yaml
+import pykwalify.core
+from west import log
+
+DOMAINS_SCHEMA = '''
+## A pykwalify schema for basic validation of the structure of a
+## domains YAML file.
+##
+# The domains.yaml file is a simple list of domains from a multi image build
+# along with the default domain to use.
+type: map
+mapping:
+  default:
+    required: true
+    type: str
+  build_dir:
+    required: true
+    type: str
+  domains:
+    required: false
+    type: seq
+    sequence:
+      - type: map
+        mapping:
+          name:
+            required: true
+            type: str
+          build_dir:
+            required: true
+            type: str
+'''
+
+schema = yaml.safe_load(DOMAINS_SCHEMA)
+
+
+class Domains:
+
+    def __init__(self, data):
+        self._domains = []
+        self._domain_names = []
+        self._domain_default = []
+
+        self._build_dir = data.get('build_dir')
+        domain_list = data.get('domains')
+        if not domain_list:
+            log.wrn("no domains defined; this probably won't work")
+
+        for d in domain_list:
+            domain = Domain(d['name'], d['build_dir'])
+            self._domains.append(domain)
+            self._domain_names.append(domain.name)
+            if domain.name == data['default']:
+                self._default_domain = domain
+
+    @staticmethod
+    def from_file(domains_file):
+        '''Load domains from domains.yaml.
+
+        Exception raised:
+           - ``FileNotFoundError`` if the domains file is not found.
+        '''
+        try:
+            with open(domains_file, 'r') as f:
+                domains = yaml.safe_load(f.read())
+        except FileNotFoundError:
+            log.die(f'domains.yaml file not found: {domains_file}')
+
+        try:
+            pykwalify.core.Core(source_data=domains, schema_data=schema)\
+                .validate()
+        except pykwalify.errors.SchemaError:
+            log.die(f'ERROR: Malformed yaml in file: {domains_file}')
+
+        return Domains(domains)
+
+    @staticmethod
+    def from_data(domains_data):
+        '''Load domains from domains dictionary.
+        '''
+        return Domains(domains_data)
+
+    def get_domains(self, names=None):
+        ret = []
+
+        if not names:
+            return self._domains
+
+        for n in names:
+            found = False
+            for d in self._domains:
+                if n == d.name:
+                    ret.append(d)
+                    found = True
+                    break
+            # Getting here means the domain was not found.
+            # Todo: throw an error.
+            if not found:
+                log.die(f'domain {n} not found, '
+                        f'valid domains are:', *self._domain_names)
+        return ret
+
+    def get_default_domain(self):
+        return self._default_domain
+
+    def get_top_build_dir(self):
+        return self._build_dir
+
+
+class Domain:
+
+    def __init__(self, name, build_dir):
+        self.name = name
+        self.build_dir = build_dir
+
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, value):
+        self._name = value
+
+    @property
+    def build_dir(self):
+        return self._build_dir
+
+    @build_dir.setter
+    def build_dir(self, value):
+        self._build_dir = value