#!/usr/bin/env python

# Copyright (C) 2014 Red Hat, Inc.
#
# This file is part of csmock.
#
# csmock 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 3 of the License, or
# any later version.
#
# csmock 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with csmock.  If not, see <http://www.gnu.org/licenses/>.

import csmock.common.util

import argparse
import codecs
import copy
import datetime
import imp
import os.path
import re
import signal
import socket
import shutil
import subprocess
import sys
import tempfile
import time

csmock_datadir = "/usr/share/csmock"

patch_rawbuild = csmock_datadir + "/scripts/patch-rawbuild.sh"

cwe_list_file = csmock_datadir + "/cwe-map.csv"

plugin_dir= "/usr/lib/python2.7/site-packages/csmock/plugins"

default_cswrap_timeout = 30

default_jobs_cnt = 13

default_rpm_opts = [
        "--define",     "_without_testsuite 1",
        "--define",     "apidocs 0",
        "--define",     "check %%check\\\nexit 0",
        "--define",     "libguestfs_runtests 0",
        "--define",     "runselftest 0",
        "--define",     "with_docs 0",
        "--define",     "with_publican 0",
        "--without",    "binfilter",
        "--without",    "langpacks"]

rawbuild_rpm_opts = [
        "--define",     "__patch " + patch_rawbuild,
        "--define",     "_rawbuild -b _RAWBUILD",
        "--define",     "nofips 1",
        "--define",     "nopam 1",
        "--define",     "norunuser 1",
        "--define",     "noselinux 1",
        "--define",     "_with_vanilla 1"]

default_cswrap_filters = [
        "csgrep --quiet --path '^/builddir/build/BUILD/' \
--event='error|warning' --remove-duplicates"]

# remember to use --mode=json for csgrep (TODO: improve csgrep's interface)
default_result_filters = [
        "csgrep --mode=json --path '^/builddir/build/BUILD/' \
--strip-path-prefix /builddir/build/BUILD/",
        "csgrep --mode=json --invert-match --path '^ksh-.*[0-9]+\.c$'",
        "csgrep --mode=json --invert-match --path 'CMakeFiles/CMakeTmp|conftest.c'"]

csgrep_final_filter_args = "--invert-match --event \"internal warning\" \
--prune-events=1"

def current_iso_date():
    now = datetime.datetime.now()
    return "%04u-%02u-%02u %02u:%02u:%02u" \
            % (now.year, now.month, now.day, now.hour, now.minute, now.second)

def shell_quote(str_in):
    str_out = ""
    for i in range(0, len(str_in)):
        c = str_in[i]
        if c == "\\":
            str_out += "\\\\"
        elif c == "\"":
            str_out += "\\\""
        else:
            str_out += c
    return "\"" + str_out + "\""

def strlist_to_shell_cmd(cmd_in, escape_special=False):
    def translate_one(i):
        if escape_special:
            return shell_quote(i)
        else:
            return "'%s'" % i

    if type(cmd_in) is str:
        return "sh -c %s" % translate_one(cmd_in)
    cmd_out = ""
    for i in cmd_in:
        cmd_out += " " + translate_one(i)
    return cmd_out.lstrip()

def find_missing_pkgs(pkgs, results, mock):
    # dump list of RPMs installed in the chroot (for debugging purposes)
    tmp_var_lib = "%s/var/lib" % results.tmpdir
    if os.path.isdir(tmp_var_lib):
        shutil.rmtree(tmp_var_lib)
    os.makedirs(tmp_var_lib)
    mock.copy_out(["/var/lib/rpm", "%s/rpm" % tmp_var_lib])
    tmp_rpm = "%s/rpm" % tmp_var_lib
    installed = "%s/rpm-list-mock.txt" % results.dbgdir
    provides = "%s/rpm-list-mock-provides.txt" % results.tmpdir
    results.exec_cmd("chmod -w '%s' && rpm -qa --root '%s' | sort -V \
> '%s' && rpm -qa --provides --root '%s' > '%s' && chmod u+w '%s'" % (tmp_rpm,
        results.tmpdir, installed, results.tmpdir, provides, tmp_rpm),
        shell=True)

    missing = []
    installed = set()
    with open(provides) as f:
        lines = f.readlines()
        for l in lines:
            pkg = re.sub(" .*$", "", l.strip())
            installed.add(pkg)
    for dep in pkgs:
        if re.match("^.*(/|rpmlib\\(.*\\))", dep) is not None:
            # FIXME: we do not check this kind of dependencies
            continue
        pkg = re.sub(" .*$", "", dep)
        if pkg not in installed:
            missing += [pkg]
    return missing

class ScanResults:
    def __init__(self, output, keep_going=False):
        self.output = output
        self.keep_going = keep_going
        self.use_xz = False
        self.use_tar = False
        self.dirname = os.path.basename(output)
        self.codec = codecs.lookup('utf8')
        self.ec = 0

        m = re.match("^(.*)\\.xz$", self.dirname)
        if m is not None:
            self.use_xz = True
            self.dirname = m.group(1)

        m = re.match("^(.*)\\.tar$", self.dirname)
        if m is not None:
            self.use_tar = True
            self.dirname = m.group(1)

    def utf8_wrap(self, fd):
        # the following hack is needed to support both Python 2 and 3
        return codecs.StreamReaderWriter(fd,
                self.codec.streamreader,
                self.codec.streamwriter)

    def __enter__(self):
        self.tmpdir = tempfile.mkdtemp(prefix="csmock")
        if self.use_tar:
            self.resdir = "%s/%s" % (self.tmpdir, self.dirname)
        else:
            if os.path.exists(self.output):
                shutil.rmtree(self.output)
            self.resdir = self.output

        try:
            os.mkdir(self.resdir)
        except OSError as e:
            sys.stderr.write(
                    "error: failed to create output directory: %s\n" % e)
            sys.exit(1)

        self.dbgdir = "%s/debug" % self.resdir
        os.mkdir(self.dbgdir)

        self.dbgdir_raw = "%s/raw-results" % self.dbgdir
        os.mkdir(self.dbgdir_raw)

        self.dbgdir_uni = "%s/uni-results" % self.dbgdir
        os.mkdir(self.dbgdir_uni)

        tee = ["tee", "%s/scan.log" % self.resdir]
        self.log_pid = subprocess.Popen(tee, stdin=subprocess.PIPE,
                preexec_fn=os.setsid)
        self.log_fd = self.utf8_wrap(self.log_pid.stdin)

        def signal_handler(signal, frame):
            # FIXME: we should use Async-signal-safe functions only
            self.fatal_error("caught signal %d" % signal, ec=(0x80 + signal))
        for i in [signal.SIGINT, signal.SIGTERM]:
            signal.signal(i, signal_handler)

        self.ini_writer = IniWriter(self)
        return self

    def __exit__(self, type, value, bt):
        self.ini_writer.close()
        if hasattr(self, "p") and self.p.returncode is None:
            # FIXME: TOCTOU race
            try:
                os.kill(self.p.pid, signal.SIGTERM)
                self.p.wait()
            except Exception:
                pass
        self.print_with_ts("csmock exit code: %d\n" % self.ec, prefix="<<< ")
        self.log_fd.close()
        self.log_fd = sys.stderr
        self.log_pid.wait()
        if self.use_tar:
            tar_opts = "-c"
            if self.use_xz:
                tar_opts += "J"
            tar_cmd = "tar %s -f '%s' -C '%s' '%s'" % (tar_opts,
                    self.output, self.tmpdir, self.dirname)
            if os.system(tar_cmd) != 0:
                self.fatal_error("failed to write '%s', not removing '%s'..."
                        % (self.output, self.tmpdir))

        sys.stderr.write("Wrote: %s\n\n" % self.output)
        shutil.rmtree(self.tmpdir)

    def print_with_ts(self, msg, prefix=">>> "):
        self.log_fd.write("%s%s\t%s\n" % (prefix, current_iso_date(), msg))
        self.log_fd.flush()

    def error(self, msg, ec=1, err_prefix=""):
        self.print_with_ts("%serror: %s\n" % (err_prefix, msg), prefix="!!! ")
        if (self.ec < ec):
            self.ec = ec
        if not self.keep_going and (0 != self.ec):
            sys.exit(ec)

    def fatal_error(self, msg, ec=1):
        self.error(msg, err_prefix="fatal ", ec=ec)
        sys.exit(ec)

    def exec_cmd(self, cmd, shell=False, emul_pty=False):
        if emul_pty:
            # workaround for bug https://bugzilla.redhat.com/1166609
            sh_cmd = strlist_to_shell_cmd(cmd, escape_special=True)
            cmd = ["script", "-aefqc", sh_cmd, "/dev/null"]

        self.print_with_ts(strlist_to_shell_cmd(cmd, escape_special=True))
        self.p = subprocess.Popen(cmd, stdout=self.log_fd, stderr=self.log_fd,
                shell=shell)
        rv = self.p.wait()
        self.log_fd.write("\n")
        if 128 <= rv:
            # if the child has been signalled, signal self with the same signal
            os.kill(os.getpid(), rv - 128)
        return rv

    def get_cmd_output(self, cmd, input=None, shell=True):
        self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                stderr=self.log_fd, shell=shell)
        (out, _) = self.p.communicate()
        out = out.decode("utf8")
        return (self.p.returncode, out)

    def open_res_file(self, rel_path):
        abs_path = "%s/%s" % (self.resdir, rel_path)
        return open(abs_path, "w")

class IniWriter:
    def __init__(self, results):
        self.results = results
        self.ini = self.results.open_res_file("scan.ini")
        self.write("[scan]\n")
        self.append("tool", "csmock")
        self.append("tool-version", "csmock-1.8.3-1.fc21")
        self.append("tool-args", strlist_to_shell_cmd(sys.argv))
        self.append("host", socket.gethostname())
        self.append("store-results-to", self.results.output)
        self.append("time-created", current_iso_date())

    def close(self):
        if self.ini is None:
            return
        self.append("time-finished", current_iso_date())
        self.append("exit-code", self.results.ec)
        self.ini.close()
        self.ini = None

    def write(self, text):
        self.ini.write(text)
        self.results.log_fd.write("scan.ini: " + text)

    def append(self, key, value):
        self.write("%s = %s\n" % (key, value))

class MockWrapper:
    def __init__(self, results, mock_profile):
        self.results = results
        self.mock_profile = mock_profile
        self.lock_file = "/tmp/.csmock-%s.lock" % mock_profile
        self.meta_lock_file = "/tmp/.csmock-%s.metalock" % mock_profile
        self.pid = os.getpid()
        self.init_done = False
        self.emul_pty = False

    def __enter__(self):
        cmd = "flock -w15 '%s' -c \"test ! -f '%s' && echo %d > '%s'\"" % (
                self.meta_lock_file, self.lock_file, self.pid, self.lock_file)
        while (os.system(cmd) != 0):
            f = open(self.lock_file)
            other_pid = ""
            if f is not None:
                other_pid = f.readline().rstrip()
                f.close()
            msg = "waiting till %s (PID %s) disappears..."
            self.results.print_with_ts(msg % (self.lock_file, other_pid))
            time.sleep(15)

        # prepare the mock command template with default arguments
        if os.path.exists("/usr/bin/mock-unbuffered"):
            # mock wrapper writing debug output without buffering
            mock = "/usr/bin/mock-unbuffered"
        elif os.path.exists("/usr/bin/mock"):
            # mock wrapper for non-privileged users (members of group mock)
            mock = "/usr/bin/mock"
        else:
            # fallback to any mock in $PATH (e.g. /usr/local/bin/mock)
            mock = "mock"
        self.def_cmd = [mock, "-r", self.mock_profile]

        # make csmock work in case the 'tmpfs' plug-in is enabled
        # (see <https://bugzilla.redhat.com/1190100> for details)
        self.def_cmd += ["--plugin-option=tmpfs:keep_mounted=True"]

        if (self.results.get_cmd_output("mock --help | grep package_state")[0] == 0):
            self.def_cmd += ["--disable-plugin=package_state"]

        if (self.results.get_cmd_output("mock --version | grep ^1.2.")[0] == 0) \
            and (self.results.get_cmd_output(
                "script -aec true /dev/null 2>/dev/null")[0] == 0):
            # workaround for bug https://bugzilla.redhat.com/1166609
            self.emul_pty = True

        return self

    def __exit__(self, type, value, bt):
        cmd = "test -r '%s' && test %d = \"$(<%s)\" && rm -f '%s'" % (
                self.lock_file, self.pid, self.lock_file, self.lock_file)
        os.system(cmd)

    def get_mock_cmd(self, args):
        return self.def_cmd + args

    def exec_mock_cmd(self, args):
        cmd = self.get_mock_cmd(args)
        return self.results.exec_cmd(cmd, emul_pty=self.emul_pty)

    def exec_chroot_cmd(self, cmd):
        return self.exec_mock_cmd(["--chroot", cmd])

    def exec_mockbuild_cmd(self, cmd):
        return self.exec_chroot_cmd("/bin/su mockbuild -lc %s"
                % shell_quote(cmd))

    def copy_out(self, args):
        cmd = ["--disable-plugin=selinux", "--copyout"] + args
        return self.exec_mock_cmd(cmd)

    def try_install(self, pkg):
        return self.exec_mock_cmd(["--install", pkg])

    def init_and_install(self, pkgs):
        for scrub_root in [False, True]:
            if scrub_root:
                self.exec_mock_cmd(["--scrub=root-cache"])
                if self.exec_mock_cmd(["--init"]) != 0:
                    self.results.fatal_error("failed to init mock profile: %s"
                            % self.mock_profile)

            elif not self.init_done and self.exec_mock_cmd(["--init"]) != 0:
                # try --scrub=root-cache
                continue

            self.init_done = True
            if len(pkgs) == 0:
                return True

            # install required packages (all at once)
            self.exec_mock_cmd(["--install"] + pkgs)
            missing_deps = find_missing_pkgs(pkgs, self.results, self)
            if len(missing_deps) == 0:
                # no misssing dependencies
                return True

            self.results.error(
                    "hard to install dependencies (%s), still trying..." % \
                    strlist_to_shell_cmd(missing_deps), ec=0)

            if not scrub_root:
                # try --scrub=root-cache
                continue

            # try to install the packages one by one
            for pkg in pkgs:
                self.try_install(pkg)

            # check that all dependencies are installed
            missing_deps = find_missing_pkgs(pkgs, self.results, self)
            if len(missing_deps) == 0:
                # no missing dependencies
                return True

            self.results.error("failed to install dependencies: %s" % \
                    strlist_to_shell_cmd(missing_deps))
            return False

class ScanProps:
    def __init__(self):
        self.install_pkgs = []
        self.install_opt_pkgs = []
        self.copy_in_files = []
        self.pre_mock_hooks = []
        self.post_depinst_hooks = []
        self.rpm_opts = default_rpm_opts
        self.path = []
        self.env = { }
        self.copy_out_files = []
        self.cswrap_enabled = False
        self.cswrap_filters = default_cswrap_filters
        self.result_filters = default_result_filters
        self.build_cmd_wrappers = []
        self.post_build_chroot_cmds = []
        self.post_process_hooks = []
        self.keep_going = False
        self.cswrap_timeout = default_cswrap_timeout
        self.no_scan = False
        self.need_rpm_bi = False
        self.shell_cmd_to_build = None

    def enable_cswrap(self):
        if self.cswrap_enabled:
            # already enabled
            return
        self.cswrap_enabled = True

        # resolve cswrap_path by querying cswrap binary
        cmd = ["cswrap", "--print-path-to-wrap"]
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (out, err) = p.communicate()
        cswrap_path = out.strip()

        self.copy_in_files += ["/usr/bin/cswrap", cswrap_path]
        self.path = [cswrap_path] + self.path
        self.env["CSWRAP_CAP_FILE"] = "/builddir/cswrap-capture.err"
        self.env["CSWRAP_TIMEOUT"] = "%d" % self.cswrap_timeout
        self.env["CSWRAP_TIMEOUT_FOR"] = ":"
        self.copy_out_files += ["/builddir/cswrap-capture.err"]

    def pick_cswrap_results(self, results):
        if not self.cswrap_enabled:
            # not enabled --> succeeded trivially
            return 0

        # apply all filters using a shell pipe
        fin = "%s/builddir/cswrap-capture.err" % results.dbgdir_raw
        out = "%s/cswrap-capture.err" % results.dbgdir_uni
        cmd = "cat '%s'" % fin
        for filt in self.cswrap_filters:
            cmd += " | %s" % filt
        cmd += " > '%s'" % out
        (rv, _) = results.get_cmd_output(cmd)
        return rv

    def wrap_build_cmd(self, cmd_in):
        cmd_out = cmd_in
        for w in self.build_cmd_wrappers:
            cmd_out = "sh -c %s" % shell_quote(cmd_out)
            cmd_out = w % cmd_out
        return cmd_out

    def wrap_shell_cmd_by_env(self, cmd_in):
        # merge self.env with self.path
        merged_env = copy.deepcopy(self.env)
        path_str = ""
        for p in self.path:
            path_str += p + ":"
        path_str += "$PATH"
        assert "PATH" not in merged_env
        merged_env["PATH"] = path_str

        # serialize all environment variables
        cmd_out = ""
        for var in merged_env:
            cmd_out += "%s='%s' " % (var, merged_env[var])

        # run a new instance of shell for the specified command
        cmd_out += "sh -c %s" % shell_quote(cmd_in)
        return cmd_out

class PluginManager:
    def __init__(self):
        self.plug_by_prio = { }
        self.plug_by_name = { }

    def try_load(self, modname, path):
        fp, pathname, description = imp.find_module(modname, [path])
        try:
            module = imp.load_module(modname, fp, pathname, description)
            plugin = module.Plugin()
        finally:
            fp.close()

        props = plugin.get_props()
        # TODO: check API version
        prio = props.pass_priority
        assert prio not in self.plug_by_prio
        self.plug_by_prio[prio] = plugin
        self.plug_by_name[modname] = plugin

    def load_default_plugins(self):
        try:
            files = os.listdir(plugin_dir)
        except:
            return

        for fname in files:
            parts = fname.split(".")
            if len(parts) != 2:
                continue
            if parts[1] != "py":
                continue
            self.try_load(parts[0], path=plugin_dir)

    def get_name_list(self):
        return sorted(self.plug_by_name.keys())

    def enable(self, plugin_name):
        plugin = self.plug_by_name[plugin_name]
        plugin.enable()

    def enable_all(self):
        for prio in sorted(self.plug_by_prio):
            plugin = self.plug_by_prio[prio]
            plugin.enable()

    def init_parser(self, parser):
        for prio in sorted(self.plug_by_prio):
            plugin = self.plug_by_prio[prio]
            plugin.init_parser(parser)

    def handle_args(self, parser, args, props):
        for prio in sorted(self.plug_by_prio):
            plugin = self.plug_by_prio[prio]
            plugin.handle_args(parser, args, props)

    def num_enabled(self):
        cnt = 0
        for prio in sorted(self.plug_by_prio):
            plugin = self.plug_by_prio[prio]
            if getattr(plugin, "enabled", False):
                cnt = cnt + 1
        return cnt

def deplist_from_srpm(results, srpm):
    (_, deps) = results.get_cmd_output("rpm -qp '%s' --requires" % srpm)
    raw_deps = filter(None, deps.split("\n"))
    deps = []
    for d in raw_deps:
        deps += [d.strip()]
    return deps

def transform_results(js_file, results):
    err_file  = re.sub("\.js", ".err",  js_file)
    html_file = re.sub("\.js", ".html", js_file)
    results.exec_cmd("csgrep --mode=grep %s '%s' > '%s'" \
            % (csgrep_final_filter_args, js_file,  err_file), shell=True)
    results.exec_cmd("csgrep --mode=json %s '%s' | cshtml - > '%s'" \
            % (csgrep_final_filter_args, js_file, html_file), shell=True)
    return (err_file, html_file)

# transform scan-results.js to scan-results.{err,html} and write stats
def finalize_results(js_file, results, print_defects):
    (err_file, _) = transform_results(js_file, results)
    summary_file = "%s/scan-results-summary.txt" % results.resdir
    cmd = "csgrep --mode=stat %s '%s' | tee '%s'" \
            % (csgrep_final_filter_args, err_file, summary_file)
    ec = results.exec_cmd(cmd, shell=True)
    if print_defects:
        os.system("csgrep '%s'" % err_file)
    return ec

# argparse._VersionAction would write to stderr, which breaks help2man
class VersionPrinter(argparse.Action):
    def __init__(self, option_strings, dest=None, default=None, help=None):
        super(VersionPrinter, self).__init__(option_strings=option_strings,
                dest=dest, default=default, nargs=0, help=help)
    def __call__(self, parser, namespace, values, option_string=None):
        print("csmock-1.8.3-1.fc21")
        sys.exit(0)

def require_file(parser, name):
    """Print an error and exit unsuccessfully if 'name' is not a file"""
    if not os.path.isfile(name):
        parser.error("'%s' is not a file" % name)

# load plug-ins
plugins = PluginManager()
plugins.load_default_plugins()
plugin_list = plugins.get_name_list()

# list available tools
# FIXME: --list-available-tools takes precedence over --help and --version
class ToolsPrinter(argparse.Action):
    def __init__(self, option_strings, dest=None, default=None, help=None):
        super(ToolsPrinter, self).__init__(option_strings=option_strings,
                dest=dest, default=default, nargs=0, help=help)
    def __call__(self, parser, namespace, values, option_string=None):
        for i in plugin_list:
            # TODO: print description?
            sys.stdout.write("%s\n" % i)
        sys.exit(0)

# initialize argument parser
parser = argparse.ArgumentParser()
parser.add_argument("SRPM", nargs="?",
        help="source RPM package to be scanned by static analyzers")

# define optional arguments
parser.add_argument("-r", "--root", dest="mock_profile", default="default",
        help="mock profile to use (defaults to mock's default)")

parser.add_argument("-t", "--tools", action="append", default=[],
        help="comma-spearated list of tools to enable \
(use --list-available-tools to see the list of available tools)")

parser.add_argument("-a", "--all-tools", action="store_true",
        help="enable all available tools \
(use --list-available-tools to see the list of available tools)")

parser.add_argument("-l", "--list-available-tools", action=ToolsPrinter,
        help="list available tools and exit")

parser.add_argument("--install", dest="list_of_pkgs",
        help="space-separated list of packages to install into the chroot")

parser.add_argument("-o", "--output",
        help="name of the tarball or directory to put the results to")

parser.add_argument("-f", "--force", action="store_true",
        help="overwrite the resulting file or directory if it exists already")

parser.add_argument("-j", "--jobs", type=int, default=default_jobs_cnt,
        help="maximal number of jobs running in parallel (passed to 'make')")

parser.add_argument("--cswrap-timeout", type=int, default=default_cswrap_timeout,
        help="maximal amount of time taken by analysis of a single module [s]")

parser.add_argument("-U", "--embed-context", type=int, default=3,
        help="embed a number of lines of context from the source file for the \
key event (defaults to 3).")

parser.add_argument("-k", "--keep-going", action="store_true",
        help="continue as much as possible after an error")

parser.add_argument("--no-clean", action="store_true",
        help="do not clean chroot when it becomes unused")

parser.add_argument("--no-scan", action="store_true",
        help="do not analyze any package, just check versions of the analyzers")

csmock.common.util.add_paired_flag(parser, "print-defects",
        help="print the resulting list of defects (default if connected to a tty)")

# --skip-patches, --diff-patches, and --shell-cmd are mutually exclusive
group = parser.add_mutually_exclusive_group()
group.add_argument("--skip-patches", action="store_true",
        help="skip patches not annotated by %%{?_rawbuild} (vanilla build)")
group.add_argument("--diff-patches", action="store_true",
        help="scan with/without patches and diff the lists of defects")
group.add_argument("-c", "--shell-cmd",
        help="use shell command to build the given tarball (instead of SRPM)")

# needed for help2man
parser.add_argument("--version", action=VersionPrinter,
        help="print the version of csmock and exit")

# add command-line options handled by plugins
plugins.init_parser(parser)

# parse command-line arguments
args = parser.parse_args()

if args.print_defects is None:
    args.print_defects = sys.stdout.isatty()

# check that only available tools are requested (and enable them)
for i in args.tools:
    for j in i.split(","):
        tool = j.strip()
        if len(tool) == 0:
            continue
        if tool in plugin_list:
            # explicitly enable this tool
            plugins.enable(tool)
        else:
            parser.error("tool not available: %s" % tool)

if args.all_tools:
    # enable all available tools
    plugins.enable_all()

srpm = args.SRPM
output = args.output
if srpm is None:
    if args.no_scan:
        if output is None:
            parser.error("unable to infer --output (despite --no-scan was given)")
    else:
        parser.error("no SRPM (or tarball) specified on the command line")

if args.no_scan and args.shell_cmd is not None:
    parser.error("--shell-cmd makes no sense with --no-scan")

props = ScanProps()
props.cswrap_timeout        = args.cswrap_timeout
props.keep_going            = args.keep_going
props.no_scan               = args.no_scan
props.print_defects         = args.print_defects
props.shell_cmd_to_build    = args.shell_cmd

if 0 < args.embed_context:
    # we need 'csgrep --embed-context' to work in the chroot for --embed-context
    props.install_opt_pkgs += ["csdiff >= 1.2.1"]

# initialize the %{_smp_mflags} RPM macro
mflags_rpmm = "_smp_mflags"
mflags_rpmm += " -j%d" % args.jobs
if props.keep_going:
    mflags_rpmm += " -k"
props.rpm_opts += ["--define", mflags_rpmm]

# make sure that we have a configuration for the selected mock profile
mock_profile = args.mock_profile
require_file(parser, "/etc/mock/%s.cfg" % mock_profile)

if args.list_of_pkgs is not None:
    # append the list of packages to install specified on command-line
    props.install_pkgs += args.list_of_pkgs.split()

if not props.no_scan:
    # make sure that 'srpm' is a file (it can be a tar archive instead of SRPM)
    require_file(parser, srpm)

if srpm is not None:
    # resolve NVR
    srpm_base = os.path.basename(srpm)
    if props.shell_cmd_to_build is None:
        nvr = re.sub("\.src\.rpm$", "", srpm_base)
    else:
        nvr = re.sub("\.tar$", "", re.sub("\.[^.]*$", "", srpm_base))

# resolve name of the file/dir we are going to store the results to
if args.output is None:
    output = nvr + ".tar.xz"
output = os.path.realpath(output)

# FIXME: TOCTOU race
if os.path.exists(output) and not args.force:
    parser.error("'%s' already exists, use --force to proceed" % output)

# poll plug-ins to reflect themselves in ScanProps
plugins.handle_args(parser, args, props)
any_tool = (0 < plugins.num_enabled())

def do_scan(props, output, skip_patches):
    if skip_patches:
        props.copy_in_files += [patch_rawbuild]
        props.rpm_opts += rawbuild_rpm_opts

    with ScanResults(output, props.keep_going) as results:
        results.ini_writer.append("mock-config", mock_profile)

        if not any_tool:
            # no tool enabled
            results.error("No tools are enabled, only trying to build \
the package.  Use --tools or --all-tools to enable them!\n", ec=0)

        # dump list of RPMs installed on the host (for debugging purposes)
        results.exec_cmd("rpm -qa | sort -V > '%s/rpm-list-host.txt'"
                % results.dbgdir, shell=True)

        if not props.no_scan:
            if props.shell_cmd_to_build is None:
                # check the given SRPM
                if (results.get_cmd_output("rpm -pq '%s'" % srpm)[0] != 0):
                    results.fatal_error("failed to open SRPM: %s" % srpm)
                (ec, spec) = results.get_cmd_output("rpm -lpq '%s' | grep '\.spec$'"
                        % srpm)
                if ec != 0:
                    results.fatal_error("no specfile found in SRPM: %s" % srpm)
                spec = spec.rstrip()
                spec_in = "/builddir/build/SPECS/%s" % spec

            # copy the given SRPM into our tmp dir
            srpm_dup = "%s/%s" % (results.tmpdir, srpm_base)
            shutil.copyfile(srpm, srpm_dup)
            props.copy_in_files += [srpm_dup]

        # run pre-mock hooks
        for hook in props.pre_mock_hooks:
            rv = hook(results)
            if rv != 0:
                results.error("pre-mock hook failed", ec=rv)

        with MockWrapper(results, mock_profile) as mock:
            if not props.no_scan and props.shell_cmd_to_build is None:
                # first rebuild the given SRPM
                mock.init_and_install(["python"])

                # install the copied SRPM into the chroot
                srpm_in = "/builddir/%s" % srpm_base
                mock.exec_mock_cmd(["--copyin", srpm_dup, srpm_in])
                mock.exec_chroot_cmd("rpm -Uvh --nodeps '%s'" % srpm_in)

                # rebuild the given SRPM (and rename to match the original one)
                mock.exec_mockbuild_cmd("rpmbuild -bs --nodeps %s %s && sh -c 'cd \
/builddir/build/SRPMS && eval mv -v *.src.rpm %s || :'"
                        % (spec_in, strlist_to_shell_cmd(props.rpm_opts), srpm_base))

                # use the rebuilt SRPM to get the dependency list
                mock.copy_out(["/builddir/build/SRPMS/%s" % srpm_base, srpm_dup])
                props.install_pkgs += deplist_from_srpm(results, srpm_dup)

            # run 'mock --init' and 'mock --install'
            mock.init_and_install(props.install_pkgs)

            # install optional packages (if any)
            if 0 < len(props.install_opt_pkgs):
                mock.exec_mock_cmd(["--install"] + props.install_opt_pkgs)
                # just to update rpm-list-mock.txt
                find_missing_pkgs([], results, mock)

            if props.shell_cmd_to_build is not None:
                # prepare a build script in our tmp dir
                build_script = "%s/build.sh" % results.tmpdir
                results.exec_cmd("printf '#!/bin/sh\n\
cd /builddir/build/BUILD || exit $?\n\
cd %%s*/\n\
%%s' '%s' '%s' | tee '%s' >&2\n" % (nvr,
                props.shell_cmd_to_build, build_script), shell=True)
                props.copy_in_files += [build_script]

            # copy required files into the chroot
            cmd = "tar -cP "
            cmd += strlist_to_shell_cmd(props.copy_in_files)
            cmd += " | "
            cmd += strlist_to_shell_cmd(
                    mock.get_mock_cmd(["--shell", "tar -xC/"]))
            results.exec_cmd(cmd, shell=True)

            # run post-depinst hooks
            for hook in props.post_depinst_hooks:
                rv = hook(results, mock)
                if rv != 0:
                    results.error("post-depinst hook failed", ec=rv)

            if not props.no_scan:
                if props.shell_cmd_to_build is None:
                    # install the copied SRPM into the chroot
                    mock.exec_chroot_cmd("rpm -Uvh --nodeps '%s'" % srpm_dup)
                    # make the installed SRPM accessible (if the maintainer did not)
                    mock.exec_chroot_cmd("chmod -R +r /builddir")

                # run fixups scripts
                mock.exec_chroot_cmd("find %s/scripts -name 'fixups-*.sh' \
| xargs -n1 sh -x" % csmock_datadir)

                if props.shell_cmd_to_build is None:
                    # run %prep phase without pluggin-in any static analyzers
                    ec = mock.exec_mockbuild_cmd("rpmbuild -bp --nodeps %s %s"
                            % (spec_in, strlist_to_shell_cmd(props.rpm_opts)))
                else:
                    # extract the given archive (we got instead of SRPM)
                    ec = mock.exec_mockbuild_cmd(
                            "tar -xvf '%s' -C /builddir/build/BUILD" % srpm_dup)

                # make the unpacked contents accessible (if the maintainer did not)
                mock.exec_chroot_cmd("chmod -R +r /builddir/build")

                if (ec != 0):
                    results.error("%prep failed", ec=ec)

                if props.shell_cmd_to_build is None:
                    # run %build phase with static analyzers plugged-in
                    build_cmd = "rpmbuild -bc --nodeps --short-circuit %s %s" \
                            % (spec_in, strlist_to_shell_cmd(props.rpm_opts))
                else:
                    # run the above prepared build script
                    build_cmd = "sh -x '%s'" % build_script

                # wrap build_cmd by all the necessary wrappers
                build_cmd = props.wrap_build_cmd(build_cmd)

                # initialize environment variables according to ScanProps
                build_cmd = props.wrap_shell_cmd_by_env(build_cmd)

                ec = mock.exec_mockbuild_cmd(build_cmd)
                if (ec != 0):
                    results.error("%build failed", ec=ec)

                if props.need_rpm_bi:
                    if props.shell_cmd_to_build is not None:
                        results.fatal_error("SRPM is required by a plug-in")
                    cmd = "rpmbuild -bi --nodeps --short-circuit %s %s" \
                        % (spec_in, strlist_to_shell_cmd(props.rpm_opts))

                    # initialize environment variables according to ScanProps
                    cmd = props.wrap_shell_cmd_by_env(cmd)

                    ec = mock.exec_mockbuild_cmd(cmd)
                    if (ec != 0):
                        results.error("%install failed", ec=ec)
                    props.result_filters = ["sed 's|/builddir/build/BUILDROOT/[^/]*/|/builddir/build/BUILD//|'"] \
                            + props.result_filters

                # execute post-build commands in the chroot
                for cmd in props.post_build_chroot_cmds:
                    mock.exec_chroot_cmd(cmd)

                # get the (intermediate) results out of the chroot
                if len(props.copy_out_files) > 0:
                    cmd = strlist_to_shell_cmd(mock.get_mock_cmd(["--shell",
                        "tar -c " + strlist_to_shell_cmd(props.copy_out_files)]))

                    cmd += " | tar -xC '%s'" % results.dbgdir_raw
                    if results.exec_cmd(cmd, shell=True) != 0:
                        results.error("field to get intermediate results from mock")

            if not props.no_scan:
                if props.pick_cswrap_results(results) != 0:
                    results.error("failed to pick cswrap results")

                # run post-process hooks
                for hook in props.post_process_hooks:
                    rv = hook(results)
                    if rv != 0:
                        results.error("post-process hook failed", ec=rv)

            # we are done with IniWriter
            results.ini_writer.close()

            # merge all results into a single file named scan-results-all.js
            ini_file = "%s/scan.ini" % results.resdir
            js_file = "%s/scan-results.js" % results.resdir
            all_file = "%s/scan-results-all.js" % results.dbgdir
            cmd = "cslinker --quiet --cwelist '%s' --inifile '%s' $(ls %s/*.err) \
> '%s'" % (cwe_list_file, ini_file, results.dbgdir_uni, all_file)
            results.exec_cmd(cmd, shell=True)

            if 0 < args.embed_context:
                # embed context lines from source program files
                tmp_file = "%s.tmp" % all_file
                cmd = strlist_to_shell_cmd( mock.get_mock_cmd(["--shell",
                    "csgrep --mode=json --embed-context %d" % args.embed_context]))
                cmd += " < '%s' > '%s'" % (all_file, tmp_file)
                if (0 == results.exec_cmd(cmd, shell=True)):
                    shutil.move(tmp_file, all_file)

            if not args.no_clean:
                # clean up thhe mock root
                if (mock.exec_mock_cmd(["--clean"]) != 0):
                    results.error("failed to clean mock profile", ec=0)

        # we are done with mock

        # apply filters, sort the list and store the result as scan-results.js
        cmd = "cat '%s'" % all_file
        for filt in props.result_filters:
            cmd += " | %s" % filt
        cmd += " | cssort --key=path > '%s'" % js_file
        results.exec_cmd(cmd, shell=True)

        finalize_results(js_file, results, props.print_defects)
        return results.ec

def do_diff_scan(props, output):
    with ScanResults(output, props.keep_going) as results:
        run0 = "%s/run0" % results.resdir
        ec = do_scan(copy.deepcopy(props), run0, skip_patches=True)
        if 0 != ec:
            results.error("vanilla scan failed", ec=ec)

        run1 = "%s/run1" % results.resdir
        ec = do_scan(props, run1, skip_patches=False)
        if 0 != ec:
            results.error("regular scan failed", ec=ec)

        # finalize scan.ini
        results.ini_writer.append("title", "%s - Defects in Patches" % nvr)
        results.ini_writer.close()
        ini_file = "%s/scan.ini" % results.resdir

        # diff both runs and serialize the result using the JSON format
        run0_file = "%s/scan-results.js" % run0
        run1_file = "%s/scan-results.js" % run1
        js_file = "%s/scan-results.js" % results.resdir
        if 0 != results.exec_cmd("csdiff %s %s | cslinker --inifile %s - > %s" \
                % (run0_file, run1_file, ini_file, js_file), shell=True):
            results.error("csdiff failed")

        finalize_results(js_file, results, props.print_defects)
        return results.ec

if args.diff_patches:
    ec = do_diff_scan(props, output)
else:
    ec = do_scan(props, output, args.skip_patches)

sys.exit(ec)
