#!/usr/bin/env zsh # pkg - /home package manager and zsh playground typeset -a -U triggers ## ## Internal functions for displaying stuff ## info () { [[ $SILENT = 1 ]] || echo -ne "${info}$*${reset}" } warn () { echo -ne "${error}$*${reset}" > /dev/stderr } die () { echo -ne "${error}$*${reset}" > /dev/stderr exit 100 } check_installed () { [[ -n "$1" && -d $PDIR/$1 ]] || die "Not installed: '$1'\n" } check_valid () { [[ -d $PDIR/$1/.hg || -d $PDIR/$1/.git ]] || die "Not a valid package name: '$1'\n" } clear_line () { string="\r" for char in {0..80}; { string+=" " } string+="\r" echo -ne $string } # Read local configuration if [[ -f $HOME/.pkg.conf ]] { . $HOME/.pkg.conf } # Default values [[ -n $PDIR ]] || PDIR="$HOME/packages" [[ -n $PKG_ROOT ]] || PKG_ROOT="ssh://derf.homelinux.org/~/var/packages_root" [[ -n $CL_OPTIONS ]] || CL_OPTIONS=(-q) [[ -n $SILENT ]] || SILENT=0 info=$'\e[0;36m' error=$'\e[0;31m' reset=$'\e[0m' 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' || $REPLY = 'Y' || $REPLY = $'\n' ]] { true } else { false } } # Default reply: No confirm_no () { echo -n "$* [y/N] " read -q } ## ## Major internal functions ## progress () { current=$1 max=$2 desc=$3 desc2=$4 output='' currentper=$[$[$current*100]/$max] item () { for j in {0..$1}; { [[ $j -gt 0 ]] && output+=$2 } } clear_line c=$[$currentper/5] a=$[20-$c] output+="${info}$desc${reset} [" item $c '=' item $a ' ' output+="] $currentper% $desc2" [[ $SILENT = 1 ]] || echo -ne $output } ## VCS Wrappers vcs_to_list () { 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 $(list_type "$1") in git) git clone "$PKG_ROOT/$1" ;; hg) hg clone "$PKG_ROOT/$1" ;; *) die "Cannot handle repository format '$1'\n" ;; esac } vcs_log () { [[ -d .hg ]] && {hg glog | less ; return} [[ -d .git ]] && {git log ; return} } vcs_upgrade () { [[ -d .hg ]] && {hg pull --update ; return} [[ -d .git ]] && {git pull ; return} } vcs_push () { [[ -d .hg ]] && {hg push ; return} [[ -d .git ]] && {git push ; return} } vcs_status () { $(list_type $1) status } ## List stuff list_is_installed () { grep ^"$1 " $PDIR/.list } list_exists () { grep ^"$1 " $PDIR/.list-remote } list_incoming () { [[ "$(list_local_version $1)" != "$(list_remote_version $1)" ]] } list_type () { grep ^"$1 " $PDIR/.list-remote | cut -d ' ' -f 2 } list_type_local () { grep ^"$1 " $PDIR/.list | cut -d ' ' -f 2 } list_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 } } list_update_local () { cd $PDIR rm -f .list all=$(ls -1d $PDIR/*(/) | wc -l) current=0 for i in *(/); { current=$[$current+1] progress $current $all "Updating package list" $i vcs_to_list $i >> .list } } list_update_package () { cd $PDIR LIST=$(cat .list | grep -v ^"$1 ") echo $LIST > .list vcs_to_list $1 >> .list } list_remove_package () { cd $PDIR LIST=$(cat .list | grep -v ^"$1 ") echo $LIST > .list } list_local_version () { grep ^"$1 " $PDIR/.list | cut -d ' ' -f 3 } list_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 $package $hook hook\n" cd $PDIR/$package (source hooks/$hook) } } # Check dependencies, conflicts etc. check_prereqs () { package=$1 [[ -r $PDIR/$package/prereqs ]] || return 0 info "checking prerequisites\n" install=() is_installed () { [[ -d $PDIR/$1 ]] } perlmodule () { perl -M$1 < /dev/null 2> /dev/null } file_in_path () { which $1 > /dev/null } offer_install () { install+=$1 } (source prereqs) if [[ -n $install ]] { info "$1 requires the following packages: $install\n" if (confirm_yes "Install them?") { for i in $install; { pkg_add $i } } } } # 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 } } } } update_provides () { [[ -d $PDIR/$1/provides ]] || return cd $PDIR/$1/provides for package in *; { if [[ -d $PDIR/$package ]] { [[ $2 = 'remove' ]] && rm -r $package triggers+=$package } } } apply_triggers () { for package in $triggers; { exec_hook $package 'post-update' } } wrap () { function=$1 arg=$2 progress=$3 if [[ -n $2 ]] { $function $2 } else { cd $PDIR [[ -n $progress ]] && all=$(wc -l < $PDIR/.list) [[ -n $progress ]] && current=0 for i in *(/); { current=$[$current+1] [[ -n $progress ]] && progress $current $all $progress $i $function $i } [[ -n $progress ]] && clear_line } } ## ## Finally - the functions actually doing something ## pkg_add () { if [[ -d $PDIR/$1 ]] { info "Package '$1' is already installed!\n" return 100 } info "Retrieving package $1...\n" cd $PDIR || return 255 vcs_add $1 || return 255 cd $1 || return 255 check_prereqs $1 if [[ -r $PDIR/$1/Makefile ]] { info "Building binaries\n" make } exec_hook $1 "post-add" checklinks $CL_OPTIONS populate_collected $1 update_provides $1 list_update_package $1 } pkg_push () { check_installed $1 check_valid $1 cd $PDIR/$1 list_incoming $1 if [[ $? = 0 ]] { clear_line info "Pushing $1\n" vcs_push } } 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 checklinks -r list_remove_package $1 update_provides $1 'remove' rm -rf $PDIR/$1 info "Package removed.\n" } pkg_upgrade () { check_installed $1 check_valid $1 cd $PDIR/$1 if [[ $(list_type $1) != $(list_type_local $1) ]] { clear_line warn "Incompatible systems. Please reinstall: $1\n" return 9 } if {list_incoming $1} { clear_line info "Updating $1 to $(list_remote_version $1)\n" vcs_upgrade check_prereqs $1 if [[ -r Makefile ]] { info "Building binaries\n" make } triggers+=$1 checklinks $CL_OPTIONS populate_collected $1 update_provides $1 list_update_package $1 } } # 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 vcs_status=$(PAGER='' vcs_status $1) if [[ -n $vcs_status && $(echo $vcs_status | tail -n 1) != 'nothing to commit (working directory clean)' ]] { clear_line info "$1:\n" echo $vcs_status } } pkg_check () { check_installed $1 cd $PDIR/$1 checklinks $CL_OPTIONS } pkg_update () { pkg_update_remote pkg_update_local } pkg_update_remote () { cd $PDIR info "Updating remote package list..." list_update_remote clear_line } pkg_update_local () { info "Updating local package list..." list_update_local clear_line } # Various information related to a package pkg_info () { cd $PDIR # Fetch the infos NAME=$1 LOCAL_VERSION=$(list_local_version $1) REMOTE_VERSION=$(list_remote_version $1) REPO_TYPE=$(list_type $1) if [[ -d $1 ]] { cd $1 if [[ -r priority ]] { PRIORITY=$(cat priority) PRIORITY_NAME=$(real_priority "$PRIORITY") } 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 .$(list_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 "Repository Size" $SIZE 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 ;; check) wrap pkg_check "$2" "Checking" ;; 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 ;; push) wrap pkg_push "$2" "Pushing" ;; remote-update) pkg_update_remote ;; remove) pkg_remove $2 ;; status) wrap pkg_status "$2" "Checking package status" ;; update) pkg_update ;; upgrade) wrap pkg_upgrade "$2" "Looking for updates" ;; eval) shift; eval $* ;; *) die "wait, what?\n" ;; esac apply_triggers