buildman: Allow adjusting board config on the fly

Add a -a option to specify changes to the config before the build
commences. For example

   buildman -a ~CONFIG_CMDLINE

disables CONFIG_CMDLINE before doing the build.

This makes it easier to try things out as well as to write tests without
creating a new board or manually manging the .config file.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/buildman/README b/tools/buildman/README
index ce27788..bafb3b0 100644
--- a/tools/buildman/README
+++ b/tools/buildman/README
@@ -1095,6 +1095,55 @@
 specify the output directory with -o when using -w.
 
 
+Changing the configuration
+==========================
+
+Sometimes it is useful to change the CONFIG options for a build on the fly. This
+can be used to build a board (or multiple) with a few changes to see the impact.
+The -a option supports this:
+
+   -a <cfg>
+
+where <cfg> is a CONFIG option (with or without the CONFIG_ prefix) to enable.
+For example:
+
+    buildman -a CMD_SETEXPR_FMT
+
+will build with CONFIG_CMD_SETEXPR_FMT enabled.
+
+You can disable options by preceding them with tilde (~). You can specify the
+-a option multiple times:
+
+    buildman -a CMD_SETEXPR_FMT -a ~CMDLINE
+
+Some options have values, in which case you can change them:
+
+    buildman -a 'BOOTCOMMAND="echo hello"' CONFIG_SYS_LOAD_ADDR=0x1000
+
+Note that you must put quotes around string options and the whole thing must be
+in single quotes, to make sure the shell leave it alone.
+
+If you try to set an option that does not exist, or that cannot be changed for
+some other reason (e.g. it is 'selected' by another option), then buildman
+shows an error:
+
+   buildman --board sandbox -a FRED
+   Building current source for 1 boards (1 thread, 32 jobs per thread)
+       0    0    0 /1       -1      (starting)errs
+   Some CONFIG adjustments did not take effect. This may be because
+   the request CONFIGs do not exist or conflict with others.
+
+   Failed adjustments:
+
+   FRED                  Missing expected line: CONFIG_FRED=y
+
+
+One major caveat with this feature with branches (-b) is that buildman does not
+name the output directories differently when you change the configuration, so
+doing the same build again with different configuration will not trigger a
+rebuild. You can use -f to work around that.
+
+
 Other options
 =============
 
diff --git a/tools/buildman/builder.py b/tools/buildman/builder.py
index 122f0d1..720bbb2 100644
--- a/tools/buildman/builder.py
+++ b/tools/buildman/builder.py
@@ -250,7 +250,7 @@
                  mrproper=False, per_board_out_dir=False,
                  config_only=False, squash_config_y=False,
                  warnings_as_errors=False, work_in_output=False,
-                 test_thread_exceptions=False):
+                 test_thread_exceptions=False, adjust_cfg=None):
         """Create a new Builder object
 
         Args:
@@ -280,6 +280,15 @@
             test_thread_exceptions: Uses for tests only, True to make the
                 threads raise an exception instead of reporting their result.
                 This simulates a failure in the code somewhere
+            adjust_cfg_list (list of str): List of changes to make to .config
+                file before building. Each is one of (where C is the config
+                option with or without the CONFIG_ prefix)
+
+                    C to enable C
+                    ~C to disable C
+                    C=val to set the value of C (val must have quotes if C is
+                        a string Kconfig
+
         """
         self.toolchains = toolchains
         self.base_dir = base_dir
@@ -315,6 +324,8 @@
         self.squash_config_y = squash_config_y
         self.config_filenames = BASE_CONFIG_FILENAMES
         self.work_in_output = work_in_output
+        self.adjust_cfg = adjust_cfg
+
         if not self.squash_config_y:
             self.config_filenames += EXTRA_CONFIG_FILENAMES
         self._terminated = False
@@ -1747,6 +1758,7 @@
             job.commits = commits
             job.keep_outputs = keep_outputs
             job.work_in_output = self.work_in_output
+            job.adjust_cfg = self.adjust_cfg
             job.step = self._step
             if self.num_threads:
                 self.queue.put(job)
diff --git a/tools/buildman/builderthread.py b/tools/buildman/builderthread.py
index 0faa3ac..ecb285c 100644
--- a/tools/buildman/builderthread.py
+++ b/tools/buildman/builderthread.py
@@ -9,6 +9,7 @@
 import sys
 import threading
 
+from buildman import cfgutil
 from patman import command
 from patman import gitutil
 
@@ -130,7 +131,8 @@
                 **kwargs)
 
     def RunCommit(self, commit_upto, brd, work_dir, do_config, config_only,
-                  force_build, force_build_failures, work_in_output):
+                  force_build, force_build_failures, work_in_output,
+                  adjust_cfg):
         """Build a particular commit.
 
         If the build is already done, and we are not forcing a build, we skip
@@ -147,6 +149,13 @@
                 failure
             work_in_output: Use the output directory as the work directory and
                 don't write to a separate output directory.
+            adjust_cfg (list of str): List of changes to make to .config file
+                before building. Each is one of (where C is either CONFIG_xxx
+                or just xxx):
+                     C to enable C
+                     ~C to disable C
+                     C=val to set the value of C (val must have quotes if C is
+                         a string Kconfig
 
         Returns:
             tuple containing:
@@ -261,7 +270,8 @@
                         os.remove(fname)
 
                 # If we need to reconfigure, do that now
-                if do_config:
+                cfg_file = os.path.join(out_dir, '.config')
+                if do_config or adjust_cfg:
                     config_out = ''
                     if self.mrproper:
                         result = self.Make(commit, brd, 'mrproper', cwd,
@@ -271,11 +281,19 @@
                             *(args + config_args), env=env)
                     config_out += result.combined
                     do_config = False   # No need to configure next time
+                    if adjust_cfg:
+                        cfgutil.adjust_cfg_file(cfg_file, adjust_cfg)
                 if result.return_code == 0:
                     if config_only:
                         args.append('cfg')
                     result = self.Make(commit, brd, 'build', cwd, *args,
                             env=env)
+                    if adjust_cfg:
+                        errs = cfgutil.check_cfg_file(cfg_file, adjust_cfg)
+                        if errs:
+                            print('errs', errs)
+                            result.stderr += errs
+                            result.return_code = 1
                 result.stderr = result.stderr.replace(src_dir + '/', '')
                 if self.builder.verbose_build:
                     result.stdout = config_out + result.stdout
@@ -486,7 +504,7 @@
                         work_dir, do_config, self.builder.config_only,
                         force_build or self.builder.force_build,
                         self.builder.force_build_failures,
-                        work_in_output=job.work_in_output)
+                        job.work_in_output, job.adjust_cfg)
                 failed = result.return_code or result.stderr
                 did_config = do_config
                 if failed and not do_config:
@@ -495,7 +513,7 @@
                     if self.builder.force_config_on_failure:
                         result, request_config = self.RunCommit(commit_upto,
                             brd, work_dir, True, False, True, False,
-                            work_in_output=job.work_in_output)
+                            job.work_in_output, job.adjust_cfg)
                         did_config = True
                 if not self.builder.force_reconfig:
                     do_config = request_config
@@ -540,8 +558,8 @@
             # Just build the currently checked-out build
             result, request_config = self.RunCommit(None, brd, work_dir, True,
                         self.builder.config_only, True,
-                        self.builder.force_build_failures,
-                        work_in_output=job.work_in_output)
+                        self.builder.force_build_failures, job.work_in_output,
+                        job.adjust_cfg)
             result.commit_upto = 0
             self._WriteResult(result, job.keep_outputs, job.work_in_output)
             self._SendResult(result)
diff --git a/tools/buildman/cmdline.py b/tools/buildman/cmdline.py
index 092a82f..8586bdf 100644
--- a/tools/buildman/cmdline.py
+++ b/tools/buildman/cmdline.py
@@ -13,6 +13,8 @@
             args: command lin arguments
     """
     parser = OptionParser()
+    parser.add_option('-a', '--adjust-cfg', type=str, action='append',
+          help='Adjust the Kconfig settings in .config before building')
     parser.add_option('-A', '--print-prefix', action='store_true',
           help='Print the tool-chain prefix for a board (CROSS_COMPILE=)')
     parser.add_option('-b', '--branch', type='string',
diff --git a/tools/buildman/control.py b/tools/buildman/control.py
index fd9664c..eee8113 100644
--- a/tools/buildman/control.py
+++ b/tools/buildman/control.py
@@ -10,6 +10,7 @@
 
 from buildman import board
 from buildman import bsettings
+from buildman import cfgutil
 from buildman import toolchain
 from buildman.builder import Builder
 from patman import command
@@ -321,6 +322,8 @@
             output_dir = os.path.join(options.output_dir, dirname)
         if clean_dir and os.path.exists(output_dir):
             shutil.rmtree(output_dir)
+    adjust_cfg = cfgutil.convert_list_to_dict(options.adjust_cfg)
+
     builder = Builder(toolchains, output_dir, options.git_dir,
             options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
             show_unknown=options.show_unknown, step=options.step,
@@ -332,7 +335,8 @@
             squash_config_y=not options.preserve_config_y,
             warnings_as_errors=options.warnings_as_errors,
             work_in_output=options.work_in_output,
-            test_thread_exceptions=test_thread_exceptions)
+            test_thread_exceptions=test_thread_exceptions,
+            adjust_cfg=adjust_cfg)
     builder.force_config_on_failure = not options.quick
     if make_func:
         builder.do_make = make_func