#!/usr/bin/python
#
# Copyright 2005 Lars Wirzenius (liw@iki.fi)
# 
# 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 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.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA


"""Debian package installation and uninstallation tester.

This program sets up a minimal Debian system in a chroot, and installs
and uninstalls packages and their dependencies therein, looking for
problems.

See the manual page (piuparts.1, generated from piuparts.docbook) for
more usage information.

Lars Wirzenius <liw@iki.fi>
"""


VERSION = "0.14"


import time
import logging
import getopt
import sys
import commands
import tempfile
import shutil
import os
import tarfile
import stat
import re
import pickle


class Settings:

    """Global settings for this program."""
    
    def __init__(self):
        self.tmpdir = None
        self.keep_tmpdir = False
        self.max_command_output_size = 1024 * 1024
        self.args_are_package_files = True
        self.debian_mirrors = []
        self.debian_distros = []
        self.basetgz = None
        self.savetgz = None
        self.endmeta = None
        self.saveendmeta = None
        self.ignored_files = [
            "/etc/aliases",
            "/etc/group",
            "/etc/group-",
            "/etc/inetd.conf",
            "/etc/inittab",
            "/etc/ld.so.cache",
            "/etc/ld.so.conf",
            "/etc/ld.so.conf.old",
            "/etc/modules.conf",
            "/etc/modules.conf.old",
            "/etc/passwd",
            "/etc/passwd-",
            "/etc/shells",
            "/home/ftp",
            "/usr/share/info/dir",
            "/usr/share/info/dir.old",
            "/var/backups/infodir.bak",
            "/var/cache/apt/archives/lock",
            "/var/cache/apt/pkgcache.bin", 
            "/var/cache/debconf",
            "/var/cache/debconf/config.dat",
            "/var/cache/debconf/config.dat-old",
            "/var/cache/debconf/passwords.dat",
            "/var/cache/debconf/templates.dat",
            "/var/cache/debconf/templates.dat-old",
            "/var/cache/man/index.db",
            "/var/lib/dpkg/available", 
            "/var/lib/dpkg/available-old", 
            "/var/lib/dpkg/diversions",
            "/var/lib/dpkg/diversions-old",
            "/var/lib/dpkg/lock", 
            "/var/lib/dpkg/status", 
            "/var/lib/dpkg/status-old", 
            "/var/lib/dpkg/statoverride",
            "/var/lib/dpkg/statoverride-old",
            "/var/lib/dpkg/firebird",
            "/var/lib/logrotate/status",
            "/var/lib/rbldns",
            "/var/log/dpkg.log",
            "/var/log/faillog",
            "/var/log/lastlog",
            "/",
            ]
        self.ignored_patterns = [
            "/dev/",
            "/lib/modules/.*/modules.*",
            "/var/lib/cvs(/.*)?",
            "/var/lib/dpkg/alternatives",
            "/var/lib/onak(/.*)?",
            "/var/log/exim/.*",
            "/var/log/exim4/.*",
            "/var/mail/.*",
            "/var/spool/exim/.*",
            "/var/spool/exim4/.*",
            "/var/spool/news(/.*)?",
            "/var/run/.*",
            ]


settings = Settings()


on_panic_hooks = {}
counter = 0


def do_on_panic(hook):
    global counter
    id = counter
    counter += 1
    on_panic_hooks[id] = hook
    return id


def dont_do_on_panic(id):
    del on_panic_hooks[id]


class TimeOffsetFormatter(logging.Formatter):

    def __init__(self, fmt=None, datefmt=None):
        self.startup_time = time.time()
        logging.Formatter.__init__(self, fmt, datefmt)

    def formatTime(self, record, datefmt):
        t = time.time() - self.startup_time
        min = int(t / 60)
        s = t % 60.0
        return "%dm%.1fs" % (min, s)


DUMP = logging.DEBUG - 1
HANDLERS = []

def setup_logging(log_level, log_file_name):
    logging.addLevelName(DUMP, "DUMP")

    logger = logging.getLogger()
    logger.setLevel(log_level)
    formatter = TimeOffsetFormatter("%(asctime)s %(levelname)s: %(message)s")

    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    HANDLERS.append(handler)
    
    if log_file_name:
        handler = logging.FileHandler(log_file_name)
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        HANDLERS.append(handler)


def dump(msg):
    logger = logging.getLogger()
    logger.log(DUMP, msg)
    for handler in HANDLERS:
        handler.flush()


def panic():
    for i in range(counter):
        if i in on_panic_hooks:
            on_panic_hooks[i]()
    sys.exit(1)
    

def indent_string(str):
    """Indent all lines in a string with two spaces and return result."""
    return "\n".join(["  " + line for line in str.split("\n")])


safechars = ("abcdefghijklmnopqrstuvwxyz" +
             "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
             "0123456789" +
             ",.-_!%/=+:")

def shellquote(str):
    if str == "":
        return "''"

    result = []
    for c in str:
        if c == "'":
            result.append("\"'\"")
        elif c in safechars:
            result.append(c)
        else:
            result.append("\\" + c)
    return "".join(result)


def run(command, ignore_errors=False):
    """Run an external command and die with error message if it failes."""
    logging.debug("Starting command: %s" % command)
    f = os.popen("{ export LC_ALL=C LANGUAGES=; %s ; } 2>&1" % 
                 command, "r")
    output = ""
    while 1:
        line = f.readline()
        if not line:
            break
        output += line
        output = output[-settings.max_command_output_size:]
        if line[-1:] == "\n":
            line = line[:-1]
        dump("  " + line)
    status = f.close()
    if status is None:
        status = 0
    failed = not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0
    if not failed:
        logging.debug("Command ok: %s" % repr(command))
    elif ignore_errors:
        logging.debug("Command failed (status=%d), but ignoring error: %s" % 
              (status, repr(command)))
    else:
        logging.error("Command failed (status=%d): %s\n%s" % 
              (status, repr(command), indent_string(output)))
        panic()
    return status, output


def create_temp_file():
    """Create a temporary file and return its full path."""
    (fd, path) = tempfile.mkstemp(dir=settings.tmpdir)
    logging.debug("Created temporary file %s" % path)
    return (fd, path)


def create_file(name, contents):
    """Create a new file with the desired name and contents."""
    try:
        f = file(name, "w")
        f.write(contents)
        f.close()
    except IOError, detail:
        logging.error("Couldn't create file %s: %s" % (name, detail))
        panic()


def remove_files(filenames):
    """Remove some files."""
    for filename in filenames:
        logging.debug("Removing %s" % filename)
        try:
            os.remove(filename)
        except OSError, detail:
            logging.error("Couldn't remove %s: %s" % (filename, detail))
            panic()



class Chroot:

    """A chroot for testing things in."""
    
    def __init__(self):
        self.name = None
        
    def create_temp_dir(self):
        """Create a temporary directory for the chroot."""
        self.name = tempfile.mkdtemp(dir=settings.tmpdir)
        os.chmod(self.name, 0755)
        logging.debug("Created temporary directory %s" % self.name)

    def create(self):
        """Create a chroot according to user's wishes."""
        self.create_temp_dir()
        id = do_on_panic(self.remove)
        if settings.basetgz:
            self.unpack_from_tgz(settings.basetgz)
            self.configure_chroot()
            self.run("apt-get update")
            self.run("apt-get clean")
        else:
            self.setup_minimal_chroot()
            self.run("apt-get update")
            self.run("apt-get clean")
            if settings.savetgz:
                self.pack_into_tgz(settings.savetgz)
        dont_do_on_panic(id)

    def remove(self):
        """Remove a chroot and all its contents."""
        if not settings.keep_tmpdir and os.path.exists(self.name):
            shutil.rmtree(self.name)
            logging.debug("Removed directory tree at %s" % self.name)

    def pack_into_tgz(self, result):
        """Tar and compress all files in the chroot."""
        logging.debug("Saving %s to %s." % (self.name, result))
        try:
            tf = tarfile.open(result, "w:gz")
            tf.add(self.name, arcname=".")
            tf.close()
        except tarfile.TarError, detail:
            logging.error("Couldn't create tar file %s: %s" % 
                          (result, detail))
            panic()

    def unpack_from_tgz(self, tarball):
        """Unpack a tarball to a chroot."""
        logging.debug("Unpacking %s into %s" % (tarball, self.name))
        run("tar -C %s -zxf %s" % 
                (shellquote(self.name), shellquote(tarball)))

    def run(self, command, ignore_errors=False):
        return run("chroot %s %s" % (shellquote(self.name), command), 
                   ignore_errors=ignore_errors)

    def create_apt_sources(self, distro):
        """Create an /etc/apt/sources.list with a given distro."""
        lines = []
        for mirror, components in settings.debian_mirrors:
            lines.append("deb %s %s %s\n" % 
                         (mirror, distro, " ".join(components)))
        create_file(os.path.join(self.name, "etc/apt/sources.list"), 
                    "".join(lines))

    def create_apt_conf(self):
        """Create /etc/apt/apt.conf inside the chroot."""
        create_file(self.relative("etc/apt/apt.conf"),
                    'APT { Get { AllowUnauthenticated "yes"; }; };')

    def create_policy_rc_d(self):
        """Create a policy-rc.d that prevents daemons from running."""
	full_name = os.path.join(self.name, "usr/sbin/policy-rc.d")
        create_file(full_name, "#!/bin/sh\nexit 101\n")
	os.chmod(full_name, 0777)
	logging.debug("Created policy-rc.d and chmodded it.")

    def setup_minimal_chroot(self):
        """Set up a minimal Debian system in a chroot."""
        logging.debug("Setting up minimal chroot for %s at %s." % 
              (settings.debian_distros[0], self.name))
        run("debootstrap --resolve-deps %s %s %s" % 
            (settings.debian_distros[0], 
             shellquote(self.name), 
             shellquote(settings.debian_mirrors[0][0])))
        self.configure_chroot()

    def configure_chroot(self):
        """Configure a chroot according to current settings"""
        self.create_apt_sources(settings.debian_distros[0])
        self.create_apt_conf()
        self.create_policy_rc_d()

    def upgrade_to_distros(self, distros):
        """Upgrade a chroot installation to each successive distro."""
        for distro in distros:
            logging.debug("Upgrading %s to %s" % (self.name, distro))
            self.create_apt_sources(distro)
            self.run("apt-get update")
            self.run("apt-get -yf dist-upgrade")
    
    def apt_get_knows(self, package_names):
        """Does apt-get (or apt-cache) know about a set of packages?"""
        for name in package_names:
            (status, output) = self.run("apt-cache show " + shellquote(name),
                                        ignore_errors=True)
            if not os.WIFEXITED(status):
                logging.error("Error occurred when running apt-cache " +
                              " in chroot:\n" + output)
                panic()
            if os.WEXITSTATUS(status) != 0 or not output.strip():
                return False
        return True

    def copy_files(self, source_names, target_name):
        """Copy files in 'source_name' to file/dir 'target_name', relative
        to the root of the chroot."""
        target_name = os.path.join(self.name, target_name)
        logging.debug("Copying %s to %s" % 
                      (", ".join(source_names), target_name))
        for source_name in source_names:
            try:
                shutil.copy(source_name, target_name)
            except IOError, detail:
                logging.error("Error copying %s to %s: %s" % 
                      (source_name, target_name, detail))
                panic()

    def install_package_files(self, filenames):
        if filenames:
            self.copy_files(filenames, "tmp")
            tmp_files = [os.path.basename(a) for a in filenames]
            tmp_files = [os.path.join("tmp", name) for name in tmp_files]
            tmp_files = [shellquote(x) for x in tmp_files]
            self.run("dpkg -i " + " ".join(tmp_files), ignore_errors=True)
            self.run("apt-get -yf --no-remove install")
            self.run("apt-get clean")
            remove_files([os.path.join(self.name, name) 
                            for name in tmp_files])

    def get_selections(self):
        """Get current package selections in a chroot."""
        (status, output) = self.run("dpkg --get-selections '*'")
        list = [line.split() for line in output.split("\n") if line.strip()]
        dict = {}
        for name, status in list:
            dict[name] = status
        return dict

    def remove_or_purge(self, operation, packages):
        """Remove or purge packages in a chroot."""
        for name in packages:
            self.run("dpkg --%s %s" % (operation, shellquote(name)), 
                     ignore_errors=True)
        self.run("dpkg --remove --pending", ignore_errors=True)
    
    def restore_selections(self, changes, packages):
        """Restore package selections in a chroot by applying 'changes'.
           'changes' is a return value from diff_selections."""
    
        deps = {}
        nondeps = {}
        for name, state in changes.iteritems():
            if name in packages:
                nondeps[name] = state
            else:
                deps[name] = state
    
        deps_to_remove = [name for name, state in deps.iteritems()
                          if state == "remove"]
        deps_to_purge = [name for name, state in deps.iteritems()
                         if state == "purge"]
        nondeps_to_remove = [name for name, state in nondeps.iteritems()
                             if state == "remove"]
        nondeps_to_purge = [name for name, state in nondeps.iteritems()
                            if state == "purge"]
    
        # First remove all packages.
        self.remove_or_purge("remove", deps_to_remove + deps_to_purge +
                                        nondeps_to_remove + nondeps_to_purge)
    
        # Then purge all packages being depended on.
        self.remove_or_purge("purge", deps_to_purge)
        
        # Finally, purge actual packages.
        self.remove_or_purge("purge", nondeps_to_purge)
    
        # Now do a final run to see that everything worked.
        self.run("dpkg --purge --pending")
        self.run("dpkg --remove --pending")

    def save_meta_data(self):
        """Return the filesystem meta data for all objects in the chroot."""
        root = os.path.join(self.name, ".")
        dict = {}
        for dirpath, dirnames, filenames in os.walk(root):
            assert dirpath[:len(root)] == root
            for name in [dirpath] + \
                        [os.path.join(dirpath, f) for f in filenames]:
                st = os.lstat(name)
                if stat.S_ISLNK(st.st_mode):
                    target = os.readlink(name)
                else:
                    target = None
                dict[name[len(root):]] = (st, target)
        return dict

    def relative(self, pathname):
        return os.path.join(self.name, pathname)

    def get_files_owned_by_packages(self):
        """Return dict[filename] = [packagenamelist]."""
        dir = self.relative("var/lib/dpkg/info")
        dict = {}
        for basename in os.listdir(dir):
            if basename.endswith(".list"):
                pkg = basename[:-len(".list")]
                f = file(os.path.join(dir, basename), "r")
                for line in f:
                    pathname = line.strip()
                    if pathname in dict:
                        dict[pathname].append(pkg)
                    else:
                        dict[pathname] = [pkg]
                f.close()
        return dict

    def install_packages_by_name(self, packages):
        if packages:
            self.run("apt-get -y install " +
                       " ".join([shellquote(arg) for arg in packages]))


def objects_are_different(pair1, pair2):
    """Are filesystem objects different based on their meta data?"""
    (m1, target1) = pair1
    (m2, target2) = pair2
    if (m1.st_mode != m2.st_mode or 
        m1.st_uid != m2.st_uid or 
        m1.st_gid != m2.st_gid or
        target1 != target2):
        return True
    if stat.S_ISREG(m1.st_mode):
        return m1.st_size != m2.st_size # or m1.st_mtime != m2.st_mtime
    return False


def diff_meta_data(tree1, tree2):
    """Compare two dir trees and return list of new files (only in 'tree2'),
       removed files (only in 'tree1'), and modified files."""

    tree1 = tree1.copy()
    tree2 = tree2.copy()

    for name in settings.ignored_files:
        if name in tree1:
            del tree1[name]
        if name in tree2:
            del tree2[name]

    for pattern in settings.ignored_patterns:
        pat = re.compile(pattern)
        for name in tree1.keys():
            m = pat.search(name)
            if m:
                del tree1[name]
        for name in tree2.keys():
            m = pat.search(name)
            if m:
                del tree2[name]

    modified = []
    for name in tree1.keys()[:]:
        if name in tree2:
            if objects_are_different(tree1[name], tree2[name]):
                modified.append((name, tree1[name]))
            del tree1[name]
            del tree2[name]

    removed = [x for x in tree1.iteritems()]
    new = [x for x in tree2.iteritems()]

    return new, removed, modified


def file_list(meta_infos, file_owners):
    """Return list of indented filenames."""
    meta_infos = meta_infos[:]
    meta_infos.sort()
    list = []
    for name, data in meta_infos:
        list.append("  %s\n" % name)
        if name in file_owners:
            list.append("    owned by: %s\n" % ", ".join(file_owners[name]))
    return "".join(list)

def diff_selections(chroot, selections):
    """Compare original and current package selection.
       Return dict where dict[package_name] = original_status, that is,
       the value in the dict is the state that the package needs to be
       set to to restore original selections."""
    changes = {}
    current = chroot.get_selections()
    for name, value in current.iteritems():
        if name not in selections:
            changes[name] = "purge"
        elif selections[name] != current[name] and \
             selections[name] == "purge":
            changes[name] = selections[name]
    return changes


def get_package_names_from_package_files(filenames):
    """Return list of package names given list of package file names."""
    list = []
    for filename in filenames:
        (status, output) = run("dpkg --info " + shellquote(filename))
        for line in [line.lstrip() for line in output.split("\n")]:
            if line[:len("Package:")] == "Package:":
                list.append(line.split(":", 1)[1].strip())
    return list


def check_results(chroot, root_info, file_owners):
    """Check that current chroot state matches 'root_info'."""
    current_info = chroot.save_meta_data()
    (new, removed, modified) = diff_meta_data(root_info, current_info)
    ok = True
    if new:
        logging.error("Package purging left files on system:\n" + 
                      file_list(new, file_owners))
        ok = False
    if removed:
        logging.error("After purging files have disappeared:\n" +
                      file_list(removed, file_owners))
        ok = False
    if modified:
        logging.error("After purging files have been modified:\n" +
                      file_list(modified, file_owners))
        ok = False

    return ok


def install_purge_test(chroot, root_info, selections, args, packages):
    """Do an install-purge test. Return True if successful, False if not.
       Assume 'root' is a directory already populated with a working
       chroot, with packages in states given by 'selections'."""

    # Install packages into the chroot.
    if args:
        chroot.install_package_files(args)
    else:
        chroot.install_packages_by_name(packages)
        chroot.run("apt-get clean")

    file_owners = chroot.get_files_owned_by_packages()

    # Remove all packages from the chroot that weren't there initially.    
    changes = diff_selections(chroot, selections)
    chroot.restore_selections(changes, packages)
    
    return check_results(chroot, root_info, file_owners)


def install_upgrade_test(chroot, root_info, selections, args, package_names):
    """Install package via apt-get, then upgrade from package files.
    Return True if successful, False if not."""

    # First install via apt-get.
    chroot.install_packages_by_name(package_names)

    # Then from the package files.
    chroot.install_package_files(args)

    file_owners = chroot.get_files_owned_by_packages()

    # Remove all packages from the chroot that weren't there
    # initially.
    changes = diff_selections(chroot, selections)
    chroot.restore_selections(changes, package_names)
    
    return check_results(chroot, root_info, file_owners)


def save_meta_data(filename, root_info, selections):
    """Save directory tree meta data into a file for fast access later."""
    logging.debug("Saving chroot meta data to %s" % filename)
    f = file(filename, "w")
    pickle.dump((root_info, selections), f)
    f.close()


def load_meta_data(filename):
    """Load meta data saved by 'save_meta_data'."""
    logging.debug("Loading chroot meta data from %s" % filename)
    f = file(filename, "r")
    (root_info, selections) = pickle.load(f)
    f.close()
    return root_info, selections


def install_and_upgrade_between_distros(filenames, packages):
    """Install package and upgrade it between distributions, then remove.
       Return True if successful, False if not."""

    chroot = Chroot()
    chroot.create()
    id = do_on_panic(chroot.remove)

    if settings.basetgz:
        root_tgz = settings.basetgz
    else:
        (fd, root_tgz) = create_temp_file()
        chroot.pack_into_tgz(root_tgz)
        
    if settings.endmeta:
        root_info, selections = load_meta_data(settings.endmeta)
    else:
        chroot.upgrade_to_distros(settings.debian_distros[1:])
        chroot.run("apt-get clean")
    
        root_info = chroot.save_meta_data()
        selections = chroot.get_selections()
        
        if settings.saveendmeta:
            save_meta_data(settings.saveendmeta, root_info, selections)
    
        chroot.remove()
        chroot = Chroot()
        chroot.create_temp_dir()
        chroot.unpack_from_tgz(root_tgz)
    
    chroot.run("apt-get update")
    chroot.install_packages_by_name(packages)

    chroot.upgrade_to_distros(settings.debian_distros[1:])

    chroot.install_package_files(filenames)
    chroot.run("apt-get clean")
    
    file_owners = chroot.get_files_owned_by_packages()

    changes = diff_selections(chroot, selections)
    chroot.restore_selections(changes, packages)
    result = check_results(chroot, root_info, file_owners)

    if root_tgz != settings.basetgz:
        remove_files([root_tgz])
    chroot.remove()
    dont_do_on_panic(id)

    return result


def parse_mirror_spec(str, defaultcomponents=[]):
    """Parse a mirror specification from the --mirror option argument.
       Return (mirror, componentslist)."""
    parts = str.split()
    return parts[0], parts[1:] or defaultcomponents[:]


def find_default_debian_mirrors():
    """Find the default Debian mirrors."""
    mirrors = []
    try:
        f = file("/etc/apt/sources.list", "r")
        for line in f:
            parts = line.split()
            if len(parts) > 2 and parts[0] == "deb":
                mirrors.append((parts[1], parts[3:]))
                break # Only use the first one, at least for now.
        f.close()
    except IOError:
        return None
    return mirrors


def parse_command_line():
    """Parse the command line, change global settings, return non-options."""
    (opts, args) = getopt.getopt(sys.argv[1:], "ab:B:d:hi:I:kl:m:nps:S:t:vV",
                                 ["distribution=", "mirror=", "verbose",
                                  "log-file=", "ignore=", "no-ignores",
                                  "ignore-regex=", "help", "basetgz=",
                                  "save=", "save-end-meta=", "version", 
                                  "apt", "pbuilder", "end-meta",
                                  "tmpdir=", "keep-tmpdir"])
    exit = None
    log_file_name = None
    for opt, optarg in opts:
        if opt in ["-a", "--apt"]:
            settings.args_are_package_files = False
        elif opt in ["-b", "--basetgz"]:
            settings.basetgz = optarg
        elif opt in ["-B", "--end-meta"]:
            settings.endmeta = optarg
        elif opt in ["-d", "--distribution"]:
            settings.debian_distros.append(optarg)
        elif opt in ["-h", "--help"]:
            sys.stdout.write("Usage: piuparts [options] package-file ...\n")
            sys.stdout.write("See manual page piuparts(1) for more help.\n")
            exit = 0
        elif opt in ["-i", "--ignore"]:
            settings.ignored_files.append(optarg)
        elif opt in ["-I", "--ignore-regex"]:
            settings.ignored_patterns.append(optarg)
        elif opt in ["-k", "--keep-tmpdir"]:
            settings.keep_tmpdir = True
        elif opt in ["-l", "--log-file"]:
            log_file_name = optarg
        elif opt in ["-m", "--mirror"]:
            settings.debian_mirrors.append(parse_mirror_spec(optarg,
                                                             ["main",
                                                              "contrib",
                                                              "non-free"]))
        elif opt in ["-n", "--no-ignores"]:
            settings.ignored_files = []
            settings.ignored_patterns = []
        elif opt in ["-p", "--pbuilder"]:
            settings.basetgz = "/var/cache/pbuilder/base.tgz"
        elif opt in ["-s", "--save"]:
            settings.savetgz = optarg
        elif opt in ["-S", "--save-end-meta"]:
            settings.saveendmeta = optarg
        elif opt in ["-t", "--tmpdir"]:
            if not os.path.isdir(optarg):
                logging.error("Temporary directory is not a directory: %s" % 
                              optarg)
                panic()
            settings.tmpdir = optarg
        elif opt in ["-v", "--verbose"]:
            # -v (--verbose) is currently being ignored, but accepted as an
            # option for backwards compatibility
            pass
        elif opt in ["-V", "--version"]:
            sys.stdout.write("Version: %s\n" % VERSION)
            exit = 0

    setup_logging(DUMP, log_file_name)

    if not settings.tmpdir:
        if "TMPDIR" in os.environ:
            settings.tmpdir = os.environ["TMPDIR"]
        else:
            settings.tmpdir = "/tmp"

    if not settings.debian_distros:
        settings.debian_distros = ["sid"]

    if not settings.debian_mirrors:
        settings.debian_mirrors = find_default_debian_mirrors()
        if not settings.debian_mirrors:
            settings.debian_mirrors = [("http://ftp.debian.org/",
                                        ["main", "contrib", "non-free"])]

    if not args:
        logging.error("Need command line arguments: " +
                      "names of packages or package files")
        exit = 1

    if exit is not None:
        sys.exit(exit)

    return args
    

def main():
    """Main program. But you knew that."""

    args = parse_command_line()

    logging.info("-" * 78)
    logging.info("piuparts version %s starting up." % VERSION)
    logging.info("Command line arguments: %s" % " ".join(sys.argv))

    # Make sure debconf does not ask questions and stop everything.
    # Packages that don't use debconf will lose.
    os.environ["DEBIAN_FRONTEND"] = "noninteractive"

    # Find the names of packages.
    if settings.args_are_package_files:
        packages = get_package_names_from_package_files(args)
    else:
        packages = args
        args = []

    if len(settings.debian_distros) == 1:
        chroot = Chroot()
        chroot.create()
        id = do_on_panic(chroot.remove)

        root_info = chroot.save_meta_data()
        selections = chroot.get_selections()
    
        if not install_purge_test(chroot, root_info, selections,
				  args, packages):
            logging.error("FAIL: Installation and purging test.")
            panic()
        logging.info("PASS: Installation and purging test.")
    
        if not settings.args_are_package_files:
            logging.info("Can't test upgrades: -a or --apt option used.")
        elif not chroot.apt_get_knows(packages):
            logging.info("Can't test upgrade: packages not known by apt-get.")
        elif install_upgrade_test(chroot, root_info, selections, args, 
                                  packages):
            logging.info("PASS: Installation, upgrade and purging tests.")
        else:
            logging.error("FAIL: Installation, upgrade and purging tests.")
            panic()
    
        chroot.remove()
        dont_do_on_panic(id)
    else:
        if install_and_upgrade_between_distros(args, packages):
            logging.info("PASS: Upgrading between Debian distributions.")
        else:
            logging.error("FAIL: Upgrading between Debian distributions.")
            panic()

    logging.info("PASS: All tests.")
    logging.info("piuparts run ends.")


if __name__ == "__main__":
    main()
