[Concept,3/7] qconfig: Use kconfiglib for defconfig sync

Message ID 20260329111037.1352652-4-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 -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.

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

 tools/qconfig.py | 138 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 138 insertions(+)

-- 
2.43.0
  

Patch

diff --git a/tools/qconfig.py b/tools/qconfig.py
index 1e1f74a0c85..133fea41212 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."""
@@ -1928,6 +2060,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: