[Concept,01/19] buildman: Fix merge_config.sh path when using work directories

Message ID 20260130035849.3580212-2-simon.glass@canonical.com
State New
Headers
Series Enhanced command-line editing with undo/redo support |

Commit Message

Simon Glass Jan. 30, 2026, 3:58 a.m. UTC
  The run_merge_config() function constructs paths for merge_config.sh
using out_dir and cfg_file which are relative to the original working
directory. However, the commands run with cwd=src_dir (the work
directory), so these paths resolve incorrectly.

For example, with src_dir='../exph/.bm-work/00' and
out_dir='../exph/.bm-work/00/build', the -O flag would pass the full
out_dir path. When make runs from src_dir, it interprets this as
'../exph/.bm-work/00/../exph/.bm-work/00/build', doubling the path.

Fix this by converting out_dir and cfg_file to paths relative to
src_dir using os.path.relpath(). This ensures the paths resolve
correctly when commands execute from the work directory.

Fixes: 635c5f5638a0 ("buildman: Use merge_config.sh for --adjust-cfg")
Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 tools/buildman/cfgutil.py      | 21 ++++++++++-----
 tools/buildman/test_cfgutil.py | 48 ++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 7 deletions(-)
  

Patch

diff --git a/tools/buildman/cfgutil.py b/tools/buildman/cfgutil.py
index cec33e1e62b..060f2762b96 100644
--- a/tools/buildman/cfgutil.py
+++ b/tools/buildman/cfgutil.py
@@ -368,9 +368,18 @@  def run_merge_config(src_dir, out_dir, cfg_file, adjust_cfg, env):
     # Create a minimal defconfig from the current .config
     # This is necessary for 'imply' to work - the full .config has
     # '# CONFIG_xxx is not set' lines that prevent imply from taking effect
-    defconfig_path = os.path.join(out_dir or '.', 'defconfig')
-    make_cmd = ['make', f'O={out_dir}' if out_dir else None,
-                f'KCONFIG_CONFIG={cfg_file}', 'savedefconfig']
+    #
+    # Convert paths to be relative to src_dir since commands run with
+    # cwd=src_dir
+    if src_dir and out_dir:
+        rel_out_dir = os.path.relpath(out_dir, src_dir)
+        rel_cfg_file = os.path.relpath(cfg_file, src_dir)
+    else:
+        rel_out_dir = out_dir or '.'
+        rel_cfg_file = cfg_file
+    defconfig_path = os.path.join(rel_out_dir, 'defconfig')
+    make_cmd = ['make', f'O={rel_out_dir}' if rel_out_dir != '.' else None,
+                f'KCONFIG_CONFIG={rel_cfg_file}', 'savedefconfig']
     make_cmd = [x for x in make_cmd if x]  # Remove None elements
     result = command.run_one(*make_cmd, cwd=src_dir, env=env, capture=True,
                              capture_stderr=True)
@@ -382,10 +391,8 @@  def run_merge_config(src_dir, out_dir, cfg_file, adjust_cfg, env):
     try:
         # Run merge_config.sh with the minimal defconfig as base
         # -O sets output dir; defconfig is the base, fragment is merged
-        merge_script = os.path.join(src_dir or '.', 'scripts', 'kconfig',
-                                    'merge_config.sh')
-        out = out_dir or '.'
-        cmd = [merge_script, '-O', out, defconfig_path, frag_path]
+        merge_script = os.path.join('scripts', 'kconfig', 'merge_config.sh')
+        cmd = [merge_script, '-O', rel_out_dir, defconfig_path, frag_path]
         result = command.run_one(*cmd, cwd=src_dir, env=env, capture=True,
                                 capture_stderr=True)
     finally:
diff --git a/tools/buildman/test_cfgutil.py b/tools/buildman/test_cfgutil.py
index 47e522d3d6c..b623a4c4f67 100644
--- a/tools/buildman/test_cfgutil.py
+++ b/tools/buildman/test_cfgutil.py
@@ -180,6 +180,54 @@  class TestAdjustCfg(unittest.TestCase):
             result)
 
 
+class TestRunMergeConfig(unittest.TestCase):
+    """Tests for run_merge_config() function"""
+
+    def test_merge_script_path(self):
+        """Test that merge_config.sh path is relative to cwd, not absolute"""
+        from unittest import mock
+        from u_boot_pylib import command
+
+        # Track commands that were run
+        commands_run = []
+
+        def mock_run_one(*args, **kwargs):
+            commands_run.append((args, kwargs))
+            result = command.CommandResult()
+            result.return_code = 0
+            result.stdout = ''
+            result.stderr = ''
+            return result
+
+        with mock.patch.object(command, 'run_one', mock_run_one):
+            with mock.patch('os.path.exists', return_value=True):
+                with mock.patch('os.unlink'):
+                    # Use a work directory path like buildman does
+                    src_dir = '../branch/.bm-work/00'
+                    cfgutil.run_merge_config(
+                        src_dir, 'build', 'build/.config',
+                        {'LOCALVERSION_AUTO': '~LOCALVERSION_AUTO'}, {})
+
+        # Find the merge_config.sh command
+        merge_cmd = None
+        for args, kwargs in commands_run:
+            if args and 'merge_config.sh' in args[0]:
+                merge_cmd = args
+                merge_cwd = kwargs.get('cwd')
+                break
+
+        self.assertIsNotNone(merge_cmd, 'merge_config.sh command not found')
+
+        # The script path should be relative, not include src_dir
+        script_path = merge_cmd[0]
+        self.assertEqual('scripts/kconfig/merge_config.sh', script_path,
+                         f'Script path should be relative, got: {script_path}')
+
+        # The cwd should be src_dir
+        self.assertEqual(src_dir, merge_cwd,
+                         f'cwd should be src_dir, got: {merge_cwd}')
+
+
 class TestProcessConfig(unittest.TestCase):
     """Tests for process_config() function"""