From 59fb3b2a9e18f3947a50a7693bc82b69c6fd1db8 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 7 Sep 2026 09:41:41 +0300 Subject: [PATCH 1/2] kbuild: pass config make targets through to tuxmake A fragment entry of the form 'make:' names a kernel make target that generates config, and tuxmake accepts it as a magic --kconfig-add value. _parse_fragments() instead wrote every fragment entry into a fragment file and passed that file to --kconfig-add, so the directive reached kconfig as fragment content, which merged it as: .config:5652:warning: unexpected data: make:kselftest-merge The kselftest fragment consists of exactly that one entry, so no kselftest config has been merged since it was switched over. The resulting kernels lack CONFIG_IOMMUFD, CONFIG_USER_NS, CONFIG_BPF_SYSCALL and everything else tools/testing/selftests/*/config asks for, which fails whole suites: kselftest-iommu has not passed a single one of its 18376 runs, every subtest dying in fixture setup on a missing /dev/iommu. Split 'make:' directives out of the fragment content and keep them as separate kconfig additions, ordered with the fragment files so merge order is preserved, then pass them to tuxmake verbatim. The make backend skips them for now. Signed-off-by: Denys Fedoryshchenko --- kernelci/kbuild.py | 126 ++++++++++++++++++++++++++++++------------- tests/test_kbuild.py | 57 +++++++++++++++++++- 2 files changed, 146 insertions(+), 37 deletions(-) diff --git a/kernelci/kbuild.py b/kernelci/kbuild.py index 0708fb7354..287949ec97 100644 --- a/kernelci/kbuild.py +++ b/kernelci/kbuild.py @@ -54,6 +54,10 @@ LATEST_LTS_MAJOR = 6 LATEST_LTS_MINOR = 12 +# Prefix marking a fragment entry as a kernel make target generating +# config (e.g. 'make:kselftest-merge') rather than a config symbol. +MAKE_FRAGMENT_PREFIX = "make:" + DTBS_DISABLED = { "i386": True, "x86_64": True, @@ -618,13 +622,44 @@ def add_fragment(self, fragname): print(f"Using fragment {fragname} from inline configs") return self.extract_config(frag) + @staticmethod + def _split_fragment(content): + """Split fragment content into make targets and config symbols + + A fragment entry prefixed with 'make:' names a kernel make target + generating config, such as 'make:kselftest-merge', rather than a + config symbol. Those entries must be kept out of the fragment file: + kconfig does not understand them and merges them as "unexpected + data", silently dropping the config the fragment is meant to add. + + Returns: + tuple: (list of make targets, config symbol text) + """ + make_targets = [] + config_lines = [] + + for line in content.splitlines(): + entry = line.strip() + if entry.startswith(MAKE_FRAGMENT_PREFIX): + target = entry[len(MAKE_FRAGMENT_PREFIX) :] + if target: + make_targets.append(target) + else: + config_lines.append(line) + + config = "\n".join(config_lines).strip() + if config: + config += "\n" + return make_targets, config + def _parse_fragments(self, firmware=False): """Parse fragments kbuild config and create config fragments Returns: - list: List of fragment file paths + list: List of kconfig additions, each either a fragment file + path or a 'make:' directive, in merge order """ - fragment_files = [] + kconfig_adds = [] for idx, fragment in enumerate(self._fragments): content = "" @@ -646,23 +681,34 @@ def _parse_fragments(self, firmware=False): ) continue - fragfile = os.path.join(self._fragments_dir, f"{idx}.config") - with open(fragfile, "w") as f: - f.write(content) + make_targets, config = self._split_fragment(content) - config_count = len( - [line for line in content.split("\n") if line.strip()] - ) - print( - f"[_parse_fragments] Created {fragfile} ({config_count} configs)" - ) + if config: + fragfile = os.path.join(self._fragments_dir, f"{idx}.config") + with open(fragfile, "w") as f: + f.write(config) - fragment_files.append(fragfile) + config_count = len( + [line for line in config.split("\n") if line.strip()] + ) + print( + f"[_parse_fragments] Created {fragfile} ({config_count} configs)" + ) + + kconfig_adds.append(fragfile) + + # add fragment to artifacts but relative to artifacts dir + frag_rel = os.path.relpath(fragfile, self._af_dir) + self._artifacts.append(frag_rel) + + for target in make_targets: + print( + f"[_parse_fragments] Fragment {fragment_name} runs " + f"make target {target}" + ) + kconfig_adds.append(MAKE_FRAGMENT_PREFIX + target) - # add fragment to artifacts but relative to artifacts dir - frag_rel = os.path.relpath(fragfile, self._af_dir) self._config_full += "+" + fragment_name - self._artifacts.append(frag_rel) if firmware: content = 'CONFIG_EXTRA_FIRMWARE_DIR="' + self._firmware_dir + '"\n' @@ -672,22 +718,23 @@ def _parse_fragments(self, firmware=False): with open(fragfile, "w") as f: f.write(content) - fragment_files.append(fragfile) + kconfig_adds.append(fragfile) # add fragment to artifacts but relative to artifacts dir frag_rel = os.path.relpath(fragfile, self._af_dir) self._artifacts.append(frag_rel) print( - f"[_parse_fragments] Created {len(fragment_files)} fragment files" + f"[_parse_fragments] Created {len(kconfig_adds)} kconfig additions" ) - return fragment_files + return kconfig_adds - def _merge_frags(self, fragment_files): + def _merge_frags(self, kconfig_adds): """Merge config fragments to .config Args: - fragment_files: List of fragment file paths to merge + kconfig_adds: List of kconfig additions, as returned by + _parse_fragments() """ self.startjob("config_defconfig") self.addcmd("cd " + self._srcdir) @@ -715,10 +762,14 @@ def _merge_frags(self, fragment_files): self._config_full = defconfigs + self._config_full # fragments self.startjob("config_fragments") - for fragfile in fragment_files: - self.addcmd( - f"./scripts/kconfig/merge_config.sh -m .config {fragfile}" - ) + for entry in kconfig_adds: + if entry.startswith(MAKE_FRAGMENT_PREFIX): + print( + f"[_merge_frags] WARNING: ignoring {entry}, the make " + "backend does not run config make targets" + ) + continue + self.addcmd(f"./scripts/kconfig/merge_config.sh -m .config {entry}") # TODO: olddefconfig should be optional/configurable # TODO: log all warnings/errors of olddefconfig to separate file self.addcmd("make olddefconfig") @@ -729,12 +780,12 @@ def _merge_frags(self, fragment_files): def _generate_script(self): """Generate shell script for complete build""" print("Generating shell script") - self._fragment_files = self._parse_fragments(firmware=True) + self._kconfig_adds = self._parse_fragments(firmware=True) if self._backend == "tuxmake": self._build_with_tuxmake() else: - self._merge_frags(self._fragment_files) + self._merge_frags(self._kconfig_adds) self._build_with_make() self._write_metadata() @@ -800,12 +851,12 @@ def _build_with_tuxmake(self): """Build kernel using tuxmake with native fragment support""" print("[_build_with_tuxmake] Starting tuxmake build") - if not hasattr(self, "_fragment_files"): - print("[_build_with_tuxmake] ERROR: No fragment files available") - self._fragment_files = [] + if not hasattr(self, "_kconfig_adds"): + print("[_build_with_tuxmake] ERROR: No kconfig additions available") + self._kconfig_adds = [] print( - f"[_build_with_tuxmake] Using {len(self._fragment_files)} fragment files" + f"[_build_with_tuxmake] Using {len(self._kconfig_adds)} kconfig additions" ) # Handle defconfigs - first goes to --kconfig, rest to --kconfig-add @@ -931,17 +982,20 @@ def _tuxmake_base(self, output_dir, defconfig, extra_defconfigs): for extra in extra_defconfigs: parts.append(f"--kconfig-add={extra}") print(f"[_tuxmake_base] Adding extra defconfig: {extra}") - for fragfile in self._fragment_files: - if os.path.exists(fragfile): - parts.append(f"--kconfig-add={fragfile}") + for entry in self._kconfig_adds: + if entry.startswith(MAKE_FRAGMENT_PREFIX): + # tuxmake runs the make target during config preparation + parts.append(f"--kconfig-add={entry}") + print(f"[_tuxmake_base] Adding make target: {entry}") + elif os.path.exists(entry): + parts.append(f"--kconfig-add={entry}") print( "[_tuxmake_base] Adding fragment: " - f"{os.path.basename(fragfile)}" + f"{os.path.basename(entry)}" ) else: print( - "[_tuxmake_base] WARNING: Fragment file not found: " - f"{fragfile}" + f"[_tuxmake_base] WARNING: Fragment file not found: {entry}" ) return parts diff --git a/tests/test_kbuild.py b/tests/test_kbuild.py index c92db7515c..598e061df0 100644 --- a/tests/test_kbuild.py +++ b/tests/test_kbuild.py @@ -18,7 +18,7 @@ def _kbuild(tmp_path, compiler="clang-21", arch="x86_64"): kbuild._compiler = compiler kbuild._defconfig = "defconfig" kbuild._fragments = [] - kbuild._fragment_files = [] + kbuild._kconfig_adds = [] kbuild._config_full = "" kbuild._backend = "tuxmake" kbuild._dtbs_check = True @@ -83,6 +83,61 @@ def test_no_probe_for_gcc_without_tuxmake(self, tmp_path, monkeypatch): assert not any("--version" in s for s in kbuild._steps) +class TestFragments: + @staticmethod + def _fragments(tmp_path, fragments, fragment_configs): + kbuild = _kbuild(tmp_path) + kbuild._fragments = fragments + kbuild._fragment_configs = fragment_configs + kbuild._fragments_dir = os.path.join(kbuild._af_dir, "fragments") + os.makedirs(kbuild._fragments_dir) + return kbuild + + def test_make_target_is_not_written_to_a_fragment_file(self, tmp_path): + kbuild = self._fragments( + tmp_path, + ["kselftest"], + {"kselftest": {"configs": ["make:kselftest-merge"]}}, + ) + + kconfig_adds = kbuild._parse_fragments() + + # kconfig would merge the directive as "unexpected data" + assert kconfig_adds == ["make:kselftest-merge"] + assert os.listdir(kbuild._fragments_dir) == [] + assert kbuild._artifacts == [] + assert kbuild._config_full == "+kselftest" + + def test_make_targets_are_split_from_config_symbols(self, tmp_path): + kbuild = self._fragments( + tmp_path, + ["kselftest"], + { + "kselftest": { + "configs": [ + "make:kselftest-merge", + "CONFIG_KUNIT=y", + ] + } + }, + ) + + kconfig_adds = kbuild._parse_fragments() + + fragfile = os.path.join(kbuild._fragments_dir, "0.config") + assert kconfig_adds == [fragfile, "make:kselftest-merge"] + with open(fragfile) as f: + assert f.read() == "CONFIG_KUNIT=y\n" + + def test_make_target_is_passed_to_tuxmake(self, tmp_path): + kbuild = _kbuild(tmp_path) + kbuild._kconfig_adds = ["make:kselftest-merge"] + + parts = kbuild._tuxmake_base(kbuild._af_dir, "defconfig", []) + + assert "--kconfig-add=make:kselftest-merge" in parts + + class TestKselftestSuiteResults: def test_names_identify_build_results(self, tmp_path): kbuild = _kbuild(tmp_path) From f0c93624cc9a4cc74cb67c98c2d549d9c52b5a7f Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 7 Sep 2026 09:42:15 +0300 Subject: [PATCH 2/2] kbuild: run config make targets in the make backend The make backend built its .config with merge_config.sh per fragment and had no way to act on a 'make:' entry, so it skipped them and lost whatever config the target would have generated, exactly as the tuxmake backend did before it learned to forward them. Run the target instead. It merges into the .config produced so far and is emitted in fragment order, so 'make:kselftest-merge' behaves like the merge_config.sh call it replaces. Signed-off-by: Denys Fedoryshchenko --- kernelci/kbuild.py | 8 ++++---- tests/test_kbuild.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/kernelci/kbuild.py b/kernelci/kbuild.py index 287949ec97..00c492ebfa 100644 --- a/kernelci/kbuild.py +++ b/kernelci/kbuild.py @@ -764,10 +764,10 @@ def _merge_frags(self, kconfig_adds): self.startjob("config_fragments") for entry in kconfig_adds: if entry.startswith(MAKE_FRAGMENT_PREFIX): - print( - f"[_merge_frags] WARNING: ignoring {entry}, the make " - "backend does not run config make targets" - ) + # the target merges its own config into the .config built + # so far, so run it in place of a merge_config.sh call + target = entry[len(MAKE_FRAGMENT_PREFIX) :] + self.addcmd(f"make {target}") continue self.addcmd(f"./scripts/kconfig/merge_config.sh -m .config {entry}") # TODO: olddefconfig should be optional/configurable diff --git a/tests/test_kbuild.py b/tests/test_kbuild.py index 598e061df0..21ed683770 100644 --- a/tests/test_kbuild.py +++ b/tests/test_kbuild.py @@ -137,6 +137,24 @@ def test_make_target_is_passed_to_tuxmake(self, tmp_path): assert "--kconfig-add=make:kselftest-merge" in parts + def test_make_target_is_run_by_the_make_backend(self, tmp_path): + kbuild = _kbuild(tmp_path) + kbuild._backend = "make" + fragfile = os.path.join(kbuild._af_dir, "0.config") + + kbuild._merge_frags(["make:kselftest-merge", fragfile]) + + steps = kbuild._steps + merge = steps.index("make kselftest-merge") + assert ( + steps.index( + f"./scripts/kconfig/merge_config.sh -m .config {fragfile}" + ) + > merge + ) + # kselftest-merge needs a .config to merge into + assert steps.index("make defconfig") < merge + class TestKselftestSuiteResults: def test_names_identify_build_results(self, tmp_path):