#!/usr/bin/env zsh ## pkg - /home package manager and zsh playground ## Copyright (C) 2008 Daniel Friesel ## ## Permission to use, copy, modify, and/or distribute this software for any ## purpose with or without fee is hereby granted, provided that the above ## copyright notice and this permission notice appear in all copies. ## ## THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES ## WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF ## MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ## ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES ## WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ## ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF ## OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. typeset -a -U triggers ## ## Internal functions for displaying stuff ## info=$'\e[0;36m' error=$'\e[0;31m' reset=$'\e[0m' function debug { typeset func line if [[ ${#*} -ge 3 ]] { func=$1 line=$2 shift 2 } echo "(debug) $func:$line: $*" >&2 } # I need function name and line number of the function _calling_ debug, # so I can't get them from inside the debug function. alias debug='debug ${(%):-%N} ${(%):-%i}' function info { (( SILENT )) || echo -ne "${info}$*${reset}" } function warn { echo -ne "${error}$*${reset}" > /dev/stderr } function die { echo -ne "${error}$*${reset}" > /dev/stderr exit 100 } function say { (( SILENT )) || echo $* } function check_installed { [[ -n $1 && -d $PDIR/$1 ]] || die "Not installed: '$1'\n" } function check_valid { if ! list_exists $1; then die "No such package: '$1'\n" fi } function clear_line { (( SILENT )) || echo -ne "\r\e[2K" } # Read local configuration if [[ -f $HOME/.pkg.conf ]] { . $HOME/.pkg.conf } # Default values... or not [[ -n $PKG_ROOT ]] || die "No PKG_ROOT specified. Please edit ~/.pkg.conf\n" : ${PDIR:="$HOME/packages"} : ${CL_OPTIONS:=-q} : ${SILENT=0} : ${DEBUG=0} : ${AUTOUPDATE=1} export PDIR export PKG_ROOT ## ## commandline parsing ## # commandline options override everything else while [[ $1 == [-+]* ]] { case $1 in -q|--quiet) SILENT=1 ;; +q|--no-quiiet) SILENT=0 ;; -d|--debug) DEBUG=1 ;; +d|--no-debug) DEBUG=0 ;; -au|--auto-update) AUTOUPDATE=1 ;; +au|--no-auto-update) AUTOUPDATE=0 ;; -co|--checklinks-options) CL_OPTIONS+=$1; shift ;; -p|--packagedir) PDIR=$1; shift ;; *) die "Unknown argument: '$1'\n" ;; esac shift } action=$1 shift # Avoid calling debug without debug mode... just in case one needs more speed if (( !DEBUG )) { unalias debug function debug {} } ## ## Make sure everything's sane ## Warn otherwise ## if [[ ! -d $PDIR ]] { die "Error: Package directory '$PDIR' not found\n" } ## ## Setup some additional variables related to PKG_ROOT ## # Protocol if [[ ${PKG_ROOT#ssh} != $PKG_ROOT ]] { PKG_PROTO='ssh' } elif [[ ${PKG_ROOT#'/'} != $PKG_ROOT ]] { PKG_PROTO='file' } else { die "Error: Unknown protocol in PKG_ROOT '$PKG_ROOT'" } # user, host, path if [[ $PKG_PROTO == 'ssh' ]] { PKG_HOST=${${PKG_ROOT#'ssh://'}%%/*} PKG_PATH=${PKG_ROOT#"ssh://$PKG_HOST"} if [[ $PKG_HOST == *@* ]] { PKG_USER=${PKG_HOST%%@*} PKG_HOST=${PKG_HOST#*@} } else { PKG_USER=$USERNAME } } elif [[ $PKG_PROTO == 'file' ]] { PKG_PATH=$PKG_ROOT } debug "PKG_ROOT: '$PKG_ROOT'" debug "PKG_PROTO: '$PKG_PROTO'" debug "PKG_USER: '$PKG_USER'" debug "PKG_HOST: '$PKG_HOST'" ## ## Ask the user for confirmation ## # Default reply: Yes function 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 function confirm_no { echo -n "$* [y/N] " read -q } ## ## Major internal functions ## function progress { (( SILENT )) && return current=$1 max=$2 desc=$3 desc2=$4 output='' currentper=$(( (current*100)/max )) function item { for j in {0..$1}; { (( j > 0 )) && output+=$2 } } clear_line c=$(( currentper/5 )) a=$(( 20-c )) output+="${info}$desc${reset} [" item $c '=' item $a ' ' output+="] $currentper% $desc2" echo -ne $output } ## VCS Wrappers function vcs_to_list { if [[ -d $1/.git ]] { echo -n "$1 git " echo ${$(git --git-dir=$1/.git log -n 1 master)[2]} } else { warn "No git repository found: $1\n" } } function vcs_add { cd $PDIR case $(list_type $1) in git) git clone "$PKG_ROOT/$1" ;; *) die "$1: Cannot handle repository format '$(list_type $1)'\n" ;; esac } function vcs_log { git log } function vcs_upgrade { git pull $PKG_ROOT/${PWD:t} master } function vcs_push { git push $PKG_ROOT/${PWD:t} master } function vcs_status { git status } ## List stuff function list_is_installed { grep "^$1 " $PDIR/.list &> /dev/null } function list_exists { grep "^$1 " $PDIR/.list-remote &> /dev/null } function list_incoming { [[ $(list_local_version $1) != $(list_remote_version $1) ]] } function list_type { echo ${$(grep "^$1 " $PDIR/.list-remote)[2]} } function list_type_local { echo ${$(grep "^$1 " $PDIR/.list)[2]} } function list_update_remote { export PDIR if [[ $PKG_PROTO == 'ssh' ]] { ssh $PKG_USER@$PKG_HOST "PDIR='$PDIR' $PKG_PATH/core/include/pkglist $PKG_PATH" > .list-remote } elif [[ $PKG_PROTO == 'file' ]] { $PKG_PATH/core/include/pkglist $PKG_PATH > .list-remote } } function list_update_local { cd $PDIR rm -f .list all=${#$(echo $PDIR/*(/))} current=0 for i in *(/); { (( current++ )) progress $current $all 'Updating package list' $i vcs_to_list $i >> .list } } function list_update_package { cd $PDIR list=$(grep -v "^$1 " .list) echo $list > .list vcs_to_list $1 >> .list } function list_remove_package { cd $PDIR list=$(grep -v "^$1 " .list) echo $list > .list } function list_local_version { echo ${$(grep "^$1 " $PDIR/.list)[3]} } function list_remote_version { echo ${$(grep "^$1 " $PDIR/.list-remote)[3]} } # Return an understandable priority from the numeric one function 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 function exec_hook { package=$1 hook=$2 if [[ -r $PDIR/$package/hooks/$hook ]] { info "$package: executing hook $hook\n" cd $PDIR/$package (source hooks/$hook) } } # Check dependencies, conflicts etc. function check_prereqs { package=$1 [[ -r $PDIR/$package/prereqs ]] || return 0 cd $PDIR/$package info "$package: checking prerequisites\n" typeset -a install typeset warn function is_installed { [[ -d $PDIR/$1 ]] } function perlmodule { perl -M$1 < /dev/null 2> /dev/null } function file_in_path { which $1 > /dev/null } function offer_install { install+=$1 } function require { if [[ $1 == 'package' ]] { is_installed $2 || offer_install $2 } else { $* || warn+="Requirement failed: $*\n" } } { source prereqs } always { if (( TRY_BLOCK_ERROR )) { warn 'Error in prereqs script' TRY_BLOCK_ERROR=0 } } if [[ -n $warn ]] { warn $warn read -q } 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 package's documentation to .collected # and symlink it's binaries from ~/bin function populate_collected { cd $PDIR/$1 || return typeset -i bins=0 bino=0 mans=0 mano=0 if (( wrapped )) { clear_line info "$1: " } 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 man/$section/$manpage > $PDIR/.collected/man/man$section/$manpage.$section say -n "+" (( mans++ )) } else { say -n "." (( mano++ )) } } } if [[ -d bin ]] { for i in bin/*; { if (podchecker $i &> /dev/null) { pod2man $i > $PDIR/.collected/man/man1/${i:t}.1 say -n "+" (( bins++ )) } else { say -n "." (( bino++ )) } } } clear_line if (( bins + mans > 0 )) { (( wrapped )) && info "$1: " info "Compiled documentation " say -n '(' if (( bins + bino > 0 )) { say -n "$(( bins*100/(bins+bino) ))% bin" (( mans + mano > 0 )) && say -n ', ' } if (( mans + mano > 0 )) { say -n "$(( mans*100/(mans+mano) ))% man" } say ')' } if [[ -d bin ]] { for i in bin/*(*); { if [[ -L $HOME/$i || ! -e $HOME/$i ]] { if [[ $(readlink $HOME/$i) != "../$1/$i" ]] { rm -f $HOME/$i ln -s ../${PDIR//$HOME\/}/$1/$i $HOME/$i } } else { warn "populate_collected: Not updating ~/$i since it's not a symlink\n" } } } } # Remove a package's files from .collected # Assuming there are no packages with colliding files # TODO: Make sure there are none function 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 } } } } function 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 } } } function apply_triggers { for package in $triggers; { exec_hook $package 'post-update' } } # Iterate a function over every installed package function wrap { function=$1 arg=$2 progress=$3 if [[ -n $2 ]] { wrapped=0 $function $2 } else { wrapped=1 cd $PDIR [[ -n $progress ]] && all=$(wc -l < $PDIR/.list) [[ -n $progress ]] && current=0 for i in *(/); { (( current++ )) [[ -n $progress ]] && progress $current $all $progress $i $function $i } [[ -n $progress ]] && clear_line } } ## ## Finally - the functions actually doing something ## function pkg_add { if [[ -d $PDIR/$1 ]] { info "Package '$1' is already installed!\n" return 100 } list_exists $1 || die "No such package: $1\n" 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 "Running make\n" make } exec_hook $1 'post-add' exec_hook $1 'post-update' checklinks $CL_OPTIONS populate_collected $1 update_provides $1 list_update_package $1 } function pkg_push { check_installed $1 check_valid $1 cd $PDIR/$1 list_incoming $1 if [[ $? = 0 ]] { clear_line info "Pushing $1\n" vcs_push check_prereqs $1 if [[ -r Makefile ]] { info "Running make\n" make } triggers+=$1 checklinks $CL_OPTIONS populate_collected $1 update_provides $1 } } function pkg_remove { check_installed $1 cd $PDIR/$1 if [[ -r priority ]] { if (( $(cat priority) > 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" } function 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" warn " remote '$(list_type $1)' <-> local '$(list_type_local)'\n" return 9 } if list_incoming $1; then 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 fi } function pkg_list_installed { cut -d ' ' -f 1 $PDIR/.list } function pkg_list_available { cut -d ' ' -f 1 $PDIR/.list-remote } function 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 } } function pkg_refresh { check_installed $1 cd $PDIR/$1 if [[ -r Makefile ]] { clear_line info "$1: re-building with make\n" make clean make } populate_collected $1 checklinks $CL_OPTIONS check_prereqs $1 triggers+=$1 } function pkg_update { pkg_update_remote pkg_update_local } function pkg_update_remote { cd $PDIR info "Updating remote package list..." list_update_remote clear_line } function pkg_update_local { info "Updating local package list..." list_update_local clear_line } # Various information related to a package function pkg_info { cd $PDIR check_valid $1 # 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 -m hooks) } if [[ -r Makefile ]] { MAKEFILE=1 } SIZE=$(du -sh .$(list_type $1) | grep -o '.*[KMG]') if [[ -r description ]] { DESCRIPTION=$(cat description) } state='installed' if list_incoming $1; then state+=', needs update' else state+=', up-to-date' fi } else { state='not installed' } function show_info { [[ -z $2 ]] && return info "$1: " echo $2 } show_info "Package" $NAME show_info 'State' $state [[ -n $PRIORITY ]] && 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 } function pkg_log { check_installed $1 cd $PDIR/$1 vcs_log } ## ## Now what shall we do... ## # Note: # wrap foobar "$1" <- the "" are neccessary here, since $1 is optional (and therefore may be empty) case $action in add) pkg_add $* ;; delete) pkg_remove $* ;; info) pkg_info $* ;; install) pkg_add $* ;; list) pkg_list_installed $* ;; list-all) pkg_list_available $* ;; local-update) pkg_update_local $* ;; log) pkg_log $* ;; push) (( AUTOUPDATE )) && pkg_update wrap pkg_push "$1" 'Pushing' ;; refresh) wrap pkg_refresh "$1" 'Refreshing' ;; remote-update) pkg_update_remote $* ;; remove) pkg_remove $* ;; status) wrap pkg_status "$1" 'Checking package status' ;; update) pkg_update $* ;; upgrade|pull) (( AUTOUPDATE )) && pkg_update_remote wrap pkg_upgrade "$1" 'Looking for updates' ;; eval) eval $* ;; *) die "wait, what?\npkg: unknown action: '$action'\n" ;; esac apply_triggers