west: parse options from testcase.yaml/sample.yaml during build

Allow the use of build configurations from testcase/sample yaml files.
This addition makes it easy to build a sample or a test with the options
provided in the test file by pointing to the test section to be built on
the command line of `west build`.

Fixes #31993

Signed-off-by: Anas Nashif <anas.nashif@intel.com>
diff --git a/scripts/west_commands/build.py b/scripts/west_commands/build.py
index 9fa5f81..96cd9b5 100644
--- a/scripts/west_commands/build.py
+++ b/scripts/west_commands/build.py
@@ -7,6 +7,7 @@
 import pathlib
 import shlex
 import sys
+import yaml
 
 from west import log
 from west.configuration import config
@@ -112,6 +113,9 @@
         group.add_argument('-t', '--target',
                            help='''run build system target TARGET
                            (try "-t usage")''')
+        group.add_argument('-T', '--test-item',
+                           help='''Build based on test data in testcase.yaml
+                           or sample.yaml''')
         group.add_argument('-o', '--build-opt', default=[], action='append',
                            help='''options to pass to the build tool
                            (make or ninja); may be given more than once''')
@@ -135,6 +139,9 @@
         # Store legacy -s option locally
         source_dir = self.args.source_dir
         self._parse_remainder(remainder)
+        # Parse testcase.yaml or sample.yaml files for additional options.
+        if self.args.test_item:
+            self._parse_test_item()
         if source_dir:
             if self.args.source_dir:
                 log.die("source directory specified twice:({} and {})".format(
@@ -207,6 +214,7 @@
     def _parse_remainder(self, remainder):
         self.args.source_dir = None
         self.args.cmake_opts = None
+
         try:
             # Only one source_dir is allowed, as the first positional arg
             if remainder[0] != _ARG_SEPARATOR:
@@ -221,6 +229,37 @@
         except IndexError:
             return
 
+    def _parse_test_item(self):
+        for yp in ['sample.yaml', 'testcase.yaml']:
+            yf = os.path.join(self.args.source_dir, yp)
+            if not os.path.exists(yf):
+                continue
+            with open(yf, 'r') as stream:
+                try:
+                    y = yaml.safe_load(stream)
+                except yaml.YAMLError as exc:
+                    log.die(exc)
+            tests = y.get('tests')
+            if not tests:
+                continue
+            item = tests.get(self.args.test_item)
+            if not item:
+                continue
+
+            for data in ['extra_args', 'extra_configs']:
+                extra = item.get(data)
+                if not extra:
+                    continue
+                if isinstance(extra, str):
+                    arg_list = extra.split(" ")
+                else:
+                    arg_list = extra
+                args = ["-D{}".format(arg.replace('"', '')) for arg in arg_list]
+                if self.args.cmake_opts:
+                    self.args.cmake_opts.extend(args)
+                else:
+                    self.args.cmake_opts = args
+
     def _sanity_precheck(self):
         app = self.args.source_dir
         if app: