diff options
Diffstat (limited to 'bin/ct')
-rwxr-xr-x | bin/ct | 860 |
1 files changed, 860 insertions, 0 deletions
@@ -0,0 +1,860 @@ +#!/usr/bin/env zsh +## caretaker - /home package manager and zsh playground +## Copyright © 2008-2009 by Daniel Friesel <derf@derf.homelinux.org> +## +## 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. + +setopt extended_glob +typeset -a -U triggers +typeset -a CL_OPTIONS + +c_info=$'\e[0;36m' +c_error=$'\e[0;31m' +c_reset=$'\e[0m' + +function info { + echo -ne "${c_info}$*${c_reset}" +} + +function warn { + echo -ne "${c_error}$*${c_reset}" > /dev/stderr +} + +function die { + echo -ne "${c_error}$*${c_reset}" > /dev/stderr + exit 100 +} + +function say { + echo $* +} + +function clear_line { + echo -ne "\r\e[2K" +} + +# Read local configuration +: ${XDG_CONFIG_HOME=$HOME/.config} +if [[ -r $XDG_CONFIG_HOME/caretaker/caretaker.conf ]] { + source $XDG_CONFIG_HOME/caretaker/caretaker.conf +} elif [[ -r $HOME/.caretaker.conf ]] { + source $HOME/.caretaker.conf +} elif [[ -r $XDG_CONFIG_HOME/pkg/pkg.conf ]] { + warn "pkg was renamed to caretaker, so move your config to $XDG_CONFIG_HOME/caretaker/caretaker.conf" + source $XDG_CONFIG_HOME/pkg/pkg.conf +} elif [[ -r $HOME/.pkg.conf ]] { + warn "pkg was renamed to caretaker, so move your config to $HOME/.caretaker.conf" + source $HOME/.pkg.conf +} + +# Parse commandline options +while [[ $1 == --* ]] { + case $1 in + --coluors) COLOURS=1 ;; + --no-colours) COLOURS=0 ;; + --quiet) SILENT=1 ;; + --no-quiiet) SILENT=0 ;; + --debug) DEBUG=1 ;; + --no-debug) DEBUG=0 ;; + --auto-update) AUTOUPDATE=1 ;; + --no-auto-update) AUTOUPDATE=0 ;; + --checklinks-options) CL_OPTIONS+=$2; shift ;; + --packagedir) PKG_DIR=$2; shift ;; + --packageroot) PKG_ROOT=$2; shift ;; + --progress) PROGRESS=1 ;; + --no-progress) PROGRESS=0 ;; + *) die "Unknown argument: '$1'\n" ;; + esac + shift +} + +action=$1 +((#)) && shift + +[[ -n $PKG_ROOT ]] || die "No PKG_ROOT specified. Please edit your caretaker.conf\n" +: ${PKG_DIR:="$HOME/packages"} +: ${PKGLIST_LOCAL=0} +: ${CL_OPTIONS:=--quiet} +: ${SILENT=0} +: ${DEBUG=0} +: ${AUTOUPDATE=1} +: ${GIT_USE_ORIGIN=1} +: ${COLOURS=1} +: ${PROGRESS=1} +export PKG_DIR +export PKG_ROOT + +function debug { + typeset func line + if (( $# >= 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 ${(z%):-%N %i}' + +# Avoid calling debug without debug mode... just in case one needs more speed +if (( !DEBUG )) { + unalias debug + function debug {} +} + +if (( SILENT )) { + # The goal is not to override anything set by the user... + # So, an alias should be safer than fiddling with $MAKEFLAGS + alias make='make -s' + function info {} + function say {} + function clear_line {} + PROGRESS=0 +} + +if (( !COLOURS )) { + c_info='' + c_reset='' + c_error='' +} + +if [[ ! -d $PKG_DIR ]] { + die "Error: Package directory '$PKG_DIR' not found\n" +} + + +## +## Setup some additional variables related to PKG_ROOT +## + +# Protocol +if [[ $PKG_ROOT == ssh://* ]] { + PKG_PROTO='ssh' +} elif [[ $PKG_ROOT == git://* ]] { + PKG_PROTO='git' +} elif [[ $PKG_ROOT == /* ]] { + PKG_PROTO='file' +} else { + die "Error: Unknown protocol in PKG_ROOT '$PKG_ROOT'\n" +} + +# user, host, path +if [[ $PKG_PROTO == (git|ssh) ]] { + PKG_HOST=${${PKG_ROOT#"${PKG_PROTO}://"}%%/*} + PKG_PATH=${PKG_ROOT#"${PKG_PROTO}://$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 +} + +: ${PKGLIST_PATH:=$PKG_PATH/pkglist} + +if ((DEBUG)) { + info "caretaker: running in debug mode. Infos follow:\n" + echo "--- running ---" + echo " zsh $ZSH_VERSION" + echo " git "${$(git --version)[3]} + echo " caretaker "${$(git --git-dir=$PKG_DIR/${${(s:/:)$(readlink $0)}[-3]}/.git/ log -n 1)[2]} + echo "--- settings ---" + echo " PKG_ROOT $PKG_ROOT" + echo " PKG_PROTO $PKG_PROTO" + echo " PKG_USER $PKG_USER" + echo " PKG_HOST $PKG_HOST" + echo " PKGLIST_PATH $PKGLIST_PATH" + echo " PKGLIST_LOCAL $PKGLIST_LOCAL" + echo " PKG_DIR $PKG_DIR" + echo " CL_OPTIONS $CL_OPTIONS" + echo " SILENT $SILENT" + echo " COLOURS $COLOURS" + echo " PROGRESS $PROGRESS" + echo " AUTOUPDATE $AUTOUPDATE" + echo " GIT_USE_ORIGIN $GIT_USE_ORIGIN" +} + +function check_installed { + [[ -n $1 && -d $PKG_DIR/$1 ]] || die "Package not installed: '$1'\n" +} + +function check_valid { + list_exists $1 || die "Package does not exist: '$1'\n" +} + +# Default reply: Yes +function confirm_yes { + echo -n "$* [Y/n] " + read -k 1 + [[ $REPLY != $'\n' ]] && echo + [[ $REPLY == (y|Y|$'\n') ]] +} + +# Default reply: No +function confirm_no { + echo -n "$* [y/N] " + read -q +} + + +## +## Major internal functions +## + +# this function only has content when wrap is used. +# but since it's always called, it will be empty by default +function wrap_info {} + +function progress { + ((PROGRESS)) || return + typeset -i current=$1 + typeset -i max=$2 + typeset desc=$3 + typeset desc2=$4 + typeset output='' + typeset -i currentper=$(( (current*100)/max )) + typeset item j c a + function item { + for j in {0..$1}; { + (( j > 0 )) && output+=$2 + } + } + c=$(( currentper/5 )) + a=$(( 20-c )) + output+="${c_info}$desc${c_reset} [" + item $c '=' + item $a ' ' + output+="] $currentper% $desc2" + clear_line + echo -ne $output +} + +## VCS Wrappers + +function vcs_setup { + cd $PKG_DIR/$1 +} + +function vcs_to_list ( + vcs_setup $1 + if [[ -d $PKG_DIR/$1/.git ]] { + echo -n "$1 git " + echo ${$(git log -n 1 master)[2]} + } else { + warn "No git repository found: $1\n" + } +) + +function vcs_add ( + if [[ $(list_type $1) == git ]] { + git clone "$PKG_ROOT/$1" + vcs_setup $1 + git config push.default matching + } else { + die "$1: Cannot handle repository format '$(list_type $1)'\n" + } +) + +function vcs_log ( + vcs_setup $1 + git log +) + +function vcs_branch_is_master ( + vcs_setup $1 + typeset IFS=$'\n' branch line + for line in $(git branch); { + [[ $line == \*\ * ]] && branch=${line#* } + } + if [[ $branch != master ]] { + warn "$1: The currently checked out branch is not master, but '$branch'\n" \ + "Currently, with GIT_USE_ORIGIN=0, caretaker can only operate on the branch master\n" \ + " -> skipping repo, please fix manually\n" + return 1 + } +) + +function vcs_pull ( + vcs_setup $1 + if ((GIT_USE_ORIGIN)) { + # the package might be newly created and not have an origin yet + vcs_fix_origin $1 + git pull + } else { + vcs_branch_is_master $1 && git pull $PKG_ROOT/${PWD:t} master + } +) + +function vcs_push ( + vcs_setup $1 + if ((GIT_USE_ORIGIN)) { + # see above + vcs_fix_origin $1 + git push + } else { + vcs_branch_is_master $1 && git push $PKG_ROOT/${PWD:t} master + } +) + +function vcs_status ( + vcs_setup $1 + git status +) + +function vcs_fix_origin ( + vcs_setup $1 + if [[ ! -r .git/remotes/origin && ! -r .git/refs/remotes/origin/HEAD ]] { + fgrep -q '[remote "origin"]' .git/config || + git remote add origin $PKG_ROOT/$1 + } +) + + +## List stuff + +function list_is_installed { + grep -q "^$1 " $PKG_DIR/.list +} + +function list_exists { + grep -q "^$1 " $PKG_DIR/.list-remote +} + +function list_packages_local { + cut -d ' ' -f 1 $PKG_DIR/.list +} + +function list_packages_remote { + cut -d ' ' -f 1 $PKG_DIR/.list-remote +} + +function list_incoming { + [[ $(list_version_local $1) != $(list_version_remote $1) ]] +} + +function list_type { + echo ${$(grep "^$1 " $PKG_DIR/.list-remote)[2]} +} + +function list_type_local { + echo ${$(grep "^$1 " $PKG_DIR/.list)[2]} +} + +function list_update_remote { + typeset tmpfile=$(mktemp -t pkglist.XXXXXX) + typeset -i ret=0 + export PKG_DIR + if [[ $PKGLIST_LOCAL == 1 || $PKG_PROTO == 'file' ]] { + $PKGLIST_PATH $PKG_PATH > $tmpfile + } elif [[ $PKG_PROTO == 'ssh' ]] { + ssh $PKG_USER@$PKG_HOST "PKG_DIR='$PKG_DIR' $PKGLIST_PATH $PKG_PATH" > $tmpfile + } + if [[ -n $(cat $tmpfile) ]] { + cp $tmpfile .list-remote + } else { + die "remote list update failed\n" + } + rm $tmpfile +} + +function list_update_local { + typeset all=${#$(echo $PKG_DIR/*(/))} + typeset -i current=0 + typeset i + rm -f $PKG_DIR/.list + for package in *(-/); { + (( current++ )) + progress $current $all 'Updating package list' $package + vcs_to_list $package >> $PKG_DIR/.list + } +} + +function list_package_update { + typeset list + list=$(grep -v "^$1 " $PKG_DIR/.list) + echo $list > $PKG_DIR/.list + vcs_to_list $1 >> $PKG_DIR/.list +} + +function list_package_remove { + typeset list + list=$(grep -v "^$1 " $PKG_DIR/.list) + echo $list > $PKG_DIR/.list +} + +function list_version_local { + echo ${$(grep "^$1 " $PKG_DIR/.list)[3]} +} + +function list_version_remote { + echo ${$(grep "^$1 " $PKG_DIR/.list-remote)[3]} +} + + +function priority_name { + case $1 in + 6) echo 'essential' ;; + 5) echo 'important' ;; + 4) echo 'required' ;; + 3) echo 'standard' ;; + 2) echo 'optional' ;; + 1) echo 'extra' ;; + *) warn "invalid priority: $!" ;; + esac +} + +function exec_hook { + typeset package=$1 + typeset hook=$2 + if [[ -r $PKG_DIR/$package/hooks/$hook ]] { + info "$package: executing hook $hook\n" + cd $PKG_DIR/$package + (source hooks/$hook) + } +} + +function global_hook { + cd $PKG_DIR/$1 + case $2 in + post-add) + exec_hook $1 post-add + global_hook $1 post-update + ;; + pre-update) + ;; + post-update) + triggers+=$1 + check_prereqs $1 + if [[ -r Makefile ]] { + wrap_info $1 + info "Running make\n" + make + } + checklinks $CL_OPTIONS \ + --parameter package=${${PWD#$HOME}#/##} \ + --parameter etc=${${PWD#$HOME}#/##}/etc + populate_collected $1 + update_provides $1 + list_package_update $1 + ;; + pre-remove) + exec_hook $1 pre-remove + genocide_collected $1 + checklinks $CL_OPTIONS --remove + list_package_remove $1 + update_provides $1 + ;; + esac + (( $+functions[pkg_hook_$2] )) && pkg_hook_$2 $1 +} + +function check_prereqs { + typeset -a -U install maybe_install + typeset warn info i + typeset package=$1 + [[ -r $PKG_DIR/$package/prereqs ]] || return 0 + cd $PKG_DIR/$package + wrap_info $1 + info "checking prerequisites\n" + + # function scope → typeset and localoptions are possible + function source_prereqs { + { + source prereqs + } always { + if (( TRY_BLOCK_ERROR )) { + warn "Error in prereqs script\n" + TRY_BLOCK_ERROR=0 + } + } + } + + function is_installed { + [[ -d $PKG_DIR/$1 ]] + } + function perlmodule { + perl -M$1 < /dev/null 2> /dev/null + } + function executable file_in_path { + if [[ $0 == file_in_path ]] { + warn "'file_in_path' is deprecated, use 'executable' instead\n" + } + which $1 > /dev/null + } + function offer_install { + install+=$1 + } + function depend require { + if [[ $0 == require ]] { + warn "'require' is depracated, use 'depend' instead\n" + } + if [[ $1 == 'package' ]] { + is_installed $2 || offer_install $2 + } else { + $* || warn+="Requirement failed: $*\n" + } + } + + function suggest { + if [[ $1 == 'package' ]] { + is_installed $2 || info "$package suggests package $2\n" + } else { + $* || echo "Suggest failed: $*\n" + } + } + + function recommend { + if [[ $1 == 'package' ]] { + is_installed $2 || maybe_install+=$2 + } else { + $* || info+="Recommend failed: $*\n" + } + } + + source_prereqs + + if [[ -n $warn || -n $info ]] { + [[ -n $warn ]] && warn $warn + [[ -n $info ]] && echo -n $info + read -q '?continue [] ' + } + + if [[ -n $install ]] { + info "$1 requires the following packages: ${(j:, :)install}\n" + if confirm_yes "Install them?"; then + for package in $install; { + pkg_add $package + } + fi + } + if [[ -n $maybe_install ]] { + info "$1 recommends the following packages: ${(j:, :)maybe_install}\n" + if confirm_no "Install them?"; then + for package in $maybe_install; { + pkg_add $package + } + fi + } +} + +# Write a package's documentation to .collected +# and symlink its binaries from ~/bin +function populate_collected { + cd $PKG_DIR/$1 || return + typeset -i bins=0 bino=0 mans=0 mano=0 + typeset man section manpage file + + wrap_info $1 + info "Enabling documentation\n" + for man in man/*/*(N); { + section=${man:h:t} + manpage=${man:t} + if podchecker man/$section/$manpage &> /dev/null; then + pod2man -u -s $section -c "$1 package" -r $HOME man/$section/$manpage > $PKG_DIR/.collected/man/man$section/${manpage%.pod}.$section + fi + } + for file in bin/*(N); { + if podchecker $file &> /dev/null; then + pod2man -u $file > $PKG_DIR/.collected/man/man1/${file:t}.1 + fi + } + for file in bin/*(-*N); { + if [[ -L $HOME/$file || ! -e $HOME/$file ]] { + if [[ $(readlink $HOME/$file) != (../${PKG_DIR//$HOME\/}|$PKG_DIR)/$1/$file ]] { + rm -f $HOME/$file + if [[ $PKG_DIR == ${HOME}* ]] { + ln -s ../${PKG_DIR//$HOME\/}/$1/$file $HOME/$file + } else { + ln -s $PKG_DIR/$1/$file $HOME/$file + } + } + } else { + warn "populate_collected: Not updating ~/$file since it's not a symlink\n" + } + } +} + +# Remove a package's files from .collected +# Assuming there are no packages with colliding files +function genocide_collected { + typeset i file man manual section + cd $PKG_DIR/$1 || return + wrap_info $1 + info "Removing documentation" + for man in man/*/*(N); { + section=${man:h:t} + manual=${man:t} + if [[ -e $PKG_DIR/.collected/man/man$section/$manual.$section ]] { + rm $PKG_DIR/.collected/man/man$section/$manual.$section + } + } + for file in bin/*(N); { + rm -f $PKG_DIR/.collected/man/man1/${file:t}.1 + } + clear_line + for file in bin/*(-*N); { + if [[ $(readlink $HOME/$file) == (../${PKG_DIR//$HOME\/}|$PKG_DIR)/$1/$file ]] { + rm -f $HOME/$file + } + } +} + +function update_provides { + typeset package + for package in $PKG_DIR/$1/provides/*(N:t); { + if [[ -d $PKG_DIR/$package ]] { + triggers+=$package + } + } +} + +function apply_triggers { + typeset package + for package in $triggers; { + exec_hook $package 'post-update' + } +} + +# Iterate a function over every installed package +function wrap { + typeset function=$1 + typeset arg=$2 + typeset progress=$3 + typeset i + typeset -i all current + + if [[ -n $2 ]] { + $function $2 + } else { + function wrap_info { + clear_line + info "$1: " + } + [[ -n $progress ]] && all=$(list_packages_local | wc -l) + [[ -n $progress ]] && current=0 + for package in *(-/); { + cd $PKG_DIR + (( current++ )) + [[ -n $progress ]] && progress $current $all $progress $package + $function $package + } + [[ -n $progress ]] && clear_line + } +} + +## +## The "frontend" functions +## + +function pkg_add { + if [[ -d $PKG_DIR/$1 ]] { + info "Package '$1' is already installed!\n" + exit 1 + } + check_valid $1 + info "Retrieving package $1...\n" + vcs_add $1 || return 255 + global_hook $1 post-add +} + +function pkg_push { + check_installed $1 + check_valid $1 + if list_incoming $1; then + clear_line + info "Pushing $1\n" + global_hook $1 pre-update + vcs_push $1 + global_hook $1 post-update + fi +} + +function pkg_remove { + check_installed $1 + if [[ -r $PKG_DIR/$1/priority ]] { + if (( $(cat $PKG_DIR/$1/priority) > 3 )) { + confirm_no "Package '$1' is $(priority_name $(cat $PKG_DIR/$1/priority)). Really remove?" || return + } + } + global_hook $1 pre-remove + rm -rf $PKG_DIR/$1 + info "Package removed.\n" +} + +function pkg_upgrade { + check_installed $1 + check_valid $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_version_remote $1)\n" + global_hook $1 pre-update + vcs_pull $1 + global_hook $1 post-update + fi +} + +function pkg_list { + typeset package crap + case $1 in + ''|local) + list_packages_local + ;; + all|remote) + list_packages_remote + ;; + not-installed) + for package in $(list_packages_remote); { + list_is_installed $package || echo $package + } + ;; + esac +} + +function pkg_status { + typeset vcs_status + check_installed $1 + vcs_status=$(PAGER='' vcs_status $1) + if [[ -n $vcs_status && $vcs_status != *'nothing to commit (working directory clean)' ]] { + if ((SILENT)) { + echo $1 + } else { + clear_line + info "$1:\n" + echo $vcs_status + } + } +} + +function pkg_refresh { + check_installed $1 + global_hook $1 pre-update + global_hook $1 post-update +} + +function pkg_update { + if [[ -z $1 || $1 == local ]] { + info "Updating local package list\n" + list_update_local + clear_line + } + if [[ -z $1 || $1 == remote ]] { + info "Updating remote package list\n" + list_update_remote + } +} + +function pkg_info { + check_valid $1 + + typeset name=$1 + typeset repo_type=$(list_type $1) + typeset priority priority_name + typeset hooks makefile discription state + if [[ -d $1 ]] { + cd $1 + if [[ -r priority ]] { + priority=$(cat priority) + priority_name=$(priority_name $priority) + } + 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' $(list_version_local $1) + show_info 'Remote Version' $(list_version_remote $1) + show_info 'Repository Type' $repo_type + show_info 'Repository Size' $size + show_info 'Hooks' $hooks + show_info 'Description' $description +} + +function pkg_log { + check_installed $1 + vcs_log $1 +} + + +cd $PKG_DIR || die "Cannot cd $PKG_DIR" + +# Note: +# wrap foobar "$1" <- the "" are neccessary here, since $1 is optional (and therefore may be empty) +case $action in + a|add|install) + if [[ $action == install ]] { + warn "ct install is deprecated, use ct add instead\n" + } + pkg_add $* + ;; + rm|delete|remove) + if [[ $action == delete ]] { + warn "ct delete is deprecated, use ct remove instead\n" + } + pkg_remove $* + ;; + i|info) pkg_info $* ;; + ls|list) pkg_list $* ;; + l|log) pkg_log $* ;; + p|push) + (( AUTOUPDATE )) && pkg_update + wrap pkg_push "$1" 'Pushing' + ;; + r|refresh) wrap pkg_refresh "$1" 'Refreshing' ;; + s|status) wrap pkg_status "$1" 'Checking package status' ;; + u|update) pkg_update $* ;; + f|upgrade|pull) + if [[ $action == upgrade ]] { + warn "ct updgrade is deprecated, use ct pull instead\n" + } + (( AUTOUPDATE )) && pkg_update remote + wrap pkg_upgrade "$1" 'Looking for updates' + ;; + e|eval) eval $* ;; + *) die "wait, what?\nct: unknown action: '$action'\n" ;; +esac + +apply_triggers |