<reponame>oddlama/autokernel<gh_stars>10-100
import autokernel.kconfig
import autokernel.config
import autokernel.lkddb
import autokernel.node_detector
import autokernel.symbol_tracking
from autokernel import __version__
from autokernel import log
from autokernel import util
from autokernel.symbol_tracking import set_value_detect_conflicts

import argparse
import glob
import grp
import gzip
import kconfiglib
import os
import pwd
import re
import shutil
import stat
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path


def check_program_exists(exe):
    if shutil.which(exe) is None:
        log.die("Missing program '{}'. Please ensure that it is installed.".format(exe))

def check_execution_environment(args):
    """
    Checks that some required external programs exist, and some miscellaneous things.
    """
    check_program_exists('uname')
    check_program_exists('mount')
    check_program_exists('umount')
    check_program_exists('make')

    cur_uid = os.geteuid()
    with autokernel.config.config_file_path(args.autokernel_config, warn=True) as config_file:
        def _die_writable_config_by(component, name):
            log.die("Refusing to run, because the path '{0}' is writable by {1}. This allows {1} to replace the configuration '{2}' and thus inject commands.".format(component, name, config_file))

        if not config_file.exists():
            log.die("Configuration file '{}' does not exist!".format(config_file))

        # Ensure that the config file has the correct mode, to prevent command-injection by other users.
        # No component of the path may be modifiable by anyone else but the current user (or root).
        config_path = config_file.resolve()
        for component in [config_path] + [p for p in config_path.parents]:
            st = component.stat()
            if st.st_uid != cur_uid and st.st_uid != 0 and st.st_mode & stat.S_IWUSR:
                _die_writable_config_by(component, 'user {} ({})'.format(st.st_uid, pwd.getpwuid(st.st_uid).pw_name))
            if st.st_gid != 0 and st.st_mode & stat.S_IWGRP:
                _die_writable_config_by(component, 'group {} ({})'.format(st.st_gid, grp.getgrgid(st.st_gid).gr_name))
            if st.st_mode & stat.S_IWOTH:
                _die_writable_config_by(component, 'others')

def replace_common_vars(args, p):
    p = str(p)
    p = p.replace('{KERNEL_DIR}', args.kernel_dir)
    p = p.replace('{KERNEL_VERSION}', autokernel.kconfig.get_kernel_version(args.kernel_dir))
    p = p.replace('{UNAME_ARCH}', autokernel.kconfig.get_uname_arch())
    p = p.replace('{ARCH}', autokernel.kconfig.get_arch())
    return p

def has_proc_config_gz():
    """
    Checks if /proc/config.gz exists
    """
    return os.path.isfile("/proc/config.gz")

def unpack_proc_config_gz():
    """
    Unpacks /proc/config.gz into a temporary file
    """
    tmp = tempfile.NamedTemporaryFile()
    with gzip.open("/proc/config.gz", "rb") as f:
        shutil.copyfileobj(f, tmp)
    return tmp

def kconfig_load_file_or_current_config(kconfig, config_file):
    """
    Applies the given kernel config file to kconfig, or uses /proc/config.gz if config_file is None.
    """

    if config_file:
        log.info("Applying kernel config from '{}'".format(config_file))
        kconfig.load_config(os.path.realpath(config_file))
    else:
        log.info("Applying kernel config from '/proc/config.gz'")
        with unpack_proc_config_gz() as tmp:
            kconfig.load_config(os.path.realpath(tmp.name))

def generated_by_autokernel_header():
    return "# Generated by autokernel on {}\n".format(datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"))

def vim_config_modeline_header():
    return "# vim: set ft=ruby ts=4 sw=4 sts=-1 noet:\n"

def apply_autokernel_config(args, kconfig, config):
    """
    Applies the given autokernel configuration to a freshly loaded kconfig object,
    and returns gathered extra information such as the resulting kernel cmdline
    """
    log.info("Applying autokernel configuration")

    # Build cmdline on demand
    kernel_cmdline = []
    # Reset symbol_changes
    autokernel.symbol_tracking.symbol_changes.clear()

    # Asserts that the symbol has the given value
    def get_sym(stmt):
        # Get the kconfig symbol, and change the value
        try:
            return kconfig.syms[stmt.sym_name]
        except KeyError:
            log.die_print_error_at(stmt.at, "symbol '{}' does not exist".format(stmt.sym_name))

    # Asserts that the symbol has the given value
    def assert_symbol(stmt):
        if not stmt.assert_condition.evaluate(kconfig):
            if stmt.message:
                log.die_print_error_at(stmt.at, "assertion failed: {}".format(stmt.message))
            else:
                log.die_print_error_at(stmt.at, "assertion failed")

    # Sets a symbols value if and asserts that there are no conflicting double assignments
    def set_symbol(stmt):
        # Get the kconfig symbol, and change the value
        sym = get_sym(stmt)
        value = stmt.value

        if not autokernel.kconfig.symbol_can_be_user_assigned(sym):
            log.die_print_error_at(stmt.at, "symbol {} can't be user-assigned".format(sym.name))

        # Skip assignment if value is already pinned and the statement is in try mode.
        if stmt.has_try and sym in autokernel.symbol_tracking.symbol_changes:
            log.verbose("skipping {} {}".format(autokernel.kconfig.value_to_str(value), sym.name))
            return

        if util.is_env_var(value):
            value = util.resolve_env_variable(stmt.at, value)

        if not set_value_detect_conflicts(sym, value, stmt.at):
            log.die_print_error_at(stmt.at, "invalid value {} for symbol {}".format(autokernel.kconfig.value_to_str(value), sym.name))

        if sym.str_value != value:
            if not stmt.has_try:
                # Only throw an error if it wasn't a try
                log.die_print_error_at(stmt.at, "symbol assignment failed: {} from {} → {}".format(
                    sym.name,
                    autokernel.kconfig.value_to_str(sym.str_value),
                    autokernel.kconfig.value_to_str(value)))
            else:
                log.verbose("failed try set {} {} (symbol is currently not assignable to the chosen value)".format(autokernel.kconfig.value_to_str(stmt.value), sym.name))

    # Visit all module nodes and apply configuration changes
    visited = set()
    def visit(module):
        # Ensure we visit only once
        if module.name in visited:
            return
        visited.add(module.name)

        def stmt_use(stmt):
            visit(stmt.module)

        def stmt_merge(stmt):
            filename = replace_common_vars(args, stmt.filename)
            log.verbose("Merging external kconf '{}'".format(filename))
            kconfig.load_config(os.path.realpath(filename), replace=False)

            # Assert that there are no conflicts
            for sym in autokernel.symbol_tracking.symbol_changes:
                sc = autokernel.symbol_tracking.symbol_changes[sym]
                if sym.str_value != sc.value:
                    autokernel.symbol_tracking.die_print_conflict(stmt.at, 'merge', sym, sym.str_value, sc)

        def stmt_assert(stmt):
            assert_symbol(stmt)

        def stmt_set(stmt):
            set_symbol(stmt)

        def stmt_add_cmdline(stmt):
            kernel_cmdline.append(stmt.param)

        dispatch_stmt = {
            autokernel.config.ConfigModule.StmtUse: stmt_use,
            autokernel.config.ConfigModule.StmtMerge: stmt_merge,
            autokernel.config.ConfigModule.StmtAssert: stmt_assert,
            autokernel.config.ConfigModule.StmtSet: stmt_set,
            autokernel.config.ConfigModule.StmtAddCmdline: stmt_add_cmdline,
        }

        def conditions_met(stmt):
            for condition in stmt.conditions:
                if not condition.evaluate(kconfig):
                    return False
            return True

        for stmt in module.all_statements_in_order:
            # Ensure all attached conditions are met for the statement.
            if conditions_met(stmt):
                dispatch_stmt[stmt.__class__](stmt)

    # Visit the root node and apply all symbol changes
    visit(config.kernel.module)
    log.verbose("  Changed {} symbols".format(len(autokernel.symbol_tracking.symbol_changes)))

    # Lastly, invalidate all non-assigned symbols to process new default value conditions
    for sym in kconfig.unique_defined_syms:
        if sym.user_value is None:
            sym._invalidate() # pylint: disable=protected-access

    return kernel_cmdline

def execute_command(args, name, cmd, _replace_vars):
    if len(cmd.value) > 0:
        command = [_replace_vars(args, p) for p in cmd.value]
        log.info("Executing {}: [{}]".format(name, ', '.join(["'{}'".format(i) for i in command])))
        try:
            # Replace variables in command and run it
            subprocess.run(command, check=True)
        except subprocess.CalledProcessError as e:
            log.die("{} failed with code {}. Aborting.".format(name, e.returncode))

def main_setup(args):
    """
    Main function for the 'setup' command.
    """
    log.info("Setting up autokernel configuration at '{}'".format(args.setup_dir))

    setup_dir = Path(args.setup_dir)
    if setup_dir.exists():
        log.die("Refusing to setup: directory '{}' exists".format(args.setup_dir))

    saved_umask = os.umask(0o077)

    setup_dir.mkdir()
    modules_d_dir = setup_dir / 'modules.d'
    modules_d_dir.mkdir()

    import autokernel.contrib.etc as etc
    import autokernel.contrib.etc.modules_d as modules_d
    for i in util.resource_contents(etc):
        if i.endswith('.conf'):
            with (setup_dir / i).open('w') as f:
                f.write(util.read_resource(i, pkg=etc))

    for i in util.resource_contents(modules_d):
        if i.endswith('.conf'):
            with (modules_d_dir / i).open('w') as f:
                f.write(util.read_resource(i, pkg=modules_d))

    os.umask(saved_umask)

    log.info("A default configuration has been installed")
    log.info("You might want to edit it now.")

def main_check_config(args):
    """
    Main function for the 'check' command.
    """
    if args.compare_config:
        if not args.compare_kernel_dir:
            args.compare_kernel_dir = args.kernel_dir

        kname_cmp = "'{}'".format(args.compare_config)
    else:
        if not has_proc_config_gz():
            log.die("This kernel does not expose /proc/config.gz. Please provide the path to a valid config file manually.")

        if not args.compare_kernel_dir:
            # Use /usr/src/linux-{kernel_version} as the directory.
            running_kver = subprocess.run(['uname', '-r'], check=True, stdout=subprocess.PIPE).stdout.decode().strip().splitlines()[0]
            args.compare_kernel_dir = os.path.join('/usr/src/linux-{}'.format(running_kver))
            try:
                check_kernel_dir(args.compare_kernel_dir)
            except argparse.ArgumentTypeError:
                log.die("Could not find sources for running kernel (version {}) in '{}', use --check_kernel_dir to specify it manually.".format(running_kver, args.compare_kernel_dir))

        kname_cmp = 'running kernel'

    log.info("Comparing {} against generated config".format(kname_cmp))

    # Load configuration file
    config = autokernel.config.load_config(args.autokernel_config)

    # Load symbols from Kconfig
    kconfig_gen = autokernel.kconfig.load_kconfig(args.kernel_dir)
    # Apply autokernel configuration
    apply_autokernel_config(args, kconfig_gen, config)

    # Load symbols from Kconfig
    kconfig_cmp = autokernel.kconfig.load_kconfig(args.compare_kernel_dir)
    # Load the given config file or the current kernel's config
    kconfig_load_file_or_current_config(kconfig_cmp, args.compare_config)

    indicator_del = log.color("[31m-[m", "-")
    indicator_add = log.color("[32m+[m", "+")
    indicator_mod = log.color("[33m~[m", "~")

    log.info("Comparing existing config (left) against generated config (right)")
    log.info("  ({}) symbol was removed".format(indicator_del))
    log.info("  ({}) symbol is new".format(indicator_add))
    log.info("  ({}) symbol value changed".format(indicator_mod))

    gen_syms = [s.name for s in kconfig_gen.unique_defined_syms]
    cmp_syms = [s.name for s in kconfig_cmp.unique_defined_syms]

    def intersection(a, b):
        return [i for i in a if i in b]
    def comprehension(a, b):
        return [i for i in a if i not in b]

    common_syms = intersection(gen_syms, set(cmp_syms))
    common_syms_set = set(common_syms)
    only_gen_syms = comprehension(gen_syms, common_syms_set)
    only_cmp_syms = comprehension(cmp_syms, common_syms_set)


    supress_new, supress_del, supress_chg = (args.suppress_columns or (False, False, False))

    if not supress_new:
        for sym in only_gen_syms:
            sym_gen = kconfig_gen.syms[sym]
            print(indicator_add + " {} {}".format(
                autokernel.kconfig.value_to_str(sym_gen.str_value),
                sym))

    if not supress_del:
        for sym in only_cmp_syms:
            sym_cmp = kconfig_cmp.syms[sym]
            print(indicator_del + " {} {}".format(
                autokernel.kconfig.value_to_str(sym_cmp.str_value),
                sym))

    if not supress_chg:
        for sym in common_syms:
            sym_gen = kconfig_gen.syms[sym]
            sym_cmp = kconfig_cmp.syms[sym]
            if sym_gen.str_value != sym_cmp.str_value:
                print(indicator_mod + " {} → {} {}".format(
                    autokernel.kconfig.value_to_str(sym_cmp.str_value),
                    autokernel.kconfig.value_to_str(sym_gen.str_value),
                    sym))

def main_generate_config(args, config=None):
    """
    Main function for the 'generate_config' command.
    """
    log.info("Generating kernel configuration")
    if not config:
        # Load configuration file
        config = autokernel.config.load_config(args.autokernel_config)

    # Fallback for config output
    if not hasattr(args, 'output') or not args.output:
        args.output = os.path.join(args.kernel_dir, '.config')

    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)
    # Apply autokernel configuration
    apply_autokernel_config(args, kconfig, config)

    # Write configuration to file
    kconfig.write_config(
            filename=args.output,
            header=generated_by_autokernel_header(),
            save_old=False)

    log.info("Configuration written to '{}'".format(args.output))

def clean_kernel_dir(args):
    """
    Clean the kernel tree (call make distclean)
    """
    try:
        subprocess.run(['make', 'distclean'], cwd=args.kernel_dir, check=True)
    except subprocess.CalledProcessError as e:
        log.die("'make distclean' failed in {} with code {}".format(args.kernel_dir, e.returncode))

def build_kernel(args):
    """
    Build the kernel (call make)
    """
    try:
        subprocess.run(['make'], cwd=args.kernel_dir, check=True)
    except subprocess.CalledProcessError as e:
        log.die("'make' failed in {} with code {}".format(args.kernel_dir, e.returncode))

def build_initramfs(args, config, modules_prefix, initramfs_output):
    log.info("Building initramfs")

    def _replace_vars(args, p):
        p = replace_common_vars(args, p)
        if '{MODULES_PREFIX}' in p:
            if modules_prefix is None:
                log.die(f"A variable used {{MODULES_PREFIX}}, but kernel module support is disabled!")
            p = p.replace('{MODULES_PREFIX}', modules_prefix)
        p = p.replace('{INITRAMFS_OUTPUT}', initramfs_output)
        return p

    # Execute initramfs build_command
    execute_command(args, 'initramfs.build_command', config.initramfs.build_command, _replace_vars)
    if config.initramfs.build_output:
        cmd_output_file = _replace_vars(args, config.initramfs.build_output.value)
        try:
            # Move the output file as stated in the configuration to the kernel tree
            shutil.move(cmd_output_file, initramfs_output)
        except IOError as e:
            log.die("Could not copy initramfs from '{}' to '{}': {}".format(cmd_output_file, initramfs_output, str(e)))

def install_modules(args, prefix="/"):
    """
    Installs the modules to the given prefix
    """
    # Use correct 022 umask when installing modules
    saved_umask = os.umask(0o022)
    try:
        subprocess.run(['make', 'modules_install', 'INSTALL_MOD_PATH=' + prefix], cwd=args.kernel_dir, check=True, stdout=None)
    except subprocess.CalledProcessError as e:
        log.die("'make modules_install INSTALL_MOD_PATH={}' failed in {} with code {}".format(prefix, args.kernel_dir, e.returncode))
    os.umask(saved_umask)

def main_build(args, config=None):
    """
    Main function for the 'build' command.
    """
    if not config:
        # Load configuration file
        config = autokernel.config.load_config(args.autokernel_config)

    # Set umask for build
    saved_umask = os.umask(config.build.umask.value)

    # Execute pre hook
    execute_command(args, 'build.hooks.pre', config.build.hooks.pre, replace_common_vars)

    # Clean the kernel dir, if the user wants that
    if args.clean:
        log.info("Cleaning kernel directory")
        clean_kernel_dir(args)

    kernel_version = autokernel.kconfig.get_kernel_version(args.kernel_dir)
    # Config output is "{KERNEL_DIR}/.config"
    config_output = os.path.join(args.kernel_dir, '.config.autokernel')
    # Initramfs basename "initramfs-{KERNEL_VERSION}.cpio"
    # The .cpio suffix is cruical, as the kernel makefile requires it to detect initramfs archives
    initramfs_basename = 'initramfs-{}.cpio'.format(kernel_version)
    # Initramfs output is "{KERNEL_DIR}/initramfs-{KERNEL_VERSION}.cpio"
    initramfs_output = os.path.join(args.kernel_dir, initramfs_basename)

    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)
    sym_cmdline_bool = kconfig.syms['CMDLINE_BOOL']
    sym_cmdline = kconfig.syms['CMDLINE']
    sym_initramfs_source = kconfig.syms['INITRAMFS_SOURCE']
    sym_modules = kconfig.syms['MODULES']

    # Set some defaults
    sym_cmdline_bool.set_value('y')
    sym_cmdline.set_value('')
    sym_initramfs_source.set_value('{INITRAMFS}')

    # Apply autokernel configuration
    kernel_cmdline = apply_autokernel_config(args, kconfig, config)

    def _build_kernel():
        # Write configuration to file
        kconfig.write_config(
                filename=config_output,
                header=generated_by_autokernel_header(),
                save_old=False)

        # Copy file to .config, which may get changed by the makefiles
        shutil.copyfile(config_output, os.path.join(args.kernel_dir, '.config'))
        # Build the kernel
        build_kernel(args)

    def set_cmdline():
        kernel_cmdline_str = ' '.join(kernel_cmdline)

        has_user_cmdline_bool = sym_cmdline_bool in autokernel.symbol_tracking.symbol_changes
        has_user_cmdline = sym_cmdline in autokernel.symbol_tracking.symbol_changes

        if has_user_cmdline_bool and sym_cmdline_bool.str_value == 'n':
            # The user has explicitly disabled the builtin commandline,
            # so there is no need to set it.
            pass
        else:
            sym_cmdline_bool.set_value('y')

            # Issue a warning, if a custom cmdline does not contain "{CMDLINE}", and we have gathered add_cmdline options.
            if has_user_cmdline and not sym_cmdline.str_value.contains('{CMDLINE}') and len(kernel_cmdline) > 0:
                log.warn("CMDLINE was set manually and doesn't contain a '{CMDLINE}' token, although add_cmdline has also been used.")

            if has_user_cmdline:
                sym_cmdline.set_value(sym_cmdline.str_value.replace('{CMDLINE}', kernel_cmdline_str))
            else:
                sym_cmdline.set_value(kernel_cmdline_str)

    def check_initramfs_source(sym_initramfs_source):
        has_user_initramfs_source = sym_initramfs_source in autokernel.symbol_tracking.symbol_changes

        # It is an error to explicitly set INITRAMFS_SOURCE, if our initramfs is set to builtin.
        if has_user_initramfs_source \
                and config.initramfs.enabled \
                and config.initramfs.builtin \
                and autokernel.symbol_tracking.symbol_changes[sym_initramfs_source].reason == 'explicitly set':
            log.die("INITRAMFS_SOURCE was set manually, although a custom initramfs should be built and integrated into the kernel.")

    # Set CMDLINE_BOOL and CMDLINE
    set_cmdline()
    # Preprocess INITRAMFS_SOURCE
    check_initramfs_source(sym_initramfs_source)

    # Kernel build pass #1
    log.info("Building kernel")
    # On the first pass, disable all initramfs sources
    sym_initramfs_source.set_value('')
    # Start the build process
    _build_kernel()

    # Build the initramfs, if enabled
    if config.initramfs.enabled:
        with tempfile.TemporaryDirectory() as tmppath:
            if sym_modules.str_value != 'n':
                # Temporarily install modules so the initramfs generator has access to them
                log.info("Copying modules into temporary directory")
                tmp_modules_prefix = os.path.join(tmppath, 'modules')
                install_modules(args, prefix=tmp_modules_prefix)
            else:
                tmp_modules_prefix = None

            # Build the initramfs
            build_initramfs(args, config, tmp_modules_prefix, initramfs_output)

            # Pack the initramfs into the kernel if desired
            if config.initramfs.builtin:
                log.info("Rebuilding kernel to pack external resources")
                # On the second pass, we enable the initramfs cpio archive, which is now in the kernel_dir
                sym_initramfs_source.set_value(initramfs_basename)
                # Rebuild the kernel to pack the new images
                _build_kernel()

    # Execute post hook
    execute_command(args, 'build.hooks.post', config.build.hooks.post, replace_common_vars)

    os.umask(saved_umask)

def main_install(args, config=None):
    """
    Main function for the 'install' command.
    """
    if not config:
        # Load configuration file
        config = autokernel.config.load_config(args.autokernel_config)

    # Use correct umask when installing
    saved_umask = os.umask(config.install.umask.value)

    # Mount
    new_mounts = []
    for i in config.install.mount:
        if not os.access(i, os.R_OK):
            log.die("Permission denied on accessing '{}'. Aborting.".format(i))

        if not os.path.ismount(i):
            log.info("Mounting {}".format(i))
            new_mounts.append(i)
            try:
                subprocess.run(['mount', '--', i], check=True)
            except subprocess.CalledProcessError as e:
                log.die("Could not mount '{}', mount returned code {}. Aborting.".format(i, e.returncode))

    # Check mounts
    for i in config.install.mount + config.install.assert_mounted:
        if not os.access(i, os.R_OK):
            log.die("Permission denied on accessing '{}'. Aborting.".format(i))

        if not os.path.ismount(i):
            log.die("'{}' is not mounted. Aborting.".format(i))

    # Execute pre hook
    execute_command(args, 'install.hooks.pre', config.install.hooks.pre, replace_common_vars)

    kernel_version = autokernel.kconfig.get_kernel_version(args.kernel_dir)
    target_dir = replace_common_vars(args, config.install.target_dir)
    # Config output is "{KERNEL_DIR}/.config"
    config_output = os.path.join(args.kernel_dir, '.config.autokernel')
    # Initramfs basename "initramfs-{KERNEL_VERSION}.cpio"
    # The .cpio suffix is cruical, as the kernel makefile requires it to detect initramfs archives
    initramfs_basename = 'initramfs-{}.cpio'.format(kernel_version)
    # Initramfs output is "{KERNEL_DIR}/initramfs-{KERNEL_VERSION}.cpio"
    initramfs_output = os.path.join(args.kernel_dir, initramfs_basename)
    # bzImage output
    bzimage_output = os.path.join(args.kernel_dir, 'arch', autokernel.kconfig.get_uname_arch(), 'boot/bzImage')

    def _purge_old(path):
        keep_old = config.install.keep_old.value
        # Disable purging on negative count
        if keep_old < 0:
            return

        # Disable purging for non versionated paths
        if not '{KERNEL_VERSION}' in path:
            return

        tokens = path.split('{KERNEL_VERSION}')
        if len(tokens) > 2:
            log.warn("Cannot purge path with more than one {{KERNEL_VERSION}} token: '{}'".format(path))
            return

        re_semver = re.compile(r'^[\d\.]+\d')
        def _version_sorter(i):
            suffix = i[len(tokens[0]):]
            basename = suffix.split('/')[0]

            st = os.stat(i)
            try:
                time_create = st.st_birthtime
            except AttributeError:
                time_create = st.st_mtime

            semver = re_semver.match(basename).group()
            val = autokernel.config.semver_to_int(semver)
            return val, time_create

        escaped_kv = re.escape('{KERNEL_VERSION}')
        # matches from {KERNEL_VERSION} until first / exclusive in an regex escaped path
        match_basename = re.compile(re.escape(escaped_kv) + r"(.+?(?=\\\/|$)).*$")
        # derive regex to check if a valid semver is contained and prefix and suffix are given
        re_match_valid_paths = re.compile('^' + match_basename.sub(lambda m: r'[0-9]+(\.[0-9]+(\.[0-9]+)?)?(-[^\/]*)?' + m.group(1) + r'.*$', re.escape(path)))

        # matches from {KERNEL_VERSION} until first / exclusive in a normal path
        re_replace_wildcard = re.compile(escaped_kv + r"[^\/]*")
        # replace {KERNEL_VERSION}-* component with *
        wildcard_path = re_replace_wildcard.sub('*', glob.escape(path))

        # sort out paths that don't contain valid semvers
        valid_globbed = [i for i in glob.glob(wildcard_path) if re_match_valid_paths.match(i)]
        for i in sorted(valid_globbed, key=_version_sorter)[:-(keep_old + 1)]:
            # For security, we will not call rmtree on a path that doesn't end with a slash,
            # or if the realpath has less then two slash characters in it.
            # Otherwise we only call unlink
            if i[-1] == '/' and os.path.realpath(i).count('/') >= 2:
                try:
                    shutil.rmtree(i)
                except OSError as e:
                    log.warn("Could not remove {}: {}".format(i, str(e)))
            else:
                try:
                    os.unlink(i)
                except IOError as e:
                    log.warn("Could not remove {}: {}".format(i, str(e)))

    def _move_to_old(path):
        re_old_suffix = re.compile(r'^.*\.old(\.\d+)?\/*$')
        dst = path + '.old'
        highest_num = -1
        for i in glob.glob(glob.escape(dst) + '*'):
            m = re_old_suffix.match(i)
            old_num = int((m.group(1) or '.0')[1:]) if m else 0
            if highest_num < old_num:
                highest_num = old_num

        if highest_num >= 0:
            dst += ".{:d}".format(highest_num + 1)

        shutil.move(path, dst)

    def _install(name, src, target_var):
        # If the target is disabled, return.
        if not target_var:
            return

        # Figure out destination, and move existing filed if necessary
        dst = os.path.join(target_dir, replace_common_vars(args, target_var))
        if os.path.exists(dst):
            _move_to_old(dst)

        # Create directory if it doesn't exist
        Path(os.path.dirname(dst)).mkdir(parents=True, exist_ok=True)

        log.info("Installing {:<11s} {}".format(name + ':', dst))
        # Install target file
        shutil.copyfile(src, dst)
        # Purge old files
        _purge_old(os.path.join(target_dir, str(target_var)))

    # Move target_dir, if it is dynamic
    if '{KERNEL_VERSION}' in str(config.install.target_dir) and os.path.exists(target_dir):
        _move_to_old(os.path.realpath(target_dir))

    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)
    sym_modules = kconfig.syms['MODULES']

    # Install modules
    if config.install.modules_prefix and sym_modules.str_value != 'n':
        modules_prefix = str(config.install.modules_prefix)
        modules_prefix_with_lib = os.path.join(modules_prefix, "lib/modules")
        modules_dir = os.path.join(modules_prefix_with_lib, kernel_version)
        if os.path.exists(modules_dir):
            _move_to_old(os.path.realpath(modules_dir))
        log.info("Installing modules:    {}".format(modules_prefix_with_lib))
        install_modules(args, prefix=modules_prefix)
        _purge_old(modules_prefix_with_lib + "/{KERNEL_VERSION}/")

    # Install targets
    _install('bzimage', bzimage_output, config.install.target_kernel)
    _install('config',  config_output,  config.install.target_config)
    if config.initramfs.enabled:
        _install('initramfs', initramfs_output, config.install.target_initramfs)

    # Purge old target_dirs (will only be done if it is dynamic)
    _purge_old(str(config.install.target_dir) + '/')

    # Execute post hook
    execute_command(args, 'install.hooks.post', config.install.hooks.post, replace_common_vars)

    # Undo what we have mounted
    for i in reversed(new_mounts):
        log.info("Unmounting {}".format(i))
        try:
            subprocess.run(['umount', '--', i], check=True)
        except subprocess.CalledProcessError as e:
            log.warn("Could not umount '{}' (returned {})".format(i, e.returncode))

    # Restore old umask
    os.umask(saved_umask)

def main_build_all(args):
    """
    Main function for the 'all' command.
    """
    log.info("Started full build")
    # Load configuration file
    config = autokernel.config.load_config(args.autokernel_config)

    main_build(args, config)
    main_install(args, config)

class Module():
    """
    A module consists of dependencies (other modules) and option assignments.
    """
    def __init__(self, name):
        self.name = name
        self.deps = []
        self.assignments = []
        self.assertions = []
        self.rev_deps = []

def check_config_against_detected_modules(kconfig, modules, differences_only):
    log.info("Here are the detected options with both current and desired value.")
    log.info("The output format is: [current] OPTION_NAME = desired")
    log.info("HINT: Options are ordered by dependencies, i.e. applying")
    log.info("      them from top to buttom will work")
    if differences_only:
        log.info("Detected options (differences only):")
    else:
        log.info("Indicators: (=) same, (~) changed")
        log.info("Detected options:")

    visited = set()
    visited_opts = set()

    if differences_only:
        indicator_same = ""
        indicator_changed = ""
    else:
        indicator_same = log.color('[32m=[m', '=')
        indicator_changed = log.color('[33m~[m', '~')

    def visit_opt(opt, new_value):
        # Ensure we visit only once
        if opt in visited_opts:
            return
        visited_opts.add(opt)

        sym = kconfig.syms[opt]
        changed = sym.str_value != new_value

        if changed:
            print(indicator_changed + " {} → {} {}".format(autokernel.kconfig.value_to_str(sym.str_value), autokernel.kconfig.value_to_str(new_value), sym.name))
        else:
            if not differences_only:
                print(indicator_same + "       {} {}".format(autokernel.kconfig.value_to_str(sym.str_value), sym.name))

    def visit(m):
        # Ensure we visit only once
        if m in visited:
            return
        visited.add(m)

        # First visit all dependencies
        for d in m.deps:
            visit(d)
        # Then print all assignments
        for a, v in m.assignments:
            visit_opt(a, v)

    # Visit all modules
    for m in modules:
        visit(modules[m])

class KernelConfigWriter:
    """
    Writes modules to the given file in kernel config format.
    """
    def __init__(self, file):
        self.file = file
        self.file.write(generated_by_autokernel_header())
        self.file.write(vim_config_modeline_header())

    def write_module(self, module):
        if len(module.assignments) == len(module.assertions) == 0:
            return

        content = ""
        for d in module.rev_deps:
            content += "# required by {}\n".format(d.name)
        content += "# module {}\n".format(module.name)
        for a, v in module.assignments:
            if v in "nmy":
                content += "CONFIG_{}={}\n".format(a, v)
            else:
                content += "CONFIG_{}=\"{}\"\n".format(a, v)
        for o, v in module.assertions:
            content += "# REQUIRES {} {}\n".format(o, v)
        self.file.write(content)

class ModuleConfigWriter:
    """
    Writes modules to the given file in the module config format.
    """
    def __init__(self, file):
        self.file = file
        self.file.write(generated_by_autokernel_header())
        self.file.write(vim_config_modeline_header())

    def write_module(self, module):
        content = ""
        for d in module.rev_deps:
            content += "# required by {}\n".format(d.name)
        content += "module {} {{\n".format(module.name)
        for d in module.deps:
            content += "\tuse {};\n".format(d.name)
        for a, v in module.assignments:
            content += "\tset {} {};\n".format(a, v)
        for o, v in module.assertions:
            content += "\t#assert {} == {};\n".format(o, v)
        content += "}\n\n"
        self.file.write(content)

class ModuleCreator:
    def __init__(self, module_prefix=''):
        self.modules = {}
        self.module_for_sym = {}
        self.module_select_all = Module('module_select_all')
        self.module_prefix = module_prefix

    def _create_reverse_deps(self):
        # Clear rev_deps
        for m in self.modules:
            self.modules[m].rev_deps = []
        self.module_select_all.rev_deps = []

        # Fill in reverse dependencies for all modules
        for m in self.modules:
            for d in self.modules[m].deps:
                d.rev_deps.append(self.modules[m])

        # Fill in reverse dependencies for select_all module
        for d in self.module_select_all.deps:
            d.rev_deps.append(self.module_select_all)

    def _add_module_for_option(self, sym):
        """
        Recursively adds a module for the given option,
        until all dependencies are satisfied.
        """
        mod = Module(self.module_prefix + "config_{}".format(sym.name.lower()))

        # Find dependencies if needed
        needs_deps = not kconfiglib.expr_value(sym.direct_dep)
        if needs_deps:
            req_deps = autokernel.kconfig.required_deps(sym)
            if req_deps is False:
                # Dependencies can never be satisfied. The module should be skipped.
                log.warn("Cannot satisfy dependencies for {}".format(sym.name))
                return False

        if not autokernel.kconfig.symbol_can_be_user_assigned(sym):
            # If we cannot assign the symbol, we add an assertion instead.
            mod.assertions.append((sym.name, 'y'))
        else:
            mod.assignments.append((sym.name, 'y'))

            if needs_deps:
                for d, v in req_deps:
                    if v:
                        depm = self.add_module_for_sym(d)
                        if depm is False:
                            return False
                        mod.deps.append(depm)
                    else:
                        if autokernel.kconfig.symbol_can_be_user_assigned(sym):
                            mod.assignments.append((d.name, 'n'))
                        else:
                            mod.assertions.append((d.name, 'n'))

        self.modules[mod.name] = mod
        return mod

    def add_module_for_sym(self, sym):
        """
        Adds a module for the given symbol (and its dependencies).
        """
        if sym in self.module_for_sym:
            return self.module_for_sym[sym]

        # Create a module for the symbol, if it doesn't exist already
        mod = self._add_module_for_option(sym)
        if mod is False:
            return False
        self.module_for_sym[sym] = mod
        return mod

    def select_module(self, mod):
        self.module_select_all.deps.append(mod)

    def add_external_module(self, mod):
        self.modules[mod.name] = mod

    def _write_detected_modules(self, f, output_type, output_module_name):
        """
        Writes the collected modules to a file / stdout, in the requested output format.
        """
        if output_type == 'kconf':
            writer = KernelConfigWriter(f)
        elif output_type == 'module':
            writer = ModuleConfigWriter(f)
        else:
            log.die("Invalid output_type '{}'".format(output_type))

        # Set select_all name
        self.module_select_all.name = output_module_name

        # Fill in reverse dependencies for all modules
        self._create_reverse_deps()

        visited = set()
        def visit(m):
            # Ensure we visit only once
            if m in visited:
                return
            visited.add(m)
            writer.write_module(m)

        # Write all modules in topological order
        for m in self.modules:
            visit(self.modules[m])

        # Lastly, write "select_all" module, if it has been used
        if len(self.module_select_all.deps) > 0:
            writer.write_module(self.module_select_all)

    def write_detected_modules(self, args):
        # Write all modules in the given format to the given output file / stdout
        if args.output:
            try:
                with open(args.output, 'w') as f:
                    self._write_detected_modules(f, args.output_type, args.output_module_name)
                    log.info("Module configuration written to '{}'".format(args.output))
            except IOError as e:
                log.die(str(e))
        else:
            self._write_detected_modules(sys.stdout, args.output_type, args.output_module_name)

def detect_modules(kconfig):
    """
    Detects required options for the current system organized into modules.
    Any option with dependencies will also be represented as a module. It returns
    a dict which maps module names to the module objects. The special module returned
    additionaly is the module which selects all detected modules as dependencies.
    """
    log.info("Detecting kernel configuration for local system")
    log.info("HINT: It might be beneficial to run this while using a very generic")
    log.info("      and modular kernel, such as the default kernel on Arch Linux.")

    local_module_count = 0
    def next_local_module_id():
        """
        Returns the next id for a local module
        """
        nonlocal local_module_count
        i = local_module_count
        local_module_count += 1
        return i

    module_creator = ModuleCreator(module_prefix='detected_')
    def add_module_for_detected_node(node, opts):
        """
        Adds a module for the given detected node
        """
        mod = Module("{:04d}_{}".format(next_local_module_id(), node.get_canonical_name()))
        for o in opts:
            try:
                sym = kconfig.syms[o]
            except KeyError:
                log.warn("Skipping unknown symbol {}".format(o))
                continue

            m = module_creator.add_module_for_sym(sym)
            if m is False:
                log.warn("Skipping module {} (unsatisfiable dependencies)".format(mod.name))
                return None
            mod.deps.append(m)
        module_creator.add_external_module(mod)
        return mod

    # Load the configuration database
    config_db = autokernel.lkddb.Lkddb()
    # Inspect the current system
    detector = autokernel.node_detector.NodeDetector()

    # Try to find detected nodes in the database
    log.info("Matching detected nodes against database")

    # First sort all nodes for more consistent output between runs
    all_nodes = []
    # Find options in database for each detected node
    for detector_node in detector.nodes:
        all_nodes.extend(detector_node.nodes)
    all_nodes.sort(key=lambda x: x.get_canonical_name())

    for node in all_nodes:
        opts = config_db.find_options(node)
        if len(opts) > 0:
            # If there are options for the node in the database,
            # add a module for the detected node and its options
            mod = add_module_for_detected_node(node, opts)
            if mod:
                # Select the module in the global selector module
                module_creator.select_module(mod)

    return module_creator

def main_detect(args):
    """
    Main function for the 'main_detect' command.
    """
    # Check if we should write a config or report differences
    check_only = args.check_config != 0

    # Assert that --check is not used together with --type
    if check_only and args.output_type:
        log.die("--check and --type are mutually exclusive")

    # Assert that --check is not used together with --output
    if check_only and args.output:
        log.die("--check and --output are mutually exclusive")

    # Assert that --check is not used together with --output
    if not check_only and args.check_differences:
        log.die("--differences cannot be used without --check")

    # Determine the config file to check against, if applicable.
    if check_only:
        if args.check_config:
            log.info("Checking generated config against '{}'".format(args.check_config))
        else:
            if not has_proc_config_gz():
                log.die("This kernel does not expose /proc/config.gz. Please provide the path to a valid config file manually.")
            log.info("Checking generated config against currently running kernel")

    # Ensure that some required external programs are installed
    check_program_exists('find')
    check_program_exists('findmnt')

    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)
    # Detect system nodes and create modules
    module_creator = detect_modules(kconfig)

    if check_only:
        # Load the given config file or the current kernel's config
        kconfig_load_file_or_current_config(kconfig, args.check_config)
        # Check all detected symbols' values and report them
        check_config_against_detected_modules(kconfig, module_creator.modules, differences_only=args.check_differences)
    else:
        # Add fallback for output type.
        if not args.output_type:
            args.output_type = 'module'

        # Allow - as an alias for stdout
        if args.output == '-':
            args.output = None

        # Write all modules in the given format to the given output file / stdout
        module_creator.write_detected_modules(args)

def get_sym_by_name(kconfig, sym_name):
    if sym_name.startswith('CONFIG_'):
        sym_name = sym_name[len('CONFIG_'):]

    # Get symbol
    try:
        return kconfig.syms[sym_name]
    except KeyError:
        log.die("Symbol '{}' does not exist".format(sym_name))

def main_info(args):
    """
    Main function for the 'info' command.
    """
    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)

    for config_symbol in args.config_symbols:
        sym = get_sym_by_name(kconfig, config_symbol)
        log.info("Information for {}:".format(sym.name))
        print(sym)

def main_revdeps(args):
    """
    Main function for the 'revdeps' command.
    """
    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)

    for config_symbol in args.config_symbols:
        sym = get_sym_by_name(kconfig, config_symbol)
        log.info("Dependents for {}:".format(sym.name))
        for d in sym._dependents: # pylint: disable=protected-access
            print(d)

def main_satisfy(args):
    """
    Main function for the 'satisfy' command.
    """
    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)

    # Apply autokernel configuration only if we want our dependencies based on the current configuration
    if not args.dep_global:
        # Load configuration file
        config = autokernel.config.load_config(args.autokernel_config)
        # Apply kernel config
        apply_autokernel_config(args, kconfig, config)

    # Create a module for the detected option
    module_creator = ModuleCreator()

    for config_symbol in args.config_symbols:
        sym = get_sym_by_name(kconfig, config_symbol)
        mod = module_creator.add_module_for_sym(sym)
        if mod is False:
            log.warn("Skipping {} (unsatisfiable dependencies)".format(sym.name))
            continue
        module_creator.select_module(mod)

    # Add fallback for output type.
    if not args.output_type:
        args.output_type = 'module'

    # Allow - as an alias for stdout
    if args.output == '-':
        args.output = None

    # Write the module
    module_creator.write_detected_modules(args)

def check_file_exists(value):
    """
    Checks if the given exists
    """
    if not os.path.isfile(value):
        raise argparse.ArgumentTypeError("'{}' is not a file".format(value))
    return value

def check_kernel_dir(value):
    """
    Checks if the given value is a valid kernel directory path.
    """
    if not os.path.isdir(value):
        raise argparse.ArgumentTypeError("'{}' is not a directory".format(value))

    if not os.path.exists(os.path.join(value, 'Kconfig')):
        raise argparse.ArgumentTypeError("'{}' is not a valid kernel directory, as it does not contain a Kconfig file".format(value))

    return value

def suppress_columns_list(value):
    """
    Checks if the given value is a csv of columns to suppress.
    """
    valid_values_new = ['new', 'n']
    valid_values_del = ['del', 'd']
    valid_values_chg = ['changed', 'chg', 'c']
    valid_values = valid_values_new + valid_values_del + valid_values_chg

    supress_new = False
    supress_del = False
    supress_chg = False
    for i in value.split(','):
        if i in valid_values_new:
            supress_new = True
        elif i in valid_values_del:
            supress_del = True
        elif i in valid_values_chg:
            supress_chg = True
        else:
            raise argparse.ArgumentTypeError("'{}' is not a valid suppression type. Must be one of [{}]".format(i, ', '.join(["'{}'".format(v) for v in valid_values])))

    return (supress_new, supress_del, supress_chg)

class ArgumentParserError(Exception):
    pass

class ThrowingArgumentParser(argparse.ArgumentParser):
    def error(self, message):
        raise ArgumentParserError(message)

def autokernel_main():
    """
    Parses options and dispatches control to the correct subcommand function
    """
    parser = ThrowingArgumentParser(description="Autokernel is a kernel configuration management tool. For more information please refer to the documentation (https://autokernel.oddlama.org). If no mode is given, 'autokernel --help' will be executed.")
    subparsers = parser.add_subparsers(title="commands",
            description="Use 'autokernel command --help' to view the help for any command.",
            metavar='command')

    # General options
    parser.add_argument('-K', '--kernel-dir', dest='kernel_dir', default='/usr/src/linux', type=check_kernel_dir,
            help="The kernel directory to operate on. The default is /usr/src/linux.")
    parser.add_argument('-C', '--config', dest='autokernel_config', default=None, type=check_file_exists,
            help="The autokernel configuration file to use. Default is to use '/etc/autokernel/autokernel.conf' or an internal fallback if the default path doesn't exist.")
    parser.add_argument('--no-color', dest='use_color', action='store_false',
            help="Disables coloring in normal output.")
    parser.add_argument('--version', action='version',
            version='%(prog)s {version}'.format(version=__version__))

    # Output options
    output_options = parser.add_mutually_exclusive_group()
    output_options.add_argument('-q', '--quiet', dest='quiet', action='store_true',
            help="Disables any additional output except for errors, and output from tools.")
    output_options.add_argument('-v', '--verbose', dest='verbose', action='store_true',
            help="Enables verbose output.")

    # Setup
    parser_setup = subparsers.add_parser('setup', help='Setup a default configuration in /etc/autokernel if the directory does not exist yet.')
    parser_setup.add_argument('-d', '--dir', dest='setup_dir', default='/etc/autokernel',
            help="The directory to copy the default configuration to. The default is /etc/autokernel.")
    parser_setup.set_defaults(func=main_setup)

    # Check
    parser_check = subparsers.add_parser('check', help="Reports differences between the config that will be generated by autokernel, and the given config file. If no config file is given, the script will try to load the current kernel's configuration from '/proc/config.gz'.")
    parser_check.add_argument('-c', '--compare-config', dest='compare_config', type=check_file_exists,
            help="The .config file to compare the generated configuration against.")
    parser_check.add_argument('-k', '--compare-kernel-dir', dest='compare_kernel_dir', type=check_kernel_dir,
            help="The kernel directory for the given comparison config.")
    parser_check.add_argument('--suppress', dest='suppress_columns', type=suppress_columns_list,
            help="Comma separated list of columns to suppress. 'new' or 'n' supresses new symbols, 'del' or 'd' suppresses removed symbols, 'changed', 'chg' or 'c' supresses changed symbols.")
    parser_check.set_defaults(func=main_check_config)

    # Config generation options
    parser_generate_config = subparsers.add_parser('generate-config', help='Generates the kernel configuration file from the autokernel configuration.')
    parser_generate_config.add_argument('-o', '--output', dest='output',
            help="The output filename. An existing configuration file will be overwritten. The default is '{KERNEL_DIR}/.config'.")
    parser_generate_config.set_defaults(func=main_generate_config)

    # Build options
    parser_build = subparsers.add_parser('build', help='Generates the configuration, and then builds the kernel (and initramfs if required) in the kernel tree.')
    parser_build.add_argument('-c', '--clean', dest='clean', action='store_true',
            help="Clean the kernel tree before building")
    parser_build.set_defaults(func=main_build)

    # Installation options
    parser_install = subparsers.add_parser('install', help='Installs the finished kernel, modules and other resources on the system.')
    parser_install.set_defaults(func=main_install)

    # Full build options
    parser_all = subparsers.add_parser('all', help='First builds and then installs the kernel.')
    parser_all.add_argument('-c', '--clean', dest='clean', action='store_true',
            help="Clean the kernel tree before building")
    parser_all.set_defaults(func=main_build_all)

    # Show symbol infos
    parser_info = subparsers.add_parser('info', help='Displays information for the given symbols')
    parser_info.add_argument('config_symbols', nargs='+',
            help="A list of configuration symbols to show infos for")
    parser_info.set_defaults(func=main_info)

    # Show symbol reverse dependencies
    parser_revdeps = subparsers.add_parser('revdeps', help='Displays all symbols that somehow depend on the given symbol')
    parser_revdeps.add_argument('config_symbols', nargs='+',
            help="A list of configuration symbols to show revdeps for")
    parser_revdeps.set_defaults(func=main_revdeps)

    # Single config module generation options
    parser_satisfy = subparsers.add_parser('satisfy', help='Generates required modules to enable the given symbol')
    parser_satisfy.add_argument('-g', '--global', action='store_true', dest='dep_global',
            help="Report changes solely based on kernel default instead of basing the on the current autokernel configuration")
    parser_satisfy.add_argument('-t', '--type', choices=['module', 'kconf'], dest='output_type',
            help="Selects the output type. 'kconf' will output options in the kernel configuration format. 'module' will output a list of autokernel modules to reflect the necessary configuration.")
    parser_satisfy.add_argument('-m', '--module-name', dest='output_module_name', default='rename_me',
            help="The name of the generated module, which will enable all given options (default: 'rename_me').")
    parser_satisfy.add_argument('-o', '--output', dest='output',
            help="Writes the output to the given file. Use - for stdout (default).")
    parser_satisfy.add_argument('config_symbols', nargs='+',
            help="The configuration symbols to generate modules for (including dependencies)")
    parser_satisfy.set_defaults(func=main_satisfy)

    # Config detection options
    parser_detect = subparsers.add_parser('detect', help='Detects configuration options based on information gathered from the running system')
    parser_detect.add_argument('-c', '--check', nargs='?', default=0, dest='check_config', type=check_file_exists,
            help="Instead of outputting the required configuration values, compare the detected options against the given kernel configuration and report the status of each option. If no config file is given, the script will try to load the current kernel's configuration from '/proc/config.gz'.")
    parser_detect.add_argument('-d', '--differences', dest='check_differences', action='store_true',
            help="Requires --check. Only report options when the suggested value differs from the current value.")
    parser_detect.add_argument('-t', '--type', choices=['module', 'kconf'], dest='output_type',
            help="Selects the output type. 'kconf' will output options in the kernel configuration format. 'module' will output a list of autokernel modules to reflect the necessary configuration.")
    parser_detect.add_argument('-m', '--module-name', dest='output_module_name', default='local',
            help="The name of the generated module, which will enable all detected options (default: 'local').")
    parser_detect.add_argument('-o', '--output', dest='output',
            help="Writes the output to the given file. Use - for stdout (default).")
    parser_detect.set_defaults(func=main_detect)

    try:
        args = parser.parse_args()
    except ArgumentParserError as e:
        log.die(str(e))

    # Set logging options
    log.set_verbose(args.verbose)
    log.set_quiet(args.quiet)
    log.set_use_color(args.use_color)

    if 'func' not in args:
        # Fallback to --help.
        parser.print_help()
    elif args.func is main_setup:
        # Check if we have chosen 'setup', which is special
        # as it has no previous requirements and will not
        # open any configuration files.
        main_setup(args)
    else:
        # Initialize important environment variables
        autokernel.kconfig.initialize_environment()
        # Assert that some required programs exist
        check_execution_environment(args)
        # Execute the mode's function
        args.func(args)

def main():
    try:
        autokernel_main()
    except PermissionError as e:
        log.die(str(e))
    except Exception as e: # pylint: disable=broad-except
        import traceback
        traceback.print_exc()
        log.die("Aborted because of previous errors")

if __name__ == '__main__':
    main()

import autokernel.kconfig
import autokernel.config
import autokernel.lkddb
import autokernel.node_detector
import autokernel.symbol_tracking
from autokernel import __version__
from autokernel import log
from autokernel import util
from autokernel.symbol_tracking import set_value_detect_conflicts

import argparse
import glob
import grp
import gzip
import kconfiglib
import os
import pwd
import re
import shutil
import stat
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path


def check_program_exists(exe):
    if shutil.which(exe) is None:
        log.die("Missing program '{}'. Please ensure that it is installed.".format(exe))

def check_execution_environment(args):
    """
    Checks that some required external programs exist, and some miscellaneous things.
    """
    check_program_exists('uname')
    check_program_exists('mount')
    check_program_exists('umount')
    check_program_exists('make')

    cur_uid = os.geteuid()
    with autokernel.config.config_file_path(args.autokernel_config, warn=True) as config_file:
        def _die_writable_config_by(component, name):
            log.die("Refusing to run, because the path '{0}' is writable by {1}. This allows {1} to replace the configuration '{2}' and thus inject commands.".format(component, name, config_file))

        if not config_file.exists():
            log.die("Configuration file '{}' does not exist!".format(config_file))

        # Ensure that the config file has the correct mode, to prevent command-injection by other users.
        # No component of the path may be modifiable by anyone else but the current user (or root).
        config_path = config_file.resolve()
        for component in [config_path] + [p for p in config_path.parents]:
            st = component.stat()
            if st.st_uid != cur_uid and st.st_uid != 0 and st.st_mode & stat.S_IWUSR:
                _die_writable_config_by(component, 'user {} ({})'.format(st.st_uid, pwd.getpwuid(st.st_uid).pw_name))
            if st.st_gid != 0 and st.st_mode & stat.S_IWGRP:
                _die_writable_config_by(component, 'group {} ({})'.format(st.st_gid, grp.getgrgid(st.st_gid).gr_name))
            if st.st_mode & stat.S_IWOTH:
                _die_writable_config_by(component, 'others')

def replace_common_vars(args, p):
    p = str(p)
    p = p.replace('{KERNEL_DIR}', args.kernel_dir)
    p = p.replace('{KERNEL_VERSION}', autokernel.kconfig.get_kernel_version(args.kernel_dir))
    p = p.replace('{UNAME_ARCH}', autokernel.kconfig.get_uname_arch())
    p = p.replace('{ARCH}', autokernel.kconfig.get_arch())
    return p

def has_proc_config_gz():
    """
    Checks if /proc/config.gz exists
    """
    return os.path.isfile("/proc/config.gz")

def unpack_proc_config_gz():
    """
    Unpacks /proc/config.gz into a temporary file
    """
    tmp = tempfile.NamedTemporaryFile()
    with gzip.open("/proc/config.gz", "rb") as f:
        shutil.copyfileobj(f, tmp)
    return tmp

def kconfig_load_file_or_current_config(kconfig, config_file):
    """
    Applies the given kernel config file to kconfig, or uses /proc/config.gz if config_file is None.
    """

    if config_file:
        log.info("Applying kernel config from '{}'".format(config_file))
        kconfig.load_config(os.path.realpath(config_file))
    else:
        log.info("Applying kernel config from '/proc/config.gz'")
        with unpack_proc_config_gz() as tmp:
            kconfig.load_config(os.path.realpath(tmp.name))

def generated_by_autokernel_header():
    return "# Generated by autokernel on {}\n".format(datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"))

def vim_config_modeline_header():
    return "# vim: set ft=ruby ts=4 sw=4 sts=-1 noet:\n"

def apply_autokernel_config(args, kconfig, config):
    """
    Applies the given autokernel configuration to a freshly loaded kconfig object,
    and returns gathered extra information such as the resulting kernel cmdline
    """
    log.info("Applying autokernel configuration")

    # Build cmdline on demand
    kernel_cmdline = []
    # Reset symbol_changes
    autokernel.symbol_tracking.symbol_changes.clear()

    # Asserts that the symbol has the given value
    def get_sym(stmt):
        # Get the kconfig symbol, and change the value
        try:
            return kconfig.syms[stmt.sym_name]
        except KeyError:
            log.die_print_error_at(stmt.at, "symbol '{}' does not exist".format(stmt.sym_name))

    # Asserts that the symbol has the given value
    def assert_symbol(stmt):
        if not stmt.assert_condition.evaluate(kconfig):
            if stmt.message:
                log.die_print_error_at(stmt.at, "assertion failed: {}".format(stmt.message))
            else:
                log.die_print_error_at(stmt.at, "assertion failed")

    # Sets a symbols value if and asserts that there are no conflicting double assignments
    def set_symbol(stmt):
        # Get the kconfig symbol, and change the value
        sym = get_sym(stmt)
        value = stmt.value

        if not autokernel.kconfig.symbol_can_be_user_assigned(sym):
            log.die_print_error_at(stmt.at, "symbol {} can't be user-assigned".format(sym.name))

        # Skip assignment if value is already pinned and the statement is in try mode.
        if stmt.has_try and sym in autokernel.symbol_tracking.symbol_changes:
            log.verbose("skipping {} {}".format(autokernel.kconfig.value_to_str(value), sym.name))
            return

        if util.is_env_var(value):
            value = util.resolve_env_variable(stmt.at, value)

        if not set_value_detect_conflicts(sym, value, stmt.at):
            log.die_print_error_at(stmt.at, "invalid value {} for symbol {}".format(autokernel.kconfig.value_to_str(value), sym.name))

        if sym.str_value != value:
            if not stmt.has_try:
                # Only throw an error if it wasn't a try
                log.die_print_error_at(stmt.at, "symbol assignment failed: {} from {} → {}".format(
                    sym.name,
                    autokernel.kconfig.value_to_str(sym.str_value),
                    autokernel.kconfig.value_to_str(value)))
            else:
                log.verbose("failed try set {} {} (symbol is currently not assignable to the chosen value)".format(autokernel.kconfig.value_to_str(stmt.value), sym.name))

    # Visit all module nodes and apply configuration changes
    visited = set()
    def visit(module):
        # Ensure we visit only once
        if module.name in visited:
            return
        visited.add(module.name)

        def stmt_use(stmt):
            visit(stmt.module)

        def stmt_merge(stmt):
            filename = replace_common_vars(args, stmt.filename)
            log.verbose("Merging external kconf '{}'".format(filename))
            kconfig.load_config(os.path.realpath(filename), replace=False)

            # Assert that there are no conflicts
            for sym in autokernel.symbol_tracking.symbol_changes:
                sc = autokernel.symbol_tracking.symbol_changes[sym]
                if sym.str_value != sc.value:
                    autokernel.symbol_tracking.die_print_conflict(stmt.at, 'merge', sym, sym.str_value, sc)

        def stmt_assert(stmt):
            assert_symbol(stmt)

        def stmt_set(stmt):
            set_symbol(stmt)

        def stmt_add_cmdline(stmt):
            kernel_cmdline.append(stmt.param)

        dispatch_stmt = {
            autokernel.config.ConfigModule.StmtUse: stmt_use,
            autokernel.config.ConfigModule.StmtMerge: stmt_merge,
            autokernel.config.ConfigModule.StmtAssert: stmt_assert,
            autokernel.config.ConfigModule.StmtSet: stmt_set,
            autokernel.config.ConfigModule.StmtAddCmdline: stmt_add_cmdline,
        }

        def conditions_met(stmt):
            for condition in stmt.conditions:
                if not condition.evaluate(kconfig):
                    return False
            return True

        for stmt in module.all_statements_in_order:
            # Ensure all attached conditions are met for the statement.
            if conditions_met(stmt):
                dispatch_stmt[stmt.__class__](stmt)

    # Visit the root node and apply all symbol changes
    visit(config.kernel.module)
    log.verbose("  Changed {} symbols".format(len(autokernel.symbol_tracking.symbol_changes)))

    # Lastly, invalidate all non-assigned symbols to process new default value conditions
    for sym in kconfig.unique_defined_syms:
        if sym.user_value is None:
            sym._invalidate() # pylint: disable=protected-access

    return kernel_cmdline

def execute_command(args, name, cmd, _replace_vars):
    if len(cmd.value) > 0:
        command = [_replace_vars(args, p) for p in cmd.value]
        log.info("Executing {}: [{}]".format(name, ', '.join(["'{}'".format(i) for i in command])))
        try:
            # Replace variables in command and run it
            subprocess.run(command, check=True)
        except subprocess.CalledProcessError as e:
            log.die("{} failed with code {}. Aborting.".format(name, e.returncode))

def main_setup(args):
    """
    Main function for the 'setup' command.
    """
    log.info("Setting up autokernel configuration at '{}'".format(args.setup_dir))

    setup_dir = Path(args.setup_dir)
    if setup_dir.exists():
        log.die("Refusing to setup: directory '{}' exists".format(args.setup_dir))

    saved_umask = os.umask(0o077)

    setup_dir.mkdir()
    modules_d_dir = setup_dir / 'modules.d'
    modules_d_dir.mkdir()

    import autokernel.contrib.etc as etc
    import autokernel.contrib.etc.modules_d as modules_d
    for i in util.resource_contents(etc):
        if i.endswith('.conf'):
            with (setup_dir / i).open('w') as f:
                f.write(util.read_resource(i, pkg=etc))

    for i in util.resource_contents(modules_d):
        if i.endswith('.conf'):
            with (modules_d_dir / i).open('w') as f:
                f.write(util.read_resource(i, pkg=modules_d))

    os.umask(saved_umask)

    log.info("A default configuration has been installed")
    log.info("You might want to edit it now.")

def main_check_config(args):
    """
    Main function for the 'check' command.
    """
    if args.compare_config:
        if not args.compare_kernel_dir:
            args.compare_kernel_dir = args.kernel_dir

        kname_cmp = "'{}'".format(args.compare_config)
    else:
        if not has_proc_config_gz():
            log.die("This kernel does not expose /proc/config.gz. Please provide the path to a valid config file manually.")

        if not args.compare_kernel_dir:
            # Use /usr/src/linux-{kernel_version} as the directory.
            running_kver = subprocess.run(['uname', '-r'], check=True, stdout=subprocess.PIPE).stdout.decode().strip().splitlines()[0]
            args.compare_kernel_dir = os.path.join('/usr/src/linux-{}'.format(running_kver))
            try:
                check_kernel_dir(args.compare_kernel_dir)
            except argparse.ArgumentTypeError:
                log.die("Could not find sources for running kernel (version {}) in '{}', use --check_kernel_dir to specify it manually.".format(running_kver, args.compare_kernel_dir))

        kname_cmp = 'running kernel'

    log.info("Comparing {} against generated config".format(kname_cmp))

    # Load configuration file
    config = autokernel.config.load_config(args.autokernel_config)

    # Load symbols from Kconfig
    kconfig_gen = autokernel.kconfig.load_kconfig(args.kernel_dir)
    # Apply autokernel configuration
    apply_autokernel_config(args, kconfig_gen, config)

    # Load symbols from Kconfig
    kconfig_cmp = autokernel.kconfig.load_kconfig(args.compare_kernel_dir)
    # Load the given config file or the current kernel's config
    kconfig_load_file_or_current_config(kconfig_cmp, args.compare_config)

    indicator_del = log.color("[31m-[m", "-")
    indicator_add = log.color("[32m+[m", "+")
    indicator_mod = log.color("[33m~[m", "~")

    log.info("Comparing existing config (left) against generated config (right)")
    log.info("  ({}) symbol was removed".format(indicator_del))
    log.info("  ({}) symbol is new".format(indicator_add))
    log.info("  ({}) symbol value changed".format(indicator_mod))

    gen_syms = [s.name for s in kconfig_gen.unique_defined_syms]
    cmp_syms = [s.name for s in kconfig_cmp.unique_defined_syms]

    def intersection(a, b):
        return [i for i in a if i in b]
    def comprehension(a, b):
        return [i for i in a if i not in b]

    common_syms = intersection(gen_syms, set(cmp_syms))
    common_syms_set = set(common_syms)
    only_gen_syms = comprehension(gen_syms, common_syms_set)
    only_cmp_syms = comprehension(cmp_syms, common_syms_set)


    supress_new, supress_del, supress_chg = (args.suppress_columns or (False, False, False))

    if not supress_new:
        for sym in only_gen_syms:
            sym_gen = kconfig_gen.syms[sym]
            print(indicator_add + " {} {}".format(
                autokernel.kconfig.value_to_str(sym_gen.str_value),
                sym))

    if not supress_del:
        for sym in only_cmp_syms:
            sym_cmp = kconfig_cmp.syms[sym]
            print(indicator_del + " {} {}".format(
                autokernel.kconfig.value_to_str(sym_cmp.str_value),
                sym))

    if not supress_chg:
        for sym in common_syms:
            sym_gen = kconfig_gen.syms[sym]
            sym_cmp = kconfig_cmp.syms[sym]
            if sym_gen.str_value != sym_cmp.str_value:
                print(indicator_mod + " {} → {} {}".format(
                    autokernel.kconfig.value_to_str(sym_cmp.str_value),
                    autokernel.kconfig.value_to_str(sym_gen.str_value),
                    sym))

def main_generate_config(args, config=None):
    """
    Main function for the 'generate_config' command.
    """
    log.info("Generating kernel configuration")
    if not config:
        # Load configuration file
        config = autokernel.config.load_config(args.autokernel_config)

    # Fallback for config output
    if not hasattr(args, 'output') or not args.output:
        args.output = os.path.join(args.kernel_dir, '.config')

    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)
    # Apply autokernel configuration
    apply_autokernel_config(args, kconfig, config)

    # Write configuration to file
    kconfig.write_config(
            filename=args.output,
            header=generated_by_autokernel_header(),
            save_old=False)

    log.info("Configuration written to '{}'".format(args.output))

def clean_kernel_dir(args):
    """
    Clean the kernel tree (call make distclean)
    """
    try:
        subprocess.run(['make', 'distclean'], cwd=args.kernel_dir, check=True)
    except subprocess.CalledProcessError as e:
        log.die("'make distclean' failed in {} with code {}".format(args.kernel_dir, e.returncode))

def build_kernel(args):
    """
    Build the kernel (call make)
    """
    try:
        subprocess.run(['make'], cwd=args.kernel_dir, check=True)
    except subprocess.CalledProcessError as e:
        log.die("'make' failed in {} with code {}".format(args.kernel_dir, e.returncode))

def build_initramfs(args, config, modules_prefix, initramfs_output):
    log.info("Building initramfs")

    def _replace_vars(args, p):
        p = replace_common_vars(args, p)
        if '{MODULES_PREFIX}' in p:
            if modules_prefix is None:
                log.die(f"A variable used {{MODULES_PREFIX}}, but kernel module support is disabled!")
            p = p.replace('{MODULES_PREFIX}', modules_prefix)
        p = p.replace('{INITRAMFS_OUTPUT}', initramfs_output)
        return p

    # Execute initramfs build_command
    execute_command(args, 'initramfs.build_command', config.initramfs.build_command, _replace_vars)
    if config.initramfs.build_output:
        cmd_output_file = _replace_vars(args, config.initramfs.build_output.value)
        try:
            # Move the output file as stated in the configuration to the kernel tree
            shutil.move(cmd_output_file, initramfs_output)
        except IOError as e:
            log.die("Could not copy initramfs from '{}' to '{}': {}".format(cmd_output_file, initramfs_output, str(e)))

def install_modules(args, prefix="/"):
    """
    Installs the modules to the given prefix
    """
    # Use correct 022 umask when installing modules
    saved_umask = os.umask(0o022)
    try:
        subprocess.run(['make', 'modules_install', 'INSTALL_MOD_PATH=' + prefix], cwd=args.kernel_dir, check=True, stdout=None)
    except subprocess.CalledProcessError as e:
        log.die("'make modules_install INSTALL_MOD_PATH={}' failed in {} with code {}".format(prefix, args.kernel_dir, e.returncode))
    os.umask(saved_umask)

def main_build(args, config=None):
    """
    Main function for the 'build' command.
    """
    if not config:
        # Load configuration file
        config = autokernel.config.load_config(args.autokernel_config)

    # Set umask for build
    saved_umask = os.umask(config.build.umask.value)

    # Execute pre hook
    execute_command(args, 'build.hooks.pre', config.build.hooks.pre, replace_common_vars)

    # Clean the kernel dir, if the user wants that
    if args.clean:
        log.info("Cleaning kernel directory")
        clean_kernel_dir(args)

    kernel_version = autokernel.kconfig.get_kernel_version(args.kernel_dir)
    # Config output is "{KERNEL_DIR}/.config"
    config_output = os.path.join(args.kernel_dir, '.config.autokernel')
    # Initramfs basename "initramfs-{KERNEL_VERSION}.cpio"
    # The .cpio suffix is cruical, as the kernel makefile requires it to detect initramfs archives
    initramfs_basename = 'initramfs-{}.cpio'.format(kernel_version)
    # Initramfs output is "{KERNEL_DIR}/initramfs-{KERNEL_VERSION}.cpio"
    initramfs_output = os.path.join(args.kernel_dir, initramfs_basename)

    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)
    sym_cmdline_bool = kconfig.syms['CMDLINE_BOOL']
    sym_cmdline = kconfig.syms['CMDLINE']
    sym_initramfs_source = kconfig.syms['INITRAMFS_SOURCE']
    sym_modules = kconfig.syms['MODULES']

    # Set some defaults
    sym_cmdline_bool.set_value('y')
    sym_cmdline.set_value('')
    sym_initramfs_source.set_value('{INITRAMFS}')

    # Apply autokernel configuration
    kernel_cmdline = apply_autokernel_config(args, kconfig, config)

    def _build_kernel():
        # Write configuration to file
        kconfig.write_config(
                filename=config_output,
                header=generated_by_autokernel_header(),
                save_old=False)

        # Copy file to .config, which may get changed by the makefiles
        shutil.copyfile(config_output, os.path.join(args.kernel_dir, '.config'))
        # Build the kernel
        build_kernel(args)

    def set_cmdline():
        kernel_cmdline_str = ' '.join(kernel_cmdline)

        has_user_cmdline_bool = sym_cmdline_bool in autokernel.symbol_tracking.symbol_changes
        has_user_cmdline = sym_cmdline in autokernel.symbol_tracking.symbol_changes

        if has_user_cmdline_bool and sym_cmdline_bool.str_value == 'n':
            # The user has explicitly disabled the builtin commandline,
            # so there is no need to set it.
            pass
        else:
            sym_cmdline_bool.set_value('y')

            # Issue a warning, if a custom cmdline does not contain "{CMDLINE}", and we have gathered add_cmdline options.
            if has_user_cmdline and not sym_cmdline.str_value.contains('{CMDLINE}') and len(kernel_cmdline) > 0:
                log.warn("CMDLINE was set manually and doesn't contain a '{CMDLINE}' token, although add_cmdline has also been used.")

            if has_user_cmdline:
                sym_cmdline.set_value(sym_cmdline.str_value.replace('{CMDLINE}', kernel_cmdline_str))
            else:
                sym_cmdline.set_value(kernel_cmdline_str)

    def check_initramfs_source(sym_initramfs_source):
        has_user_initramfs_source = sym_initramfs_source in autokernel.symbol_tracking.symbol_changes

        # It is an error to explicitly set INITRAMFS_SOURCE, if our initramfs is set to builtin.
        if has_user_initramfs_source \
                and config.initramfs.enabled \
                and config.initramfs.builtin \
                and autokernel.symbol_tracking.symbol_changes[sym_initramfs_source].reason == 'explicitly set':
            log.die("INITRAMFS_SOURCE was set manually, although a custom initramfs should be built and integrated into the kernel.")

    # Set CMDLINE_BOOL and CMDLINE
    set_cmdline()
    # Preprocess INITRAMFS_SOURCE
    check_initramfs_source(sym_initramfs_source)

    # Kernel build pass #1
    log.info("Building kernel")
    # On the first pass, disable all initramfs sources
    sym_initramfs_source.set_value('')
    # Start the build process
    _build_kernel()

    # Build the initramfs, if enabled
    if config.initramfs.enabled:
        with tempfile.TemporaryDirectory() as tmppath:
            if sym_modules.str_value != 'n':
                # Temporarily install modules so the initramfs generator has access to them
                log.info("Copying modules into temporary directory")
                tmp_modules_prefix = os.path.join(tmppath, 'modules')
                install_modules(args, prefix=tmp_modules_prefix)
            else:
                tmp_modules_prefix = None

            # Build the initramfs
            build_initramfs(args, config, tmp_modules_prefix, initramfs_output)

            # Pack the initramfs into the kernel if desired
            if config.initramfs.builtin:
                log.info("Rebuilding kernel to pack external resources")
                # On the second pass, we enable the initramfs cpio archive, which is now in the kernel_dir
                sym_initramfs_source.set_value(initramfs_basename)
                # Rebuild the kernel to pack the new images
                _build_kernel()

    # Execute post hook
    execute_command(args, 'build.hooks.post', config.build.hooks.post, replace_common_vars)

    os.umask(saved_umask)

def main_install(args, config=None):
    """
    Main function for the 'install' command.
    """
    if not config:
        # Load configuration file
        config = autokernel.config.load_config(args.autokernel_config)

    # Use correct umask when installing
    saved_umask = os.umask(config.install.umask.value)

    # Mount
    new_mounts = []
    for i in config.install.mount:
        if not os.access(i, os.R_OK):
            log.die("Permission denied on accessing '{}'. Aborting.".format(i))

        if not os.path.ismount(i):
            log.info("Mounting {}".format(i))
            new_mounts.append(i)
            try:
                subprocess.run(['mount', '--', i], check=True)
            except subprocess.CalledProcessError as e:
                log.die("Could not mount '{}', mount returned code {}. Aborting.".format(i, e.returncode))

    # Check mounts
    for i in config.install.mount + config.install.assert_mounted:
        if not os.access(i, os.R_OK):
            log.die("Permission denied on accessing '{}'. Aborting.".format(i))

        if not os.path.ismount(i):
            log.die("'{}' is not mounted. Aborting.".format(i))

    # Execute pre hook
    execute_command(args, 'install.hooks.pre', config.install.hooks.pre, replace_common_vars)

    kernel_version = autokernel.kconfig.get_kernel_version(args.kernel_dir)
    target_dir = replace_common_vars(args, config.install.target_dir)
    # Config output is "{KERNEL_DIR}/.config"
    config_output = os.path.join(args.kernel_dir, '.config.autokernel')
    # Initramfs basename "initramfs-{KERNEL_VERSION}.cpio"
    # The .cpio suffix is cruical, as the kernel makefile requires it to detect initramfs archives
    initramfs_basename = 'initramfs-{}.cpio'.format(kernel_version)
    # Initramfs output is "{KERNEL_DIR}/initramfs-{KERNEL_VERSION}.cpio"
    initramfs_output = os.path.join(args.kernel_dir, initramfs_basename)
    # bzImage output
    bzimage_output = os.path.join(args.kernel_dir, 'arch', autokernel.kconfig.get_uname_arch(), 'boot/bzImage')

    def _purge_old(path):
        keep_old = config.install.keep_old.value
        # Disable purging on negative count
        if keep_old < 0:
            return

        # Disable purging for non versionated paths
        if not '{KERNEL_VERSION}' in path:
            return

        tokens = path.split('{KERNEL_VERSION}')
        if len(tokens) > 2:
            log.warn("Cannot purge path with more than one {{KERNEL_VERSION}} token: '{}'".format(path))
            return

        re_semver = re.compile(r'^[\d\.]+\d')
        def _version_sorter(i):
            suffix = i[len(tokens[0]):]
            basename = suffix.split('/')[0]

            st = os.stat(i)
            try:
                time_create = st.st_birthtime
            except AttributeError:
                time_create = st.st_mtime

            semver = re_semver.match(basename).group()
            val = autokernel.config.semver_to_int(semver)
            return val, time_create

        escaped_kv = re.escape('{KERNEL_VERSION}')
        # matches from {KERNEL_VERSION} until first / exclusive in an regex escaped path
        match_basename = re.compile(re.escape(escaped_kv) + r"(.+?(?=\\\/|$)).*$")
        # derive regex to check if a valid semver is contained and prefix and suffix are given
        re_match_valid_paths = re.compile('^' + match_basename.sub(lambda m: r'[0-9]+(\.[0-9]+(\.[0-9]+)?)?(-[^\/]*)?' + m.group(1) + r'.*$', re.escape(path)))

        # matches from {KERNEL_VERSION} until first / exclusive in a normal path
        re_replace_wildcard = re.compile(escaped_kv + r"[^\/]*")
        # replace {KERNEL_VERSION}-* component with *
        wildcard_path = re_replace_wildcard.sub('*', glob.escape(path))

        # sort out paths that don't contain valid semvers
        valid_globbed = [i for i in glob.glob(wildcard_path) if re_match_valid_paths.match(i)]
        for i in sorted(valid_globbed, key=_version_sorter)[:-(keep_old + 1)]:
            # For security, we will not call rmtree on a path that doesn't end with a slash,
            # or if the realpath has less then two slash characters in it.
            # Otherwise we only call unlink
            if i[-1] == '/' and os.path.realpath(i).count('/') >= 2:
                try:
                    shutil.rmtree(i)
                except OSError as e:
                    log.warn("Could not remove {}: {}".format(i, str(e)))
            else:
                try:
                    os.unlink(i)
                except IOError as e:
                    log.warn("Could not remove {}: {}".format(i, str(e)))

    def _move_to_old(path):
        re_old_suffix = re.compile(r'^.*\.old(\.\d+)?\/*$')
        dst = path + '.old'
        highest_num = -1
        for i in glob.glob(glob.escape(dst) + '*'):
            m = re_old_suffix.match(i)
            old_num = int((m.group(1) or '.0')[1:]) if m else 0
            if highest_num < old_num:
                highest_num = old_num

        if highest_num >= 0:
            dst += ".{:d}".format(highest_num + 1)

        shutil.move(path, dst)

    def _install(name, src, target_var):
        # If the target is disabled, return.
        if not target_var:
            return

        # Figure out destination, and move existing filed if necessary
        dst = os.path.join(target_dir, replace_common_vars(args, target_var))
        if os.path.exists(dst):
            _move_to_old(dst)

        # Create directory if it doesn't exist
        Path(os.path.dirname(dst)).mkdir(parents=True, exist_ok=True)

        log.info("Installing {:<11s} {}".format(name + ':', dst))
        # Install target file
        shutil.copyfile(src, dst)
        # Purge old files
        _purge_old(os.path.join(target_dir, str(target_var)))

    # Move target_dir, if it is dynamic
    if '{KERNEL_VERSION}' in str(config.install.target_dir) and os.path.exists(target_dir):
        _move_to_old(os.path.realpath(target_dir))

    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)
    sym_modules = kconfig.syms['MODULES']

    # Install modules
    if config.install.modules_prefix and sym_modules.str_value != 'n':
        modules_prefix = str(config.install.modules_prefix)
        modules_prefix_with_lib = os.path.join(modules_prefix, "lib/modules")
        modules_dir = os.path.join(modules_prefix_with_lib, kernel_version)
        if os.path.exists(modules_dir):
            _move_to_old(os.path.realpath(modules_dir))
        log.info("Installing modules:    {}".format(modules_prefix_with_lib))
        install_modules(args, prefix=modules_prefix)
        _purge_old(modules_prefix_with_lib + "/{KERNEL_VERSION}/")

    # Install targets
    _install('bzimage', bzimage_output, config.install.target_kernel)
    _install('config',  config_output,  config.install.target_config)
    if config.initramfs.enabled:
        _install('initramfs', initramfs_output, config.install.target_initramfs)

    # Purge old target_dirs (will only be done if it is dynamic)
    _purge_old(str(config.install.target_dir) + '/')

    # Execute post hook
    execute_command(args, 'install.hooks.post', config.install.hooks.post, replace_common_vars)

    # Undo what we have mounted
    for i in reversed(new_mounts):
        log.info("Unmounting {}".format(i))
        try:
            subprocess.run(['umount', '--', i], check=True)
        except subprocess.CalledProcessError as e:
            log.warn("Could not umount '{}' (returned {})".format(i, e.returncode))

    # Restore old umask
    os.umask(saved_umask)

def main_build_all(args):
    """
    Main function for the 'all' command.
    """
    log.info("Started full build")
    # Load configuration file
    config = autokernel.config.load_config(args.autokernel_config)

    main_build(args, config)
    main_install(args, config)

class Module():
    """
    A module consists of dependencies (other modules) and option assignments.
    """
    def __init__(self, name):
        self.name = name
        self.deps = []
        self.assignments = []
        self.assertions = []
        self.rev_deps = []

def check_config_against_detected_modules(kconfig, modules, differences_only):
    log.info("Here are the detected options with both current and desired value.")
    log.info("The output format is: [current] OPTION_NAME = desired")
    log.info("HINT: Options are ordered by dependencies, i.e. applying")
    log.info("      them from top to buttom will work")
    if differences_only:
        log.info("Detected options (differences only):")
    else:
        log.info("Indicators: (=) same, (~) changed")
        log.info("Detected options:")

    visited = set()
    visited_opts = set()

    if differences_only:
        indicator_same = ""
        indicator_changed = ""
    else:
        indicator_same = log.color('[32m=[m', '=')
        indicator_changed = log.color('[33m~[m', '~')

    def visit_opt(opt, new_value):
        # Ensure we visit only once
        if opt in visited_opts:
            return
        visited_opts.add(opt)

        sym = kconfig.syms[opt]
        changed = sym.str_value != new_value

        if changed:
            print(indicator_changed + " {} → {} {}".format(autokernel.kconfig.value_to_str(sym.str_value), autokernel.kconfig.value_to_str(new_value), sym.name))
        else:
            if not differences_only:
                print(indicator_same + "       {} {}".format(autokernel.kconfig.value_to_str(sym.str_value), sym.name))

    def visit(m):
        # Ensure we visit only once
        if m in visited:
            return
        visited.add(m)

        # First visit all dependencies
        for d in m.deps:
            visit(d)
        # Then print all assignments
        for a, v in m.assignments:
            visit_opt(a, v)

    # Visit all modules
    for m in modules:
        visit(modules[m])

class KernelConfigWriter:
    """
    Writes modules to the given file in kernel config format.
    """
    def __init__(self, file):
        self.file = file
        self.file.write(generated_by_autokernel_header())
        self.file.write(vim_config_modeline_header())

    def write_module(self, module):
        if len(module.assignments) == len(module.assertions) == 0:
            return

        content = ""
        for d in module.rev_deps:
            content += "# required by {}\n".format(d.name)
        content += "# module {}\n".format(module.name)
        for a, v in module.assignments:
            if v in "nmy":
                content += "CONFIG_{}={}\n".format(a, v)
            else:
                content += "CONFIG_{}=\"{}\"\n".format(a, v)
        for o, v in module.assertions:
            content += "# REQUIRES {} {}\n".format(o, v)
        self.file.write(content)

class ModuleConfigWriter:
    """
    Writes modules to the given file in the module config format.
    """
    def __init__(self, file):
        self.file = file
        self.file.write(generated_by_autokernel_header())
        self.file.write(vim_config_modeline_header())

    def write_module(self, module):
        content = ""
        for d in module.rev_deps:
            content += "# required by {}\n".format(d.name)
        content += "module {} {{\n".format(module.name)
        for d in module.deps:
            content += "\tuse {};\n".format(d.name)
        for a, v in module.assignments:
            content += "\tset {} {};\n".format(a, v)
        for o, v in module.assertions:
            content += "\t#assert {} == {};\n".format(o, v)
        content += "}\n\n"
        self.file.write(content)

class ModuleCreator:
    def __init__(self, module_prefix=''):
        self.modules = {}
        self.module_for_sym = {}
        self.module_select_all = Module('module_select_all')
        self.module_prefix = module_prefix

    def _create_reverse_deps(self):
        # Clear rev_deps
        for m in self.modules:
            self.modules[m].rev_deps = []
        self.module_select_all.rev_deps = []

        # Fill in reverse dependencies for all modules
        for m in self.modules:
            for d in self.modules[m].deps:
                d.rev_deps.append(self.modules[m])

        # Fill in reverse dependencies for select_all module
        for d in self.module_select_all.deps:
            d.rev_deps.append(self.module_select_all)

    def _add_module_for_option(self, sym):
        """
        Recursively adds a module for the given option,
        until all dependencies are satisfied.
        """
        mod = Module(self.module_prefix + "config_{}".format(sym.name.lower()))

        # Find dependencies if needed
        needs_deps = not kconfiglib.expr_value(sym.direct_dep)
        if needs_deps:
            req_deps = autokernel.kconfig.required_deps(sym)
            if req_deps is False:
                # Dependencies can never be satisfied. The module should be skipped.
                log.warn("Cannot satisfy dependencies for {}".format(sym.name))
                return False

        if not autokernel.kconfig.symbol_can_be_user_assigned(sym):
            # If we cannot assign the symbol, we add an assertion instead.
            mod.assertions.append((sym.name, 'y'))
        else:
            mod.assignments.append((sym.name, 'y'))

            if needs_deps:
                for d, v in req_deps:
                    if v:
                        depm = self.add_module_for_sym(d)
                        if depm is False:
                            return False
                        mod.deps.append(depm)
                    else:
                        if autokernel.kconfig.symbol_can_be_user_assigned(sym):
                            mod.assignments.append((d.name, 'n'))
                        else:
                            mod.assertions.append((d.name, 'n'))

        self.modules[mod.name] = mod
        return mod

    def add_module_for_sym(self, sym):
        """
        Adds a module for the given symbol (and its dependencies).
        """
        if sym in self.module_for_sym:
            return self.module_for_sym[sym]

        # Create a module for the symbol, if it doesn't exist already
        mod = self._add_module_for_option(sym)
        if mod is False:
            return False
        self.module_for_sym[sym] = mod
        return mod

    def select_module(self, mod):
        self.module_select_all.deps.append(mod)

    def add_external_module(self, mod):
        self.modules[mod.name] = mod

    def _write_detected_modules(self, f, output_type, output_module_name):
        """
        Writes the collected modules to a file / stdout, in the requested output format.
        """
        if output_type == 'kconf':
            writer = KernelConfigWriter(f)
        elif output_type == 'module':
            writer = ModuleConfigWriter(f)
        else:
            log.die("Invalid output_type '{}'".format(output_type))

        # Set select_all name
        self.module_select_all.name = output_module_name

        # Fill in reverse dependencies for all modules
        self._create_reverse_deps()

        visited = set()
        def visit(m):
            # Ensure we visit only once
            if m in visited:
                return
            visited.add(m)
            writer.write_module(m)

        # Write all modules in topological order
        for m in self.modules:
            visit(self.modules[m])

        # Lastly, write "select_all" module, if it has been used
        if len(self.module_select_all.deps) > 0:
            writer.write_module(self.module_select_all)

    def write_detected_modules(self, args):
        # Write all modules in the given format to the given output file / stdout
        if args.output:
            try:
                with open(args.output, 'w') as f:
                    self._write_detected_modules(f, args.output_type, args.output_module_name)
                    log.info("Module configuration written to '{}'".format(args.output))
            except IOError as e:
                log.die(str(e))
        else:
            self._write_detected_modules(sys.stdout, args.output_type, args.output_module_name)

def detect_modules(kconfig):
    """
    Detects required options for the current system organized into modules.
    Any option with dependencies will also be represented as a module. It returns
    a dict which maps module names to the module objects. The special module returned
    additionaly is the module which selects all detected modules as dependencies.
    """
    log.info("Detecting kernel configuration for local system")
    log.info("HINT: It might be beneficial to run this while using a very generic")
    log.info("      and modular kernel, such as the default kernel on Arch Linux.")

    local_module_count = 0
    def next_local_module_id():
        """
        Returns the next id for a local module
        """
        nonlocal local_module_count
        i = local_module_count
        local_module_count += 1
        return i

    module_creator = ModuleCreator(module_prefix='detected_')
    def add_module_for_detected_node(node, opts):
        """
        Adds a module for the given detected node
        """
        mod = Module("{:04d}_{}".format(next_local_module_id(), node.get_canonical_name()))
        for o in opts:
            try:
                sym = kconfig.syms[o]
            except KeyError:
                log.warn("Skipping unknown symbol {}".format(o))
                continue

            m = module_creator.add_module_for_sym(sym)
            if m is False:
                log.warn("Skipping module {} (unsatisfiable dependencies)".format(mod.name))
                return None
            mod.deps.append(m)
        module_creator.add_external_module(mod)
        return mod

    # Load the configuration database
    config_db = autokernel.lkddb.Lkddb()
    # Inspect the current system
    detector = autokernel.node_detector.NodeDetector()

    # Try to find detected nodes in the database
    log.info("Matching detected nodes against database")

    # First sort all nodes for more consistent output between runs
    all_nodes = []
    # Find options in database for each detected node
    for detector_node in detector.nodes:
        all_nodes.extend(detector_node.nodes)
    all_nodes.sort(key=lambda x: x.get_canonical_name())

    for node in all_nodes:
        opts = config_db.find_options(node)
        if len(opts) > 0:
            # If there are options for the node in the database,
            # add a module for the detected node and its options
            mod = add_module_for_detected_node(node, opts)
            if mod:
                # Select the module in the global selector module
                module_creator.select_module(mod)

    return module_creator

def main_detect(args):
    """
    Main function for the 'main_detect' command.
    """
    # Check if we should write a config or report differences
    check_only = args.check_config != 0

    # Assert that --check is not used together with --type
    if check_only and args.output_type:
        log.die("--check and --type are mutually exclusive")

    # Assert that --check is not used together with --output
    if check_only and args.output:
        log.die("--check and --output are mutually exclusive")

    # Assert that --check is not used together with --output
    if not check_only and args.check_differences:
        log.die("--differences cannot be used without --check")

    # Determine the config file to check against, if applicable.
    if check_only:
        if args.check_config:
            log.info("Checking generated config against '{}'".format(args.check_config))
        else:
            if not has_proc_config_gz():
                log.die("This kernel does not expose /proc/config.gz. Please provide the path to a valid config file manually.")
            log.info("Checking generated config against currently running kernel")

    # Ensure that some required external programs are installed
    check_program_exists('find')
    check_program_exists('findmnt')

    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)
    # Detect system nodes and create modules
    module_creator = detect_modules(kconfig)

    if check_only:
        # Load the given config file or the current kernel's config
        kconfig_load_file_or_current_config(kconfig, args.check_config)
        # Check all detected symbols' values and report them
        check_config_against_detected_modules(kconfig, module_creator.modules, differences_only=args.check_differences)
    else:
        # Add fallback for output type.
        if not args.output_type:
            args.output_type = 'module'

        # Allow - as an alias for stdout
        if args.output == '-':
            args.output = None

        # Write all modules in the given format to the given output file / stdout
        module_creator.write_detected_modules(args)

def get_sym_by_name(kconfig, sym_name):
    if sym_name.startswith('CONFIG_'):
        sym_name = sym_name[len('CONFIG_'):]

    # Get symbol
    try:
        return kconfig.syms[sym_name]
    except KeyError:
        log.die("Symbol '{}' does not exist".format(sym_name))

def main_info(args):
    """
    Main function for the 'info' command.
    """
    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)

    for config_symbol in args.config_symbols:
        sym = get_sym_by_name(kconfig, config_symbol)
        log.info("Information for {}:".format(sym.name))
        print(sym)

def main_revdeps(args):
    """
    Main function for the 'revdeps' command.
    """
    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)

    for config_symbol in args.config_symbols:
        sym = get_sym_by_name(kconfig, config_symbol)
        log.info("Dependents for {}:".format(sym.name))
        for d in sym._dependents: # pylint: disable=protected-access
            print(d)

def main_satisfy(args):
    """
    Main function for the 'satisfy' command.
    """
    # Load symbols from Kconfig
    kconfig = autokernel.kconfig.load_kconfig(args.kernel_dir)

    # Apply autokernel configuration only if we want our dependencies based on the current configuration
    if not args.dep_global:
        # Load configuration file
        config = autokernel.config.load_config(args.autokernel_config)
        # Apply kernel config
        apply_autokernel_config(args, kconfig, config)

    # Create a module for the detected option
    module_creator = ModuleCreator()

    for config_symbol in args.config_symbols:
        sym = get_sym_by_name(kconfig, config_symbol)
        mod = module_creator.add_module_for_sym(sym)
        if mod is False:
            log.warn("Skipping {} (unsatisfiable dependencies)".format(sym.name))
            continue
        module_creator.select_module(mod)

    # Add fallback for output type.
    if not args.output_type:
        args.output_type = 'module'

    # Allow - as an alias for stdout
    if args.output == '-':
        args.output = None

    # Write the module
    module_creator.write_detected_modules(args)

def check_file_exists(value):
    """
    Checks if the given exists
    """
    if not os.path.isfile(value):
        raise argparse.ArgumentTypeError("'{}' is not a file".format(value))
    return value

def check_kernel_dir(value):
    """
    Checks if the given value is a valid kernel directory path.
    """
    if not os.path.isdir(value):
        raise argparse.ArgumentTypeError("'{}' is not a directory".format(value))

    if not os.path.exists(os.path.join(value, 'Kconfig')):
        raise argparse.ArgumentTypeError("'{}' is not a valid kernel directory, as it does not contain a Kconfig file".format(value))

    return value

def suppress_columns_list(value):
    """
    Checks if the given value is a csv of columns to suppress.
    """
    valid_values_new = ['new', 'n']
    valid_values_del = ['del', 'd']
    valid_values_chg = ['changed', 'chg', 'c']
    valid_values = valid_values_new + valid_values_del + valid_values_chg

    supress_new = False
    supress_del = False
    supress_chg = False
    for i in value.split(','):
        if i in valid_values_new:
            supress_new = True
        elif i in valid_values_del:
            supress_del = True
        elif i in valid_values_chg:
            supress_chg = True
        else:
            raise argparse.ArgumentTypeError("'{}' is not a valid suppression type. Must be one of [{}]".format(i, ', '.join(["'{}'".format(v) for v in valid_values])))

    return (supress_new, supress_del, supress_chg)

class ArgumentParserError(Exception):
    pass

class ThrowingArgumentParser(argparse.ArgumentParser):
    def error(self, message):
        raise ArgumentParserError(message)

def autokernel_main():
    """
    Parses options and dispatches control to the correct subcommand function
    """
    parser = ThrowingArgumentParser(description="Autokernel is a kernel configuration management tool. For more information please refer to the documentation (https://autokernel.oddlama.org). If no mode is given, 'autokernel --help' will be executed.")
    subparsers = parser.add_subparsers(title="commands",
            description="Use 'autokernel command --help' to view the help for any command.",
            metavar='command')

    # General options
    parser.add_argument('-K', '--kernel-dir', dest='kernel_dir', default='/usr/src/linux', type=check_kernel_dir,
            help="The kernel directory to operate on. The default is /usr/src/linux.")
    parser.add_argument('-C', '--config', dest='autokernel_config', default=None, type=check_file_exists,
            help="The autokernel configuration file to use. Default is to use '/etc/autokernel/autokernel.conf' or an internal fallback if the default path doesn't exist.")
    parser.add_argument('--no-color', dest='use_color', action='store_false',
            help="Disables coloring in normal output.")
    parser.add_argument('--version', action='version',
            version='%(prog)s {version}'.format(version=__version__))

    # Output options
    output_options = parser.add_mutually_exclusive_group()
    output_options.add_argument('-q', '--quiet', dest='quiet', action='store_true',
            help="Disables any additional output except for errors, and output from tools.")
    output_options.add_argument('-v', '--verbose', dest='verbose', action='store_true',
            help="Enables verbose output.")

    # Setup
    parser_setup = subparsers.add_parser('setup', help='Setup a default configuration in /etc/autokernel if the directory does not exist yet.')
    parser_setup.add_argument('-d', '--dir', dest='setup_dir', default='/etc/autokernel',
            help="The directory to copy the default configuration to. The default is /etc/autokernel.")
    parser_setup.set_defaults(func=main_setup)

    # Check
    parser_check = subparsers.add_parser('check', help="Reports differences between the config that will be generated by autokernel, and the given config file. If no config file is given, the script will try to load the current kernel's configuration from '/proc/config.gz'.")
    parser_check.add_argument('-c', '--compare-config', dest='compare_config', type=check_file_exists,
            help="The .config file to compare the generated configuration against.")
    parser_check.add_argument('-k', '--compare-kernel-dir', dest='compare_kernel_dir', type=check_kernel_dir,
            help="The kernel directory for the given comparison config.")
    parser_check.add_argument('--suppress', dest='suppress_columns', type=suppress_columns_list,
            help="Comma separated list of columns to suppress. 'new' or 'n' supresses new symbols, 'del' or 'd' suppresses removed symbols, 'changed', 'chg' or 'c' supresses changed symbols.")
    parser_check.set_defaults(func=main_check_config)

    # Config generation options
    parser_generate_config = subparsers.add_parser('generate-config', help='Generates the kernel configuration file from the autokernel configuration.')
    parser_generate_config.add_argument('-o', '--output', dest='output',
            help="The output filename. An existing configuration file will be overwritten. The default is '{KERNEL_DIR}/.config'.")
    parser_generate_config.set_defaults(func=main_generate_config)

    # Build options
    parser_build = subparsers.add_parser('build', help='Generates the configuration, and then builds the kernel (and initramfs if required) in the kernel tree.')
    parser_build.add_argument('-c', '--clean', dest='clean', action='store_true',
            help="Clean the kernel tree before building")
    parser_build.set_defaults(func=main_build)

    # Installation options
    parser_install = subparsers.add_parser('install', help='Installs the finished kernel, modules and other resources on the system.')
    parser_install.set_defaults(func=main_install)

    # Full build options
    parser_all = subparsers.add_parser('all', help='First builds and then installs the kernel.')
    parser_all.add_argument('-c', '--clean', dest='clean', action='store_true',
            help="Clean the kernel tree before building")
    parser_all.set_defaults(func=main_build_all)

    # Show symbol infos
    parser_info = subparsers.add_parser('info', help='Displays information for the given symbols')
    parser_info.add_argument('config_symbols', nargs='+',
            help="A list of configuration symbols to show infos for")
    parser_info.set_defaults(func=main_info)

    # Show symbol reverse dependencies
    parser_revdeps = subparsers.add_parser('revdeps', help='Displays all symbols that somehow depend on the given symbol')
    parser_revdeps.add_argument('config_symbols', nargs='+',
            help="A list of configuration symbols to show revdeps for")
    parser_revdeps.set_defaults(func=main_revdeps)

    # Single config module generation options
    parser_satisfy = subparsers.add_parser('satisfy', help='Generates required modules to enable the given symbol')
    parser_satisfy.add_argument('-g', '--global', action='store_true', dest='dep_global',
            help="Report changes solely based on kernel default instead of basing the on the current autokernel configuration")
    parser_satisfy.add_argument('-t', '--type', choices=['module', 'kconf'], dest='output_type',
            help="Selects the output type. 'kconf' will output options in the kernel configuration format. 'module' will output a list of autokernel modules to reflect the necessary configuration.")
    parser_satisfy.add_argument('-m', '--module-name', dest='output_module_name', default='rename_me',
            help="The name of the generated module, which will enable all given options (default: 'rename_me').")
    parser_satisfy.add_argument('-o', '--output', dest='output',
            help="Writes the output to the given file. Use - for stdout (default).")
    parser_satisfy.add_argument('config_symbols', nargs='+',
            help="The configuration symbols to generate modules for (including dependencies)")
    parser_satisfy.set_defaults(func=main_satisfy)

    # Config detection options
    parser_detect = subparsers.add_parser('detect', help='Detects configuration options based on information gathered from the running system')
    parser_detect.add_argument('-c', '--check', nargs='?', default=0, dest='check_config', type=check_file_exists,
            help="Instead of outputting the required configuration values, compare the detected options against the given kernel configuration and report the status of each option. If no config file is given, the script will try to load the current kernel's configuration from '/proc/config.gz'.")
    parser_detect.add_argument('-d', '--differences', dest='check_differences', action='store_true',
            help="Requires --check. Only report options when the suggested value differs from the current value.")
    parser_detect.add_argument('-t', '--type', choices=['module', 'kconf'], dest='output_type',
            help="Selects the output type. 'kconf' will output options in the kernel configuration format. 'module' will output a list of autokernel modules to reflect the necessary configuration.")
    parser_detect.add_argument('-m', '--module-name', dest='output_module_name', default='local',
            help="The name of the generated module, which will enable all detected options (default: 'local').")
    parser_detect.add_argument('-o', '--output', dest='output',
            help="Writes the output to the given file. Use - for stdout (default).")
    parser_detect.set_defaults(func=main_detect)

    try:
        args = parser.parse_args()
    except ArgumentParserError as e:
        log.die(str(e))

    # Set logging options
    log.set_verbose(args.verbose)
    log.set_quiet(args.quiet)
    log.set_use_color(args.use_color)

    if 'func' not in args:
        # Fallback to --help.
        parser.print_help()
    elif args.func is main_setup:
        # Check if we have chosen 'setup', which is special
        # as it has no previous requirements and will not
        # open any configuration files.
        main_setup(args)
    else:
        # Initialize important environment variables
        autokernel.kconfig.initialize_environment()
        # Assert that some required programs exist
        check_execution_environment(args)
        # Execute the mode's function
        args.func(args)

def main():
    try:
        autokernel_main()
    except PermissionError as e:
        log.die(str(e))
    except Exception as e: # pylint: disable=broad-except
        import traceback
        traceback.print_exc()
        log.die("Aborted because of previous errors")

if __name__ == '__main__':
    main()
Checks that some required external programs exist, and some miscellaneous things. # Ensure that the config file has the correct mode, to prevent command-injection by other users. # No component of the path may be modifiable by anyone else but the current user (or root). Checks if /proc/config.gz exists Unpacks /proc/config.gz into a temporary file Applies the given kernel config file to kconfig, or uses /proc/config.gz if config_file is None. Applies the given autokernel configuration to a freshly loaded kconfig object,     and returns gathered extra information such as the resulting kernel cmdline # Build cmdline on demand # Reset symbol_changes # Asserts that the symbol has the given value # Get the kconfig symbol, and change the value # Asserts that the symbol has the given value # Sets a symbols value if and asserts that there are no conflicting double assignments # Get the kconfig symbol, and change the value # Skip assignment if value is already pinned and the statement is in try mode. # Only throw an error if it wasn't a try # Visit all module nodes and apply configuration changes # Ensure we visit only once # Assert that there are no conflicts # Ensure all attached conditions are met for the statement. # Visit the root node and apply all symbol changes # Lastly, invalidate all non-assigned symbols to process new default value conditions # pylint: disable=protected-access # Replace variables in command and run it Main function for the 'setup' command. Main function for the 'check' command. # Use /usr/src/linux-{kernel_version} as the directory. # Load configuration file # Load symbols from Kconfig # Apply autokernel configuration # Load symbols from Kconfig # Load the given config file or the current kernel's config Main function for the 'generate_config' command. # Load configuration file # Fallback for config output # Load symbols from Kconfig # Apply autokernel configuration # Write configuration to file Clean the kernel tree (call make distclean) Build the kernel (call make) # Execute initramfs build_command # Move the output file as stated in the configuration to the kernel tree Installs the modules to the given prefix # Use correct 022 umask when installing modules Main function for the 'build' command. # Load configuration file # Set umask for build # Execute pre hook # Clean the kernel dir, if the user wants that # Config output is "{KERNEL_DIR}/.config" # Initramfs basename "initramfs-{KERNEL_VERSION}.cpio" # The .cpio suffix is cruical, as the kernel makefile requires it to detect initramfs archives # Initramfs output is "{KERNEL_DIR}/initramfs-{KERNEL_VERSION}.cpio" # Load symbols from Kconfig # Set some defaults # Apply autokernel configuration # Write configuration to file # Copy file to .config, which may get changed by the makefiles # Build the kernel # The user has explicitly disabled the builtin commandline, # so there is no need to set it. # Issue a warning, if a custom cmdline does not contain "{CMDLINE}", and we have gathered add_cmdline options. # It is an error to explicitly set INITRAMFS_SOURCE, if our initramfs is set to builtin. # Set CMDLINE_BOOL and CMDLINE # Preprocess INITRAMFS_SOURCE # Kernel build pass #1 # On the first pass, disable all initramfs sources # Start the build process # Build the initramfs, if enabled # Temporarily install modules so the initramfs generator has access to them # Build the initramfs # Pack the initramfs into the kernel if desired # On the second pass, we enable the initramfs cpio archive, which is now in the kernel_dir # Rebuild the kernel to pack the new images # Execute post hook Main function for the 'install' command. # Load configuration file # Use correct umask when installing # Mount # Check mounts # Execute pre hook # Config output is "{KERNEL_DIR}/.config" # Initramfs basename "initramfs-{KERNEL_VERSION}.cpio" # The .cpio suffix is cruical, as the kernel makefile requires it to detect initramfs archives # Initramfs output is "{KERNEL_DIR}/initramfs-{KERNEL_VERSION}.cpio" # bzImage output # Disable purging on negative count # Disable purging for non versionated paths # matches from {KERNEL_VERSION} until first / exclusive in an regex escaped path # derive regex to check if a valid semver is contained and prefix and suffix are given # matches from {KERNEL_VERSION} until first / exclusive in a normal path # replace {KERNEL_VERSION}-* component with * # sort out paths that don't contain valid semvers # For security, we will not call rmtree on a path that doesn't end with a slash, # or if the realpath has less then two slash characters in it. # Otherwise we only call unlink # If the target is disabled, return. # Figure out destination, and move existing filed if necessary # Create directory if it doesn't exist # Install target file # Purge old files # Move target_dir, if it is dynamic # Load symbols from Kconfig # Install modules # Install targets # Purge old target_dirs (will only be done if it is dynamic) # Execute post hook # Undo what we have mounted # Restore old umask Main function for the 'all' command. # Load configuration file A module consists of dependencies (other modules) and option assignments. # Ensure we visit only once # Ensure we visit only once # First visit all dependencies # Then print all assignments # Visit all modules Writes modules to the given file in kernel config format. Writes modules to the given file in the module config format. #assert {} == {};\n".format(o, v) # Clear rev_deps # Fill in reverse dependencies for all modules # Fill in reverse dependencies for select_all module Recursively adds a module for the given option,         until all dependencies are satisfied. # Find dependencies if needed # Dependencies can never be satisfied. The module should be skipped. # If we cannot assign the symbol, we add an assertion instead. Adds a module for the given symbol (and its dependencies). # Create a module for the symbol, if it doesn't exist already Writes the collected modules to a file / stdout, in the requested output format. # Set select_all name # Fill in reverse dependencies for all modules # Ensure we visit only once # Write all modules in topological order # Lastly, write "select_all" module, if it has been used # Write all modules in the given format to the given output file / stdout Detects required options for the current system organized into modules.     Any option with dependencies will also be represented as a module. It returns     a dict which maps module names to the module objects. The special module returned     additionaly is the module which selects all detected modules as dependencies. Returns the next id for a local module Adds a module for the given detected node # Load the configuration database # Inspect the current system # Try to find detected nodes in the database # First sort all nodes for more consistent output between runs # Find options in database for each detected node # If there are options for the node in the database, # add a module for the detected node and its options # Select the module in the global selector module Main function for the 'main_detect' command. # Check if we should write a config or report differences # Assert that --check is not used together with --type # Assert that --check is not used together with --output # Assert that --check is not used together with --output # Determine the config file to check against, if applicable. # Ensure that some required external programs are installed # Load symbols from Kconfig # Detect system nodes and create modules # Load the given config file or the current kernel's config # Check all detected symbols' values and report them # Add fallback for output type. # Allow - as an alias for stdout # Write all modules in the given format to the given output file / stdout # Get symbol Main function for the 'info' command. # Load symbols from Kconfig Main function for the 'revdeps' command. # Load symbols from Kconfig # pylint: disable=protected-access Main function for the 'satisfy' command. # Load symbols from Kconfig # Apply autokernel configuration only if we want our dependencies based on the current configuration # Load configuration file # Apply kernel config # Create a module for the detected option # Add fallback for output type. # Allow - as an alias for stdout # Write the module Checks if the given exists Checks if the given value is a valid kernel directory path. Checks if the given value is a csv of columns to suppress. Parses options and dispatches control to the correct subcommand function # General options # Output options # Setup # Check # Config generation options # Build options # Installation options # Full build options # Show symbol infos # Show symbol reverse dependencies # Single config module generation options # Config detection options # Set logging options # Fallback to --help. # Check if we have chosen 'setup', which is special # as it has no previous requirements and will not # open any configuration files. # Initialize important environment variables # Assert that some required programs exist # Execute the mode's function # pylint: disable=broad-except