#!/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 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",     "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'"]

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):
        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")
        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.5.1-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

    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]
        if (self.results.get_cmd_output("mock --help | grep package_state")[0] == 0):
            self.def_cmd += ["--disable-plugin=package_state"]

        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):
        return self.results.exec_cmd(self.get_mock_cmd(args))

    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.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 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 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 --prune-events=1 '%s' > '%s'" \
            % (js_file,  err_file), shell=True)
    results.exec_cmd("csgrep --mode=json --prune-events=1 '%s' \
| cshtml - > '%s'" % (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):
    (err_file, _) = transform_results(js_file, results)
    summary_file = "%s/scan-results-summary.txt" % results.resdir
    cmd = "csgrep --mode=stat '%s' | tee '%s'" % (err_file, summary_file)
    return results.exec_cmd(cmd, shell=True)

# 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.5.1-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")

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("-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")

# --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()

# check that only available tools are requested
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)

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.keep_going = args.keep_going
props.cswrap_timeout = args.cswrap_timeout
props.no_scan = args.no_scan
props.shell_cmd_to_build = args.shell_cmd

# 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)

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)

        # 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)

            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))
                    ec = mock.exec_mockbuild_cmd(cmd)
                    if (ec != 0):
                        results.error("%install failed", ec=ec)

                # 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")

                # TODO

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

        # we are done with 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.js
        ini_file = "%s/scan.ini" % results.resdir
        js_file = "%s/scan-results.js" % results.resdir
        cmd = "cslinker --quiet --cwelist '%s' --inifile '%s' $(ls %s/*.err)" \
                % (cwe_list_file, ini_file, results.dbgdir_uni)
        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)
        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)
        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)
