#!/bin/bash
# vim: set et sw=4 sts=4 tw=80:
# Copyright 2007-2008 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

# A bit of hackery to update everything that is humanly possible
# that maybe related to an older version of python. This script can
# be run as many times as you like.
#
# OLD_PY_VER      = old python version we are upgrading from
# NEW_PY_VER      = new python version we are upgrading to
# PKGS_EXCEPTIONS = packages that should NOT be re-emerged for any reason
# PKGS_MANUAL     = packages that should be re-emerged even if they don't
#                   fit the criteria (eg. ones that have python compiled
#                   statically)
#
# Runtime Variables:
#
# PKGS_TO_REMERGE = list of packages we deem to need re-emerging
# PKGS_OK         = list of packages that should be merged without any problems
# PKGS_MISSING    = list of packages that are installed, but cannot be merged
#                   because they have been pruned from portage
# PKGS_MASKED     = list of packages that are installed, but masked.
#

VERSION="0.5"
NEW_PY_VER=$(python -V 2>&1 | sed 's:Python ::' | cut -d. -f1-2)

PKGS_EXCEPTIONS="dev-lang/python sys-apps/portage"
PKGS_MANUAL="app-office/gnumeric app-office/dia dev-libs/boost x11-libs/vte"

PRETEND=0
IGNORE_VERSIONS=0
VERBOSE=0
PKGS_TO_REMERGE=""
PKGS_COUNT_REMERGE=0
PORTAGE_PYTHON="/usr/bin/python"

SUPPORTED_PMS="portage pkgcore paludis"
PMS_COMMAND=( "emerge" "pmerge" "paludis" )
PMS_OPTIONS=( "-vD1" "-Do" "-i1" )

# Checks
CHECK_ECLASS=0
CHECK_MANUAL=1
CHECK_PYLIBDIR=1
CHECK_SONAME=1

# load the gentoo-style info macros, but hack to get around
# it thinking this is an rc script
EBUILD="1"
source /etc/init.d/functions.sh

# portage variables
PKG_DBDIR=/var/db/pkg

# usage()
# display usage
usage() {
    cat <<EOF_USAGE
${0##*/} -- Find & rebuild packages broken due to a python upgrade

Usage: python-updater [OPTION]

Options:
    -h, --help      Print usage
    -V, --version   Print version
    -p, --pretend   Pretend (don't do anything)
    -v, --verbose   Increase verbosity (may be specified multiple times)
    -o PYVER, --old-version PYVER
                    Set old python version to upgrade from to PYVER
    -i, --ignore-versions
                    Ignore versions when remerging packages
                    (still respects SLOTs)
    -P PM, --package-manager PM
                    Use package manager PM, where PM can be one of:
$(for p in ${SUPPORTED_PMS} ; do
echo -ne $'\t\t    '\* ${p}
if [[ ${p} == portage ]]; then
    echo ' (Default)'
else
    echo
fi
done)
    -c CMD, --command CMD
                    Pipe found packages to command CMD instead of invoking package
                    manager. Only for debug and script use.
    -eCHECK --enable-CHECK
                    Enable CHECK where CHECK can be one of:
                    * eclass   (Disabled by default)
                    * pylibdir (Enabled by default)
                    * soname   (Enabled by default)
                    * manual   (Enabled by default)
    -dCHECK --disable-CHECK
                    Disable CHECK. See --enable option for a list of checks

See CHECKS section in the manpage for explanations about checks and
EXAMPLES section to learn how to use them.
EOF_USAGE
}

# veinfo(verbosity, message)
# einfo message if VERBOSE is bigger than verbosity
veinfo() {
    if [[ VERBOSE -ge $1 ]]; then
        shift
        einfo $@
    fi
}

# get_old_pyver()
# Find old python version, return non-zero if not found
get_old_pyver() {
    for old in 2.5 2.4 2.3 2.2 2.1; do
        if [[ "${old}" != "${NEW_PY_VER}" ]]; then
            if [[ -e /usr/bin/python${old} ]]; then
                echo -n "${old}"
                return 0
            fi
        fi
    done
    eerror "Couldn't determine any previous Python version(s)."
    return 1
}

# get_portage_python(oldpy=2.4,newpy=2.5)
# Find where portage is, in python2.2 or somewhere else?
get_portage_python() {
    local oldpy newpy
    if [[ ! -z "$1" ]]; then
        oldpy="$1"
    else
        oldpy=2.4
    fi

    if [ ! -z "$2" ]; then
        newpy="$2"
    else
        newpy=2.5
    fi

    pybin=/usr/bin/python
    for py in ${pybin} ${pybin}${oldpy} ${pybin}${newpy}; do
        if ${py} -c "import portage" > /dev/null 2>&1; then
            echo -n "${py}"
            return 0
        fi
    done
    eerror "Could't determine portage python"
    return 1
}

# get_portage_portdir()
# Check if portage knows about PORTDIR and return it
get_portage_portdir() {
    local portdir="$(/usr/bin/portageq portdir)"

    if [[ -z "${portdir}" ]]; then
        eerror "Unable to proceed. Can not find PORTDIR. Make sure the command:"
        eerror " "
        eerror "  portageq portdir"
        eerror "returns a value. If it doesn't, make sure you have updated to"
        eerror "latest portage version."
        eerror " "
        eerror "Report bugs to http://bugs.gentoo.org/"
        return 1
    else
        echo -n "${portdir}"
        return 0
    fi
}

# Respect PYUPDATER_OPTIONS
if [[ -n "${PYUPDATER_OPTIONS}" ]]; then
    set -- ${PYUPDATER_OPTIONS} $@
fi

# Command Line Parsing
while [[ -n "$1" ]]; do
    case "$1" in
        -h|--help)
            usage
            exit 0
            ;;
        -V|--version)
            echo "${VERSION}"
            exit 0
            ;;
        -p|--pretend)
            PRETEND=1
            ;;
        -v|--verbose)
            VERBOSE=$(( $VERBOSE + 1 ))
            ;;
        -o|--old-version)
            shift
            OLD_PY_VER="$1"
            ;;
        -i|--ignore-versions)
            IGNORE_VERSIONS=1
            ;;
        -P|--package-manager)
            shift
            PACKAGE_MANAGER="$1"
            case "${PACKAGE_MANAGER}" in
                portage|pkgcore|paludis)
                    ;;
                *)
                    echo "unrecognised package manager selected. please select between ${SUPPORTED_PMS}"
                    exit
                    ;;
            esac

            # PMS_INDEX is used to select the right commands and options for the selected package manager
            PMS_INDEX=0
            for PM in ${SUPPORTED_PMS}; do
                [[ ${PM} == ${PACKAGE_MANAGER} ]] && break
                PMS_INDEX=$((${PMS_INDEX} + 1))
            done
            ;;
        -c|--command)
            shift
            PIPE_COMMAND="$1"
            ;;
        -ee*|--enable-e*)
            CHECK_ECLASS=1
            ;;
        -de*|--disable-e*)
            CHECK_ECLASS=0
            ;;
        -em*|--enable-m*)
            CHECK_MANUAL=1
            ;;
        -dm*|--disable-m*)
            CHECK_MANUAL=0
            ;;
        -ep*|--enable-p*)
            CHECK_PYLIBDIR=1
            ;;
        -dp*|--disable-p*)
            CHECK_PYLIBDIR=0
            ;;
        -es*|--enable-s*)
            CHECK_SONAME=1
            ;;
        -ds*|--disable-s*)
            CHECK_SONAME=0
            ;;
        *)
            usage
            echo "unrecognised option: $1"
            exit 0
            ;;
    esac
    shift
done

# Sanity check
PORTDIR="$(get_portage_portdir)"
[[ $? != 0 ]] && exit 1

# Determine old python version
if [[ -z "${OLD_PY_VER}" ]]; then
    OLD_PY_VER="$(get_old_pyver)"
    if [[ $? -ne 0 ]]; then
        exit 1
    fi
fi

# Get portage python
PORTAGE_PYTHON="$(get_portage_python ${OLD_PY_VER} ${NEW_PY_VER})"
[[ $? != 0 ]] && exit 1


einfo "Starting Python Updater from ${OLD_PY_VER} to ${NEW_PY_VER} :"
if [[ CHECK_SONAME -ne 0 ]]; then
    if ! type -P scanelf >/dev/null 2>&1; then
        ewarn "scanelf not found!"
        ewarn "check soname is disabled."
        CHECK_SONAME=0
    else
        veinfo 1 'check "soname" enabled.'
        OLD_SONAME="$(readlink -n /usr/lib/libpython${OLD_PY_VER}.so)"
        if [[ -z "${OLD_SONAME}" ]]; then
            ewarn "Couldn't find old libpython soname"
            ewarn "Disabling soname check."
            CHECK_SONAME=0
        fi
    fi
else
    veinfo 1 'check "soname" disabled.'
fi
[[ CHECK_PYLIBDIR -ne 0 ]] \
    && veinfo 1 'check "pylibdir" enabled.' \
    || veinfo 1 'check "pylibdir" disabled.'
[[ CHECK_ECLASS -ne 0 ]] \
    && veinfo 1 'check "eclass" enabled.' \
    || veinfo 1 'check "eclass" disabled.'
[[ CHECK_MANUAL -ne 0 ]] \
    && veinfo 1 'check "manual" enabled.' \
    || veinfo 1 'check "manual" disabled.'

# iterate thru all the installed package's contents
for content in `find ${PKG_DBDIR} -name CONTENTS`; do
    # extract the category, package name and package version
    CATPKGVER=$(echo ${content} | sed "s:${PKG_DBDIR}/\(.*\)/CONTENTS:\1:")
    CATPKG="${CATPKGVER%%-[0-9]*}"
    veinfo 2 "Checking ${CATPKGVER}"

    # exclude packages that are an exception, like portage and python itself.
    exception=0
    for exp in ${PKGS_EXCEPTIONS}; do
        if [[ -z "${CATPKG##${exp}}" ]]; then
            veinfo 2 "Skipping ${CATPKG}, reason: exception"
            exception=1
            break;
        fi
    done

    [[ ${exception} == 1 ]] && continue

    # Check if package is in PKGS_MANUAL
    if [[ CHECK_MANUAL -ne 0 ]]; then
        for pkg in ${PKGS_MANUAL}; do
            if [ -z "${CATPKG##${pkg}}" ]; then
                exception=2
                break;
            fi
        done
    fi

    # replace version number by SLOT if IGNORE_VERSIONS != 0
    # Don't ignore versions when SLOT doesn't exist, bug 201848
    if [[ IGNORE_VERSIONS -ne 0 && -f "${content/CONTENTS/SLOT}" ]]; then
        SLOT=$(< ${content/CONTENTS/SLOT})
        CATPKGVER="${CATPKG}:${SLOT}"
    else
        CATPKGVER="=${CATPKGVER}"
    fi

    if [[ ${exception} = 2 ]]; then
        PKGS_TO_REMERGE="${PKGS_TO_REMERGE} ${CATPKGVER}"
        eindent
        einfo "Adding to list: ${CATPKGVER}"
        eindent
        veinfo 1 "check: manual [Added to list manually]"
        eoutdent && eoutdent
        continue
    fi

    if [[ CHECK_PYLIBDIR -ne 0 ]]; then
        # Search for possible old python dirs in CONTENTS
        # /usr/include/python$old
        # /usr/lib/python$old
        # /usr/lib32/python$old
        # /usr/lib64/python$old
        if grep -qe "/usr/\(include\|lib\(32\|64\)\?\)/python${OLD_PY_VER}" ${content}; then
            PKGS_TO_REMERGE="${PKGS_TO_REMERGE} ${CATPKGVER}"
            eindent
            einfo "Adding to list: ${CATPKGVER}"
            eindent
            veinfo 1 "check: pylibdir [ Installed file under old python library directory ]"
            eoutdent && eoutdent
            continue
        fi
    fi

    if [[ CHECK_SONAME -ne 0 ]]; then
        broken_libs="$(scanelf -qBN ${OLD_SONAME} < <(
            grep -e '^obj' ${content} | cut -d' ' -f2))"
        if [[ -n "${broken_libs}" ]]; then
            PKGS_TO_REMERGE="${PKGS_TO_REMERGE} ${CATPKGVER}"
            eindent
            einfo "Adding to list: ${CATPKGVER}"
            eindent
            veinfo 1 "check: soname [ Libraries linked to old libpython found:"
            veinfo 1 "${broken_libs}"
            veinfo 1 "]"
            eoutdent && eoutdent
        fi
    fi

    if [[ CHECK_ECLASS -ne 0 ]]; then
        ENVIRON="${content/CONTENTS/environment.bz2}"
        if bzip2 -dc ${ENVIRON} | grep -qe "^\(export \)\?PYVER=${OLD_PY_VER}"; then 
            PKGS_TO_REMERGE="${PKGS_TO_REMERGE} ${CATPKGVER}"
            eindent
            einfo "Adding to list: ${CATPKGVER}"
            eindent
            veinfo 1 "check: eclass [ Ebuild set PYVER=${OLD_PY_VER} ]"
            eoutdent && eoutdent
            continue
        fi
    fi
done

# Pipe to command if we have one
if [[ -n "${PIPE_COMMAND}" ]]; then
    echo "${PKGS_TO_REMERGE}" | ${PIPE_COMMAND}
    exit $?
fi

# only pretending?
[[ PRETEND -eq 1 ]] && PMS_OPTIONS[${PMS_INDEX}]="${PMS_OPTIONS[${PMS_INDEX}]}p"

# (Pretend to) remerge packages
if [[ -n "${PKGS_TO_REMERGE}" ]]; then
    ${PMS_COMMAND[${PMS_INDEX}]} ${PMS_OPTIONS[${PMS_INDEX}]} ${PKGS_TO_REMERGE}
else
    einfo "No packages needs to be remerged."
fi
