#!/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 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.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 /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())