mirror of
https://gitlab.postmarketos.org/postmarketOS/pmbootstrap.git
synced 2025-07-12 19:09:56 +03:00
349 lines
12 KiB
Python
349 lines
12 KiB
Python
# Copyright 2023 Attila Szollosi
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
from pathlib import Path
|
|
from pmb.helpers import logging
|
|
import re
|
|
import os
|
|
from typing import Literal, overload
|
|
|
|
import pmb.build
|
|
import pmb.config
|
|
import pmb.parse
|
|
import pmb.helpers.pmaports
|
|
import pmb.parse.kconfigcheck
|
|
from pmb.helpers.exceptions import NonBugError
|
|
from pmb.types import PathString
|
|
|
|
|
|
def is_set(config: str, option: str) -> bool:
|
|
"""
|
|
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: str, option: str, string: str) -> bool:
|
|
"""
|
|
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: str, option: str, string: str) -> bool:
|
|
"""
|
|
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: str,
|
|
details: bool,
|
|
config: str,
|
|
config_path: PathString,
|
|
option: str,
|
|
option_value: bool | str | list[str],
|
|
) -> bool:
|
|
"""
|
|
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: str) -> bool:
|
|
config_name = os.path.basename(config_path)
|
|
if details:
|
|
logging.warning(
|
|
f"WARNING: {config_name}: CONFIG_{option} should {should_str} ({component})"
|
|
)
|
|
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: str,
|
|
config_path: PathString,
|
|
config_arch: str, # TODO: Replace with Arch type?
|
|
options: dict[str, dict],
|
|
component: str,
|
|
pkgver: str,
|
|
details: bool = False,
|
|
) -> bool:
|
|
"""
|
|
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: dictionary returned by pmb.parse.kconfigcheck.read_category().
|
|
: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, arch_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 arch_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
|
|
|
|
|
|
# TODO: This should probably use Arch and not str for config_arch
|
|
def check_config(
|
|
config_path: PathString,
|
|
config_arch: str,
|
|
pkgver: str,
|
|
categories: list[str],
|
|
details: bool = False,
|
|
) -> bool:
|
|
"""
|
|
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 categories: what to check for, e.g. ["waydroid", "iwd"]
|
|
:param details: print all warnings if True, otherwise one per component
|
|
:returns: True if the check passed, False otherwise
|
|
"""
|
|
logging.debug(f"Check kconfig: {config_path}")
|
|
with open(config_path) as handle:
|
|
config = handle.read()
|
|
|
|
if "default" not in categories:
|
|
categories += ["default"]
|
|
|
|
# Get all rules
|
|
rules: dict = {}
|
|
for category in categories:
|
|
rules |= pmb.parse.kconfigcheck.read_category(category)
|
|
|
|
# Check the rules of each category
|
|
ret = []
|
|
for category in rules.keys():
|
|
ret += [
|
|
check_config_options_set(
|
|
config, config_path, config_arch, rules[category], category, pkgver, details
|
|
)
|
|
]
|
|
|
|
return all(ret)
|
|
|
|
|
|
@overload
|
|
def check(
|
|
pkgname: str,
|
|
components_list: list[str] = ...,
|
|
details: bool = ...,
|
|
must_exist: Literal[False] = ...,
|
|
) -> bool | None: ...
|
|
|
|
|
|
@overload
|
|
def check(
|
|
pkgname: str,
|
|
components_list: list[str] = ...,
|
|
details: bool = ...,
|
|
must_exist: Literal[True] = ...,
|
|
) -> bool: ...
|
|
|
|
|
|
def check(
|
|
pkgname: str, components_list: list[str] = [], details: bool = False, must_exist: bool = True
|
|
) -> bool | None:
|
|
"""
|
|
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: Path
|
|
try:
|
|
aport = pmb.helpers.pmaports.find("linux-" + flavor)
|
|
except RuntimeError as e:
|
|
if must_exist:
|
|
raise e
|
|
return None
|
|
apkbuild = pmb.parse.apkbuild(aport / "APKBUILD")
|
|
pkgver = apkbuild["pkgver"]
|
|
|
|
# Get categories from the APKBUILD
|
|
categories = []
|
|
for option in apkbuild["options"]:
|
|
if not option.startswith("pmb:kconfigcheck-"):
|
|
continue
|
|
category = option.split("-", 1)[1]
|
|
categories += [category]
|
|
|
|
for config_path in aport.glob("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 NonBugError(
|
|
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,
|
|
categories,
|
|
details=details,
|
|
)
|
|
return ret
|
|
|
|
|
|
# TODO: Make this use the Arch type probably
|
|
def extract_arch(config_path: PathString) -> str:
|
|
# 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: PathString) -> str:
|
|
# 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: PathString, components_list: list[str] = [], details: bool = False
|
|
) -> bool:
|
|
"""
|
|
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 file: {config_path}")
|
|
return check_config(config_path, arch, version, components_list, details=details)
|