#!/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.4 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] [-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. 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 (..). QUERY OPTIONS -a list all packages, 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. 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 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) dirs=$(find $root -type d) for dir in $dirs do echo $dir | sed "s:$root:mkdir -p $target:" done files=$(find $root ! -type d) 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) files=$(find $root ! -type d) 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() { #test "$1" || set -- ${LTPATH//:/ } test "$1" || 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 } 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= while getopts :acdfilnqst:uv 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;; *) 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#./} test -d $tree && tree=$PWD/$tree || #for d in ${LTPATH//:/ } 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