#! /usr/bin/python
# Copyright (c) 2009 The Chromium OS Authors. All rights reserved.
# Copyright (c) 2010 Canonical Ltd.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Written by Colin Watson for Canonical Ltd.

from __future__ import print_function

import multiprocessing
import optparse
import os
import re
import shutil
import sys

from debian import deb822, debfile, debian_support

from config import TargetConfig
import utils
import aptutils
import tree
import tsort


# TODO(ajwong): Remove this global.
target_config = None


# Abstractions for aptutils/tree functions.

def want_apt_version(options, src):
    if options.prefer_apt:
        apt_ver = aptutils.get_src_version(src)
        tree_ver = tree.get_src_version(options, src)
        if apt_ver is None:
            return False
        elif tree_ver is None:
            return True
        else:
            return (debian_support.Version(apt_ver) >
                    debian_support.Version(tree_ver))
    elif options.apt_source:
        return tree.get_src_version(options, src) is None
    else:
        return False


def get_real_pkg(options, pkg):
    """Get the real name of binary package pkg, resolving Provides."""
    if options.apt_source or options.prefer_apt:
        real_pkg = aptutils.get_real_pkg(pkg)
        if real_pkg is not None:
            return real_pkg
    return tree.get_real_pkg(options, pkg)


def get_src_name(options, pkg):
    """Return the source package name that produces binary package pkg."""

    if options.apt_source or options.prefer_apt:
        src = aptutils.get_src_name(pkg)
        if src is not None:
            return src
    return tree.get_src_name(options, pkg)


def get_src_record(options, src):
    """Return a parsed source package record for source package src."""
    if want_apt_version(options, src):
        return aptutils.get_src_record(src)
    else:
        return tree.get_src_record(options, src)


def get_pkg_record(options, pkg):
    """Return a parsed binary package record for binary package pkg."""
    src = get_src_name(options, pkg)
    if src and want_apt_version(options, src):
        return aptutils.get_pkg_record(pkg)
    else:
        return tree.get_pkg_record(options, pkg)


def get_src_version(options, src):
    """Return the current version for source package src."""
    if want_apt_version(options, src):
        return aptutils.get_src_version(src)
    else:
        return tree.get_src_version(options, src)


def get_src_binaries(options, src):
    """Return all the binaries produced by source package src."""
    if want_apt_version(options, src):
        return aptutils.get_src_binaries(src)
    else:
        return tree.get_src_binaries(options, src)


dpkg_architectures = None


def dpkg_architecture_allowed(arch):
    """Check if dpkg can install packages for the host architecture."""
    global dpkg_architectures

    if dpkg_architectures is not None:
        return arch in dpkg_architectures

    dpkg_architectures = set()
    dpkg_architectures.add(
        utils.get_output(['dpkg', '--print-architecture']).strip())
    with open('/dev/null', 'w') as devnull:
        dpkg_architectures.update(
            utils.get_output(['dpkg', '--print-foreign-architectures'],
                             mayfail=True, stderr=devnull).split())

    return arch in dpkg_architectures


def apt_architecture_allowed(arch):
    return (dpkg_architecture_allowed(arch) or
            aptutils.apt_architecture_allowed(arch))


def is_multiarch_foreign(options, pkg):
    """Check if Multi-Arch: foreign is set on the binary package pkg.

    Multi-Arch: foreign packages don't need to be built if they are just
    build-deps and dpkg is configured to install packages of the appropriate
    foreign architecture.
    """

    if not dpkg_architecture_allowed(options.architecture):
        return False

    real_pkg = get_real_pkg(options, pkg)
    if real_pkg is None:
        return False
    record = get_pkg_record(options, real_pkg)
    if record is None:
        return False

    if 'multi-arch' in record:
        return record['multi-arch'] == 'foreign'
    else:
        return False


def is_toolchain(pkg):
    """Is this package provided by the cross-toolchain?"""

    # We really ought to check if they actually are installed or equived
    # rather than assuming
    return ('libc6' in pkg or 'lib32gcc' in pkg or 'lib64gcc' in pkg or
            'libgcc' in pkg or 'libgcj' in pkg or 'lib32stdc++' in pkg or
            'lib64stdc++' in pkg or 'libstdc++' in pkg or 'lib64c' in pkg or
            'multilib' in pkg or pkg == 'linux-libc-dev')


def is_crossable(pkg, conversion=False):
    """Can pkg sensibly be cross-built?"""
    global target_config
    if pkg in target_config.blacklist:
        return False  # manually blacklisted
    if conversion and pkg in target_config.cross_blacklist:
        return False  # manually blacklisted for cross-conversion
    if pkg in target_config.whitelist:
        return True  # manually whitelisted
    if (pkg.endswith('-bin') or pkg.endswith('-common') or
            pkg.endswith('-data') or pkg.endswith('-dbg') or
            pkg.endswith('-doc') or pkg.endswith('-i18n') or
            pkg.endswith('-perl') or pkg.endswith('-pic') or
            pkg.endswith('-refdbg') or pkg.endswith('-tcl') or
            pkg.endswith('-util') or pkg.endswith('-utils') or
            pkg.startswith('python-')):
        return False  # generally want build versions of these, not host
    if 'lib' in pkg:
        return True
    if 'x11proto' in pkg:
        return True
    return False


def need_loop_break(parent, pkg):
    """Sometimes we need to break loops manually."""
    if (parent in ('python2.5', 'python2.6', 'python2.7') and
            pkg == 'libbluetooth-dev'):
        # Python build-depends on several things that are optional for
        # building. These are only used for module builds, and not for the
        # core Python binary and libraries that we need to cross-convert.
        return True
    return False


explicit_requests = set()
# Sets of source/binary packages that have been analysed
all_srcs = {}
all_pkgs = set()
depends = {}
needs_build = {}

# set of graph edges in dependency tree
dot_relationships = dict()


def print_relations(options, graph_pkg, parent, pkg, src, depth,
                    parent_binary):
    if options.generate_graph:
        if options.generate_compact_graph:
            rel = '"%s" -> "%s";' % (parent, src)
            dot_relationships[graph_pkg].append(rel)
        else:
            style = '"bin-%s" [label="%s",shape=box];' % (pkg, pkg)
            rel = '"%s" -> "bin-%s" -> "%s";' % (parent, pkg, src)
            dot_relationships[graph_pkg].append(style)
            dot_relationships[graph_pkg].append(rel)
    if options.debug:
        if parent_binary is not None:
            print("%s%s (%s) -> %s (%s)" % (' ' * depth,
                                            parent, parent_binary,
                                            src, pkg))
        else:
            print("%s%s -> %s (%s)" % (' ' * depth, parent, src, pkg))


def filter_arch(deps, arch):
    """Filter out (build-)dependencies that do not apply to arch."""
    new_deps = []
    for dep in deps:
        new_or_deps = []
        for or_dep in dep:
            ok = False
            if or_dep['arch'] is None:
                ok = True
            else:
                negated = False
                for or_dep_arch in or_dep['arch']:
                    if not or_dep_arch[0]:
                        negated = True
                    if or_dep_arch[1] == arch:
                        ok = or_dep_arch[0]
                        break
                else:
                    # Reached end of list without finding our architecture.
                    # If there were no negated elements, skip this
                    # dependency; otherwise, include it. See policy 7.1.
                    if negated:
                        ok = True
            if ok:
                new_or_deps.append(or_dep)
        if new_or_deps:
            new_deps.append(new_or_deps)
    return new_deps


def should_expand(options, parent, pkg):
    if pkg in explicit_requests:
        # We're going to build it anyway because it was requested on the
        # command line, so we might as well sequence it properly.
        return True
    return (is_crossable(pkg) and not is_toolchain(pkg) and
            not is_multiarch_foreign(options, pkg) and
            not need_loop_break(parent, pkg))


def expand_depends(options, graph_pkg, pkg, depth, builddep_depth,
                   parent=None, parent_binary=None):
    """Recursively expand (build-)dependencies of pkg."""
    # Get control data from apt or local dir
    # depth and builddep-depth are current level of recursion
    # optionally output results in human readable (debug option),
    #  or dot format (generate-graph option)
    #
    src = get_src_name(options, pkg)
    if not src:
        # Is it already a source package name?
        src = pkg
    if not tree.get_src_directory(options, src):
        # Maybe it's a directory name, either relative to the current
        # directory or a build directory.
        if os.path.isdir(src):
            trydir = src
        else:
            for builddir in options.builddirs:
                if os.path.isdir(os.path.join(builddir, src)):
                    trydir = os.path.join(builddir, src)
                    break
            else:
                trydir = None
        if trydir:
            tree.scan_dir(trydir)
            realsrc = tree.get_directory_src(options, trydir)
            if realsrc:
                src = realsrc
    src_record = get_src_record(options, src)
    if not src_record:
        if options.debug:
            print("Did not find a source record for %s" % src)
        return

    if parent is not None and parent != src:
        print_relations(options, graph_pkg, parent, pkg, src, depth,
                        parent_binary)
        depends[parent].add(src)

    if src not in all_srcs:
        # mark source as visited
        all_srcs[src] = get_src_version(options, src)
        depends[src] = set()

        # In --only-explicit mode, we need to expand one level of
        # build-dependencies and all their runtime dependencies in order
        # that we can native-import them, but we don't want to go further
        # down transitive build-dependencies.
        if not options.only_explicit or not builddep_depth:
            if options.stage1:
                builddeps = src_record.relations['build-depends-stage1']
                if not builddeps:
                    builddeps = src_record.relations['build-depends']
            else:
                builddeps = src_record.relations['build-depends']
            if options.architecture == build_arch:
                builddeps.extend(src_record.relations['build-depends-indep'])
            builddeps = filter_arch(builddeps, options.architecture)
            for builddep in builddeps:
                if [d for d in builddep if d['name'] == 'linux-gnu']:
                    # e.g. glib2.0 Build-Depends: libgamin-dev | libfam-dev |
                    #                             linux-gnu
                    # libgamin-dev Build-Depends: libglib2.0-dev, so we need
                    # to break this cycle
                    continue
                bd_pkg = builddep[0]['name']
                if should_expand(options, src, bd_pkg):
                    expand_depends(options, graph_pkg, bd_pkg, depth + 1,
                                   builddep_depth + 1, parent=src)

    # Find the (install-)dependencies of the source package to ensure that
    # what is built can be installed. However we only want to do this
    # for binary deps that are actually needed as build-deps
    if (pkg not in all_pkgs and pkg in get_src_binaries(options, src) and
            is_crossable(pkg) and not is_toolchain(pkg)):
        all_pkgs.add(pkg)
        parsed_binary = get_pkg_record(options, pkg)
        deps = parsed_binary.relations['pre-depends']
        deps.extend(parsed_binary.relations['depends'])
        filter_arch(deps, options.architecture)
        for dep in deps:
            for or_dep in dep:
                # Check should_expand twice, once for the virtual package
                # name and once for the real package name (which may be
                # different).
                if not should_expand(options, src, or_dep['name']):
                    continue
                # TODO version handling?
                real_dep = get_real_pkg(options, or_dep['name'])
                if real_dep is None:
                    continue
                # only recurse further if src not already done
                src_name = get_src_name(options, real_dep)
                if src_name is None:
                    continue
                if should_expand(options, src, real_dep):
                    expand_depends(options, graph_pkg, real_dep, depth + 1,
                                   builddep_depth,
                                   parent=src, parent_binary=pkg)
                break


def mark_needs_build(options, src, force=False):
    """Decide whether a package needs to be (re)built."""
    if src in needs_build:
        return
    # moan if we end up getting a binary package name here
    assert src in all_srcs
    ver = all_srcs[src]
    newer = False

    if not options.force_rebuild and not force:
        ver_obj = debian_support.Version(ver)
        built_vers = sorted([b[1] for b in all_builds(options, src)])
        if built_vers:
            if built_vers[-1] >= ver_obj:
                if options.debug:
                    print("%s already built at version %s" %
                          (src, built_vers[-1]))
                newer = False
            else:
                newer = True
        else:
            newer = True

    if options.force_rebuild or force or newer:
        needs_build[src] = True
        for depsrc, depset in depends.items():
            if src in depset:
                if options.debug:
                    print("Recursing:", src, "->", depsrc)
                mark_needs_build(options, depsrc, force=True)
    else:
        needs_build[src] = False


re_changes_filename = re.compile(r"(.+?)_(.+?)_(.+)\.changes$")


def all_builds(options, src=None):
    """Return the versions of all builds for source package src."""
    for name in sorted(os.listdir(options.destdir)):
        if not name.endswith('.changes'):
            continue
        path = os.path.join(options.destdir, name)
        if not os.path.isfile(path):
            continue
        matchobj = re_changes_filename.match(name)
        if not matchobj:
            continue
        if ((src is None or matchobj.group(1) == src) and
                matchobj.group(3) == options.architecture):
            with open(path) as changes_file:
                changes = deb822.Changes(changes_file)
                if 'version' in changes:
                    yield (matchobj.group(1),
                           debian_support.Version(changes['version']))


build_arch = utils.get_output(['dpkg-architecture',
                               '-qDEB_BUILD_ARCH']).strip()
all_builddeps = set()

# regexes from dak
re_no_epoch = re.compile(r"^\d+\:")
re_package = re.compile(r"^(.+?)_(.+?)_([^.]+).*")

re_deb_filename = re.compile(r"(.+?)_(.+?)_(.+)\.deb$")


def install_build_depends(options, srcs):
    available_builddeps = set()
    if options.architecture != build_arch:
        if ('binutils-multiarch' in aptutils.cache and
                not aptutils.cache['binutils-multiarch'].is_installed):
            all_builddeps.add('binutils-multiarch')
            available_builddeps.add('binutils-multiarch')
    for src in srcs:
        src_record = get_src_record(options, src)
        if src_record is None:
            continue
        if options.stage1:
            builddeps = src_record.relations['build-depends-stage1']
            if not builddeps:
                builddeps = src_record.relations['build-depends']
        else:
            builddeps = src_record.relations['build-depends']
        builddeps.extend(src_record.relations['build-depends-indep'])
        for builddep in builddeps:
            if [d for d in builddep if d['name'] == 'linux-gnu']:
                continue
            # TODO versioned dependencies?
            bd_pkg = builddep[0]['name']
            real_bd_pkg = get_real_pkg(options, bd_pkg)
            if real_bd_pkg is None:
                real_bd_pkg = bd_pkg
            all_builddeps.add(real_bd_pkg)
            if (real_bd_pkg in aptutils.cache and
                    not aptutils.cache[real_bd_pkg].is_installed):
                available_builddeps.add(real_bd_pkg)
            if is_crossable(real_bd_pkg):
                cross_bd_pkg = '%s-%s-cross' % (real_bd_pkg,
                                                options.architecture)
                if (cross_bd_pkg in aptutils.cache and
                        not aptutils.cache[cross_bd_pkg].is_installed):
                    available_builddeps.add(cross_bd_pkg)

    if available_builddeps:
        command = ['apt-get', '-y']
        command.extend(aptutils.apt_options(options))
        command.extend(
            ['-o', 'Dir::Etc::sourcelist=%s' %
                   aptutils.sources_list_path(options),
             '--no-install-recommends', 'install'])
        command.extend(sorted(available_builddeps))
        utils.spawn_root(command)
        aptutils.reopen_cache()


def cross_convert(options, debs, outdir='.'):
    crossable_debs = []
    exclude_deps = set()
    for deb in debs:
        pkg = re_package.sub(r"\1", deb)
        if not is_crossable(pkg, conversion=True):
            continue
        crossable_debs.append(deb)
        control = debfile.DebFile(filename='%s/%s' % (outdir,
                                                      deb)).debcontrol()
        for field in ('pre-depends', 'depends', 'conflicts', 'breaks',
                      'provides', 'replaces'):
            if field not in control:
                continue
            for dep in deb822.PkgRelation.parse_relations(control[field]):
                for or_dep in dep:
                    if not is_crossable(or_dep['name'], conversion=True):
                        exclude_deps.add(or_dep['name'])

    crossed_debs = []
    if crossable_debs:
        convert = ['dpkg-cross', '-a', options.architecture, '-A', '-M', '-b']
        for dep in exclude_deps:
            convert.extend(('-X', dep))
        convert.extend(crossable_debs)
        utils.spawn(convert, cwd=outdir)

        # .debs use package name suffixed with -%arch-cross and arch: all;
        # \1 and \2 are package name and version from re_deb_filename
        re_cross_deb_name = (r"\1-%s-cross_\2_all.deb" % options.architecture)
        for deb in crossable_debs:
            crossed_debs.append(re_deb_filename.sub(re_cross_deb_name, deb))
        print(crossed_debs)

    return crossed_debs


def native_import(options, src):
    """Import a native build of source package src at version ver."""
    src_record = aptutils.get_src_record(src)
    if not src_record:
        return
    ver = src_record['version']
    ver_no_epoch = re_no_epoch.sub('', ver)

    print()
    print("===== Importing %s_%s =====" % (src, ver))
    print()

    debs = []
    previously_imported = False
    for binary in aptutils.get_src_binaries(src):
        if options.debug:
            print("Considering binary %s" % binary)
        command = ['apt-cache']
        if apt_architecture_allowed(options.architecture):
            command.append('-oAPT::Architecture=%s' % options.architecture)
        command.append('show')
        if apt_architecture_allowed(options.architecture):
            command.append('%s:%s' % (binary, options.architecture))
        else:
            command.append(binary)
        try:
            bin_cache = utils.get_output(command).splitlines()
        except Exception:
            if options.debug:
                print("skipping - %s" % binary)
            continue  # might be a udeb or not built for specified arch
        bin_stanzas = deb822.Packages.iter_paragraphs(bin_cache)
        while True:
            try:
                bin_stanza = next(bin_stanzas)
                if 'version' not in bin_stanza or bin_stanza['version'] != ver:
                    continue
                if 'filename' not in bin_stanza:
                    continue
                deb = bin_stanza['filename']
                deb_bits = re_deb_filename.match(deb)
                if deb_bits is None:
                    continue

                if deb_bits.group(3) == build_arch:
                    deb = re_deb_filename.sub(
                        r'\1_\2_%s.deb' % options.architecture, deb)

                if apt_architecture_allowed(options.architecture):
                    command = ['apt-get',
                               '-oAPT::Architecture=%s' % options.architecture,
                               'download',
                               '%s:%s' % (binary, options.architecture)]
                else:
                    assert target_config.native_import_source, \
                           "No native_import_source configured for arch %s" \
                           % options.architecture
                    command = ['wget', '-N',
                               '%s/%s' % (target_config.native_import_source,
                                          deb)]
                deb_base = deb.split('/')[-1]
                if os.path.exists(os.path.join(options.destdir, deb_base)):
                    previously_imported = True
                utils.spawn(command, cwd=options.builddirs[0])
                debs.append(deb_base)
            except StopIteration:
                break

    # fake up a changes file
    changes = '%s_%s_%s.changes' % (src, ver_no_epoch, options.architecture)
    with open(
            os.path.join(options.builddirs[0], changes), 'w') as changes_file:
        print('Version: %s\nFake: yes' % ver, file=changes_file)

    crossed_debs = cross_convert(options, debs, options.builddirs[0])

    if options.builddirs[0] != options.destdir:
        files = debs + crossed_debs
        files.append(changes)
        for f in files:
            os.rename(os.path.join(options.builddirs[0], f),
                      os.path.join(options.destdir, f))

    aptutils.update_apt_repository(options, force_rebuild=previously_imported)


class BuildException(RuntimeError):
    pass


def build(options, src, ver):
    """Build source package src at version ver."""
    install_build_depends(options, [src])

    ver_no_epoch = re_no_epoch.sub('', ver)
    srcdir = tree.get_src_directory(options, src)

    use_apt = want_apt_version(options, src)
    if use_apt:
        refetch = False
        if not srcdir or not os.path.isdir(srcdir):
            refetch = True
        else:
            tree_ver = tree.get_src_version(options, src)
            if debian_support.Version(ver) < debian_support.Version(tree_ver):
                refetch = True

    if use_apt and refetch:
        utils.spawn(['apt-get', '-d', 'source', '%s=%s' % (src, ver)],
                    cwd=options.builddirs[0])
        dsc = '%s_%s.dsc' % (src, ver_no_epoch)
        if srcdir:
            shutil.rmtree(srcdir, ignore_errors=True)
        utils.spawn(['dpkg-source', '-x', dsc, src], cwd=options.builddirs[0])
        tree.scan_dir(os.path.join(options.builddirs[0], src))
        srcdir = tree.get_src_directory(options, src)
    else:
        # Re-acquire version from the source tree, since it may be newer
        # than what we asked for.
        ver = tree.get_src_version(options, src)
        if not ver:
            return
        ver_no_epoch = re_no_epoch.sub('', ver)

    arches = tree.architectures(options, src)
    if ('any' not in arches and 'all' not in arches and
            options.architecture not in arches):
        print("%s_%s not buildable for %s" % (src, ver, options.architecture))
        return

    changes = '%s_%s_%s.changes' % (src, ver_no_epoch, options.architecture)
    previously_built = os.path.exists(os.path.join(options.destdir, changes))

    print()
    print("===== Building %s_%s =====" % (src, ver))
    print()

    checkbuilddeps = ['dpkg-checkbuilddeps']
    if options.stage1:
        checkbuilddeps.append('--stage=1')
    utils.spawn(checkbuilddeps, cwd=srcdir)

    buildpackage = ['debuild', '--no-lintian', '-eUSER']

    build_options = []
    if options.stage1:
        build_options.append('stage=1')

    global target_config
    if options.parallel and src not in target_config.parallel_blacklist:
        cpu_count = multiprocessing.cpu_count()
        if cpu_count > 1:
            # Rule of thumb is to spawn 1 more than the number of CPUs when
            # building.
            buildpackage.append('-j%s' % (cpu_count + 1))
    if options.architecture != build_arch:
        buildpackage.append('-eCONFIG_SITE=/etc/dpkg-cross/cross-config.%s' %
                            options.architecture)

        build_options.append('nocheck')
        deb_host_gnu_type = utils.get_output(
            ['dpkg-architecture', '-a%s' % options.architecture,
             '-qDEB_HOST_GNU_TYPE']).rstrip('\n')
        buildpackage.append('-eGTEST_INCLUDEDIR=/usr/%s/include' %
                            deb_host_gnu_type)
        buildpackage.append('-eGTEST_LIBDIR=/usr/%s/lib' % deb_host_gnu_type)
        # Set PKG_CONFIG search dirs for when there is no $host-pkg-config
        # available
        if not utils.file_on_path('%s-pkg-config' % deb_host_gnu_type,
                                  os.environ['PATH']):
            pkg_config_libdir = ('/usr/%s/lib/pkgconfig' % deb_host_gnu_type,
                                 '/usr/%s/share/pkgconfig' % deb_host_gnu_type,
                                 '/usr/share/pkgconfig')
            buildpackage.append('-ePKG_CONFIG_LIBDIR=%s' %
                                os.pathsep.join(pkg_config_libdir))
        if options.debug:
            buildpackage.append('-eDH_VERBOSE=1')
        buildpackage.append('-a%s' % options.architecture)
    if build_options:
        if 'DEB_BUILD_OPTIONS' in os.environ:
            build_options_arg = '%s %s' % (
                os.environ['DEB_BUILD_OPTIONS'], ' '.join(build_options))
        else:
            build_options_arg = ' '.join(build_options)
        buildpackage.append('-eDEB_BUILD_OPTIONS=%s' % build_options_arg)
    buildpackage.extend(['-b', '-uc', '-us'])
    if options.clean_after:
        buildpackage.append('-tc')
    utils.spawn(buildpackage, cwd=srcdir)

    outdir = os.path.normpath(os.path.join(
        tree.get_src_directory(options, src), '..'))
    build_log = '%s_%s_%s.build' % (src, ver_no_epoch, options.architecture)
    debs = utils.get_output(['dcmd', '--deb', changes],
                            cwd=outdir).splitlines()
    print("Built packages:", ' '.join(debs))

    if options.architecture != build_arch:
        if options.lintian:
            utils.spawn(['lintian', '-C', 'xdeb', '-o', changes], cwd=outdir)
        crossed_debs = cross_convert(options, debs, outdir)

    if outdir != options.destdir:
        files = utils.get_output(['dcmd', changes], cwd=outdir).splitlines()
        for f in files:
            os.rename(os.path.join(outdir, f),
                      os.path.join(options.destdir, f))
        if os.path.exists(os.path.join(outdir, build_log)):
            os.rename(os.path.join(outdir, build_log),
                      os.path.join(options.destdir, build_log))
        if options.architecture != build_arch:
            for deb in crossed_debs:
                os.rename(os.path.join(outdir, deb),
                          os.path.join(options.destdir, deb))

    aptutils.update_apt_repository(options, force_rebuild=previously_built)

    if previously_built:
        # We'll need to install all these again.
        command = ['apt-get', '-y']
        command.extend(aptutils.apt_options(options))
        command.append('purge')
        for deb in crossed_debs:
            pkg = re_package.sub(r"\1", deb)
            if pkg in aptutils.cache and aptutils.cache[pkg].is_installed:
                command.append(pkg)
        utils.spawn_root(command)
        aptutils.reopen_cache()


def parse_options(args=sys.argv[1:]):
    usage = '%prog [options] package ...'
    parser = optparse.OptionParser(usage=usage)
    parser.add_option('-C', '--config-files', dest='config_files',
                      help='read these config files [e.g., file1, file2]')
    parser.add_option('-a', '--architecture',
                      dest='architecture', default=build_arch,
                      help='build for architecture ARCH', metavar='ARCH')
    parser.add_option('--variant',
                      dest='variant', default='generic',
                      help='build for VARIANT variant of the architecture '
                           '(default: generic)',
                      metavar='VARIANT')
    parser.add_option('-b', '--build-directory',
                      action='append', dest='builddirs',
                      help='build packages in DIR (default: .)', metavar='DIR')
    parser.add_option('-d', '--dest-directory',
                      dest='destdir', default=None,
                      help='leave built packages in DIR '
                           '(default: value of --build-directory)',
                      metavar='DIR')
    parser.add_option('-f', '--force-rebuild', dest='force_rebuild',
                      action='store_true', default=False,
                      help="force rebuild even if unchanged")
    parser.add_option('--apt-source', dest='apt_source',
                      action='store_true', default=False,
                      help='fetch source code using apt-get')
    parser.add_option('--prefer-apt', dest='prefer_apt',
                      action='store_true', default=False,
                      help='prefer source packages available using apt-get')
    parser.add_option('--only-explicit', dest='only_explicit',
                      action='store_true', default=False,
                      help='only build packages on the command line; '
                           'native-import everything else')
    parser.add_option('--debug', dest='debug',
                      action='store_true', default=False,
                      help='debug build sequencing')
    parser.add_option('--parallel', dest='parallel',
                      action='store_true', default=False,
                      help='use as many jobs as there are CPUs on the system')
    parser.add_option('--no-clean-after', dest='clean_after',
                      action='store_false', default=True,
                      help='clean source tree after build')
    parser.add_option('--no-lintian', dest='lintian',
                      action='store_false', default=True,
                      help='disable Lintian checks of cross-built packages')
    parser.add_option('--sequence', dest='sequence',
                      action='store_true', default=False,
                      help="don't build; just show build sequence")
    parser.add_option('--list-builds', dest='list_builds',
                      action='store_true', default=False,
                      help="list current successful builds")
    parser.add_option('--all', dest='all',
                      action='store_true', default=False,
                      help="build all packages in the working tree")
    parser.add_option('-x', '--exclude', dest='exclude',
                      action='append',
                      help="don't build this package (unless required by "
                           "dependencies)")
    parser.add_option('--no-native-import', dest='native_import',
                      action='store_false', default=True,
                      help='disable automatic native imports')
    parser.add_option('--convert', dest='convert',
                      action='store_true', default=False,
                      help="don't build; just cross-convert packages")
    parser.add_option('--no-convert-install', dest='convert_install',
                      action='store_false', default=True,
                      help="don't install packages after cross-conversion")
    parser.add_option('--generate-graph', default=False, action='store_true',
                      dest='generate_graph',
                      help='generate a dot file that can be drawn by an app '
                           'like GraphViz; WARNING: WILL UNSET --debug')
    parser.add_option('--generate-compact-graph',
                      dest='generate_compact_graph',
                      action='store_true', default=False,
                      help='draw a simplified graph omitting binary package '
                           'links')
    parser.add_option('--stage1',
                      dest='stage1',
                      action='store_true', default=False,
                      help='generate the dependencies of packages based on '
                           'the Build-Depends-Stage1 field of the control '
                           'file instead of the Build-Depends field')
    options, remaining_args = parser.parse_args(args)
    return parser, options, remaining_args


def main():
    parser, options, args = parse_options()

    config_paths = None
    if options.config_files:
        config_paths = options.config_files.split(',')

    global target_config
    target_config = TargetConfig(options.architecture,
                                 options.variant)
    target_config.InitializeFromConfigs(config_paths)
    if options.generate_compact_graph:
        options.generate_graph = True

    if options.generate_graph:
        print('/*xdeb dot file starts here')

    if options.debug:
        print('Configuration is:\n%s' % target_config)

    # Use config file values for options if no commandline override was given.
    for name, value in target_config.options.items():
        if name in parser.defaults:
            if getattr(options, name) == parser.defaults[name]:
                if name in ('builddirs', 'exclude'):
                    setattr(options, name, value.split())
                elif isinstance(parser.defaults[name], bool):
                    setattr(options, name, bool(value))
                else:
                    setattr(options, name, value)

    if not options.builddirs:
        options.builddirs = ['.']
    if options.destdir is None:
        options.destdir = options.builddirs[0]
    if not options.exclude:
        options.exclude = []

    for builddir in options.builddirs:
        if not os.path.exists(builddir):
            os.makedirs(builddir)
    if not os.path.exists(options.destdir):
        os.makedirs(options.destdir)

    if options.list_builds:
        build_srcs = {}
        for b in all_builds(options):
            src, ver = b
            if src not in build_srcs or ver > build_srcs[src]:
                build_srcs[src] = ver
        for src in sorted(build_srcs):
            print(src, build_srcs[src])
        sys.exit(0)

    aptutils.init(options)

    if options.convert:
        crossed_debs = cross_convert(options, args)
        if crossed_debs and options.convert_install:
            install = ['dpkg', '-i']
            install.extend(crossed_debs)
            utils.spawn_root(install)
        sys.exit(0)

    if options.all:
        args = tree.all_packages(options) + args

    explicit_requests.update(args)

    for pkg in args:
        graph_pkg = None
        if options.generate_graph:
            dot_relationships[pkg] = list()
            graph_pkg = pkg
        expand_depends(options, graph_pkg, pkg, 0, 0)

    if options.generate_graph:
        print('end of xdeb run output - graph starts here */')
        for pkg in dot_relationships:
            print('digraph "%s" {' % pkg)
            print('"%s" [shape=diamond];' % pkg)
            for rel in dot_relationships[pkg]:
                print(rel)
            print('}')
        print()
        sys.exit(0)

    for pkg in args:
        if pkg not in all_srcs:
            srcpkg = get_src_name(options, pkg)
            if srcpkg in all_srcs:
                print(("Using corresponding source %s for binary package %s" %
                      (srcpkg, pkg)))
                args.remove(pkg)
                if srcpkg not in args:
                    args.append(srcpkg)
            else:
                print("No source or binary package found: %s" % pkg)
                sys.exit(1)

    if options.only_explicit:
        # In --only-explicit mode, we don't need to do a full topological
        # sort (which relieves us from concerns of dependency cycles and the
        # like); we just need to make sure that native imports happen first
        # and then that explicit requests happen in command-line order.
        build_sequence = [d for d in depends if d not in explicit_requests]
        build_sequence.extend(args)
    else:
        try:
            build_sequence = tsort.topo_sort(depends)
        except tsort.GraphCycleError as e:
            print("Dependency cycle:", e.graph)
            sys.exit(1)

    for src in build_sequence:
        mark_needs_build(options, src)
    print("Build sequence:", end=' ')
    for src in build_sequence:
        if needs_build[src]:
            print('%s*' % src, end=' ')
        else:
            print(src, end=' ')
    print()
    if options.sequence:
        sys.exit(0)

    if not build_sequence and not (options.apt_source or options.prefer_apt):
        print("Build sequence is empty. Did you mean to use "
              "--apt-source or --prefer-apt?")

    real_build = set()
    for src in build_sequence:
        if needs_build[src]:
            if options.only_explicit and src not in explicit_requests:
                continue
            if src in target_config.native_import:
                continue
            real_build.add(src)

    install_build_depends(options,
                          [src for src in build_sequence if src in real_build])

    # In --only-explicit mode, native imports have no particular sequencing
    # requirements.
    if options.only_explicit:
        for src in build_sequence:
            if needs_build[src] and src not in real_build:
                native_import(options, src)

    for src in build_sequence:
        if options.debug:
            print("Considering source package %s" % src)
        if needs_build[src]:
            if src in real_build:
                build(options, src, all_srcs[src])
            elif not options.only_explicit:
                native_import(options, src)
        else:
            if options.debug:
                print("Skipping %s (already built)" % src)


if __name__ == '__main__':
    main()
