diff --git a/kernelci/kbuild.py b/kernelci/kbuild.py index 0708fb7354..00c492ebfa 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): + # 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 # 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..21ed683770 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,79 @@ 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 + + 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): kbuild = _kbuild(tmp_path)