moveconfig: Allow querying board configuration

It is useful to be able to find out which boards define a particular
option, or combination of options. This is not as easy as grepping the
defconfig files since many options are implied by others.

Add a -f option to the moveconfig tool to permit this. Update the
documentation to cover this, including a better title for the doc page.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/doc/develop/moveconfig.rst b/doc/develop/moveconfig.rst
index dcd4d92..2f53ea5 100644
--- a/doc/develop/moveconfig.rst
+++ b/doc/develop/moveconfig.rst
@@ -1,7 +1,7 @@
 .. SPDX-License-Identifier: GPL-2.0+
 
-moveconfig
-==========
+moveconfig - Migrating and querying CONFIG options
+==================================================
 
 Since Kconfig was introduced to U-Boot, we have worked on moving
 config options from headers to Kconfig (defconfig).
@@ -129,6 +129,24 @@
        ./tools/moveconfig.py -Cy CONFIG_CMD_FPGAD -d -
 
 
+Finding boards with particular CONFIG combinations
+--------------------------------------------------
+
+You can use `moveconfig.py` to figure out which boards have a CONFIG enabled, or
+which do not. To use it, first build a database::
+
+    ./tools/moveconfig.py -b
+
+Then you can run queries using the `-f` flag followed by a list of CONFIG terms.
+Each term is CONFIG name, with or without a tilde (~) prefix. The tool searches
+for boards which match the CONFIG name, or do not match if tilde is used. For
+example, to find boards which enabled CONFIG_SCSI but not CONFIG_BLK::
+
+    tools/moveconfig.py -f SCSI ~BLK
+    3 matches
+    pg_wcom_seli8_defconfig highbank_defconfig pg_wcom_expu1_defconfig
+
+
 Finding implied CONFIGs
 -----------------------
 
@@ -235,6 +253,9 @@
   Specify a file containing a list of defconfigs to move.  The defconfig
   files can be given with shell-style wildcards. Use '-' to read from stdin.
 
+ -f, --find
+   Find boards with a given config combination
+
  -n, --dry-run
    Perform a trial run that does not make any changes.  It is useful to
    see what is going to happen before one actually runs it.
diff --git a/tools/moveconfig.py b/tools/moveconfig.py
index 71a7736..a86c07c 100755
--- a/tools/moveconfig.py
+++ b/tools/moveconfig.py
@@ -1569,6 +1569,79 @@
                 add_imply_rule(config[CONFIG_LEN:], fname, linenum)
 
 
+def do_find_config(config_list):
+    """Find boards with a given combination of CONFIGs
+
+    Params:
+        config_list: List of CONFIG options to check (each a string consisting
+            of a config option, with or without a CONFIG_ prefix. If an option
+            is preceded by a tilde (~) then it must be false, otherwise it must
+            be true)
+    """
+    all_configs, all_defconfigs, config_db, defconfig_db = read_database()
+
+    # Get the whitelist
+    with open('scripts/config_whitelist.txt') as inf:
+        adhoc_configs = set(inf.read().splitlines())
+
+    # Start with all defconfigs
+    out = all_defconfigs
+
+    # Work through each config in turn
+    adhoc = []
+    for item in config_list:
+        # Get the real config name and whether we want this config or not
+        cfg = item
+        want = True
+        if cfg[0] == '~':
+            want = False
+            cfg = cfg[1:]
+
+        if cfg in adhoc_configs:
+            adhoc.append(cfg)
+            continue
+
+        # Search everything that is still in the running. If it has a config
+        # that we want, or doesn't have one that we don't, add it into the
+        # running for the next stage
+        in_list = out
+        out = set()
+        for defc in in_list:
+            has_cfg = cfg in config_db[defc]
+            if has_cfg == want:
+                out.add(defc)
+    if adhoc:
+        print(f"Error: Not in Kconfig: %s" % ' '.join(adhoc))
+    else:
+        print(f'{len(out)} matches')
+        print(' '.join(out))
+
+
+def prefix_config(cfg):
+    """Prefix a config with CONFIG_ if needed
+
+    This handles ~ operator, which indicates that the CONFIG should be disabled
+
+    >>> prefix_config('FRED')
+    'CONFIG_FRED'
+    >>> prefix_config('CONFIG_FRED')
+    'CONFIG_FRED'
+    >>> prefix_config('~FRED')
+    '~CONFIG_FRED'
+    >>> prefix_config('~CONFIG_FRED')
+    '~CONFIG_FRED'
+    >>> prefix_config('A123')
+    'CONFIG_A123'
+    """
+    op = ''
+    if cfg[0] == '~':
+        op = cfg[0]
+        cfg = cfg[1:]
+    if not cfg.startswith('CONFIG_'):
+        cfg = 'CONFIG_' + cfg
+    return op + cfg
+
+
 def main():
     try:
         cpu_count = multiprocessing.cpu_count()
@@ -1596,6 +1669,8 @@
     parser.add_option('-e', '--exit-on-error', action='store_true',
                       default=False,
                       help='exit immediately on any error')
+    parser.add_option('-f', '--find', action='store_true', default=False,
+                      help='Find boards with a given config combination')
     parser.add_option('-H', '--headers-only', dest='cleanup_headers_only',
                       action='store_true', default=False,
                       help='only cleanup the headers')
@@ -1631,13 +1706,12 @@
         unittest.main()
 
     if len(configs) == 0 and not any((options.force_sync, options.build_db,
-                                      options.imply)):
+                                      options.imply, options.find)):
         parser.print_usage()
         sys.exit(1)
 
     # prefix the option name with CONFIG_ if missing
-    configs = [ config if config.startswith('CONFIG_') else 'CONFIG_' + config
-                for config in configs ]
+    configs = [prefix_config(cfg) for cfg in configs]
 
     check_top_directory()
 
@@ -1663,6 +1737,10 @@
                         options.skip_added)
         return
 
+    if options.find:
+        do_find_config(configs)
+        return
+
     config_db = {}
     db_queue = queue.Queue()
     t = DatabaseThread(config_db, db_queue)
@@ -1705,4 +1783,4 @@
                 fd.write('\n')
 
 if __name__ == '__main__':
-    main()
+    sys.exit(main())