Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 90 additions & 36 deletions kernelci/kbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:<target>' directive, in merge order
"""
fragment_files = []
kconfig_adds = []

for idx, fragment in enumerate(self._fragments):
content = ""
Expand All @@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
75 changes: 74 additions & 1 deletion tests/test_kbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down