#!/bin/sh
# This script is POSIX sh(1) compliant, i.e modern sh, ksh, bash but not
# old bourne shell (sh).

# Copyright (C) 2002 Marc Vertes

# 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, 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.

# lt-0.5

test "$HOME" = ~ || exec ksh $0 "$@"	# try ksh if sh too old (not yet POSIX)
usage()
{
cat << EOT
NAME
  lt - link trees, a package management tool
SYNOPSIS
  lt [-nuilaqfv] [-x pat] [-t tdir] [afile | dir ...]
DESCRIPTION
  lt is a package management tool, which can be used to install, query,
  check, update and remove software packages. Each package is located
  in its own directory, then symbolic links are used to make package
  files accessible in traditional common location (/usr/local). No database
  is used, and lt is simple and stateless.

  TREE ORGANISATION
  filesystem is organized the following way:

    usr/local/
    \_ pkg/
    |  \_ sample-1.0/
    |     \_ bin/
    |     |  \_ cmd1
    |     |  \_ cmd2
    |     \_ lib/
    |        \_ lib1
    \_ bin/
    |  \_ cmd1 -> ../pkg/sample-1.0/bin/cmd1
    |  \_ cmd2 -> ../pkg/sample-1.0/bin/cmd1
    \_ lib/
       \_ lib1 -> ../pkg/sample-1.0/lib/lib1

   PACKAGE GENERATION
   The best way is to configure the target directory to be
   the physical path:

     $ ./configure --prefix=/usr/local/pkg/sample-1.0

   Alternatively, the target directory may be specified at
   install:

     $ make install PREFIX=/usr/local/pkg/sample-1.0

OPTIONS
  GENERAL OPTIONS
  -v            verbose mode: print actions as they are run.
  -s		silent mode: only errors are displayed.
  -n		debug mode: print actions instead of run.
  -f		force mode: ignore errors. Otherwise, by default, lt exits at 
                the first encountered error.
  -t tdir	set target to tdir. By default, it is parent directory (..).
  -x pat	exclude all files matching pattern pat, relative to item.
  		this options can be specified multiple times.

  QUERY OPTIONS
  -a afile	list all installed version for afile package, or
		everything if afile isempty, active (marked with *)
		or inactive (marked with -).
  -l afile	print all the files belonging to the same package as afile.
  -q afile	print the package which file afile belongs to.
  -c dir	check mode: verify that all files belonging to given package
		dir are correctly installed.

  INSTALL/UNINSTALL OPTIONS
  -i dir	install mode: create symbolic links to package dir content.
  -d dir    	delete mode: delete links instead of creating them.
  -u dir	upgrade mode: for a given package, uninstall any conflicting
  		package, then install dir package.
EXAMPLES
  To print the package version that contains a command:

     $ lt -f command

  To update a package:

     $ lt -u new_package_vers

TODO
  Generic way to handle package meta-data. This will allow package description,
  handling dependencies, ...
ENVIRONMENT
  LTPATH	search path for package directories
AUTHOR
  Marc Vertes <mvertes@free.fr>
EOT
}

readlink() { typeset res=$(ls -l $1); echo ${res#* -\> }; }
unlock()   { test "$lockfile" && test -f $lockfile && rm $lockfile; }
err()	   { [ "$noerror" ] || echo $* 1>&2; }

lock()
{
	test -f $lockfile && { echo "lock file $lockfile found. Abort." 1>&2; exit 1; }
	echo $$ $cmd >$lockfile
}

linkfile()
{
	to=$1
	from=$2
	dfrom=${from%/*}
	bfrom=${from##*/}
	dd=${dfrom#*/}
	[ $dd = $dfrom ] && { dd=""; nd="."; } 	# file in pkg rootdir
	tofile=$to/$dd/$bfrom
	if [ -r $tofile ]
	then
		err "lt error: $tofile already exists: $(querytree $tofile q)"
		test "$ignore" || { echo abort 1>&2; exit 1; }
	fi
	[ "$dd" ] && nd=$(echo $dd | sed 's:/[^/]*:/..:g; s:[^/]*/:../:g; s:[^/]*:..:')
	echo ln -s $nd/$bdir/$from $to/$dd/$bfrom | sed 's: \./: :;s://:/:'
}

unlinkfile()
{
	to=$1
	from=$2
	dfrom=${from%/*}
	bfrom=${from##*/}
	dd=${dfrom#*/}
	[ $dd = $dfrom ] && { dd=""; nd="."; } 	# file in pkg rootdir
	[ "$dd" ] && nd=$(echo $dd | sed 's:/[^/]*:/..:g; s:[^/]*/:../:g; s:[^/]*:..:')
	echo rm $to/$dd/$bfrom | sed 's: \./: :;s://:/:'
}

linktree()
{
	root=${1%/}
	test -d $root || { echo "lt error: $root not a directory. abort." 1>&2; exit 1; }
	root=${root##*/}
	cd $1/..
	bdir=${PWD##*/}
	target=$(cd $targetdir; pwd)
	for i in $xpat; do fstr="$fstr -path $root/$i -prune -o"; done
	dirs=$(find $root $fstr -type d -print)
	for dir in $dirs
	do
		echo $dir | sed "s:$root:mkdir -p $target:" 
	done
	files=$(find $root $fstr ! -type d -print)
	for file in $files
	do
		$(echo $file | sed "s:$root:linkfile $target ${root##*/}:")
	done
}

unlinktree()
{
	root=${1%/}
	test -d $root || { echo "lt error: $root not a directory. Abort" 1>&2; exit 1; }
	root=${root##*/}
	cd $1/..
	target=$(cd $targetdir; pwd)
	for i in $xpat; do fstr="$fstr -path $root/$i -prune -o"; done
	files=$(find $root $fstr ! -type d -print)
	for file in $files
	do
		$(echo $file | sed "s:$root:unlinkfile $target ${root##*/}:")
	done
}

# arg1 = file, arg2 = q (retrieve package name) or l (retrieve package list)
querytree()
{
	file=$1
	case $file in
	/*) :;;
	*)  file=${file#./} 
	    [ -f $file ] && file=$PWD/${file##*/} || file=$(which $file);;
	esac
	test -L $file || return
	fdir=${file%/*}
	lfile=$(readlink $file)
	case $lfile in
	../*)	:;;
	pkg/*)	:;;	# file in pkg rootdir (should be smarter)
	*)	return;;
	esac
	bpkg=${lfile##*../}; bpkg=${bpkg#*/}; bpkg1=${bpkg%%/*}
	tail=${lfile%$bpkg}$bpkg1; head=${tail##*../}
	pkgdir=$(cd $fdir/$tail; pwd)
	case $2 in
	q) echo $pkgdir;;
	l) find $pkgdir ! -type d | sed "s:$head/::";;
	esac
}

# (not so) simple heuristic to find quickly all installed packages
scantree()
{
	[ "$1" ] && post="grep $1" || post=cat
	#test "$1" || set -- ${LTPATH//:/ }
	#test "$1" || set -- $(echo $LTPATH | tr : ' ')
	set -- $(echo $LTPATH | tr : ' ')
	for bdir; do
		test -d $bdir || continue
		cd $bdir
		for dir in *; do
			[ -d $dir ] || continue
			cd $dir
			filefound=0
			for f in */*; do
				if [ -f $f ]; then
					test -L ${bdir%/*}/$f &&
					test "$(readlink ${bdir%/*}/$f)" = "../${bdir##*/}/$dir/$f" && 
					echo "*	$bdir/$dir" || echo "-	$bdir/$dir"
					filefound=1
					break
				fi
			done
			[ $filefound = 1 ] && { cd ..; continue; }
			for f in *; do
				if [ -f $f ]; then
					test -L ${bdir%/*}/$f &&
					test "$(readlink ${bdir%/*}/$f)" = "${bdir##*/}/$dir/$f" &&
					echo "*	$bdir/$dir" || echo "-	$bdir/$dir"
					break
				fi
			done
			cd ..
		done
	done | $post
}

checktree()
{
	res=0
	while read cmd opt src dest
	do
		[ "$cmd" = ln ] || continue
		#echo $dest xxxx $src
		test -L $dest || { 
			[ "$1"  = upgrade ] && continue
			res=1
			test -f $dest && echo "[ERROR] $dest not a symlink" ||
			echo "[ERROR] $dest missing"
			continue
		}
		[ "$1" = upgrade ] && { querytree $dest q; continue; }
		lnk=$(readlink $dest)
		[ "$lnk" = "$src" ] || { res=1; echo "[ERROR] bad symlink $dest -> $lnk"; continue; }
		[ "$verbose" ] && echo "[ok] $dest" 
	done
	return $res
}

updatetree()
{
	pkgs_to_del=$(linktree $1 | checktree upgrade | sort -u)
	for pkg in $pkgs_to_del; do unlinktree $pkg; done
	linktree $1
}

action=linktree
targetdir=..
post=sh
cmd="$0 $@"
LTPATH=${LTPATH:=/usr/local/pkg}
ignore=
noerror=
silent=
verbose=1
force=
xpat=
while getopts :acdfilnqst:uvx: opt
do
	case $opt in
	a) action=scantree;;
	c) post=checktree; ignore=1; noerror=1;;
	d) action=unlinktree;;
	f) force=1; ignore=1;;
	i) action=linktree;;
	l) action=querytree; querymode=l;;
	n) post=cat; ignore=1; echo "# Debug mode";;
	q) action=querytree; querymode=q;;
	s) silent=1; verbose=;;
	t) targetdir=$OPTARG;;
	u) action=updatetree; ignore=1; noerror=1;;
	v) verbose=1;;
	x) xpat="$xpat $OPTARG";;
	*) usage;;
	esac
done
shift $((OPTIND - 1))

[ "$verbose" ] && post="$post -v"
[ "$silent" -a ! "$verbose" ] && exec 1>/dev/null
lockfile=/tmp/lt.lock
lock && trap unlock EXIT

case $action in
querytree)	[ "$1" ] && querytree $1 $querymode || scantree; exit;;
scantree)	scantree $*; exit;;
esac

for tree
do
	case $tree in
	/*)	:;;
	*)	tree=${tree#./}
		for d in $(echo $LTPATH | tr : ' ')
		do
			test -d $d || continue
			test -d $d/$tree && { tree=$d/$tree; break; }
		done;;
	esac
	$action $tree | $post
done
