[Concept,1/7] qconfig: Use kconfiglib to build the CONFIG database

Message ID 20260329111037.1352652-2-sjg@u-boot.org
State New
Headers
Series qconfig: Use kconfiglib for database build and defconfig sync |

Commit Message

Simon Glass March 29, 2026, 11:10 a.m. UTC
  From: Simon Glass <sjg@chromium.org>

The -b option currently spawns two make subprocesses per board (make
defconfig + make auto.conf), requiring cross-compiler toolchains for
every architecture and taking several minutes for the full set of ~1500
boards.

Replace this with direct kconfiglib evaluation. Each worker-process
parses the Kconfig tree once, then loads each defconfig in its chunk
using kconf.load_config() and reads the resolved CONFIG values via
sym.config_string. This avoids all overhead related to subprocess and
toolchains, reducing the full database build from minutes to under two
seconds.

The multiprocessing pattern follows the existing approach in
buildman/boards.py, including handling of #include directives in
defconfig files via the C preprocessor.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 doc/develop/qconfig.rst |  50 ++++++++++++---
 tools/qconfig.py        | 132 ++++++++++++++++++++++++++++++++++++++--
 2 files changed, 169 insertions(+), 13 deletions(-)
  

Patch

diff --git a/doc/develop/qconfig.rst b/doc/develop/qconfig.rst
index 3b995355967..423fb9118d8 100644
--- a/doc/develop/qconfig.rst
+++ b/doc/develop/qconfig.rst
@@ -24,14 +24,43 @@  You may need to install 'python3-asteval' for the 'asteval' module.
 How does it work?
 -----------------
 
-When building a database (`-b`), this tool runs configuration and builds
-include/autoconf.mk for every defconfig.  The config options defined in Kconfig
-appear in the .config file (unless they are hidden because of unmet dependency.)
-On the other hand, the config options defined by board headers are seen
-in include/autoconf.mk.
+Building a database
+~~~~~~~~~~~~~~~~~~~
+
+When building a database (`-b`), this tool evaluates the Kconfig tree directly
+using kconfiglib (a pure-Python Kconfig implementation). For each defconfig, it
+loads the file with ``kconf.load_config()``, resolves all symbol dependencies,
+and collects the resulting CONFIG values. This is the same approach used by
+``buildman`` when scanning board parameters.
+
+Multiple worker processes run in parallel (one per CPU core by default,
+adjustable with ``-j``), each parsing the Kconfig tree once and then processing
+its share of defconfigs. This completes in a few seconds for all ~1500 boards,
+since no ``make`` subprocesses or cross-compiler toolchains are needed.
+
+Defconfig files containing ``#include`` directives are preprocessed with the
+C preprocessor before loading, matching the behaviour of the build system.
+
+There are two known cosmetic differences compared with the old make-based
+approach:
+
+- ``CONFIG_GCC_VERSION`` reflects the host compiler rather than each board's
+  cross-compiler, since no cross-compiler is invoked.
+
+- Backslash escape sequences in string values (e.g. ``\n``, ``\x1b``) may
+  differ slightly due to kconfiglib's unescape/escape round-trip. This affects
+  a handful of string CONFIGs such as ``CONFIG_AUTOBOOT_PROMPT``.
+
+Neither difference affects the usefulness of the database for finding CONFIG
+combinations or computing imply relationships.
+
+Resyncing defconfigs
+~~~~~~~~~~~~~~~~~~~~
 
 When resyncing defconfigs (`-s`) the .config is synced by "make savedefconfig"
-and the defconfig is updated with it.
+and the defconfig is updated with it. This path still uses ``make``
+subprocesses and therefore requires appropriate cross-compiler toolchains (see
+below).
 
 For faster processing, this tool is multi-threaded.  It creates
 separate build directories where the out-of-tree build is run.  The
@@ -44,9 +73,12 @@  Note that `*.config` fragments are not supported.
 Toolchains
 ----------
 
-Appropriate toolchains are necessary to generate include/autoconf.mk
-for all the architectures supported by U-Boot.  Most of them are available
-at the kernel.org site. This tool uses the same tools as
+Toolchains are **not** needed for building the database (`-b`), since it
+uses kconfiglib to evaluate Kconfig files directly in Python.
+
+For resyncing defconfigs (`-s`), appropriate toolchains are necessary to run
+``make savedefconfig`` for all the architectures supported by U-Boot.  Most of
+them are available at the kernel.org site. This tool uses the same tools as
 :doc:`../build/buildman`, so you can use `buildman --fetch-arch` to fetch
 toolchains.
 
diff --git a/tools/qconfig.py b/tools/qconfig.py
index 70d53b5f8d9..4e06bfec88b 100755
--- a/tools/qconfig.py
+++ b/tools/qconfig.py
@@ -283,6 +283,129 @@  def scan_kconfig():
     return kconfiglib.Kconfig()
 
 
+def _scan_defconfigs_worker(srcdir, defconfigs, queue, error_queue):
+    """Worker process that scans defconfigs using kconfiglib
+
+    Each worker creates its own Kconfig instance (parsing is done once per
+    process) then loads each defconfig in turn, collecting all CONFIG values.
+
+    Args:
+        srcdir (str): Source-tree directory
+        defconfigs (list of str): Defconfig filenames to process, e.g.
+            ['sandbox_defconfig', 'snow_defconfig']
+        queue (multiprocessing.Queue): Output queue for (defconfig, configs)
+        error_queue (multiprocessing.Queue): Output queue for failed defconfigs
+    """
+    os.environ['srctree'] = srcdir
+    os.environ['UBOOTVERSION'] = 'dummy'
+    os.environ['KCONFIG_OBJDIR'] = ''
+    os.environ['CC'] = 'gcc'
+    kconf = kconfiglib.Kconfig(warn=False)
+
+    for defconfig in defconfigs:
+        fname = os.path.join(srcdir, 'configs', defconfig)
+        try:
+            if b'#include' in tools.read_file(fname):
+                cpp = os.getenv('CPP', 'cpp').split()
+                cmd = cpp + ['-nostdinc', '-P', '-I', srcdir,
+                             '-undef', '-x', 'assembler-with-cpp', fname]
+                stdout = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
+                tmp = tempfile.NamedTemporaryFile(prefix='qconfig-', delete=False)
+                tmp.write(stdout)
+                tmp.close()
+                kconf.load_config(tmp.name)
+                os.unlink(tmp.name)
+            else:
+                kconf.load_config(fname)
+
+            configs = {}
+            for sym in kconf.unique_defined_syms:
+                conf = sym.config_string
+                if not conf or conf.startswith('#'):
+                    continue
+                config, value = conf.rstrip('\n').split('=', 1)
+                configs[config] = value
+            queue.put((defconfig, configs))
+        except Exception as exc:
+            error_queue.put((defconfig, str(exc)))
+
+
+def do_build_db(args):
+    """Build the CONFIG database using kconfiglib instead of make
+
+    This evaluates the Kconfig tree directly in Python for each defconfig,
+    avoiding the overhead of spawning make subprocesses and the need for
+    cross-compiler toolchains.
+
+    Args:
+        args (Namespace): Program arguments (uses jobs, defconfigs,
+            defconfiglist, nocolour)
+
+    Returns:
+        tuple:
+            config_db (dict): configs for each defconfig
+            Progress: progress indicator
+    """
+    srcdir = os.getcwd()
+
+    if args.defconfigs:
+        defconfigs = [os.path.basename(d)
+                      for d in get_matched_defconfigs(args.defconfigs)]
+    elif args.defconfiglist:
+        defconfigs = [os.path.basename(d)
+                      for d in get_matched_defconfigs(args.defconfiglist)]
+    else:
+        defconfigs = get_all_defconfigs()
+
+    col = terminal.Color(terminal.COLOR_NEVER if args.nocolour
+                         else terminal.COLOR_IF_TERMINAL)
+    progress = Progress(col, len(defconfigs))
+
+    jobs = args.jobs
+    total = len(defconfigs)
+    result_queue = multiprocessing.Queue()
+    error_queue = multiprocessing.Queue()
+    processes = []
+    for i in range(jobs):
+        chunk = defconfigs[total * i // jobs:total * (i + 1) // jobs]
+        if not chunk:
+            continue
+        proc = multiprocessing.Process(
+            target=_scan_defconfigs_worker,
+            args=(srcdir, chunk, result_queue, error_queue))
+        proc.start()
+        processes.append(proc)
+
+    config_db = {}
+    remaining = total
+    while remaining:
+        # Drain both queues without blocking forever
+        found = False
+        while not result_queue.empty():
+            defconfig, configs = result_queue.get()
+            config_db[defconfig] = configs
+            progress.inc(True)
+            progress.show()
+            remaining -= 1
+            found = True
+        while not error_queue.empty():
+            defconfig, msg = error_queue.get()
+            print(col.build(col.RED, f'{defconfig}: {msg}', bright=True),
+                  file=sys.stderr)
+            progress.inc(False)
+            progress.show()
+            remaining -= 1
+            found = True
+        if not found:
+            time.sleep(SLEEP_TIME)
+
+    for proc in processes:
+        proc.join()
+
+    progress.completed()
+    return config_db, progress
+
+
 # pylint: disable=R0903
 class KconfigParser:
     """A parser of .config and include/autoconf.mk."""
@@ -1745,7 +1868,7 @@  def ensure_database(threads):
                          dry_run=False, exit_on_error=False, jobs=threads,
                          git_ref=None, defconfigs=None, defconfiglist=None,
                          nocolour=False)
-        config_db, progress = move_config(args)
+        config_db, progress = do_build_db(args)
 
         write_db(config_db, progress)
 
@@ -1772,13 +1895,14 @@  def main():
     if args.find:
         return do_find_config(args.configs, args.list)
 
+    if args.build_db:
+        config_db, progress = do_build_db(args)
+        return write_db(config_db, progress)
+
     config_db, progress = move_config(args)
 
     if args.commit:
         add_commit(args.configs)
-
-    if args.build_db:
-        return write_db(config_db, progress)
     return move_done(progress)