From 324568044af4287dd25a90dc143ea26df565d1e3 Mon Sep 17 00:00:00 2001 From: Daniel Friesel Date: Sat, 11 Jul 2009 01:17:18 +0200 Subject: Renamed pkg to caretaker --- bin/checklinks | 6 +- bin/ct | 860 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ bin/pkg | 854 -------------------------------------------------------- 3 files changed, 863 insertions(+), 857 deletions(-) create mode 100755 bin/ct delete mode 100755 bin/pkg (limited to 'bin') diff --git a/bin/checklinks b/bin/checklinks index 765e5c7..307e246 100755 --- a/bin/checklinks +++ b/bin/checklinks @@ -164,9 +164,9 @@ which have a status of "ok" or "absolute", level 2 will filter anything except =item B<-p>, B<--parameter> I=I While reading the links file, replace $I with I. -When used in conjuction with pkg(1), $package will be set to the current -package's relative path (as seen from $HOME, like C), -and $etc will be set to $package/etc (like C) +When used in conjuction with ct(1), $package will be set to the current +package's relative path (as seen from $HOME, like C), +and $etc will be set to $package/etc (like C) =item B<-q>, B<--quiet> diff --git a/bin/ct b/bin/ct new file mode 100755 index 0000000..ab216ad --- /dev/null +++ b/bin/ct @@ -0,0 +1,860 @@ +#!/usr/bin/env zsh +## caretaker - /home package manager and zsh playground +## Copyright © 2008-2009 by 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. + +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 diff --git a/bin/pkg b/bin/pkg deleted file mode 100755 index 3441c0a..0000000 --- a/bin/pkg +++ /dev/null @@ -1,854 +0,0 @@ -#!/usr/bin/env zsh -## pkg - /home package manager and zsh playground -## Copyright © 2008-2009 by 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. - -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/pkg/pkg.conf ]] { - source $XDG_CONFIG_HOME/pkg/pkg.conf -} elif [[ -r $HOME/.pkg.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 pkg.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 "pkg: running in debug mode. Infos follow:\n" - echo "--- running ---" - echo " zsh $ZSH_VERSION" - echo " git "${$(git --version)[3]} - echo " pkg "${$(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, pkg 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 "pkg install is deprecated, use pkg add instead\n" - } - pkg_add $* - ;; - rm|delete|remove) - if [[ $action == delete ]] { - warn "pkg delete is deprecated, use pkg 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 "pkg updgrade is deprecated, use pkg pull instead\n" - } - (( AUTOUPDATE )) && pkg_update remote - wrap pkg_upgrade "$1" 'Looking for updates' - ;; - e|eval) eval $* ;; - *) die "wait, what?\npkg: unknown action: '$action'\n" ;; -esac - -apply_triggers -- cgit v1.2.3