[PATCH 10/13] qconfig: Use kconfiglib for defconfig sync

Simon Glass sjg at chromium.org
Tue Jul 7 13:35:11 CEST 2026


The -s option currently spawns two make subprocesses per board (make
defconfig + make savedefconfig), requiring cross-compiler toolchains for
every architecture.

Replace this with kconfiglib's load_config() and write_min_config(),
which produces output matching 'make savedefconfig'. This uses the same
multiprocessing pattern as the -b optimisation, completing a full sync
of ~1500 boards in under two seconds with no toolchain requirement.

The -r (git-ref) option still uses the old make-based path, since it
needs to build against a different source tree.

Update the documentation for the new sync approach, folding the note
about CONFIG_GCC_VERSION into the Toolchains section.

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

 doc/develop/qconfig.rst |  44 ++++++-------
 tools/qconfig.py        | 138 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 157 insertions(+), 25 deletions(-)

diff --git a/doc/develop/qconfig.rst b/doc/develop/qconfig.rst
index 431de08cff3..e5c9e13915d 100644
--- a/doc/develop/qconfig.rst
+++ b/doc/develop/qconfig.rst
@@ -41,39 +41,33 @@ 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 is one known cosmetic difference 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. This does not
-affect 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. 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
-temporary build directories are automatically created and deleted as
-needed.  The number of threads are chosen based on the number of the CPU
-cores of your system although you can change it via -j (--jobs) option.
+When resyncing defconfigs (`-s`), the tool also uses kconfiglib.  It loads
+each defconfig with ``load_config()`` and writes a minimal config with
+``write_min_config()`` (equivalent to ``make savedefconfig``).  Defconfigs
+that use ``#include`` directives are skipped, since a minimal config
+cannot preserve the include structure.
 
-Note that `*.config` fragments are not supported.
+The ``-r`` (git-ref) option still uses the old make-based path, since it
+needs to build against a different source tree.
 
 Toolchains
 ----------
 
-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.
+Toolchains are **not** needed for ``-b`` or ``-s``, since both use
+kconfiglib to evaluate Kconfig files directly in Python.  The only
+difference from using a real toolchain is that ``CONFIG_GCC_VERSION``
+reflects the host compiler rather than each board's cross-compiler.
+This does not affect database queries, imply analysis, or defconfig
+sync, since ``CONFIG_GCC_VERSION`` is a build-time value that never
+appears in defconfig files or influences Kconfig defaults.
+
+The ``-r`` (git-ref) option still requires toolchains, as it falls back
+to the make-based path.  Most toolchains 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 them.
 
 
 Examples
diff --git a/tools/qconfig.py b/tools/qconfig.py
index 0b8f14d3e65..7ca0736ff3e 100755
--- a/tools/qconfig.py
+++ b/tools/qconfig.py
@@ -406,6 +406,138 @@ def do_build_db(args):
     return config_db, progress
 
 
+def _sync_defconfigs_worker(srcdir, defconfigs, result_queue, error_queue,
+                            dry_run):
+    """Worker process that syncs defconfigs using kconfiglib
+
+    For each defconfig, loads it via kconfiglib and writes a minimal config
+    (equivalent to 'make savedefconfig'), then compares with the original.
+
+    Args:
+        srcdir (str): Source-tree directory
+        defconfigs (list of str): Defconfig filenames to process
+        result_queue (multiprocessing.Queue): Output queue for
+            (defconfig, updated) tuples
+        error_queue (multiprocessing.Queue): Output queue for failed defconfigs
+        dry_run (bool): If True, do not update defconfig files
+    """
+    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:
+        orig = os.path.join(srcdir, 'configs', defconfig)
+        try:
+            # Skip defconfigs with #include — savedefconfig mangles them
+            if b'#include' in tools.read_file(orig):
+                result_queue.put((defconfig, False, 'has #include'))
+                continue
+
+            kconf.load_config(orig)
+
+            tmp = tempfile.NamedTemporaryFile(
+                mode='w', prefix='qconfig-', suffix='_defconfig',
+                dir=os.path.join(srcdir, 'configs'), delete=False)
+            tmp.close()
+            kconf.write_min_config(tmp.name)
+
+            updated = not filecmp.cmp(orig, tmp.name)
+            if updated and not dry_run:
+                shutil.move(tmp.name, orig)
+            else:
+                os.unlink(tmp.name)
+            result_queue.put((defconfig, updated, None))
+        except Exception as exc:
+            error_queue.put((defconfig, str(exc)))
+
+
+def do_sync_defconfigs(args):
+    """Sync defconfig files using kconfiglib instead of make
+
+    Evaluates each defconfig through kconfiglib and writes a minimal config
+    (equivalent to 'make savedefconfig'), updating the original if it differs.
+
+    Args:
+        args (Namespace): Program arguments (uses jobs, defconfigs,
+            defconfiglist, nocolour, dry_run, force_sync)
+
+    Returns:
+        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=_sync_defconfigs_worker,
+            args=(srcdir, chunk, result_queue, error_queue, args.dry_run))
+        proc.start()
+        processes.append(proc)
+
+    remaining = total
+    updated_count = 0
+    while remaining:
+        found = False
+        while not result_queue.empty():
+            defconfig, updated, msg = result_queue.get()
+            if updated:
+                updated_count += 1
+                name = defconfig[:-len('_defconfig')]
+                log = col.build(col.BLUE, 'defconfig updated', bright=True)
+                if args.dry_run:
+                    log = col.build(col.YELLOW, 'would update', bright=True)
+                print(f'{name.ljust(20)} {log}')
+            elif msg:
+                name = defconfig[:-len('_defconfig')]
+                log = col.build(col.RED, f'ignored: {msg}', bright=True)
+                print(f'{name.ljust(20)} {log}')
+            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()
+    if updated_count:
+        print(col.build(col.BLUE,
+                         f'{updated_count} defconfig(s) updated', bright=True))
+    return progress
+
+
 # pylint: disable=R0903
 class KconfigParser:
     """A parser of .config and include/autoconf.mk."""
@@ -1925,6 +2057,12 @@ def main():
         config_db, progress = do_build_db(args)
         return write_db(config_db, progress)
 
+    if args.force_sync and not args.git_ref:
+        progress = do_sync_defconfigs(args)
+        if args.commit:
+            add_commit(args.configs)
+        return move_done(progress)
+
     config_db, progress = move_config(args)
 
     if args.commit:
-- 
2.43.0



More information about the U-Boot mailing list