pmbootstrap/pmb/parse/kconfig.py

336 lines
13 KiB
Python

# Copyright 2023 Attila Szollosi
# SPDX-License-Identifier: GPL-3.0-or-later
import glob
import logging
import re
import os
import pmb.build
import pmb.config
import pmb.parse
import pmb.helpers.pmaports
def get_all_component_names():
"""
Get the component names from kconfig_options variables in
pmb/config/__init__.py. This does not include the base options.
:returns: a list of component names, e.g. ["waydroid", "iwd", "nftables"]
"""
prefix = "kconfig_options_"
ret = []
for key in pmb.config.__dict__.keys():
if key.startswith(prefix):
ret += [key.split(prefix, 1)[1]]
return ret
def is_set(config, option):
"""
Check, whether a boolean or tristate option is enabled
either as builtin or module.
:param config: full kernel config as string
:param option: name of the option to check, e.g. EXT4_FS
:returns: True if the check passed, False otherwise
"""
return re.search("^CONFIG_" + option + "=[ym]$", config, re.M) is not None
def is_set_str(config, option, string):
"""
Check, whether a config option contains a string as value.
:param config: full kernel config as string
:param option: name of the option to check, e.g. EXT4_FS
:param string: the expected string
:returns: True if the check passed, False otherwise
"""
match = re.search("^CONFIG_" + option + "=\"(.*)\"$", config, re.M)
if match:
return string == match.group(1)
else:
return False
def is_in_array(config, option, string):
"""
Check, whether a config option contains string as an array element
:param config: full kernel config as string
:param option: name of the option to check, e.g. EXT4_FS
:param string: the string expected to be an element of the array
:returns: True if the check passed, False otherwise
"""
match = re.search("^CONFIG_" + option + "=\"(.*)\"$", config, re.M)
if match:
values = match.group(1).split(",")
return string in values
else:
return False
def check_option(component, details, config, config_path, option,
option_value):
"""
Check, whether one kernel config option has a given value.
:param component: name of the component to test (postmarketOS, waydroid, …)
:param details: print all warnings if True, otherwise one per component
:param config: full kernel config as string
:param config_path: full path to kernel config file
:param option: name of the option to check, e.g. EXT4_FS
:param option_value: expected value, e.g. True, "str", ["str1", "str2"]
:returns: True if the check passed, False otherwise
"""
def warn_ret_false(should_str):
config_name = os.path.basename(config_path)
if details:
logging.warning(f"WARNING: {config_name}: CONFIG_{option} should"
f" {should_str} ({component}):"
f" https://wiki.postmarketos.org/wiki/kconfig#CONFIG_{option}")
else:
logging.warning(f"WARNING: {config_name} isn't configured properly"
f" ({component}), run 'pmbootstrap kconfig check'"
" for details!")
return False
if isinstance(option_value, list):
for string in option_value:
if not is_in_array(config, option, string):
return warn_ret_false(f'contain "{string}"')
elif isinstance(option_value, str):
if not is_set_str(config, option, option_value):
return warn_ret_false(f'be set to "{option_value}"')
elif option_value in [True, False]:
if option_value != is_set(config, option):
return warn_ret_false("be set" if option_value else "*not* be set")
else:
raise RuntimeError("kconfig check code can only handle booleans,"
f" strings and arrays. Given value {option_value}"
" is not supported. If you need this, please patch"
" pmbootstrap or open an issue.")
return True
def check_config_options_set(config, config_path, config_arch, options,
component, pkgver, details=False):
"""
Check, whether all the kernel config passes all rules of one component.
Print a warning if any is missing.
:param config: full kernel config as string
:param config_path: full path to kernel config file
:param config_arch: architecture name (alpine format, e.g. aarch64, x86_64)
:param options: kconfig_options* var passed from pmb/config/__init__.py:
kconfig_options_example = {
">=0.0.0": { # all versions
"all": { # all arches
"ANDROID_PARANOID_NETWORK": False,
},
}
:param component: name of the component to test (postmarketOS, waydroid, …)
:param pkgver: kernel version
:param details: print all warnings if True, otherwise one per component
:returns: True if the check passed, False otherwise
"""
ret = True
for rules, archs_options in options.items():
# Skip options irrelevant for the current kernel's version
# Example rules: ">=4.0 <5.0"
skip = False
for rule in rules.split(" "):
if not pmb.parse.version.check_string(pkgver, rule):
skip = True
break
if skip:
continue
for archs, options in archs_options.items():
if archs != "all":
# Split and check if the device's architecture architecture has
# special config options. If option does not contain the
# architecture of the device kernel, then just skip the option.
architectures = archs.split(" ")
if config_arch not in architectures:
continue
for option, option_value in options.items():
if not check_option(component, details, config, config_path,
option, option_value):
ret = False
# Stop after one non-detailed error
if not details:
return False
return ret
def check_config(config_path, config_arch, pkgver, components_list=[],
details=False, enforce_check=True):
"""
Check, whether one kernel config passes the rules of multiple components.
:param config_path: full path to kernel config file
:param config_arch: architecture name (alpine format, e.g. aarch64, x86_64)
:param pkgver: kernel version
:param components_list: what to check for, e.g. ["waydroid", "iwd"]
:param details: print all warnings if True, otherwise one per component
:param enforce_check: set to False to not fail kconfig check as long as
everything in kconfig_options is set correctly, even
if additional components are checked
:returns: True if the check passed, False otherwise
"""
logging.debug(f"Check kconfig: {config_path}")
with open(config_path) as handle:
config = handle.read()
# Devices in all categories need basic options
# https://wiki.postmarketos.org/wiki/Device_categorization
components_list = ["postmarketOS"] + components_list
# Devices in "community" or "main" need additional options
if "community" in components_list:
components_list += [
"containers",
"filesystems",
"iwd",
"netboot",
"nftables",
"usb_gadgets",
"waydroid",
"wireguard",
"zram",
]
components = {}
for name in components_list:
if name == "postmarketOS":
pmb_config_var = "kconfig_options"
else:
pmb_config_var = f"kconfig_options_{name}"
components[name] = getattr(pmb.config, pmb_config_var, None)
assert components[name], f"invalid kconfig component name: {name}"
results = []
for component, options in components.items():
result = check_config_options_set(config, config_path, config_arch,
options, component, pkgver, details)
# We always enforce "postmarketOS" component and when explicitly
# requested
if enforce_check or component == "postmarketOS":
results += [result]
return all(results)
def check(args, pkgname, components_list=[], details=False, must_exist=True):
"""
Check for necessary kernel config options in a package.
:param pkgname: the package to check for, optionally without "linux-"
:param components_list: what to check for, e.g. ["waydroid", "iwd"]
:param details: print all warnings if True, otherwise one generic warning
:param must_exist: if False, just return if the package does not exist
:returns: True when the check was successful, False otherwise
None if the aport cannot be found (only if must_exist=False)
"""
# Don't modify the original component_list (arguments are passed as
# reference, a list is not immutable)
components_list = components_list.copy()
# Pkgname: allow omitting "linux-" prefix
if pkgname.startswith("linux-"):
flavor = pkgname.split("linux-")[1]
else:
flavor = pkgname
# Read all kernel configs in the aport
ret = True
aport = pmb.helpers.pmaports.find(args, "linux-" + flavor, must_exist=must_exist)
if aport is None:
return None
apkbuild = pmb.parse.apkbuild(f"{aport}/APKBUILD")
pkgver = apkbuild["pkgver"]
# We only enforce optional checks for community & main devices
enforce_check = aport.split("/")[-2] in ["community", "main"]
for name in get_all_component_names():
if f"pmb:kconfigcheck-{name}" in apkbuild["options"] and \
name not in components_list:
components_list += [name]
for config_path in glob.glob(aport + "/config-*"):
# The architecture of the config is in the name, so it just needs to be
# extracted
config_name = os.path.basename(config_path)
config_name_split = config_name.split(".")
if len(config_name_split) != 2:
raise RuntimeError(f"{config_name} is not a valid kernel config "
"name. Ensure that the _config property in your "
"kernel APKBUILD has a . before the "
"architecture name, e.g. .aarch64 or .armv7, "
"and that there is no excess punctuation "
"elsewhere in the name.")
config_arch = config_name_split[1]
ret &= check_config(config_path, config_arch, pkgver, components_list,
details=details, enforce_check=enforce_check)
return ret
def extract_arch(config_path):
# Extract the architecture out of the config
with open(config_path) as f:
config = f.read()
if is_set(config, "ARM"):
return "armv7"
elif is_set(config, "ARM64"):
return "aarch64"
elif is_set(config, "RISCV"):
return "riscv64"
elif is_set(config, "X86_32"):
return "x86"
elif is_set(config, "X86_64"):
return "x86_64"
# No match
logging.info("WARNING: failed to extract arch from kernel config")
return "unknown"
def extract_version(config_path):
# Try to extract the version string out of the comment header
with open(config_path) as f:
# Read the first 3 lines of the file and get the third line only
text = [next(f) for x in range(3)][2]
ver_match = re.match(r"# Linux/\S+ (\S+) Kernel Configuration", text)
if ver_match:
return ver_match.group(1).replace("-", "_")
# No match
logging.info("WARNING: failed to extract version from kernel config")
return "unknown"
def check_file(config_path, components_list=[], details=False):
"""
Check for necessary kernel config options in a kconfig file.
:param config_path: full path to kernel config file
:param components_list: what to check for, e.g. ["waydroid", "iwd"]
:param details: print all warnings if True, otherwise one generic warning
:returns: True when the check was successful, False otherwise
"""
arch = extract_arch(config_path)
version = extract_version(config_path)
logging.debug(f"Check kconfig: parsed arch={arch}, version={version} from "
f"file: {config_path}")
return check_config(config_path, arch, version, components_list,
details=details)