mirror of
https://gitlab.postmarketos.org/postmarketOS/pmbootstrap.git
synced 2025-07-13 03:19:47 +03:00
We can go up one layer, now we need to figure out how to recurse dependencies and enable them. Signed-off-by: Casey Connolly <kcxt@postmarketos.org>
485 lines
17 KiB
Python
485 lines
17 KiB
Python
# Copyright 2023 Attila Szollosi
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
from pathlib import Path
|
|
from queue import Queue
|
|
from pmb.core.chroot import Chroot
|
|
from pmb.helpers import logging
|
|
import re
|
|
import os
|
|
from typing import Literal, OrderedDict, overload
|
|
|
|
import pmb.build
|
|
import pmb.config
|
|
import pmb.parse
|
|
import pmb.helpers.pmaports
|
|
import pmb.parse.kconfigcheck
|
|
from pmb.core.arch import Arch
|
|
from pmb.helpers.exceptions import NonBugError
|
|
from pmb.types import Apkbuild, 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)
|
|
|
|
|
|
def add_missing_dependencies(apkbuild: Apkbuild, pmos_frag: OrderedDict[str, str], dotconfig: Path) -> OrderedDict[str, str]:
|
|
from pmb.parse import kconfiglib
|
|
|
|
os.environ["srctree"] = "." #str(Chroot.native() / apkbuild["builddir"])
|
|
os.environ["CC"] = "gcc"
|
|
os.environ["LD"] = "ld"
|
|
os.environ["HOSTCC"] = "gcc"
|
|
os.environ["ARCH"] = "arm64"
|
|
os.environ["SRCARCH"] = "arm64"
|
|
os.chdir(Chroot.native() / apkbuild["builddir"])
|
|
kconfig = kconfiglib.Kconfig("Kconfig", warn=False)
|
|
print("Loading .config...")
|
|
kconfig.load_config(str(Chroot.native() / dotconfig))
|
|
print("Finding missing dependencies!")
|
|
|
|
for (sname, sval) in pmos_frag.items():
|
|
if sname not in kconfig.syms:
|
|
print(f"symbol {sname} not found")
|
|
continue
|
|
|
|
sym = kconfig.syms.get(sname)
|
|
if not isinstance(sym, kconfiglib.Symbol):
|
|
print(f"sym {sname} not a Symbol? got {sym}")
|
|
# print(f"sym {sym.config_string} dep type {type(sym.direct_dep)}")
|
|
if not sym.direct_dep or not isinstance(sym.direct_dep, tuple):
|
|
# print(f"CONFIG_{sym.name}={sym.str_value} # no deps?")
|
|
continue
|
|
|
|
missing_deps: list[kconfiglib.Symbol] = []
|
|
dept: Queue[tuple[kconfiglib.Symbol]] = Queue()
|
|
dept.put(sym.direct_dep)
|
|
while not dept.empty():
|
|
next_dep = dept.get_nowait()
|
|
for dep in next_dep:
|
|
if isinstance(dep, int):
|
|
continue
|
|
if isinstance(dep, tuple):
|
|
dept.put(dep)
|
|
elif dep.str_value == "n":
|
|
missing_deps.append(dep)
|
|
# print(f" {dep.name}={dep.str_value}")
|
|
|
|
if missing_deps and sym.str_value == "n":
|
|
print(f"\nCONFIG_{sym.name}={sym.str_value} # missing dependencies:")
|
|
for dep in missing_deps:
|
|
if dep.assignable:
|
|
print(f" CONFIG_{dep.name}={dep.assignable[-1]} (current: {dep.str_value})")
|
|
else:
|
|
print(f" # CONFIG_{dep.name} can't be assigned!")
|
|
# print()
|
|
|
|
|
|
def create_fragment(apkbuild: Apkbuild, arch: Arch) -> tuple[str, dict[str, str]]:
|
|
"""
|
|
Generate a kconfig fragment based on categories and version from a kernel's
|
|
APKBUILD.
|
|
|
|
:param apkbuild: parsed apkbuild for kernel package
|
|
:param arch: target architecture
|
|
:returns: kconfig fragment as a string
|
|
"""
|
|
pkgver = apkbuild["pkgver"]
|
|
|
|
# Extract categories from APKBUILD options
|
|
categories = ["default"] # Always include default
|
|
for option in apkbuild["options"]:
|
|
if option.startswith("pmb:kconfigcheck-"):
|
|
category = option.split("-", 1)[1]
|
|
categories.append(category)
|
|
|
|
# Collect all rules from the categories
|
|
all_rules = {}
|
|
for category in categories:
|
|
try:
|
|
rules = pmb.parse.kconfigcheck.read_category(category)
|
|
all_rules.update(rules)
|
|
except Exception as e:
|
|
logging.warning(f"Failed to read category {category}: {e}")
|
|
|
|
fragment_lines = []
|
|
syms_dict: OrderedDict[str, str] = {}
|
|
|
|
# Process each category
|
|
for category_key, category_rules in all_rules.items():
|
|
# Extract category name from "category:name" format
|
|
category_name = category_key.split(":", 1)[1] if ":" in category_key else category_key
|
|
options_added = False
|
|
|
|
for version_spec, arch_options in category_rules.items():
|
|
# Check if this rule applies to our kernel version
|
|
applies = True
|
|
for rule in version_spec.split(" "):
|
|
if not pmb.parse.version.check_string(pkgver, rule):
|
|
applies = False
|
|
break
|
|
|
|
if not applies:
|
|
continue
|
|
|
|
# Process arch-specific options
|
|
for arch_spec, options in arch_options.items():
|
|
# Check if this rule applies to arch
|
|
if arch_spec != "all":
|
|
if str(arch) not in arch_spec.split(" "):
|
|
continue
|
|
|
|
# Add category header
|
|
if not options_added and options:
|
|
fragment_lines.append(f"# {category_name}")
|
|
options_added = True
|
|
|
|
# Add each option
|
|
for option, value in sorted(options.items()):
|
|
syms_dict[option] = str(value)
|
|
if isinstance(value, bool):
|
|
if value:
|
|
fragment_lines.append(f"CONFIG_{option}=y")
|
|
else:
|
|
fragment_lines.append(f"# CONFIG_{option} is not set")
|
|
elif isinstance(value, str):
|
|
fragment_lines.append(f'CONFIG_{option}="{value}"')
|
|
elif isinstance(value, list):
|
|
# For lists, join with commas
|
|
joined = ",".join(value)
|
|
fragment_lines.append(f'CONFIG_{option}="{joined}"')
|
|
|
|
if options_added:
|
|
# Padding between categories
|
|
fragment_lines.append("")
|
|
|
|
return "\n".join(fragment_lines), syms_dict
|