Set pw_command_launcher from build_example.py CLI (#23702)

* Set pw_command_launcher from build_example.py CLI

This will allow effortless ccache setup like this:

./scripts/build/build_examples.py \
  --target linux-x64-light \
	--pw-command-launcher=ccache \
	build

* Do not use setter pattern for simple flag

* Keep all builder options in dedicated dataclass

* Fix unit test module import
diff --git a/scripts/build/build/__init__.py b/scripts/build/build/__init__.py
index fc0ee05..c363f6c 100644
--- a/scripts/build/build/__init__.py
+++ b/scripts/build/build/__init__.py
@@ -5,6 +5,7 @@
 from typing import Sequence
 
 from .targets import BUILD_TARGETS
+from builders.builder import BuilderOptions
 
 
 class BuildSteps(Enum):
@@ -24,8 +25,7 @@
         self.output_prefix = output_prefix
         self.completed_steps = set()
 
-    def SetupBuilders(self, targets: Sequence[str],
-                      enable_flashbundle: bool):
+    def SetupBuilders(self, targets: Sequence[str], options: BuilderOptions):
         """
         Configures internal builders for the given platform/board/app
         combination.
@@ -35,7 +35,8 @@
         for target in targets:
             found = False
             for choice in BUILD_TARGETS:
-                builder = choice.Create(target, self.runner, self.repository_path, self.output_prefix, enable_flashbundle)
+                builder = choice.Create(target, self.runner, self.repository_path,
+                                        self.output_prefix, options)
                 if builder:
                     self.builders.append(builder)
                     found = True
diff --git a/scripts/build/build/target.py b/scripts/build/build/target.py
index 28f568b..8d28190 100644
--- a/scripts/build/build/target.py
+++ b/scripts/build/build/target.py
@@ -46,6 +46,8 @@
 from dataclasses import dataclass
 from typing import Any, Dict, List, Iterable, Optional
 
+from builders.builder import BuilderOptions
+
 
 report_rejected_parts = True
 
@@ -330,7 +332,7 @@
         return _StringIntoParts(value, suffix, self.fixed_targets, self.modifiers)
 
     def Create(self, name: str, runner, repository_path: str, output_prefix: str,
-               enable_flashbundle: bool):
+               builder_options: BuilderOptions):
 
         parts = self.StringIntoTargetParts(name)
 
@@ -348,6 +350,6 @@
         builder.identifier = name
         builder.output_dir = os.path.join(output_prefix, name)
         builder.chip_dir = repository_path
-        builder.enable_flashbundle(enable_flashbundle)
+        builder.options = builder_options
 
         return builder
diff --git a/scripts/build/build/test_target.py b/scripts/build/build/test_target.py
index 334e567..02ded28 100755
--- a/scripts/build/build/test_target.py
+++ b/scripts/build/build/test_target.py
@@ -14,15 +14,11 @@
 # limitations under the License.
 
 import unittest
+import sys
+import os
 
-try:
-    from build.target import *
-except:
-    import sys
-    import os
-
-    sys.path.append(os.path.abspath(os.path.dirname(__file__)))
-    from target import *
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+from build.target import BuildTarget, TargetPart  # noqa: E402
 
 
 class FakeBuilder:
diff --git a/scripts/build/build_examples.py b/scripts/build/build_examples.py
index 5f8a7ef..5792bfa 100755
--- a/scripts/build/build_examples.py
+++ b/scripts/build/build_examples.py
@@ -22,7 +22,7 @@
 import coloredlogs
 
 import build
-from glob_matcher import GlobMatcher
+from builders.builder import BuilderOptions
 from runner import PrintOnlyRunner, ShellRunner
 
 sys.path.append(os.path.abspath(os.path.dirname(__file__)))
@@ -107,10 +107,15 @@
     default=False,
     is_flag=True,
     help='Skip timestaps in log output')
+@click.option(
+    '--pw-command-launcher',
+    help=(
+        'Set pigweed command launcher. E.g.: "--pw-command-launcher=ccache" '
+        'for using ccache when building examples.'))
 @click.pass_context
 def main(context, log_level, target, repo,
          out_prefix, clean, dry_run, dry_run_output, enable_flashbundle,
-         no_log_timestamps):
+         no_log_timestamps, pw_command_launcher):
     # Ensures somewhat pretty logging of what is going on
     log_fmt = '%(asctime)s %(levelname)-7s %(message)s'
     if no_log_timestamps:
@@ -135,8 +140,10 @@
 
     context.obj = build.Context(
         repository_path=repo, output_prefix=out_prefix, runner=runner)
-    context.obj.SetupBuilders(
-        targets=requested_targets, enable_flashbundle=enable_flashbundle)
+    context.obj.SetupBuilders(targets=requested_targets, options=BuilderOptions(
+        enable_flashbundle=enable_flashbundle,
+        pw_command_launcher=pw_command_launcher,
+    ))
 
     if clean:
         context.obj.CleanOutputDirectories()
diff --git a/scripts/build/builders/builder.py b/scripts/build/builders/builder.py
index c221e76..99bc925 100644
--- a/scripts/build/builders/builder.py
+++ b/scripts/build/builders/builder.py
@@ -17,27 +17,33 @@
 import shutil
 import tarfile
 from abc import ABC, abstractmethod
+from dataclasses import dataclass
+
+
+@dataclass
+class BuilderOptions:
+    # Enable flashbundle generation stage
+    enable_flashbundle: bool = False
+    # Allow to wrap default build command
+    pw_command_launcher: str = None
 
 
 class Builder(ABC):
     """Generic builder base class for CHIP.
 
-    Provides ability to boostrap and copy output artifacts and subclasses can use
-    a generic shell runner.
+    Provides ability to bootstrap and copy output artifacts and subclasses can
+    use a generic shell runner.
 
     """
 
     def __init__(self, root, runner):
         self.root = os.path.abspath(root)
         self._runner = runner
-        self._enable_flashbundle = False
 
         # Set post-init once actual build target is known
         self.identifier = None
         self.output_dir = None
-
-    def enable_flashbundle(self, enable_flashbundle: bool):
-        self._enable_flashbundle = enable_flashbundle
+        self.options = BuilderOptions()
 
     @abstractmethod
     def generate(self):
@@ -81,13 +87,13 @@
 
     def outputs(self):
         artifacts = self.build_outputs()
-        if self._enable_flashbundle:
+        if self.options.enable_flashbundle:
             artifacts.update(self.flashbundle())
         return artifacts
 
     def build(self):
         self._build()
-        if self._enable_flashbundle:
+        if self.options.enable_flashbundle:
             self._generate_flashbundle()
 
     def _Execute(self, cmdarray, title=None):
diff --git a/scripts/build/builders/gn.py b/scripts/build/builders/gn.py
index 298a9e9..bacd87d 100644
--- a/scripts/build/builders/gn.py
+++ b/scripts/build/builders/gn.py
@@ -60,7 +60,10 @@
             '--root=%s' % self.root
         ]
 
-        extra_args = self.GnBuildArgs()
+        extra_args = []
+        if self.options.pw_command_launcher:
+            extra_args.append('pw_command_launcher="%s"' % self.options.pw_command_launcher)
+        extra_args.extend(self.GnBuildArgs() or [])
         if extra_args:
             cmd += ['--args=%s' % ' '.join(extra_args)]