#!/usr/bin/python3

# Copyright © 2013 Jakub Wilk <jwilk@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the “Software”), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import argparse
import collections
import contextlib
import difflib
import functools
import os
import subprocess as ipc
import sys

import apt_pkg
import apt.cache
import apt.debfile

@functools.total_ordering
class debversion(str):
    def __lt__(self, other):
        return apt_pkg.version_compare(self, other) < 0
    def __eq__(self, other):
        return apt_pkg.version_compare(self, other) == 0

def parse_deb_path(path):
    if not path.endswith('.deb'):
        raise ValueError('{path!r} is not a .deb file'.format(path=path))
    name, version, arch = path[:-4].split('_')
    return name, debversion(version)

def log_start(s):
    print(s, end=' ... ')
    sys.stdout.flush()

def log_end(s):
    print(s)

@contextlib.contextmanager
def silenced_stdout():
    stdout_fd = sys.stdout.fileno()
    stdout_copy_fd = os.dup(stdout_fd)
    try:
        null_fd = os.open(os.devnull, os.O_WRONLY)
        try:
            os.dup2(null_fd, stdout_fd)
        finally:
            os.close(null_fd)
        try:
            yield
        finally:
            os.dup2(stdout_copy_fd, stdout_fd)
    finally:
        os.close(stdout_copy_fd)

def main():
    apt_pkg.init()
    ap = argparse.ArgumentParser()
    ap.add_argument('paths', metavar='DEB-OR-CHANGES', nargs='+')
    ap.add_argument('--no-skip', action='store_true')
    ap.add_argument('--adequate', default='adequate')
    options = ap.parse_args()
    if os.getuid() != 0:
        print('{}: error: administrator privileges are required'.format(ap.prog), file=sys.stderr)
        sys.exit(1)
    packages = collections.defaultdict(list)
    for path in options.paths:
        if path.endswith('.changes'):
            with open(path, 'rt', encoding='UTF-8') as file:
                for para in apt_pkg.TagFile(file):
                    for line in para['Files'].splitlines():
                        md5, size, section, priority, path = line.split()
                        name, version = parse_deb_path(path)
                        packages[name] += [(version, path)]
        elif path.endswith('.deb'):
            name, version = parse_deb_path(path)
            packages[name] += [(version, path)]
    cache = apt.cache.Cache()
    rc = 0
    for name, versions in sorted(packages.items()):
        versions.sort()
        prev_version = None
        for version, path in versions:
            if prev_version is None:
                if len(versions) == 1:
                    log_start('install {}'.format(name))
                else:
                    log_start('install {} ({})'.format(name, version))
            else:
                log_start('upgrade {} ({} => {})'.format(name, prev_version, version))
            debpkg = apt.debfile.DebPackage(path, cache)
            if not debpkg.check():
                if options.no_skip:
                    action = 'ERROR'
                    rc = 1
                else:
                    action = 'SKIP'
                log_end('{} (not installable)'.format(action))
                continue
            elif debpkg.missing_deps:
                if options.no_skip:
                    action = 'ERROR'
                    rc = 1
                else:
                    action = 'SKIP'
                log_end('{} (missing dependencies)'.format(action))
                continue
            with silenced_stdout():
                debpkg.install()
            cache = apt.cache.Cache()
            log_end('ok')
            log_start('check {}'.format(name))
            output = ipc.check_output([options.adequate, name])
            output = output.decode('ASCII').splitlines()
            output = sorted(output)  # tag order is not deterministic
            if version == versions[-1][0]:
                [pkginfo] = cache[name].versions
                expected = [
                    '{}: {}'.format(name, line)
                    for line in pkginfo.description.splitlines(False)
                    if line
                ]
            else:
                expected = []
            if output == expected:
                log_end('ok')
            else:
                log_end('FAIL')
                diff = list(
                    difflib.unified_diff(expected, output, n=9999)
                )
                for line in diff[3:]:
                    print(line)
                rc = 1
            prev_version = version
        log_start('remove {}'.format(name))
        try:
            pkg = cache[name]
        except KeyError:
            log_end('not needed')
        else:
            pkg.mark_delete(purge=True)
            with silenced_stdout():
                cache.commit()
            log_end('ok')
            cache = apt.cache.Cache()
    sys.exit(rc)

if __name__ == '__main__':
    main()

# vim:ts=4 sw=4 et
