Skip to content

Commit

Permalink
Add script for checking resulting kernel config
Browse files Browse the repository at this point in the history
There is bunch of kernel config options that are not propagated
correctly to the kernel configuration after fragments are merged
and processed by Kconfig. Current Buildroot tools are not good at
discovering these - while we cleaned up most inconsistencies by using
linux-diff-config and output from the merge_config.sh script, there
are still options that were removed or get a different value than
intended because of dependencies, etc.

This commit adds a Python script that is using Kconfiglib to parse
current kernel's Kconfig files and the generated .config and compare
the requested values from individual kernel config fragments. The
script can be used manually by running `make linux-check-dotconfig`
from the buildroot directory (with path to BR2_EXTERNAL directory set)
and it's called also from the CI, where it generates Github Workflow
warning annotations when some of the values are not present or when set
incorrectly.

The kconfiglib.py is checked-in to the repo as well, because the library
is currently abandoned on PyPI and packaged version has a bug that causes
errors parsing Kconfigs in newer Linux versions, fixed in outstanding
pull request ulfalizer/Kconfiglib#119 - so version from this PR is used
here.

If pypi/support#2526 is ever resolved, we could remove it from our repo
and use pip for installing the package as a requirement during build
of the build container.
  • Loading branch information
sairon committed Dec 20, 2023
1 parent 069614a commit 9d5355e
Show file tree
Hide file tree
Showing 4 changed files with 7,322 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,15 @@ jobs:
${{ needs.prepare.outputs.build_container_image }} \
make BUILDDIR=/build ${{ matrix.board.defconfig }}
- name: Check Linux config
run: |
docker run --rm --privileged -v "${GITHUB_WORKSPACE}:/build" \
-e BUILDER_UID="$(id -u)" -e BUILDER_GID="$(id -g)" \
-v "/mnt/cache:/cache" \
${{ needs.prepare.outputs.build_container_image }} \
make -C buildroot O="/build/output" BR2_EXTERNAL="/build/buildroot-external" \
BR2_CHECK_DOTCONFIG_OPTS="--github-format --strip-path-prefix=/build/" linux-check-dotconfig
- name: Upload artifacts
if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build == 'true' }}
working-directory: output/images/
Expand Down
11 changes: 11 additions & 0 deletions buildroot-external/external.mk
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
include $(sort $(wildcard $(BR2_EXTERNAL_HASSOS_PATH)/package/*/*.mk))

.PHONY: linux-check-dotconfig
linux-check-dotconfig: linux-check-configuration-done
CC=$(TARGET_CC) LD=$(TARGET_LD) srctree=$(LINUX_SRCDIR) \
ARCH=$(if $(BR2_x86_64),x86,$(if $(BR2_arm)$(BR2_aarch64),arm,$(ARCH))) \
SRCARCH=$(if $(BR2_x86_64),x86,$(if $(BR2_arm)$(BR2_aarch64),arm,$(ARCH))) \
$(BR2_EXTERNAL_HASSOS_PATH)/scripts/check-dotconfig.py \
$(BR2_CHECK_DOTCONFIG_OPTS) \
--src-kconfig $(LINUX_SRCDIR)Kconfig \
--actual-config $(LINUX_SRCDIR).config \
$(shell echo $(BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES))
128 changes: 128 additions & 0 deletions buildroot-external/scripts/check-dotconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env python

import argparse
from collections import namedtuple
import re

from kconfiglib import Kconfig


# Can be either "CONFIG_OPTION=(y|m|n)" or "# CONFIG_OPTION is not set"
regex = re.compile(
r"^(CONFIG_(?P<option_set>[A-Z0-9_]+)=(?P<value>[mny])"
r"|# CONFIG_(?P<option_unset>[A-Z0-9_]+) is not set)$"
)

# use namedtuple as a lightweight representation of fragment-defined options
OptionValue = namedtuple("OptionValue", ["option", "value", "file", "line"])


def parse_fragment(
filename: str, strip_path_prefix: str = None
) -> dict[str, OptionValue]:
"""
Parse Buildroot Kconfig fragment and return dict of OptionValue objects.
"""
options: dict[str, OptionValue] = {}

with open(filename) as f:
if strip_path_prefix and filename.startswith(strip_path_prefix):
filename = filename[len(strip_path_prefix) :]

for line_number, line in enumerate(f, 1):
if matches := re.match(regex, line):
if matches["option_unset"]:
value = OptionValue(
matches["option_unset"], None, filename, line_number
)
options.update({matches.group("option_unset"): value})
else:
value = OptionValue(
matches["option_set"], matches["value"], filename, line_number
)
options.update({matches.group("option_set"): value})

return options


def _format_message(
message: str, file: str, line: int, github_format: bool = False
) -> str:
"""
Format message with source file and line number.
"""
if github_format:
return f"::warning file={file},line={line}::{message}"
return f"{message} (defined in {file}:{line})"


def compare_configs(
expected_options: dict[str, OptionValue],
kconfig: Kconfig,
github_format: bool = False,
) -> None:
"""
Compare dictionary of expected options with actual Kconfig representation.
"""
for option, spec in expected_options.items():
if option not in kconfig.syms:
print(
_format_message(
f"{option}={spec.value} not found",
file=spec.file,
line=spec.line,
github_format=github_format,
)
)
elif (val := kconfig.syms[option].str_value) != spec.value:
if spec.value is None and val == "n":
continue
print(
_format_message(
f"{option}={spec.value} requested, actual = {val}",
file=spec.file,
line=spec.line,
github_format=github_format,
)
)


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--src-kconfig", help="Path to top-level Kconfig file", required=True
)
parser.add_argument(
"--actual-config",
help="Path to config with actual config values (.config)",
required=True,
)
parser.add_argument(
"--github-format",
action="store_true",
help="Use Github Workflow commands output format",
)
parser.add_argument(
"-s",
"--strip-path-prefix",
help="Path prefix to strip in the output from config fragment paths",
)
parser.add_argument("fragments", nargs="+", help="Paths to source config fragments")

args = parser.parse_args()

expected_options: dict[str, OptionValue] = {}

for f in args.fragments:
expected_options.update(
parse_fragment(f, strip_path_prefix=args.strip_path_prefix)
)

kconfig = Kconfig(args.src_kconfig, warn_to_stderr=False)
kconfig.load_config(args.actual_config)

compare_configs(expected_options, kconfig, github_format=args.github_format)


if __name__ == "__main__":
main()
Loading

0 comments on commit 9d5355e

Please sign in to comment.