#!/usr/bin/python3 -tt
# -*- coding: utf-8 -*-
# vim:expandtab:autoindent:tabstop=4:shiftwidth=4:filetype=python:textwidth=0:

# Originally written by Seth Vidal
# Sections taken from Mach by Thomas Vander Stichele
# Major reorganization and adaptation by Michael Brown
# Copyright (C) 2007 Michael E Brown <mebrown@michaels-house.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.

# pylint: disable=pointless-string-statement,wrong-import-position
"""
       mock [options] {--init|--clean|--scrub=[all,bootstrap,chroot,cache,root-cache,c-cache,yum-cache,dnf-cache,lvm,overlayfs]}
       mock [options] [--rebuild] /path/to/srpm(s)
       mock [options] [--chain] /path/to/srpm(s)
       mock [options] --buildsrpm {--spec /path/to/spec --sources /path/to/src|
       --scm-enable [--scm-option key=value]}
       mock [options] {--shell|--chroot} <cmd>
       mock [options] --installdeps {SRPM|RPM}
       mock [options] --install PACKAGE
       mock [options] --copyin path [..path] destination
       mock [options] --copyout path [..path] destination
       mock [options] --scm-enable [--scm-option key=value]
       mock [options] --dnf-cmd arguments
       mock [options] --yum-cmd arguments
"""

from __future__ import print_function

# library imports
import argparse
import configparser
import errno
import glob
import grp
import logging
import logging.config

from pprint import pformat
import os
import os.path
import signal
import shutil
import shlex
import sys
import time
import copy

# pylint: disable=import-error
from functools import partial

# our imports
from mockbuild import config
from mockbuild import util
from mockbuild.constants import MOCKCONFDIR, VERSION, DEFAULT_UMASK
from mockbuild.file_downloader import FileDownloader
from mockbuild.mounts import BindMountPoint, FileSystemMountPoint
import mockbuild.backend
from mockbuild.backend import Commands
from mockbuild.buildroot import Buildroot
import mockbuild.exception
from mockbuild.plugin import Plugins
import mockbuild.rebuild
from mockbuild.state import State
from mockbuild.trace_decorator import traceLog
import mockbuild.uid
from mockbuild.scrub_all import scrub_all_chroots

# set up basic logging until config file can be read
FORMAT = "%(levelname)s: %(message)s"
logging.basicConfig(format=FORMAT, level=logging.WARNING)
log = logging.getLogger()


signal_names = {1: "SIGHUP",
                13: "SIGPIPE",
                15: "SIGTERM"
                }

# This line is replaced by spec file %install section.
_MOCK_NVR = None


class RepoCallback(argparse.Action):
    """ Parse --enablerepo/--disablerepo options """
    def __call__(self, parser, namespace, value, option_string=None):
        if not hasattr(namespace, self.dest):
            setattr(namespace, self.dest, [])
        options = getattr(namespace, self.dest)
        options.extend((option_string, value))


def command_parse():
    """return options and args from parsing the command line"""
    plugins = config.PLUGIN_LIST
    parser = argparse.ArgumentParser(usage=__doc__)

    parser.add_argument('--version', action='version', version=VERSION)

    # hack from optparse=>argparse migration time, use add_argument if possible
    parser.add_option = parser.add_argument

    # modes (basic commands)
    parser.add_option("--rebuild", action="store_const", const="rebuild",
                      dest="mode", default='__default__',
                      help="rebuild the specified SRPM(s)")
    parser.add_option("--calculate-build-dependencies", action="store_const",
                      const="calculatedeps", dest="mode",
                      help="Resolve and install static and dynamic build dependencies")
    parser.add_option("--chain", action="store_const", const="chain",
                      dest="mode",
                      help="build multiple RPMs in chain loop")
    parser.add_option("--buildsrpm", action="store_const", const="buildsrpm",
                      dest="mode",
                      help="Build a SRPM from spec (--spec ...) and sources"
                           "(--sources ...) or from SCM")
    parser.add_option("--debug-config", action="store_const", const="debugconfig",
                      dest="mode",
                      help="Prints all options in config_opts")
    parser.add_option("--debug-config-expanded", action="store_const", const="debugconfigexpand",
                      dest="mode",
                      help="Prints all options in config_opts with jinja template values already expanded")
    parser.add_option("--shell", action="store_const",
                      const="shell", dest="mode",
                      help="run the specified command interactively within the chroot."
                           " Default command: /bin/sh")
    parser.add_option("--chroot", action="store_const",
                      const="chroot", dest="mode",
                      help="run the specified command noninteractively within the chroot.")
    parser.add_option("--clean", action="store_const", const="clean",
                      dest="mode",
                      help="completely remove the specified chroot")
    scrub_choices = ('chroot', 'cache', 'root-cache', 'c-cache', 'yum-cache',
                     'dnf-cache', 'lvm', 'overlayfs', 'bootstrap', 'all')
    scrub_metavar = "[all|bootstrap|chroot|cache|root-cache|c-cache|yum-cache|dnf-cache]"
    parser.add_option("--scrub", action='append', choices=scrub_choices, default=[],
                      metavar=scrub_metavar,
                      help="completely remove the specified chroot, bootstrap chroot "
                           "or cache dir or all of the chroot and cache")
    parser.add_option(
        "--scrub-all-chroots", action="store_const", dest="mode",
        const="scrub-all-chroots", help=(
            "Run mock --scrub=all for all chroots that appear to have been "
            "used previously (see manual page for more info)."
        ),

    )
    parser.add_option("--init", action="store_const", const="init", dest="mode",
                      help="initialize the chroot, do not build anything")
    parser.add_option("--installdeps", action="store_const", const="installdeps",
                      dest="mode",
                      help="install build dependencies for a specified SRPM or SPEC file")
    parser.add_option("-i", "--install", action="store_const", const="install",
                      dest="mode",
                      help="install packages using package manager")
    parser.add_option("--list-chroots", action="store_const", const="listchroots",
                      dest="mode",
                      help="List all chroot's configs")
    parser.add_option("--update", action="store_const", const="update",
                      dest="mode",
                      help="update installed packages using package manager")
    parser.add_option("--remove", action="store_const", const="remove",
                      dest="mode",
                      help="remove packages using package manager")
    parser.add_option("--orphanskill", action="store_const", const="orphanskill",
                      dest="mode",
                      help="Kill all processes using specified buildroot.")

    parser.add_option("--copyin", action="store_const", const="copyin",
                      dest="mode",
                      help="Copy file(s) into the specified chroot")

    parser.add_option("--copyout", action="store_const", const="copyout",
                      dest="mode",
                      help="Copy file(s) from the specified chroot")

    parser.add_option("--pm-cmd", action="store_const", const="pm-cmd",
                      dest="mode",
                      help="Execute package management command (with yum or dnf)")

    parser.add_option("--yum-cmd", action="store_const", const="yum-cmd",
                      dest="mode",
                      help="Execute package management command with yum")

    parser.add_option("--dnf-cmd", action="store_const", const="dnf-cmd",
                      dest="mode",
                      help="Execute package management command with dnf")

    parser.add_option("--snapshot", action="store_const", const="snapshot",
                      dest="mode",
                      help="Create a new LVM/overlayfs snapshot with given name")

    parser.add_option("--remove-snapshot", action="store_const", const="remove_snapshot",
                      dest="mode",
                      help="Remove LVM/overlayfs snapshot with given name")

    parser.add_option("--rollback-to", action="store_const", const="rollback-to",
                      dest="mode",
                      help="Rollback to given snapshot")

    parser.add_option("--umount", action="store_const", const="umount",
                      dest="mode", help="Umount the buildroot if it's "
                                        "mounted from separate device (LVM/overlayfs)")
    parser.add_option("--mount", action="store_const", const="mount",
                      dest="mode", help="Mount the buildroot if it's "
                                        "mounted from separate device (LVM/overlayfs)")
    # chain
    parser.add_option('--localrepo', default=None,
                      help=("local path for the local repo, defaults to making "
                            "its own (--chain mode only)"))
    parser.add_option('-c', '--continue', default=False, action='store_true',
                      dest='cont',
                      help="if a pkg fails to build, continue to the next one")
    parser.add_option(
        '-a', '--addrepo', default=[], action='append', dest='repos',
        metavar="REPO", help=(
            "Add a repo baseurl to the DNF/YUM configuration for both the "
            "build chroot and the bootstrap chroot.  This option can be "
            "specified multiple times, allowing you to reference multiple "
            "repositories in addition to the default repository set."))
    parser.add_option('--recurse', default=False, action='store_true',
                      help="if more than one pkg and it fails to build, try to build the rest and come back to it")
    parser.add_option('--tmp_prefix', default=None, dest='tmp_prefix',
                      help="tmp dir prefix - will default to username-pid if not specified")
    # options
    parser.add_option("-r", "--root", action="store", type=str, dest="chroot",
                      help="chroot config file name or path. Taken as a path if it ends "
                           "in .cfg, otherwise looked up in the configdir.  default: %%default",
                      metavar="CONFIG",
                      default='default')

    parser.add_option("--offline", action="store_false", dest="online",
                      default=True,
                      help="activate 'offline' mode.")

    parser.add_option("-n", "--no-clean", action="store_false", dest="clean",
                      help="do not clean chroot before building", default=True)
    parser.add_option("--cleanup-after", action="store_true",
                      dest="cleanup_after", default=None,
                      help="Clean chroot after building. Use with --resultdir."
                           " Only active for 'rebuild'.")
    parser.add_option("-N", "--no-cleanup-after", action="store_false",
                      dest="cleanup_after", default=None,
                      help="Don't clean chroot after building. If automatic"
                           " cleanup is enabled, use this to disable.", )
    parser.add_option("--cache-alterations", action="store_true",
                      dest="cache_alterations", default=False,
                      help="Rebuild the root cache after making alterations to the chroot"
                           " (i.e. --install). Only useful when using tmpfs plugin.")
    parser.add_option("--nocheck", action="store_false", dest="check",
                      default=True, help="pass --nocheck to rpmbuild to skip 'make check' tests")
    parser.add_option("--separate-check",
                      choices=['best_effort', 'enforce', 'off'],
                      default=None, dest="separate_check",
                      help="Run %%check as a separate rpmbuild phase using -bk.  "
                           "'best_effort' uses it when rpmbuild supports -bk, "
                           "falls back silently otherwise.  'enforce' fails if "
                           "rpmbuild does not support -bk (rpm >= 6.0.91).  "
                           "'off' disables the feature (overrides config).  "
                           "Default: 'off'.")
    parser.add_option("--arch", action="store", dest="arch",
                      default=None, help="Sets kernel personality().")
    parser.add_option("--forcearch", action="store", dest="forcearch",
                      default=None, help="Force architecture to DNF (pass --forcearch to DNF).")
    parser.add_option("--target", action="store", dest="rpmbuild_arch",
                      default=None, help="passed to rpmbuild as --target")
    parser.add_option("-D", "--define", action="append", dest="rpmmacros",
                      default=[], type=str, metavar="'MACRO EXPR'",
                      help="define an rpm macro (may be used more than once)")
    parser.add_option("--macro-file", action="store", type=str, dest="macrofile",
                      default=[], help="Use pre-defined rpm macro file")
    parser.add_option("--with", action="append", dest="rpmwith",
                      default=[], type=str, metavar="option",
                      help="enable configure option for build (may be used more than once)")
    parser.add_option("--without", action="append", dest="rpmwithout",
                      default=[], type=str, metavar="option",
                      help="disable configure option for build (may be used more than once)")
    parser.add_option("--resultdir", action="store", type=str,
                      default=None, help="path for resulting files to be put")
    parser.add_option("--rootdir", action="store", type=str,
                      default=None, help="Path for where the chroot should be built")
    parser.add_option("--uniqueext", action="store", type=str,
                      default=None,
                      help="Arbitrary, unique extension to append to buildroot"
                           " directory name")
    parser.add_option("--configdir", action="store", dest="configdir",
                      default=None,
                      help="Change where config files are found")
    parser.add_option("--config-opts", action="append", dest="cli_config_opts",
                      default=[], help="Override configuration option.")
    parser.add_option("--rpmbuild_timeout", action="store",
                      dest="rpmbuild_timeout", type=int, default=None,
                      help="Fail build if rpmbuild takes longer than 'timeout'"
                           " seconds ")
    parser.add_option("--unpriv", action="store_true", default=False,
                      help="Drop privileges before running command when using --chroot")
    parser.add_option("--cwd", action="store", default=None,
                      metavar="DIR",
                      help="Change to the specified directory (relative to the chroot)"
                           " before running command when using --chroot")

    parser.add_option("--spec", action="store",
                      help="Specifies spec file to use to build an SRPM")
    parser.add_option("--sources", action="store",
                      help="Specifies sources (either a single file or a directory of files)"
                           "to use to build an SRPM (used only with --buildsrpm)")
    parser.add_option("--symlink-dereference", action="store_true", dest="symlink_dereference",
                      default=False, help="Follow symlinks in sources (used only with --buildsrpm)")

    parser.add_option("--short-circuit",
                      choices=['prep', 'install', 'build', 'binary'],
                      help="Pass short-circuit option to rpmbuild to skip already "
                           "complete stages. Warning: produced packages are unusable. "
                           "Implies --no-clean. Valid options: build, install, binary")
    parser.add_option("--rpmbuild-opts", action="store",
                      help="Pass additional options to rpmbuild")
    parser.add_option("--enablerepo", action=RepoCallback, type=str,
                      dest="enable_disable_repos", default=[],
                      help="Pass enablerepo option to yum/dnf", metavar='[repo]')
    parser.add_option("--disablerepo", action=RepoCallback, type=str,
                      dest="enable_disable_repos", default=[],
                      help="Pass disablerepo option to yum/dnf", metavar='[repo]')
    parser.add_option("--old-chroot", action="store_true", dest="old_chroot",
                      default=False,
                      help="Obsoleted. Use --isolation=simple")
    parser.add_option("--new-chroot", action="store_true", dest="new_chroot",
                      default=False,
                      help="Obsoleted. Use --isolation=nspawn")
    parser.add_option("--isolation", action="store", dest="isolation",
                      help="what level of isolation to use. Valid option: simple, nspawn")
    parser.add_option("--enable-network", action="store_true", dest="enable_network",
                      default=False,
                      help="enable networking.")
    parser.add_option("--postinstall", action="store_true", dest="post_install",
                      default=False, help="Try to install built packages in "
                                          "the same buildroot right after build")

    # verbosity
    parser.add_option("-v", "--verbose", action="store_const", const=2,
                      dest="verbose", default=1, help="verbose build")
    parser.add_option("-q", "--quiet", action="store_const", const=0,
                      dest="verbose", help="quiet build")
    parser.add_option("--trace", action="store_true", default=False,
                      dest="trace", help="Enable internal mock tracing output.")

    # plugins
    parser.add_option("--enable-plugin", action="append",
                      dest="enabled_plugins", type=str, default=[],
                      help="Enable plugin. Currently-available plugins: %s"
                      % repr(plugins))
    parser.add_option("--disable-plugin", action="append",
                      dest="disabled_plugins", type=str, default=[],
                      help="Disable plugin. Currently-available plugins: %s"
                      % repr(plugins))
    parser.add_option("--plugin-option", action="append", dest="plugin_opts",
                      default=[], type=str,
                      metavar="PLUGIN:KEY=VALUE",
                      help="define an plugin option (may be used more than once)")

    parser.add_option("-p", "--print-root-path", help="print path to chroot root",
                      dest="printrootpath", action="store_true",
                      default=False)

    parser.add_option("-l", "--list-snapshots",
                      help="list LVM/overlayfs snapshots associated with buildroot",
                      dest="list_snapshots", action="store_true",
                      default=False)

    # SCM options
    parser.add_option("--scm-enable", help="build from SCM repository",
                      dest="scm", action="store_true",
                      default=None)
    parser.add_option("--scm-option", action="append", dest="scm_opts",
                      default=[], type=str,
                      help="define an SCM option (may be used more than once)")

    # Package management options
    parser.add_option("--yum", help="use yum as package manager",
                      dest="pkg_manager", action="store_const", const="yum")
    parser.add_option("--dnf", help="use dnf as package manager",
                      dest="pkg_manager", action="store_const", const="dnf")

    # Bootstrap options
    parser.add_option('--bootstrap-chroot', dest='bootstrapchroot', action='store_true',
                      help="build in two stages, using chroot rpm for creating the build chroot",
                      default=None)
    parser.add_option('--no-bootstrap-chroot', dest='bootstrapchroot', action='store_false',
                      help="build in a single stage, using system rpm for creating the build chroot",
                      default=None)

    parser.add_option('--use-bootstrap-image', dest='usebootstrapimage', action='store_true',
                      help="create bootstrap chroot from container image (turns "
                           "--bootstrap-chroot on)", default=None)
    parser.add_option('--no-bootstrap-image', dest='usebootstrapimage', action='store_false',
                      help="don't create bootstrap chroot from container image", default=None)

    parser.add_option('--buildroot-image',
                      help=(
                          "Use an OCI image (or a local file containing an OCI "
                          "image as a tarball) as the base for the buildroot. "
                          "The image must contain a compatible distribution "
                          "(e.g., fedora:41 for fedora-41-x86_64)"))

    parser.add_option('--additional-package', action='append', default=[],
                      type=str, dest="additional_packages",
                      help=("Additional package to install into the buildroot before "
                            "the build is done.  Can be specified multiple times."))
    parser.add_option("--hermetic-build", nargs=2,
                      metavar=("LOCKFILE", "REPO_DIRECTORY"),
                      help="Perform a hermetic (fully offline) build")

    (options, args) = parser.parse_known_args()

    if options.mode == '__default__':
        # handle old-style commands
        if len(args) and args[0] in ('chroot', 'shell', 'rebuild', 'install',
                                     'installdeps', 'remove', 'init', 'clean'):
            options.mode = args[0]
            args = args[1:]
        else:
            options.mode = 'rebuild'

    if options.hermetic_build and options.chroot != 'default':
        raise mockbuild.exception.BadCmdline(
            "The --hermetic-build mode uses a special chroot configuration, "
            "you can not select the chroot configuration with the "
            "-r/--root option.")

    if options.hermetic_build and options.mode != "rebuild":
        raise mockbuild.exception.BadCmdline("--rebuild mode needed with --hermetic-build")

    options.calculatedeps = None
    if options.mode == "calculatedeps":
        options.mode = "rebuild"
        options.calculatedeps = True

    # Optparse.parse_args() eats '--' argument, while argparse doesn't.  Do it manually.
    if args and args[0] == '--':
        args = args[1:]

    if options.scrub:
        options.mode = 'clean'

    # explicitly disallow multiple targets in --target argument
    if options.rpmbuild_arch:
        if options.rpmbuild_arch.find(',') != -1:
            raise mockbuild.exception.BadCmdline("--target option accepts only "
                                                 "one arch. Invalid: %s" % options.rpmbuild_arch)

    if options.mode == 'buildsrpm' and not (options.spec):
        if not options.scm:
            raise mockbuild.exception.BadCmdline("Must specify both --spec and "
                                                 "--sources with --buildsrpm")

    if options.recurse:
        # --recurse implies --continue
        options.cont = True

    if options.localrepo and options.mode != 'chain':
        raise mockbuild.exception.BadCmdline(
            "The --localrepo option works only with --chain")

    if options.recurse and options.mode != 'chain':
        raise mockbuild.exception.BadCmdline(
            "Only --chain mode supports --recurse build algorithm")

    if options.cont and options.mode != 'chain':
        raise mockbuild.exception.BadCmdline(
            "Only --chain mode supports --continue build algorithm")

    if options.spec:
        options.spec = os.path.expanduser(options.spec)
    if options.sources:
        options.sources = os.path.expanduser(options.sources)

    if options.additional_packages and options.mode != 'rebuild':
        raise mockbuild.exception.BadCmdline(
            "The --additional-package option requires the --rebuild mode")

    return (options, args)


def handle_signals(buildroot, number, frame):

    log.info("\nReceived signal %s activating orphansKill", signal_names[number])
    util.orphansKill(buildroot.make_chroot_path())
    sys.exit(128 + number)


@traceLog()
def setup_logging(config_path, config_opts, options):
    log_ini = os.path.join(config_path, config_opts["log_config_file"])

    try:
        if not os.path.exists(log_ini):
            if os.path.normpath('/etc/mock') != os.path.normpath(config_path):
                log.warning("Could not find required logging config file: %s. Using default...",
                            log_ini)
                log_ini = os.path.join("/etc/mock", config_opts["log_config_file"])
                if not os.path.exists(log_ini):
                    raise IOError("Could not find log config file %s" % log_ini)
            else:
                raise IOError("Could not find log config file %s" % log_ini)
    except IOError as exc:
        log.error(exc)
        sys.exit(50)

    try:
        log_cfg = configparser.ConfigParser()
        logging.config.fileConfig(log_ini)
        log_cfg.read(log_ini)
    except (IOError, OSError, configparser.NoSectionError) as exc:
        log.error("Log config file(%s) not correctly configured: %s", log_ini, exc)
        sys.exit(50)

    try:
        # set up logging format strings
        config_opts['build_log_fmt_str'] = log_cfg.get("formatter_%s" % config_opts['build_log_fmt_name'],
                                                       "format", raw=1)
        config_opts['root_log_fmt_str'] = log_cfg.get("formatter_%s" % config_opts['root_log_fmt_name'],
                                                      "format", raw=1)
        config_opts['state_log_fmt_str'] = log_cfg.get("formatter_%s" % config_opts['state_log_fmt_name'],
                                                       "format", raw=1)
    except configparser.NoSectionError as exc:
        log.error("Log config file (%s) missing required section: %s", log_ini, exc)
        sys.exit(50)

    # set logging verbosity
    if options.verbose == 0:
        log.handlers[0].setLevel(logging.WARNING)
        tmplog = logging.getLogger("mockbuild.Root.state")
        if tmplog.handlers:
            tmplog.handlers[0].setLevel(logging.WARNING)
    elif options.verbose == 1:
        log.handlers[0].setLevel(logging.INFO)
    elif options.verbose == 2:
        log.handlers[0].setLevel(logging.DEBUG)
        logging.getLogger("mockbuild").propagate = 1

    # enable tracing if requested
    logging.getLogger("trace").propagate = 0
    if options.trace:
        logging.getLogger("trace").propagate = 1

    logging.getLogger("mockbuild").mock_stderr_line_prefix = ""
    if config_opts['stderr_line_prefix'] != "":
        logging.getLogger("mockbuild").mock_stderr_line_prefix = config_opts['stderr_line_prefix']


@traceLog()
def check_arch_combination(target_arch, config_opts):
    try:
        legal = config_opts['legal_host_arches']
    except KeyError:
        return
    host_arch = config_opts['host_arch']
    if (host_arch not in legal) and not config_opts['forcearch']:
        log.info("Unable to build arch %s natively on arch %s. Setting forcearch to use software emulation.",
                 target_arch, host_arch)
        config_opts['forcearch'] = target_arch

    config.multiply_platform_multiplier(config_opts)

    if not config_opts['forcearch']:
        return

    # Check below that we can do cross-architecture builds.

    option = f"--forcearch={config_opts['forcearch']}"
    binary_pattern = config_opts["qemu_user_static_mapping"].get(config_opts["forcearch"],
                                                                 config_opts["forcearch"])
    binary = f'/usr/bin/qemu-{binary_pattern}-static'
    if os.path.exists(binary):
        return

    # qemu-user-static is required, but seems to be missing
    if not util.is_host_rh_family():
        # On non-RH systems we are not sure where the qemu-<ARCH>-static
        # binaries reside - therefore we can't even check whether they are
        # installed.  Therefore we don't raise an exception; we at least
        # notify the user verbosely, cross our fingers, and continue.
        log.warning("Mock with %s will likely fail with missing %s",
                    option, binary)
        time.sleep(5)
        return

    raise mockbuild.exception.InvalidArchitecture(
        f'The {option} feature requires the {binary} '
        'file to be installed (typically qemu-user-static* package)')


@traceLog()
def do_debugconfig(config_opts, expand=False):
    jinja_expand = config_opts['__jinja_expand']
    defaults = config.setup_default_config_opts()
    defaults['__jinja_expand'] = expand
    config_opts['__jinja_expand'] = expand
    for key in sorted(config_opts):
        if key == '__jinja_expand':
            value = jinja_expand
        else:
            value = config_opts[key]
        if (key in defaults) and (key in config_opts) and (config_opts[key] != defaults[key]) or \
           (key not in defaults):
            print("config_opts['{}'] = {}".format(key, pformat(value)))
    config_opts['__jinja_expand'] = jinja_expand


@traceLog()
def do_listchroots(config_path, uidManager):
    uidManager.run_in_subprocess_without_privileges(
        config.list_configs, config_path,
    )


@traceLog()
def rootcheck():
    "verify mock was started correctly (either by sudo or consolehelper)"
    # if we're root due to sudo or consolehelper, we're ok
    # if not raise an exception and bail
    if os.getuid() == 0 and not (os.environ.get("SUDO_UID") or os.environ.get("USERHELPER_UID") or \
            os.environ.get("PKEXEC_UID")):
        raise RuntimeError("mock will not run from the root account (needs an unprivileged uid so it can drop privs)")


@traceLog()
def groupcheck(unprivGid, tgtGid):
    "verify that the user running mock is part of the correct group"
    # verify that we're in the correct group (so all our uid/gid manipulations work)
    inmockgrp = False
    members = []
    for gid in os.getgroups() + [unprivGid]:
        name = grp.getgrgid(gid).gr_name
        if gid == tgtGid:
            inmockgrp = True
            break
        members.append(name)
    if not inmockgrp:
        name = grp.getgrgid(tgtGid).gr_name
        raise RuntimeError("Must be member of '%s' group to run mock! (%s)" %
                           (name, ", ".join(members)))


@traceLog()
def unshare_namespace(config_opts):
    base_unshare_flags = util.CLONE_NEWNS
    # IPC ns is unshared later
    extended_unshare_flags = base_unshare_flags | util.CLONE_NEWUTS
    try:
        util.unshare(extended_unshare_flags)
    except mockbuild.exception.UnshareFailed as e:
        log.debug("unshare(%d) failed, falling back to unshare(%d)",
                  extended_unshare_flags, base_unshare_flags)
        try:
            util.unshare(base_unshare_flags)
        except mockbuild.exception.UnshareFailed as e2:
            log.error("Namespace unshare failed.")
            if util.mock_host_environment_type() == "docker" \
                and not ('docker_unshare_warning' in config_opts
                         and config_opts['docker_unshare_warning']):
                log.error("It seems we are running inside of Docker. Let skip unsharing.")
                log.error("You should *not* run anything but Mock in this container. You have been warned!")
                time.sleep(5)
            else:
                sys.exit(e2.resultcode)


@traceLog()
def main():
    "Main executable entry point."

    # initial sanity check for correct invocation method
    rootcheck()

    (options, args) = command_parse()
    if options.printrootpath or options.list_snapshots:
        options.verbose = 0

    if options.mode == "scrub-all-chroots":
        return scrub_all_chroots()

    # config path -- can be overridden on cmdline
    config_path = MOCKCONFDIR
    if options.configdir:
        config_path = options.configdir

    if options.hermetic_build:
        options.chroot = "hermetic-build"

    # Setup uidManager for privilege handling.  At this point:
    #   - setuid wrapper: real uid = unpriv, effective uid = 0
    #   - sudo: real/effective = 0, with env vars identifying the caller
    # Load config unprivileged, then reload uidManager with the configured
    # chrootuid/chrootgid so that subsequent privilege dropping uses the
    # chroot identity instead of the calling user's.
    uidManager = mockbuild.uid.setup_uid_manager()
    config_opts = uidManager.run_in_subprocess_without_privileges(
            config.load_config, config_path, options.chroot)

    uidManager = mockbuild.uid.reload_uidmanager(uidManager, config_opts)
    if os.geteuid() == 0:
        uidManager.dropPrivsTemp()

    # cmdline options override config options
    config.set_config_opts_per_cmdline(config_opts, options, args)

    sys.setrecursionlimit(config_opts["recursion_limit"])

    util.subscription_redhat_init(config_opts, uidManager)

    # allow a different mock group to be specified
    uidManager.fix_different_chrootgid(config_opts)

    # verify that our unprivileged uid is in the mock group
    groupcheck(uidManager.unprivGid, config_opts['chrootgid'])

    # configure logging
    setup_logging(config_path, config_opts, options)

    # verify that we're not trying to build an arch that we can't
    check_arch_combination(config_opts['rpmbuild_arch'], config_opts)

    # security cleanup (don't need/want this in the chroot)
    if 'SSH_AUTH_SOCK' in os.environ:
        del os.environ['SSH_AUTH_SOCK']

    # elevate privs
    uidManager.become_user_without_push(0, 0)

    # do whatever we're here to do
    py_version = '{0}.{1}.{2}'.format(*sys.version_info[:3])
    log.info("mock.py version %s starting (python version = %s%s), args: %s",
             VERSION, py_version,
             "" if not _MOCK_NVR else ", NVR = " + _MOCK_NVR,
             " ".join(shlex.quote(x) for x in sys.argv))
    state = State()
    plugins = Plugins(config_opts, state)

    # When scrubbing all, we also want to scrub a bootstrap chroot
    if options.scrub:
        config_opts['use_bootstrap'] = True

    # outer buildroot to bootstrap the installation - based on main config with some differences
    bootstrap_buildroot = None
    if config_opts['use_bootstrap']:
        # first take a copy of the config so we can make some modifications
        bootstrap_buildroot_config = config_opts.copy()
        # copy plugins configuration so we get a separate deep copy
        bootstrap_buildroot_config['plugin_conf'] = \
            copy.deepcopy(config_opts['plugin_conf'])  # pylint: disable=no-member
        # add '-bootstrap' to the end of the root name
        bootstrap_buildroot_config['root'] = bootstrap_buildroot_config['root'] + '-bootstrap'
        # don't share root cache tarball
        bootstrap_buildroot_config['plugin_conf']['root_cache_opts']['dir'] = \
            "{{cache_topdir}}/" + bootstrap_buildroot_config['root'] + "/root_cache/"
        # we don't want to affect the bootstrap.config['nspawn_args'] array, deep copy
        bootstrap_buildroot_config['nspawn_args'] = config_opts.get('nspawn_args', []).copy()

        # allow bootstrap buildroot to access the network for getting packages
        bootstrap_buildroot_config['rpmbuild_networking'] = True
        bootstrap_buildroot_config['use_host_resolv'] = True

        # disable updating bootstrap chroot
        bootstrap_buildroot_config['update_before_build'] = False

        # Enforce host-native repo architecture for bootstrap chroot (unless
        # bootstrap_forcearch=True, which should never be the case).  This
        # decision affects condPersonality() for DNF calls!
        #
        # Temporary workaround: sub-architecture variants (e.g. x86_64_v2)
        # listed in oci_platform_map run natively on the host — keep the
        # target's repo_arch so the bootstrap uses the correct sub-arch
        # repos.  See oci_platform_map in config.py and PR #1733/#1783.
        host_arch = config_opts["host_arch"]
        target_arch = config_opts["target_arch"]
        if config_opts["use_bootstrap_image"] \
                and target_arch not in config_opts.get('oci_platform_map', {}):
            # with bootstrap image, bootstrap is always native
            bootstrap_buildroot_config['repo_arch'] = config_opts['repo_arch_map'].get(host_arch, host_arch)
        elif host_arch not in config_opts.get("legal_host_arches", []) \
                and not config_opts.get('bootstrap_forcearch'):
            # target chroot uses --forcearch, but bootstrap is native
            bootstrap_buildroot_config['repo_arch'] = config_opts['repo_arch_map'].get(host_arch, host_arch)
        # else:
        # We keep the 'repo_arch' copied from target chroot (config_opts['repo_arch']):
        # - for the 'multilib' cases (we don't want to use x86_64 repos for i586 chroots)
        # - 'bootstrap_forcearch' is set

        # disable forcearch in bootstrap, per https://github.com/rpm-software-management/mock/issues/1110
        bootstrap_buildroot_config['forcearch'] = None

        bootstrap_buildroot_state = State(bootstrap=True)
        bootstrap_plugins = Plugins(bootstrap_buildroot_config, bootstrap_buildroot_state)
        bootstrap_buildroot = Buildroot(bootstrap_buildroot_config,
                                        uidManager, bootstrap_buildroot_state, bootstrap_plugins,
                                        is_bootstrap=True)

        # override configs for bootstrap_*
        for option in list(bootstrap_buildroot.config.keys()):
            # options that are not related to bootstrap chroot config_opts
            dont_copy = ["bootstrap_image"]
            prefix = "bootstrap_"
            if option.startswith(prefix) and option not in dont_copy:
                bootstrap_option = option[len(prefix):]
                bootstrap_buildroot.config[bootstrap_option] = bootstrap_buildroot.config[option]
                del bootstrap_buildroot.config[option]

        if config_opts['redhat_subscription_required']:
            key_dir = '/etc/pki/entitlement'
            chroot_dir = bootstrap_buildroot.make_chroot_path(key_dir)
            mount_point = BindMountPoint(srcpath=key_dir, bindpath=chroot_dir)
            bootstrap_buildroot.mounts.add(mount_point)

    buildroot = Buildroot(config_opts, uidManager, state, plugins, bootstrap_buildroot)

    for baseurl in options.repos:
        util.add_local_repo(config_opts, baseurl,
                            bootstrap=bootstrap_buildroot)

    if bootstrap_buildroot is not None:
        # add the extra bind mount to the outer chroot
        inner_mount = bootstrap_buildroot.make_chroot_path(buildroot.make_chroot_path())

        # Hide re-mounted chroot from host by private tmpfs.
        buildroot.mounts.bootstrap_mounts.append(
            FileSystemMountPoint(filetype='tmpfs',
                                 device='hide_root_in_bootstrap',
                                 path=inner_mount,
                                 options="private,mode=0755"))
        buildroot.mounts.bootstrap_mounts.append(
            BindMountPoint(buildroot.make_chroot_path(), inner_mount,
                           recursive=True, options="private").treat_as_chroot())

    signal.signal(signal.SIGTERM, partial(handle_signals, buildroot))
    signal.signal(signal.SIGPIPE, partial(handle_signals, buildroot))
    signal.signal(signal.SIGHUP, partial(handle_signals, buildroot))

    # postprocess option arguments for bootstrap
    if options.mode in ['installdeps', 'install']:
        args = [buildroot.wrap_host_file(arg) for arg in args]

    log.info("Signal handler active")
    commands = Commands(config_opts, uidManager, plugins, state, buildroot, bootstrap_buildroot)

    # TODO: The printrootpath and list_snapshots logic escape the finalization
    # like 'bootstrap.finalize()' or 'state.alldone()'.  Move it into
    # run_command().
    state.start("run")

    if options.printrootpath:
        print(buildroot.make_chroot_path(''))
        sys.exit(0)

    if options.list_snapshots:
        plugins.call_hooks('list_snapshots', required=True)
        if bootstrap_buildroot is not None:
            bootstrap_buildroot.plugins.call_hooks('list_snapshots', required=True)
        sys.exit(0)

    # dump configuration to log
    log.debug("mock final configuration:")
    for k, v in list(config_opts.items()):
        log.debug("    %s:  %s", k, v)

    os.umask(DEFAULT_UMASK)
    os.environ["HOME"] = buildroot.homedir

    # New namespace starting from here
    unshare_namespace(config_opts)

    if config_opts['hostname']:
        util.sethostname(config_opts['hostname'])

    # set personality (ie. setarch)
    util.condPersonality(config_opts['target_arch'])

    result = 0
    try:
        result = run_command(options, args, config_opts, commands, buildroot)
        # finish state.log if no exception was raised in run_command()
        state.finish("run")
        state.alldone()
    finally:
        buildroot.finalize()
        if bootstrap_buildroot is not None:
            bootstrap_buildroot.finalize()
    return result


@traceLog()
def run_command(options, args, config_opts, commands, buildroot):
    result = 0
    # TODO separate this
    # Fetch and prepare sources from SCM
    if config_opts['scm']:
        try:
            # pylint: disable=import-outside-toplevel
            from mockbuild import scm
        except ImportError as e:
            raise mockbuild.exception.BadCmdline(
                "Mock SCM module not installed: %s. You should install package mock-scm." % e)
        scmWorker = scm.scmWorker(log, config_opts, config_opts['macros'])
        with buildroot.uid_manager:
            scmWorker.get_sources()
            (options.sources, options.spec) = scmWorker.prepare_sources()

    if options.mode == 'init':
        if config_opts['clean']:
            commands.clean()
        commands.init()

    elif options.mode == 'clean':
        if len(options.scrub) == 0:
            commands.clean()
        else:
            commands.scrub(options.scrub)

    elif options.mode == 'chain':
        if len(args) == 0:
            log.critical("You must specify an SRPM file with --chain")
            return 50
        commands.init(do_log=True)
        result = commands.chain(args, options, buildroot)

    elif options.mode == 'shell':
        if len(args):
            cmd = args
        else:
            cmd = None
        commands.init(do_log=False)
        return commands.shell(options, cmd)

    elif options.mode == 'chroot':
        if len(args) == 0:
            log.critical("You must specify a command to run with --chroot")
            return 50
        commands.init(do_log=True)
        return commands.chroot(args, options)

    elif options.mode == 'installdeps':
        if len(args) == 0:
            log.critical("You must specify an SRPM file with --installdeps")
            return 50
        commands.init()
        rpms = []
        for file in args:
            if os.path.splitext(file)[1] == ".spec":
                commands.installSpecDeps(file)
            else:
                rpms.append(file)
        if rpms:
            util.checkSrpmHeaders(rpms, plainRpmOk=1)
            commands.installSrpmDeps(*rpms)

    elif options.mode == 'install':
        if len(args) == 0:
            log.critical("You must specify a package list to install.")
            return 50

        commands.init()
        commands.install_external(args)
        buildroot.install(*args)

    elif options.mode == 'update':
        commands.init()
        buildroot.pkg_manager.execute('update', *args)

    elif options.mode == 'remove':
        if len(args) == 0:
            log.critical("You must specify a package list to remove.")
            return 50
        commands.init()
        buildroot.remove(*args)

    elif options.mode == 'rebuild':
        if options.hermetic_build:
            # No caches with hermetic builds!  Bootstrap is extracted from
            # given tarball, buildroot installed from pre-fetched RPMs.
            commands.scrub(["all"])

        if config_opts['scm'] or (options.spec and options.sources):
            srpm = mockbuild.rebuild.do_buildsrpm(config_opts, commands, buildroot, options, args)
            if srpm:
                args.append(srpm)
            if config_opts['scm']:
                scmWorker.clean()
                options.spec = None
                options.sources = None
            else:
                config_opts['clean'] = False

        try:
            srpms = []
            for srpm_location in args:
                with buildroot.uid_manager:
                    srpm = FileDownloader.get(srpm_location)
                if not srpm:
                    raise mockbuild.exception.BadCmdline(
                        "Invalid {} source RPM".format(srpm_location))
                srpms.append(srpm)
            mockbuild.rebuild.do_rebuild(config_opts, commands, buildroot, options, srpms)
        finally:
            FileDownloader.cleanup()

    elif options.mode == 'buildsrpm':
        mockbuild.rebuild.do_buildsrpm(config_opts, commands, buildroot, options, args)

    elif options.mode == 'debugconfig':
        do_debugconfig(config_opts)

    elif options.mode == 'debugconfigexpand':
        do_debugconfig(config_opts, True)

    elif options.mode == 'listchroots':
        do_listchroots(config_opts["config_path"], buildroot.uid_manager)

    elif options.mode == 'orphanskill':
        util.orphansKill(buildroot.make_chroot_path())

    elif options.mode == 'copyin':
        commands.init()
        if len(args) < 2:
            log.critical("Must have source and destinations for copyin")
            return 50
        dest = buildroot.make_chroot_path(args[-1])
        if len(args) > 2 and not os.path.isdir(dest):
            log.critical("multiple source files and %s is not a directory!", dest)
            return 50
        args = args[:-1]
        for src in args:
            if not os.path.lexists(src):
                log.critical("No such file or directory: %s", src)
                return 50
            log.info("copying %s to %s", src, dest)
            if os.path.isdir(src):
                dest2 = dest
                if os.path.exists(dest2):
                    path_suffix = os.path.split(src)[1]
                    dest2 = os.path.join(dest2, path_suffix)
                    if os.path.exists(dest2):
                        log.critical("Destination %s already exists!", dest2)
                        return 50
                shutil.copytree(src, dest2)
            else:
                shutil.copy(src, dest)
        buildroot.chown_home_dir()

    elif options.mode == 'copyout':
        commands.init()
        with buildroot.uid_manager:
            if len(args) < 2:
                log.critical("Must have source and destinations for copyout")
                return 50
            dest = args[-1]
            sources = []
            for arg in args[:-1]:
                with util.env_var_override("HOME", buildroot.homedir):
                    arg = os.path.expanduser(arg)
                matches = glob.glob(buildroot.make_chroot_path(arg))
                if not matches:
                    log.critical("%s not found", arg)
                    return 50
                sources += matches
            if len(sources) > 1 and not os.path.isdir(dest):
                log.critical("multiple source files and %s is not a directory!", dest)
                return 50
            for src in sources:
                log.info("copying %s to %s", src, dest)
                if os.path.isdir(src):
                    shutil.copytree(src, dest, symlinks=True)
                else:
                    if os.path.islink(src):
                        linkto = os.readlink(src)
                        os.symlink(linkto, dest)
                    else:
                        shutil.copy(src, dest)

    elif options.mode in ('pm-cmd', 'yum-cmd', 'dnf-cmd'):
        log.info('Running %s %s', options.mode, ' '.join(args))
        commands.init()
        buildroot.pkg_manager.execute(*args)
    elif options.mode == 'snapshot':
        if len(args) < 1:
            log.critical("Requires a snapshot name")
            return 50
        buildroot.plugins.call_hooks('make_snapshot', args[0], required=True)
        if buildroot.bootstrap_buildroot is not None:
            buildroot.bootstrap_buildroot.plugins.call_hooks('make_snapshot', args[0], required=True)
    elif options.mode == 'rollback-to':
        if len(args) < 1:
            log.critical("Requires a snapshot name")
            return 50
        buildroot.plugins.call_hooks('rollback_to', args[0], required=True)
        if buildroot.bootstrap_buildroot is not None:
            buildroot.bootstrap_buildroot.plugins.call_hooks('rollback_to', args[0], required=True)
    elif options.mode == 'remove_snapshot':
        if len(args) < 1:
            log.critical("Requires a snapshot name")
            return 50
        buildroot.plugins.call_hooks('remove_snapshot', args[0], required=True)
        if buildroot.bootstrap_buildroot is not None:
            buildroot.bootstrap_buildroot.plugins.call_hooks('remove_snapshot', args[0], required=True)
    elif options.mode == 'umount':
        buildroot.plugins.call_hooks('umount_root')
        if buildroot.bootstrap_buildroot is not None:
            buildroot.bootstrap_buildroot.plugins.call_hooks('umount_root')
    elif options.mode == 'mount':
        buildroot.plugins.call_hooks('mount_root')
        if buildroot.bootstrap_buildroot is not None:
            buildroot.bootstrap_buildroot.plugins.call_hooks('mount_root')

    buildroot.nuke_rpm_db()
    return result


if __name__ == '__main__':
    # TODO: this was documented as "fix for python 2.4 logging module bug:"
    # TODO: ...but it is apparently still needed; without it there are various
    # TODO:    exceptions from trace_decorator like:
    # TODO:    TypeError: not enough arguments for format string
    logging.raiseExceptions = 0

    exitStatus = 0


    try:
        exitStatus = main()

    except (SystemExit,):
        raise # pylint: disable=try-except-raise

    except (OSError,) as e:
        if e.errno == errno.EPERM:
            print()
            log.error("%s", e)
            print()
            log.error("The most common cause for this error is trying to run "
                      "/usr/libexec/mock/mock as an unprivileged user.")
            log.error("You should not run /usr/libexec/mock/mock directly.")
            print()
            exitStatus = 2
        else:
            raise

    except (KeyboardInterrupt,):
        exitStatus = 7
        log.error("Exiting on user interrupt, <CTRL>-C")

    except (mockbuild.exception.ResultDirNotAccessible,) as exc:
        exitStatus = exc.resultcode
        log.error(str(exc))

    except (mockbuild.exception.BadCmdline, mockbuild.exception.BuildRootLocked) as exc:
        exitStatus = exc.resultcode
        log.error(str(exc))

    except (mockbuild.exception.Error) as exc:
        exitStatus = exc.resultcode
        log.error(str(exc))

    except (Exception,) as exc:  # pylint: disable=broad-except
        exitStatus = 1
        log.exception(exc)

    logging.shutdown()
    sys.exit(exitStatus)
