#!/usr/bin/env zsh # Recommended: hg >= 1.0 PDIR="$HOME/packages" PKG_ROOT="ssh://derf.homelinux.org/~/var/packages_root" CL_OPTIONS=(-q) info=$'\e[0;36m' error=$'\e[0;31m' reset=$'\e[0m' ## ## Internal functions for displaying stuff ## info () { echo -ne "${info}$*${reset}" } warn () { echo -ne "${error}$*${reset}" > /dev/stderr } die () { echo -ne "${error}$*${reset}" > /dev/stderr exit 100 } check_installed () { [ -n "$1" -a -d $PDIR/$1 ] || die "Not installed: $1\n" } check_valid () { [ -d $PDIR/$1/.hg -o -d $PDIR/$1/.git ] || die "Not a valid package name: '$1'\n" } clear_line () { echo -ne "\r \r" } # Read local configuration if ([ -f $HOME/.pkg.conf ]) { . $HOME/.pkg.conf } export PDIR export PKG_ROOT ## ## Make sure everything's sane ## Warn otherwise ## # we need sed -r (and sed -i, but not checking for that here) QUUX=$(echo foox | sed -r 's/^fo{2}(.)$/quu\1/' 2> /dev/null) if ([ "$QUUX" != 'quux' ]) { warn "sed is not working properly. This may produce unexpected behaviour.\n" } if ([ ! -d $PDIR ]) { die "$PDIR not found!!\n" } ## ## Some additional variables related to PKG_ROOT ## # Protocol if (echo "$PKG_ROOT" | grep "^ssh" &> /dev/null) { PKG_PROTO='ssh' } elif (echo "$PKG_ROOT" | grep "^/" &> /dev/null) { PKG_PROTO='file' } else { false } # Host if ([ "$PKG_PROTO" = "ssh" ]) { PKG_HOST=$(echo "$PKG_ROOT" | sed 's!^ssh://!!' | sed -r 's!^([^/]*)/.*$!\1!') } # Remote path if ([ "$PKG_PROTO" = "ssh" ]) { PKG_PATH=$(echo "$PKG_ROOT" | sed 's!^ssh://'"$PKG_HOST"'/!!') } elif ([ "$PKG_PROTO" = "file" ]) { PKG_PATH="$PKG_ROOT" } ## ## Ask the user for confirmation ## # Default reply: Yes confirm_yes () { echo -n "$* [Y/n] " read -k 1 [ $REPLY != $'\n' ] && echo if ([ "$REPLY" = 'y' -o "$REPLY" = 'Y' -o "$REPLY" = $'\n' ]) { true } else { false } } # Default reply: No confirm_no () { echo -n "$* [y/N] " read -q } ## ## Major internal functions ## ## Naming: ## vcs_ do something by accessing the package ## lst_ do something by accessing the list ## VCS Wrappers vcs_to_lst () { if ([ -d $1/.hg ]) { echo -n "$1 hg " hg -R $1 log | fgrep changeset | head -n 1 | cut -d ' ' -f 4 } elif ([ -d $1/.git ]) { echo -n "$1 git " git --git-dir=$1/.git log | fgrep commit | head -n 1 | cut -d ' ' -f 2 } } vcs_add () { cd "$PDIR" case $(lst_type "$1") in git) git clone "$PKG_ROOT/$1" ;; hg) hg clone "$PKG_ROOT/$1" ;; *) die "Cannot handle repository format '$1'\n" ;; esac } lst_incoming () { [ "$(lst_local_version $1)" != "$(lst_remote_version $1)" ] } vcs_log () { [ -d .hg ] && {hg glog | less ; return} [ -d .git ] && {git log ; return} } lst_type () { grep ^"$1 " $PDIR/.list-remote | cut -d ' ' -f 2 } lst_update_remote () { if ([ "$PKG_PROTO" = 'ssh' ]) { scp -q $PKG_HOST:$(echo $PKG_ROOT | cut -d / -f 4- | sed 's!~/!!')/.list .list-remote } elif ([ "$PKG_PROTO" = 'file' ]) { cp $PKG_ROOT/.list .list-remote } } lst_update_local () { cd $PDIR rm -f .list for i in *(@,/); { vcs_to_lst $i >> .list } } lst_update_package () { cd $PDIR LIST=$(cat .list | grep -v ^"$1 ") echo $LIST > .list vcs_to_lst $1 >> .list } lst_remove_package () { cd $PDIR LIST=$(cat .list | grep -v ^"$1 ") echo $LIST > .list } vcs_upgrade () { [ -d .hg ] && {hg pull --update ; return} [ -d .git ] && {git pull ; return} } lst_local_version () { grep ^"$1 " $PDIR/.list | cut -d ' ' -f 3 } lst_remote_version () { grep ^"$1 " $PDIR/.list-remote | cut -d ' ' -f 3 } # Return an understandable priority from the numeric one real_priority () { case "$1" in 6) echo "essential" ;; 5) echo "important" ;; 4) echo "required" ;; 3) echo "standard" ;; 2) echo "optional" ;; 1) echo "extra" ;; *) echo ;; esac } # Execute a hook exec_hook () { package="$1" hook="$2" if ([ -r $PDIR/$package/hooks/$hook ]) { info "Executing $hook hook\n" cd $PDIR/$package . hooks/$hook } } # Check dependencies and offer to install them check_deps () { [ -r $PDIR/$1/dependencies ] || return 0 DEPS=($(cat $PDIR/$1/dependencies)) INSTALL=() for dep in $DEPS; { if ([ ! -d $PDIR/$dep ]) { INSTALL+="$dep" } } if ([ -n "$INSTALL" ]) { info "$1 has unmet dependencies: " echo "$INSTALL" if (confirm_yes "Install dependencies?") { for pkg in $INSTALL; { pkg_add "$pkg" } } } } check_conflicts () { [ -r $PDIR/$1/conflicts ] || return 0 CONFLICTS=($(cat $PDIR/$1/conflicts)) REMOVE=() for conflict in $CONFLICTS; { if ([ -d $PDIR/$conflict ]) { REMOVE+="$conflict" } } if ([ -n "$REMOVE" ]) { info "$1 conflicts with the following packages: " echo "$REMOVE" if (confirm_no "Remove conflicting packages and proceed with installation?") { for pkg in $REMOVE; { pkg_remove "$pkg" } } else { die "Installation aborted" } } } # Write a packages' files to .collected # Currently, this is only documentation populate_collected () { cd $PDIR/$1 || return info "Enabling documentation " if ([ -d man ]) { for i in man/*/*; { section=${i:h:t} manpage=${i:t} if (podchecker man/$section/$manpage &> /dev/null) { pod2man -s $section -c "$1 package" -r "/home/derf" man/$section/$manpage > $PDIR/.collected/man/man$section/$manpage.$section echo -n "+" } else { echo -n "." } } } if ([ -d bin ]) { for i in bin/*; { if (podchecker $i &> /dev/null) { pod2man $i > $PDIR/.collected/man/man1/${i:t}.1 echo -n "+" } else { echo -n "." } } } clear_line if ([ -d bin ]) { for i in bin/*(*); { if ([ "$(readlink $HOME/$i)" != "../$1/$i" ]) { rm -f "$HOME/$i" ln -s ../${PDIR//$HOME\/}/$1/$i $HOME/$i } } } } # Remove a packages' files from .collected # Assuming there are no packages with colliding files # TODO: Make sure there are none genocide_collected () { cd $PDIR/$1 || return info "Removing documentation" if ([ -d man ]) { for i in man/*/*; { section=${i:h:t} manual=${i:t} if ([ -e $PDIR/.collected/man/man$section/$manual ]) { rm $PDIR/.collected/man/man$section/$manual } } } if ([ -d bin ]) { for i in bin/*; { rm -f $PDIR/.collected/man/man1/${i:t}.1 } } clear_line if ([ -d bin ]) { for i in bin/*(*); { if ([ "$(readlink $HOME/$i)" = "../${PDIR//$HOME\/}/$1/$i" ]) { rm -f $HOME/$i } } } } ## ## Finally - the functions actually doing something ## pkg_add () { if ([ -d $PDIR/$1 ]) { info "Package '$1' is already installed!\n" return 100 } cd $PDIR || return 255 info "Retrieving package $1...\n" vcs_add "$1" || return 255 if ([ -r $PDIR/$1/Makefile ]) { info "Building binaries\n" cd $PDIR/$1 make } exec_hook "$1" "post-add" check_deps "$1" check_conflicts "$1" cd $PDIR/$1 checklinks $CL_OPTIONS populate_collected "$1" lst_update_package $1 } pkg_remove () { check_installed "$1" check_valid "$1" cd $PDIR/$1 if ([ -r priority ]) { if ([ $(cat priority) -gt 3 ]) { confirm_no "Package '$1' is $(real_priority $(cat priority)). Really remove?" || return } } exec_hook "$1" "pre-remove" genocide_collected "$1" lst_remove_package $1 rm -rf $PDIR/$1 info "Package removed.\n" } pkg_upgrade () { check_installed "$1" cd $PDIR/$1 info "Looking for updates: $1" NEW=$(lst_incoming "$1") if ([ $? = 0 ]) { clear_line info "Updating $1 to $(lst_remote_version $1)\n" vcs_upgrade check_deps "$1" if ([ -r Makefile ]) { info "Building binaries\n" make } exec_hook "$1" "post-update" checklinks $CL_OPTIONS populate_collected "$1" lst_update_package $1 } else { clear_line } cd $PDIR } # If no package was specified, update everything pkg_upgrade_wrapper () { if ([ -n "$1" ]) { pkg_upgrade "$1" } else { cd $PDIR for i in *(/); { pkg_upgrade "$i" } } } # Change the 'default' url in every package's .hgrc pkg_changesrc () { cd $PDIR for i in *(/); { if ([ -f $i/.hg/hgrc ]) { sed -i "s!default = [^:]*://.*\$!default = $1/$i!" $i/.hg/hgrc } } } pkg_list_installed () { cut -d ' ' -f 1 $PDIR/.list } pkg_list_available () { cut -d ' ' -f 1 $PDIR/.list-remote } pkg_status () { check_installed "$1" cd $PDIR/$1 check_deps "$1" check_conflicts "$1" checklinks $CL_OPTIONS } # Same as with update - if no package is specified, check all pkg_status_wrapper () { if ([ -n "$1" ]) { pkg_status "$1" } else { cd $PDIR for i in *(/); { pkg_status "$i" } } } pkg_update () { cd $PDIR info "Updating package list..." lst_update_remote lst_update_local clear_line } pkg_update_remote () { cd $PDIR lst_update_remote } pkg_update_local () { lst_update_local } # Various information related to a package pkg_info () { cd $PDIR # Fetch the infos NAME="$1" LOCAL_VERSION=$(lst_local_version $1) REMOTE_VERSION=$(lst_remote_version $1) REPO_TYPE=$(lst_type $1) if ([ -d $1 ]) { cd $1 if ([ -r priority ]) { PRIORITY=$(cat priority) PRIORITY_NAME=$(real_priority "$PRIORITY") } if ([ -r dependencies ]) { DEPENDENCIES=$(cat dependencies | tr "\n" " " | sed 's/ /, /g' | sed 's/, $//') } if ([ -r tags ]) { TAGS=$(cat tags | tr "\n" " " | sed 's/ /, /g' | sed 's/, $//') } if ([ -d hooks ]) { HOOKS=$(ls hooks) } if ([ -r Makefile ]) { MAKEFILE=1 } SIZE=$(du -sh .$(lst_type $1) | grep -o '.*[KMG]') if ([ -r description ]) { DESCRIPTION=$(cat description) } } show_info () { [ -z "$2" ] && return info "$1: " echo "$2" } show_info "Package" "$NAME" show_info "Priority" "$PRIORITY ($PRIORITY_NAME)" show_info "Local Version" "$LOCAL_VERSION" show_info "Remote Version" "$REMOTE_VERSION" show_info "Repository Type" "$REPO_TYPE" show_info "Repostiry Size" "$SIZE" show_info "Depends" "$DEPENDENCIES" show_info "Tags" "$TAGS" show_info "Hooks" "$HOOKS" show_info "Description" "$DESCRIPTION" } pkg_log () { check_installed "$1" cd $PDIR/$1 vcs_log } pkg_changelog () { [ -r $PDIR/$1/changelog ] && view $PDIR/$1/changelog } # Almost obsoleted by the bin/* -> man/ stuff pkg_doc () { if ([ -r $PDIR/.collected/man/$1 ]) { man $PDIR/.collected/man/$1 } elif ([ -r $PDIR/.collected/doc/$1 ]) { less $PDIR/.collected/doc/$1 } else { echo "No documentation found" } } ## ## Now what shall we do... ## case "$1" in add) pkg_add "$2" ;; changelog) pkg_changelog "$2" ;; changeroot) pkg_changesrc "$2" ;; delete) pkg_remove "$2" ;; doc) pkg_doc "$2" ;; info) pkg_info "$2" ;; install) pkg_add "$2" ;; list) pkg_list_installed ;; list-all) pkg_list_available ;; local-update) pkg_update_local ;; log) pkg_log "$2" ;; remote-update) pkg_update_remote ;; remove) pkg_remove "$2" ;; status) pkg_status_wrapper "$2" ;; update) pkg_update ;; upgrade) pkg_upgrade_wrapper "$2" ;; *) die "wait, what?\n" ;; esac