#!/bin/sh
# Copyright 2009 Palm, Inc.  All Rights Reserved.

# NOTE: busybox sort leaves a 0-length file when using "sort -o x x", so
#       there are several lines where I use "sort x > tmp; mv tmp x" instead

INFODIR=/usr/lib/ipkg/info
STATUSFILE=/usr/lib/ipkg/status
ROOT=/
PREFIX=
TMP=/tmp/mmipkg.$$
OLD_IPKS=$TMP/old_ipks
MOD_IPKS=$TMP/mod_ipks
OLD_FILE_MAP=$TMP/file_map.old
NEW_FILE_MAP=$TMP/file_map.new
ALL_PKGS=$TMP/all_pkgs

noaction=0
verbose=0

usage()
{
    echo "mmipkg [-v] [-o root] [-n] install <list of ipks>" >&2
    echo "mmipkg [-v] [-o root] [-n] remove <list of pkgs>" >&2
    echo "mmipkg [-v] [-o root] [-n] info <list of pkgs>" >&2
    echo "mmipkg [-v] [-o root] [-n] status <list of pkgs>" >&2
    echo "mmipkg [-v] [-o root] [-n] files <list of pkgs>" >&2
}

errmsg()
{
    echo Error: $1
    exit 1
}

usageerr()
{
    echo Error: $1
    usage
    exit 1
}

echo_script_contents()
{
    echo "    Script contents:"
    echo "    ----------------"
    sed 's/^/    /' $INFODIR/$1.$2
    echo "    ----------------"
}

echo_run_script()
{
    if [ $verbose -eq 0 ]
    then
	return
    fi
    if [ -f $INFODIR/$1.$2 ]
    then
	if [ $ROOT = "/" ]
	then
	    echo "  Running $2 script for $1"
	    echo_script_contents $1 $2
	else
	    echo "  Not running $2 script for $1"
	fi
    fi
}

echo_list_files()
{
    if [ $verbose -eq 1 ]
    then
	echo "  $1 data files for $2"
	sed 's/^/    /' $INFODIR/$2.list
    fi
}

echo_control_files()
{
    if [ $verbose -eq 1 ]
    then
	ls -1 $INFODIR/$1.* | sort | sed 's/^/    /'
    fi
}

run_script()
{
    if [ -f $INFODIR/$1.$2 ]
    then
	if [ $ROOT = "/" ]
	then
	    echo "  Running $2 script for $1"
	    if [ $verbose -eq 1 ]
	    then
		echo_script_contents $1 $2
	    fi
	    if ! $INFODIR/$1.$2
	    then
		errmsg "Script execution failed"
	    fi
	else
	    echo "  Not running $2 script for $1"
	fi
    fi
}

remove()
{
    if [ $noaction -eq 1 ]
    then
	if [ ! -f $INFODIR/$1.control ]
	then
	    errmsg "No such package $1"
	fi
	if [ $verbose -eq 1 ]
	then
	    echo "Removing $1"
	fi
	echo_run_script $1 prerm
	echo_list_files Removing $1
	echo_run_script $1 postrm
	return
    fi
    echo "Removing $1"
    run_script $1 prerm
    if [ -f $INFODIR/$1.list ]
    then
	echo "  Removing data files for $1"
	if [ $verbose -eq 1 ]
	then
	    sed 's/^/    /' $INFODIR/$1.list
	fi
	while read file
	do
	    rm "$file"
	done < $INFODIR/$1.list
    fi
    run_script $1 postrm
    if [ -f $INFODIR/$1.control ]
    then
	echo "  Removing control files for $1"
	echo_control_files $1
	rm $INFODIR/$1.*
    fi
}

NEEDED_FILES="\
    ld-linux.so.3 \
    libc.so.6 \
    libm.so.6 \
    librt.so.1 \
    libpthread.so.0 \
    [ \
    ar \
    arm-none-linux-gnueabi-ar \
    busybox \
    cat \
    comm \
    cut \
    echo \
    grep \
    gzip \
    ls \
    mkdir \
    mv \
    rm \
    sed \
    sh \
    sort \
    tar \
"

copy_needed_files()
{
    if [ $ROOT != "/" -o $noaction -eq 1 ]
    then
	return
    fi
    PATH=$TMP/bin:$TMP/usr/bin:$PATH
    LD_LIBRARY_PATH=$TMP/lib
    mkdir -p $TMP/bin $TMP/usr/bin $TMP/lib
    for file in $NEEDED_FILES
    do
	if [ -f /lib/$file ]
	then
	    cp /lib/$file $TMP/lib
	elif [ -L /bin/$file ]
	then
	    ln -s busybox $TMP/bin/$file
	elif [ -f /bin/$file ]
	then
	    cp /bin/$file $TMP/bin
	elif [ -L /usr/bin/$file ]
	then
	    ln -s $(readlink /usr/bin/$file) $TMP/usr/bin/$file
	else [ -f /usr/bin/$file ]
	    cp /usr/bin/$file $TMP/usr/bin
	fi
    done
# Probably can delete this block eventually
    if [ $verbose -eq 1 ]
    then
	echo "Needed files:"
	ls -lR $TMP/bin $TMP/usr/bin $TMP/lib | sed 's/^/    /'
    fi
}

validate_ipk()
{
    case "$1" in
	*.ipk) ;;
	*) errmsg "$1 does not end with .ipk";;
    esac
    if [ ! -r $1 ]
    then
	errmsg "Cannot read $1"
    fi
    if ! ar t $1 > /dev/null 2>&1
    then
	errmsg "Cannot read ipk contents for $1"
    fi
    if ! ar p $1 control.tar.gz | tar tzf - > /dev/null 2>&1
    then
	errmsg "Cannot unpack control file for $1"
    fi
    if ! ar p $1 data.tar.gz | tar tzf - > /dev/null 2>&1
    then
	errmsg "Cannot unpack data file for $1"
    fi
}

unpack_control()
{
    echo "  Unpacking control file for $2"
    if ! $TMP/bin/mkdir -p $INFODIR
    then
	errmsg "Error creating info directory"
    fi
    if ! ar p $1 control.tar.gz |
	tar -C $INFODIR --transform "s,/\(.*\),/$2.\1," -xzf -
    then
	errmsg "Error unpacking control files for $2"
    fi
    echo "  Installing control files for $2"
    echo_control_files $2
}

unpack_data()
{
    echo "  Unpacking data file for $2"
    if ! ar p $1 data.tar.gz |
      tar -C $ROOT -xvzf - |
      grep -v '/$' | cut -c2- |
      sort | sed "s,^,$PREFIX," > $INFODIR/$2.list
    then
	errmsg "Error unpacking data for $2"
    fi
    echo_list_files Installing $2
}

echo_install_script()
{
    if [ $verbose -eq 0 ]
    then
	return
    fi
    if ar p $1 control.tar.gz | tar tzf - | grep -q $2
    then
	if [ $ROOT = "/" ]
	then
	    echo "  Running $2 script for $1"
	    echo "    Script contents:"
	    echo "    ----------------"
	    ar p $1 control.tar.gz | tar xOzf - ./$2 | sed 's/^/    /'
	    echo "    ----------------"
	else
	    echo "  Not running $2 script for $1"
	fi
    fi
}

echo_unpack_data()
{
    if [ $verbose -eq 0 ]
    then
	return
    fi
    echo "  Installing data files for $1"
    ar p $1 data.tar.gz |
      tar tzf - | grep -v '/$' | cut -c2- | sort | sed "s,^,    $PREFIX,"
}

convert_ipk_to_pkg()
{
    local bname=${1##*/}
    local rname=${bname%.ipk}
    echo ${rname%%_*}
}

install_prep()
{
    pkg=$(convert_ipk_to_pkg $1)
    if [ -f $INFODIR/$pkg.control ]
    then # Existing package
	# Create a list of all existing packages being updated
	echo $pkg >> $ALL_PKGS
	# Create a mapping from the package to the ipk file
	echo ${pkg}:$1 >> $OLD_IPKS
	# Create a list of mappings from the existing files to its package
	while read file
	do
	    echo ${file}:$pkg
	done < $INFODIR/$pkg.list >> $OLD_FILE_MAP
	# Create a list of mappings from the existing files to its new package
	ar p $1 data.tar.gz | tar tzf - |
	  grep -v '/$' | cut -c2- | sed "s,^,$PREFIX," |
	    while read file
	    do
		echo ${file}:$pkg
	    done >> $NEW_FILE_MAP
    else # New package
	NEW_IPKS="$NEW_IPKS $1"
    fi
}

# Based on a list of packages which are being updated (in $ALL_PKGS), a
# mapping of packages to ipk filenames (in $OLD_IPKS), and a mapping of
# files to package for both the existing system (in $OLD_FILE_MAP) and the
# system # after the update (in $NEW_FILE_MAP), create a file ($MOD_IPKS)
# which lists the updated ipks in an order so that the install will work
analyze()
{
    test -f $ALL_PKGS || return

    ONLY_OLD=$TMP/only_old
    ONLY_NEW=$TMP/only_new
    OLD_FILES=$TMP/old_files
    NEW_FILES=$TMP/new_files
    MOVED_FILES=$TMP/moved_files
    DEPS=$TMP/deps
    NO_DEPS=$TMP/no_deps

    # Find the file:pkg mappings that are only in the existing set of packages
    # or only in the new set of packages
    sort $OLD_FILE_MAP > $TMP/x; mv $TMP/x $OLD_FILE_MAP
    sort $NEW_FILE_MAP > $TMP/x; mv $TMP/x $NEW_FILE_MAP
    comm -23 $OLD_FILE_MAP $NEW_FILE_MAP > $ONLY_OLD
    comm -13 $OLD_FILE_MAP $NEW_FILE_MAP > $ONLY_NEW

    # Find which files moved between packages
    cut -d: -f1 $ONLY_OLD | sort > $OLD_FILES
    cut -d: -f1 $ONLY_NEW | sort > $NEW_FILES
    comm -12 $OLD_FILES $NEW_FILES > $MOVED_FILES

    # Create a dependency map
    while read file
    do
	echo $(grep "^$file:" $ONLY_OLD | cut -d: -f2):$(grep "^$file:" $ONLY_NEW | cut -d: -f2)
    done < $MOVED_FILES | sort -u > $DEPS

    sort $ALL_PKGS > $TMP/x; mv $TMP/x $ALL_PKGS
    while :
    do
	test -s $DEPS || break
	# Find the packages without a dependency (they can be installed first)
	cut -d: -f2 $DEPS | sort | comm -13 - $ALL_PKGS > $NO_DEPS
	test -s $NO_DEPS || errmsg "Circular dependency found"
	while read pkg
	do
	    # Remove the package from the dependency file and the package list
	    sed -i "/$pkg:/d" $DEPS
	    sed -i "/$pkg$/d" $ALL_PKGS
	    # Add the ipk for this package to the output list
	    grep "^$pkg:" $OLD_IPKS | cut -d: -f2 >> $MOD_IPKS
	done < $NO_DEPS
	rm $NO_DEPS
    done
    # Add the ipks for the remaining packages to the output list
    while read pkg
    do
	grep "^$pkg:" $OLD_IPKS | cut -d: -f2
    done < $ALL_PKGS >> $MOD_IPKS
}

install()
{
    echo "Installing $1"
    if [ $noaction -eq 1 ]
    then
	validate_ipk $1
	echo_install_script $1 preinst
	echo_unpack_data $1
	echo_install_script $1 postinst
	return
    fi
    pkg=$(convert_ipk_to_pkg $1)
    if [ -n "$2" ]
    then
	remove $pkg | sed 's/^/  /'
    fi
    unpack_control $1 $pkg
    run_script $pkg preinst
    unpack_data $1 $pkg
    run_script $pkg postinst
}

generate_status()
{
    if [ $noaction -eq 0 ]
    then
        rm -f $STATUSFILE
        for control in $INFODIR/*.control
        do
            grep -E '^Package|^Version|^Depends' $control
            echo "Status: install user installed"
            grep '^Architecture' $control
            echo ""
        done > $STATUSFILE
    fi
}

info()
{
    if [ -f $INFODIR/$1.control ]
    then
	#cat $INFODIR/$1.control
	grep -E '^Package|^Version|^Depends' $INFODIR/$1.control
    else
	errmsg "No such package $1"
    fi
}

files()
{
    if [ -f $INFODIR/$1.control ]
    then
	cat $INFODIR/$1.list
    else
	errmsg "No such package $1"
    fi
}

# Parse arguments
while getopts hno:v options
do
    case $options in
	 h) usage; exit 0;;
	 n) noaction=1;;
	 o) ROOT=$OPTARG;;
	 v) verbose=1;;
	\?) usageerr "Unknown option";;
    esac
done
shift $(($OPTIND - 1))

test $# -ne 0 || usageerr "Missing command"
case $ROOT in
    /*) ;;
     *) usageerr "-o value must be an absolute path" ;;
esac

if [ $ROOT != "/" ]
then
    INFODIR=$ROOT/usr/lib/ipkg/info
    STATUSFILE=$ROOT/usr/lib/ipkg/status
    PREFIX=$ROOT
fi

case $1 in
    install)
	shift
	mkdir $TMP
	touch $MOD_IPKS
	copy_needed_files
	# Analyze ipks and put them in a valid install order
	for ipk
	do
	    install_prep $ipk
	done
	analyze
	# Install modified ipks
	while read ipk
	do
	    install $ipk mod
	done < $MOD_IPKS
	# Install new ipks
	for ipk in $NEW_IPKS
	do
	    install $ipk
	done
	rm -rf $TMP
	generate_status;;
    remove)
	shift
	for pkg
	do
	    remove $pkg
	done;;
    info|status)
	shift
	for pkg
	do
	    info $pkg
	done;;
    files)
	shift
	for pkg
	do
	    files $pkg
	done;;
    *) usageerr "Unrecognized command $1";;
esac
