| #!/usr/bin/env python3 |
| # Copyright 2021 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # pylint: disable=missing-function-docstring |
| |
| """Chrome OS kernel configuration tool |
| |
| Script to merge all configs and run 'make oldconfig' on it to wade out bad |
| juju, then split the configs into distro-commmon and flavour-specific parts |
| |
| See this page for more details: |
| http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/kernel-configuration |
| """ |
| |
| import argparse |
| import difflib |
| import fnmatch |
| import glob |
| import logging |
| import os |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| from typing import Dict, List |
| |
| |
| MODES = ["oldconfig", "olddefconfig", "editconfig", "genconfig", "checkconfig"] |
| TOOLCHAIN_PREFIXES = { |
| "x86_64": "x86_64-cros-linux-gnu", |
| "armel": "armv7a-cros-linux-gnueabihf", |
| "arm64": "aarch64-cros-linux-gnu", |
| } |
| |
| |
| def splitconfig(srcdir: str) -> Dict[str, str]: |
| """Split and merge common configs |
| |
| Return dict of filename-to-contents. |
| """ |
| allconfigs = {} |
| |
| # Parse config files. |
| for config in os.listdir(srcdir): |
| # Only config.* |
| if not config.endswith(".config"): |
| continue |
| |
| allconfigs[config] = set() |
| |
| for line in open(os.path.join(srcdir, config), encoding="utf-8"): |
| m = re.match(r"#*\s*CONFIG_(\w+)[\s=](.*)$", line) |
| if not m: |
| continue |
| option, value = m.groups() |
| allconfigs[config].add((option, value)) |
| |
| # Split out common config options. |
| common = None |
| for config in allconfigs: |
| if common is None: |
| common = allconfigs[config].copy() |
| else: |
| common &= allconfigs[config] |
| for config in allconfigs: |
| allconfigs[config] -= common |
| allconfigs["common.config"] = common |
| |
| ret = {} |
| # Generate new splitconfigs. |
| for config in allconfigs: |
| if allconfigs[config] is None: |
| continue |
| contents = "#\n# Config options generated by splitconfig\n#\n" |
| for option, value in sorted(list(allconfigs[config])): |
| if value == "is not set": |
| contents += "# CONFIG_%s %s\n" % (option, value) |
| else: |
| contents += "CONFIG_%s=%s\n" % (option, value) |
| ret[config] = contents |
| return ret |
| |
| |
| def die(msg: str = "Aborting"): |
| logging.error(msg) |
| sys.exit(1) |
| |
| |
| def in_chroot() -> bool: |
| return os.path.exists("/etc/cros_chroot_version") |
| |
| |
| def rerun_in_chroot(): |
| cmd = [ |
| "cros_sdk", |
| "--working-dir", |
| ".", |
| "--", |
| os.path.join(os.path.curdir, os.path.relpath(sys.argv[0])), |
| ] + sys.argv[1:] |
| logging.info("Re-running in chroot") |
| logging.debug("Run: %s", cmd) |
| sys.exit(subprocess.run(cmd, check=False).returncode) |
| |
| |
| def make_toolchain_args(arch: str) -> Dict[str, str]: |
| ret = { |
| "LD": "ld.lld", |
| } |
| |
| if in_chroot(): |
| prefix = TOOLCHAIN_PREFIXES[arch] |
| ccompiler = prefix + "-clang" |
| # CrOS build tooling wants us to use these, and avoid a default of |
| # unprefixed 'gcc' or 'clang'. Supply defaults here, but also consider |
| # environment varibles below. |
| ret["HOSTCC"] = "x86_64-pc-linux-gnu-clang" |
| ret["HOSTCXX"] = "x86_64-pc-linux-gnu-clang++" |
| else: |
| prefix = "x86_64-linux-gnu" |
| ccompiler = prefix + "-gcc" |
| |
| if not shutil.which(ccompiler): |
| if in_chroot(): |
| die( |
| '%s not found. Try running "`sudo cros_setup_toolchains -t %s`"' |
| % (ccompiler, prefix) |
| ) |
| else: |
| die("%s not found. Try running inside chroot." % ccompiler) |
| |
| # Propagate a few environment variables as make args. |
| for key in ("HOSTCC", "HOSTCXX"): |
| val = os.getenv(key) |
| if val: |
| ret[key] = val |
| |
| ret.update( |
| { |
| "CC": ccompiler, |
| "CXX": prefix + "-g++", |
| "CROSS_COMPILE": prefix + "-", |
| } |
| ) |
| return ret |
| |
| |
| def getchar() -> str: |
| # We don't want line-buffering, and shelling out to bash is much simpler |
| # than messing with tty settings. |
| return ( |
| subprocess.check_output(["bash", "-c", 'read -n 1 -s; echo "${REPLY}"']) |
| .decode("utf-8") |
| .strip() |
| ) |
| |
| |
| def editconfig_prompt(variant: str) -> bool: |
| print("* %s: press <Enter> to edit, S to skip" % variant) |
| return getchar() not in ("S", "s") |
| |
| |
| def filter_match(target: str, pattern: str) -> bool: |
| return fnmatch.fnmatch(target, "*" + pattern + "*") |
| |
| |
| def is_editconfig_interactive( |
| arch_flavour: str, prompt: bool, pattern: str |
| ) -> bool: |
| if filter_match(arch_flavour, pattern): |
| if not prompt: |
| return True |
| return editconfig_prompt(arch_flavour) |
| return False |
| |
| |
| def make_cmd(builddir: str, arch: str, target: str) -> List[str]: |
| pairs = make_toolchain_args(arch) |
| pairs["ARCH"] = "arm" if arch == "armel" else arch |
| pairs["O"] = builddir |
| |
| make_args = [k + "=" + v for k, v in pairs.items()] |
| |
| return ["make", "-j"] + make_args + [target, "savedefconfig"] |
| |
| |
| def checkconfig(srcdir: str, outdir: str) -> int: |
| error = 0 |
| for src_config in glob.glob( |
| srcdir + "/chromeos/config/**/*.config", recursive=True |
| ): |
| relative = os.path.relpath(src_config, srcdir) |
| out_config = os.path.join(outdir, relative) |
| logging.debug("Compare %s to %s", src_config, out_config) |
| before = open(src_config, "r", encoding="utf-8").readlines() |
| after = open(out_config, "r", encoding="utf-8").readlines() |
| |
| if before == after: |
| continue |
| |
| error = 1 |
| diff = difflib.context_diff( |
| before, after, fromfile="a/" + relative, tofile="b/" + relative |
| ) |
| sys.stdout.writelines(diff) |
| logging.error("checkconfig failed for config %s", relative) |
| |
| if error: |
| logging.error( |
| """Consider running `%s olddefconfig` to normalize. |
| |
| NOTE: chromeos/config changes should always be in a CL by themselves and never |
| squashed into the same patch as code changes. If code and config changes need |
| to land together, consider using Cq-Depend to make a circular dependency.""", |
| sys.argv[0], |
| ) |
| else: |
| logging.info("All good!") |
| return error |
| |
| |
| def build_one_arch( |
| args, tmpdir: str, srcdir: str, family: str, arch: str |
| ) -> List[subprocess.Popen]: |
| procs = [] |
| config_base_dir = srcdir + "/chromeos/config/" + family |
| config_arch_dir = config_base_dir + "/" + arch |
| for flavourconfig in glob.glob( |
| os.path.join(srcdir, config_arch_dir, "*.flavour.config") |
| ): |
| flavour = os.path.basename(flavourconfig) |
| builddir = os.path.join(tmpdir, "build", family, arch, flavour) |
| os.makedirs(builddir) |
| |
| # Merge base.config, common.config, <flavour>.flavour.config |
| conf = "\n".join( |
| open(os.path.join(srcdir, f), encoding="utf-8").read() |
| for f in [ |
| config_base_dir + "/base.config", |
| config_arch_dir + "/common.config", |
| config_arch_dir + "/" + flavour, |
| ] |
| ) |
| open(os.path.join(builddir, ".config"), "w", encoding="utf-8").write( |
| conf |
| ) |
| |
| interactive = False |
| if args.mode == "genconfig": |
| mode = "olddefconfig" |
| elif args.mode == "editconfig": |
| interactive = is_editconfig_interactive( |
| family + "/" + arch + "/" + flavour, not args.yes, args.filter |
| ) |
| mode = "menuconfig" if interactive else "olddefconfig" |
| elif args.mode == "checkconfig": |
| mode = "olddefconfig" |
| else: |
| if args.mode == "oldconfig": |
| # 'oldconfig' may run into interactive prompts. |
| interactive = True |
| mode = args.mode |
| |
| cmd = make_cmd(builddir, arch, mode) |
| if interactive: |
| logging.info("Starting interactive cmd: %s", " ".join(cmd)) |
| ret = subprocess.run(cmd, check=False, cwd=srcdir) |
| if ret.returncode: |
| die('cmd "%s" failed' % " ".join(cmd)) |
| else: |
| logging.info("Starting background cmd: %s", " ".join(cmd)) |
| procs += [ |
| subprocess.Popen( |
| cmd, |
| cwd=srcdir, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| ) |
| ] |
| return procs |
| |
| |
| def do_splitconfig( |
| tmpdir: str, |
| srcdir: str, |
| outdir: str, |
| family: str, |
| arches: List[str], |
| save_configs: bool, |
| ): |
| family_cfg_dir = "chromeos/config/" + family |
| |
| if save_configs: |
| keep_dir = "CONFIGS/" + family |
| os.makedirs(keep_dir, exist_ok=True) |
| |
| for arch in arches: |
| dest = os.path.join(tmpdir, family, arch) |
| family_arch_dir = family_cfg_dir + "/" + arch |
| os.makedirs(dest) |
| for flavourconfig in glob.glob( |
| os.path.join(srcdir, family_arch_dir, "*.flavour.config") |
| ): |
| flavour = os.path.basename(flavourconfig) |
| builddir = os.path.join(tmpdir, "build", family, arch, flavour) |
| shutil.copy( |
| os.path.join(builddir, "defconfig"), os.path.join(dest, flavour) |
| ) |
| if save_configs: |
| shutil.copy( |
| os.path.join(builddir, ".config"), |
| os.path.join(keep_dir, arch + "-" + flavour), |
| ) |
| shutil.copy( |
| os.path.join(builddir, "defconfig"), |
| os.path.join(keep_dir, arch + "-" + flavour + ".def"), |
| ) |
| os.makedirs(os.path.join(outdir, family_arch_dir), exist_ok=True) |
| # Find per-arch common items; flavour-unique options can be written out |
| # immediately. |
| for config, contents in splitconfig(dest).items(): |
| if config == "common.config": |
| open( |
| os.path.join(tmpdir, family, arch + ".config"), |
| "w", |
| encoding="utf-8", |
| ).write(contents) |
| else: |
| open( |
| os.path.join(outdir, family_arch_dir, config), |
| "w", |
| encoding="utf-8", |
| ).write(contents) |
| |
| # Find cross-arch common items; common ones go to base.config, and unique |
| # ones to <arch>/common.config. |
| for config, contents in splitconfig(tmpdir + "/" + family).items(): |
| if config == "common.config": |
| open( |
| os.path.join(outdir, family_cfg_dir, "base.config"), |
| "w", |
| encoding="utf-8", |
| ).write(contents) |
| else: |
| assert config.endswith(".config") |
| # Python3.9: arch = config.removesuffix('.config') |
| arch = config[: -len(".config")] |
| open( |
| os.path.join(outdir, family_cfg_dir, arch, "common.config"), |
| "w", |
| encoding="utf-8", |
| ).write(contents) |
| |
| |
| # Arches relevant to this family. |
| def family_arches(srcdir: str, family: str) -> List[str]: |
| return [ |
| os.path.basename(p) |
| for p in glob.glob(srcdir + "/chromeos/config/" + family + "/*") |
| if os.path.isdir(p) |
| ] |
| |
| |
| def doit(args, tmpdir: str) -> int: |
| for d in ( |
| os.getcwd(), |
| os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "..", "..")), |
| ): |
| if os.path.exists(os.path.join(d, "MAINTAINERS")) and os.path.exists( |
| os.path.join(d, "Makefile") |
| ): |
| srcdir = d |
| logging.info("Using top kernel dir: %s", srcdir) |
| break |
| else: |
| die("This does not appear to be a kernel source directory.") |
| |
| if not in_chroot() and not args.force: |
| rerun_in_chroot() |
| assert False # Should not reach this. |
| |
| # Most commands write back to source directory. |
| if args.mode == "checkconfig": |
| outdir = tmpdir |
| else: |
| outdir = srcdir |
| |
| save_configs = args.mode == "genconfig" |
| |
| procs = [] |
| |
| families = [ |
| os.path.basename(p) |
| for p in glob.glob(srcdir + "/chromeos/config/*") |
| if os.path.isdir(p) |
| ] |
| for family in families: |
| for arch in family_arches(srcdir, family): |
| if not arch in TOOLCHAIN_PREFIXES: |
| die("Unexpected arch: %s" % arch) |
| procs += build_one_arch(args, tmpdir, srcdir, family, arch) |
| |
| for p in procs: |
| ret = p.wait() |
| if ret: |
| assert p.stderr # Remind pytype we captured stderr. |
| logging.error(p.stderr.read().decode("utf-8")) |
| die('cmd "%s" failed' % " ".join(p.args)) |
| |
| logging.info("Generating splitconfigs") |
| for family in families: |
| do_splitconfig( |
| tmpdir, |
| srcdir, |
| outdir, |
| family, |
| family_arches(srcdir, family), |
| save_configs, |
| ) |
| |
| if args.mode == "checkconfig": |
| return checkconfig(srcdir, outdir) |
| |
| return 0 |
| |
| |
| def main() -> int: |
| parser = argparse.ArgumentParser( |
| description="Chrome OS kernel configuration script", |
| epilog=""" |
| Note that Kbuild will evaluate some features depending on the |
| toolchain, so we try to enter the SDK chroot. This can be |
| overridden, with a potentially degraded experience. |
| """, |
| ) |
| |
| log_level_choices = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] |
| parser.add_argument( |
| "--log_level", "-l", choices=log_level_choices, default="INFO" |
| ) |
| parser.add_argument( |
| "--force", |
| "-F", |
| action="store_true", |
| help="Force allowing to run outside the chroot", |
| ) |
| parser.add_argument( |
| "--filter", |
| "-f", |
| default="", |
| help="Only attempt to edit configs which match filter", |
| ) |
| parser.add_argument( |
| "--yes", |
| "-y", |
| action="store_true", |
| help="Edit all configs which match unconditionally", |
| ) |
| parser.add_argument("mode", choices=MODES, help="sub-command/mode") |
| |
| args = parser.parse_args() |
| logging.basicConfig( |
| level=args.log_level, format="%(levelname)s - %(message)s" |
| ) |
| |
| with tempfile.TemporaryDirectory() as tmpdir: |
| return doit(args, tmpdir) |
| |
| |
| sys.exit(main()) |