#!/bin/bash # Ivan Grigoryevich Zaigralin a.k.a. melikamp. # Copyright (C) 2016-2022 # # This file is a part of FXP software project. # # freepkg is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # without any warranty; without even the implied warranty of # merchantability or fitness for a particular purpose. Compiling, # interpreting, executing or merely reading the text of the program # may result in lapses of consciousness and/or very being, up to and # including the end of all existence and the Universe as we know it. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program (most likely, a file named COPYING). If # not, see . FREEPKG_VERSION=15.0 # this file will be sourced on every startup FREEPKG_CONF=${FREEPKG_CONF:-/root/.freepkg} # with "no" by default, we refresh the remote package list every time # a task requires it. with "yes", this list update becomes manual, and # a cached copy will be used instead OFFLINE=${OFFLINE:-no} USE_COLOR=${USE_COLOR:-yes} # "no" to suppress terminal color GOOD_COLOR=${GOOD_COLOR:-5} # purple OK_COLOR=${OK_COLOR:-6} # cyan BAD_COLOR=${BAD_COLOR:-1} # red OLD_COLOR=${OLD_COLOR:-3} # gold NEW_COLOR=${NEW_COLOR:-2} # green GPGBIN=${GPGBIN:-/usr/bin/gpg2} GPG_SUF=${GPG_SUF:-asc} CONF=${CONF:-/etc/freepkg} # freepkg configuration TMP=${TMP:-/tmp/freepkg} # everything cached will be cached here PACKAGE_LOG=${PACKAGE_LOG:-/var/log/packages} # slackpkg package log PKGTYPE=${PKGTYPE:-tgz} # remote repository package type pattern FXP_TAG=${FXP_TAG:-"_FXP"} # package tag PKG_LIST=${PKG_LIST:-PKGLIST} # remote package list file name ALL_REQS="" # variable for the recursive requirements descent # flags for whether we parsed package lists. we should be ok with # just one pass of each, and we quit right after we install/upgrade, # so the info will not go stale PARSED_LOCAL=no PARSED_REMOTE=no # arrays for the package info data table # for local packages L_SIZE=0 declare -a L_NAMES declare -a L_VERSIONS declare -a L_ARCHES declare -a L_BUILDS # and for remote packages R_SIZE=0 declare -a R_NAMES declare -a R_VERSIONS declare -a R_ARCHES declare -a R_BUILDS declare -a R_CNAMES # canonical names declare -a R_FNAMES # file names # there are a few more variables defined below, once the action # starts. they have to do with the url of the remote fxp # directory. just like in slackpkg mirrors, the url can start with # ftp, http, rsync, or file # SOURCE is the url of the remote repository (active line in slackpkg # mirrors) # SOURCE_TYPE is either WEB for http/ftp, RSYNC for rsync, or DIR for # local dir # FXP_DIR is the url of the remote folder with fxp packages # rainbow magic function good_c { if [[ "$USE_COLOR" == yes ]] ; then tput setaf $GOOD_COLOR ; echo -n "$@" ; tput sgr0 else echo -n "$@" ; fi ; } function ok_c { if [[ "$USE_COLOR" == yes ]] ; then tput setaf $OK_COLOR ; echo -n "$@" ; tput sgr0 else echo -n "$@" ; fi ; } function bad_c { if [[ "$USE_COLOR" == yes ]] ; then tput setaf $BAD_COLOR ; echo -n "$@" ; tput sgr0 else echo -n "$@" ; fi ; } function old_c { if [[ "$USE_COLOR" == yes ]] ; then tput setaf $OLD_COLOR ; echo -n "$@" ; tput sgr0 else echo -n "$@" ; fi ; } function new_c { if [[ "$USE_COLOR" == yes ]] ; then tput setaf $NEW_COLOR ; echo -n "$@" ; tput sgr0 else echo -n "$@" ; fi ; } # user communication (will pass flags to echo) function message { ok_c "[freepkg]" ; echo -n " " ; echo $@ ; } function error { bad_c "[freepkg]" ; echo -n " " ; echo $@ ; exit 1 ; } # deliver file given by url into the current dir function getFile { local url="$1" if [[ "$SOURCE_TYPE" == WEB ]] ; then wget "$url" || error "could not wget $url" elif [[ "$SOURCE_TYPE" == DIR ]] ; then if [ -f "$url" ] ; then ln -sv "$url" ./$(basename $1) else error "file $url not found" fi elif [[ "$SOURCE_TYPE" == RSYNC ]] ; then rsync -av "$url" ./ || error "could not rsync $url" fi } # suck in the list of remotely available package files and cache it # locally function getRemotePackageList { mkdir -p $TMP cd $TMP if [[ $OFFLINE == yes ]] ; then message "using locally cached list of available packages" touch $PKG_LIST else message "getting the list of available packages" if [ -f $PKG_LIST ] ; then mv $PKG_LIST $PKG_LIST- ; fi getFile $FXP_DIR/$PKG_LIST fi } # a bit on canonical names like xz-5.2.2-x86_64-1 # they seem to consist of bash-safe characters assembled into 4 fields # separated by minuses. The first field can itself contain minuses, so # we parse from the end. # canonical package name parsing function pkgName { local a=${1%-*} ; local b=${a%-*} ; echo ${b%-*} ; } function pkgVersion { local a=${1%-*} ; local b=${a%-*} ; echo ${b##*-} ; } function pkgArch { local a=${1%-*} ; echo ${a##*-} ; } function pkgBuild { echo ${1##*-} ; } # this will ls /var/log/packages (or PACKAGE_LOG if defined) and turn # the list of canonical slackware package names like xz-5.2.2-x86_64-1 # into arrays containing the info for the locally installed packages, # with names, versions, arch, and build suffixes. function parseLocalPackages { local cnames="" # list of cannonical package names local cn # canonical name for loop if [[ $PARSED_LOCAL == yes ]] ; then return 0 ; else PARSED_LOCAL=yes ; fi cd $PACKAGE_LOG || error "slackpkg package log $PACKAGE_LOG not found" cnames=$(/bin/ls) message -n "parsing locally installed packages... " L_SIZE=0 for cn in $cnames ; do L_NAMES[$L_SIZE]=$(pkgName $cn) L_VERSIONS[$L_SIZE]=$(pkgVersion $cn) L_ARCHES[$L_SIZE]=$(pkgArch $cn) L_BUILDS[$L_SIZE]=$(pkgBuild $cn) L_SIZE=$(( L_SIZE + 1 )) done echo " done!" } # this will do the same as parseLocalPackages, but will start with a # list of remotely available file names like xz-5.2.2-x86_64-1.tgz and # make arrays containing the info for the remotely available packages # with names, versions, arch, and build suffixes. function parseRemotePackages { local cnames="" # list of canonical names local cn # canonical name for loop if [[ $PARSED_REMOTE == yes ]] ; then return 0 ; else PARSED_REMOTE=yes ; fi getRemotePackageList cd $TMP R_FNAMES=( $(cat $PKG_LIST) ) cnames=$( cat $PKG_LIST | sed 's/.'$PKGTYPE'$//' ) R_CNAMES=( $cnames ) message -n "parsing remotely available packages... " R_SIZE=0 for cn in $cnames ; do R_NAMES[$R_SIZE]=$(pkgName $cn) R_VERSIONS[$R_SIZE]=$(pkgVersion $cn) R_ARCHES[$R_SIZE]=$(pkgArch $cn) R_BUILDS[$R_SIZE]=$(pkgBuild $cn) R_SIZE=$(( R_SIZE + 1 )) done echo " done!" } # given a canonical name, echoes its index in the array of locally # installed packages; echoes NOT_FOUND if name was not found function localIndex { local i # loop index local pname="$1" # package name to look up i=0 while [ $i -lt $L_SIZE ] ; do if [[ ${L_NAMES[$i]} == $pname ]] ; then echo $i return 0 fi i=$(( i + 1 )) done echo NOT_FOUND return -1 } # given a canonical name, echoes its index in the array of remotely # available packages; echoes NOT_FOUND if name was not found function remoteIndex { local i # loop index local pname="$1" # package name to look up i=0 while [ $i -lt $R_SIZE ] ; do if [[ ${R_NAMES[$i]} == $pname ]] ; then echo $i return 0 fi i=$(( i + 1 )) done echo NOT_FOUND return -1 } # print a pretty list of installed fxp packages function listLocalPackages { local i # loop index local b # build parseLocalPackages printf "%-32s %-16s %-16s %-16s\n" NAME VERSION ARCH BUILD i=0 while [ $i -lt $L_SIZE ] ; do b=${L_BUILDS[$i]} if [[ ${b%$FXP_TAG} != $b ]] ; then printf "%-32s %-16s %-16s %-16s\n" \ ${L_NAMES[$i]} ${L_VERSIONS[$i]} \ ${L_ARCHES[$i]} $b fi i=$(( i + 1 )) done } # print a pretty list of available remote packages function listRemotePackages { local i # loop index local b # build parseRemotePackages printf "%-32s %-16s %-16s %-16s\n" NAME VERSION ARCH BUILD i=0 while [ $i -lt $R_SIZE ] ; do b=${R_BUILDS[$i]} if [[ ${b%$FXP_TAG} != $b ]] ; then printf "%-32s %-16s %-16s %-16s\n" \ ${R_NAMES[$i]} ${R_VERSIONS[$i]} \ ${R_ARCHES[$i]} $b fi i=$(( i + 1 )) done } # show the intersection of local and remote package sets function listIntersection { local i # loop index local j # loop index local cn # canonical name local rv # rainbow remote version local rb # rainbow remote build local lv # rainbow local version local lb # rainbow local build parseLocalPackages parseRemotePackages message "intersection of fxp and locally installed packages" printf "%-32s%-16s%-16s%-16s%-16s\n" \ NAME "FXP VERSION" "FXP BUILD" "LOCAL VERSION" "LOCAL BUILD" i=0 while [ $i -lt $R_SIZE ] ; do cn=${R_NAMES[$i]} j=0 while [ $j -lt $L_SIZE ] ; do if [[ $cn == ${L_NAMES[$j]} ]] ; then rv=${R_VERSIONS[$i]} rb=${R_BUILDS[$i]} lv=${L_VERSIONS[$j]} lb=${L_BUILDS[$j]} if [[ "$1" == ALL || ( "$1" == UPDATES && ( $rv != $lv || $rb != $lb ) ) ]] ; then printf "%-32s" "$cn" if [[ $rv < $lv ]] ; then old_c "$(printf "%-16s" $rv)" elif [[ $rv > $lv ]] ; then new_c "$(printf "%-16s" $rv)" else printf "%-16s" $rv fi if [[ $rb < $lb ]] ; then old_c "$(printf "%-16s" $rb)" echo elif [[ $rb > $lb ]] ; then new_c "$(printf "%-16s" $rb)" else printf "%-16s" $rb fi if [[ $rv < $lv ]] ; then new_c "$(printf "%-16s" $lv)" elif [[ $rv > $lv ]] ; then old_c "$(printf "%-16s" $lv)" else printf "%-16s" $lv fi if [[ $rb < $lb ]] ; then new_c "$(printf "%-16s" $lb)" echo elif [[ $rb > $lb ]] ; then old_c "$(printf "%-16s" $lb)" else printf "%-16s" $lb fi echo fi fi j=$(( j + 1 )) done i=$(( i + 1 )) done } # some packages have very non-standard dependencies for example, # texlive badly conflicts with tetex. here we assert that installing # the given package will not break things. function checkSpecialConditions { local pn="$1" # package name if [[ "$pn" =~ texlive-.* ]] ; then if [[ $(localIndex tetex) != NOT_FOUND || \ $(localIndex tetex-doc) != NOT_FOUND ]] ; then message "texlive packages conflict with tetex and tetex-doc" message "uninstall tetex and tetex-doc before you install texlive" error "installation aborted" fi fi } # install the given package function installPackage { local pname="$1" # given package name # normally we do nothing if a package with the same name # is already present. with FORCE, we installpkg regardless local force_flag="$2" local lpi # local package index local rpi # remote package index local fname # package file name test -z "$pname" && error "which package do you want to install?" parseLocalPackages lpi=$(localIndex "$pname") if [[ $lpi == NOT_FOUND || $force_flag == FORCE ]] ; then parseRemotePackages rpi=$(remoteIndex "$pname") if [[ $rpi == NOT_FOUND ]] ; then error "package $pname not found" else # some packages have very non-standard dependencies # for example, texlive badly conflicts with tetex # so we take care of that before installing checkSpecialConditions cd $TMP fname=${R_FNAMES[$rpi]} rm $fname $fname.$GPG_SUF &> /dev/null getFile $FXP_DIR/$fname getFile $FXP_DIR/$fname.$GPG_SUF message "verifying package signature..." $GPGBIN -q --no-verbose --verify $fname.$GPG_SUF || error bad signature message "installing the package $pname" installpkg $fname fi else message "package $pname already installed" fi } # upgrade the given package function upgradePackage { local pname="$1" # given package name local lpi # local package index local rpi # remote package index local fname # package file name test -z "$pname" && error "which package do you want to upgrade?" parseLocalPackages lpi=$(localIndex "$pname") if [[ $lpi == NOT_FOUND ]] ; then message "package $pname is not installed. install? (Y/n)" read -n 1 foo ; echo if [[ $foo == "n" || $foo == "N" ]] ; then error "installation aborted" fi installPackage "$pname" else parseRemotePackages rpi=$(remoteIndex "$pname") if [[ $rpi == NOT_FOUND ]] ; then error "package $pname not found" else if [[ ${R_VERSIONS[$rpi]} == ${L_VERSIONS[$lpi]} && \ ${R_ARCHES[$rpi]} == ${L_ARCHES[$lpi]} && \ ${R_BUILDS[$rpi]} == ${L_BUILDS[$lpi]} ]] ; then message "package $pname is already up to date" return 0 fi cd $TMP fname=${R_FNAMES[$rpi]} rm $fname $fname.$GPG_SUF &> /dev/null getFile $FXP_DIR/$fname getFile $FXP_DIR/$fname.$GPG_SUF message "verifying package signature..." $GPGBIN -q --no-verbose --verify $fname.$GPG_SUF || error bad signature message "upgrading the package $pname" upgradepkg $fname fi fi } # this recursive function will descend and flatten the requirement # tree. before calling it, null the ALL_REQS string. function listReqAux { local pname="$1" local reqs="" local p="" cd $TMP rm $pname.req &> /dev/null getFile $FXP_DIR/$pname.req reqs=$(cat $pname.req) if [ ! -z "$reqs" ] ; then for p in $reqs ; do ALL_REQS="$ALL_REQS $p" listReqAux "$p" done fi ALL_REQS=$(echo $ALL_REQS | xargs -n1 | sort -u | xargs) } # list all package prerequisites. return 0 if none, 1 otherwise function listReq { local cn="$1" # canonical package name local rpi # remote package index if [ -z "$cn" ] ; then error "which package do you want to examine?" fi parseRemotePackages rpi=$(remoteIndex "$cn") if [[ $rpi == NOT_FOUND ]] ; then error "package $cn not found" fi ALL_REQS="" listReqAux "$cn" if [ -z "$ALL_REQS" ] ; then message "package $cn has no prerequisites" return 0 else message "package $cn requires:" echo $ALL_REQS return 1 fi } # install the package and its prerequisites function installReq { local cn="$1" # canonical package name local rpi # remote package index local r # canonical name for requisites loop local foo # user input if [ -z "$cn" ] ; then error "which package do you want to install?" fi listReq "$cn" if [[ $? == 0 ]] ; then installPackage $cn else message "install everything? (Y/n)" read -n 1 foo ; echo if [[ $foo == "n" || $foo == "N" ]] ; then error "installation aborted" fi for r in $ALL_REQS ; do installPackage $r done installPackage $cn fi } # upgrade the package and its prerequisites function upgradeReq { local cn="$1" # canonical package name local rpi # remote package index local r # canonical name for requisites loop local foo # user input if [ -z "$cn" ] ; then error "which package do you want to upgrade?" fi listReq "$cn" if [[ $? == 0 ]] ; then upgradePackage $cn else message "upgrade everything? (Y/n)" read -n 1 foo ; echo if [[ $foo == "n" || $foo == "N" ]] ; then error "upgrade aborted" fi for r in $ALL_REQS ; do upgradePackage $r done upgradePackage $cn fi } function freepkgHelp { echo 'version' "$FREEPKG_VERSION" echo echo 'usage: freepkg [command]' echo echo 'i [pkg] - install package' echo 'ir [pkg] - install package and its prerequisites' echo 'fi [pkg] - force install package even when a package with the same name' echo ' is already installed' echo 'u [pkg] - upgrade package' echo 'ur [pkg] - upgrade package and its prerequisites' echo 'lr [pkg] - list package prerequisites' echo 'r - list remote fxp packages' echo 'l - list locally installed fxp packages' echo 'x - show the intersection of local and remote package sets' echo 'xu - list the packages which can be upgraded or downgraded' echo 'help - print this message' echo } #################################################################### # action #################################################################### # source the config if [ -f "$FREEPKG_CONF" ] ; then . "$FREEPKG_CONF" ; fi # obtain the source url from slackpkg config SOURCE=$(sed -e 's/^[[:blank:]]*//' $CONF/mirrors | grep -m1 -e "^\([a-z]\)") if [ -z $SOURCE ] ; then error "no valid fxp mirror found in $CONF/mirrors" fi # this is the path/url of the fxp folder on the mirror FXP_DIR=${SOURCE}fxp # how are we going to communicate with the repo? wget or ln? if [[ $SOURCE =~ ^file:/ ]] ; then SOURCE_TYPE=DIR # ln will need an absolute file name, not url FXP_DIR=${FXP_DIR#*file:/} elif [[ $SOURCE =~ ^http:/ || $SOURCE =~ ^https:/ || $SOURCE =~ ^ftp:/ ]] ; then SOURCE_TYPE=WEB elif [[ $SOURCE =~ ^rsync:/ ]] ; then SOURCE_STYPE=RSYNC fi # process parameters if [[ "$1" == "i" ]] ; then installPackage "$2" elif [[ "$1" == "ir" ]] ; then installReq "$2" elif [[ "$1" == "fi" ]] ; then installPackage "$2" FORCE elif [[ "$1" == "lr" ]] ; then listReq "$2" elif [[ "$1" == "r" ]] ; then listRemotePackages elif [[ "$1" == "l" ]] ; then listLocalPackages elif [[ "$1" == "x" ]] ; then listIntersection ALL elif [[ "$1" == "xu" ]] ; then listIntersection UPDATES elif [[ "$1" == "u" ]] ; then upgradePackage "$2" elif [[ "$1" == "ur" ]] ; then upgradeReq "$2" elif [[ "$1" == "help" || "$1" == "--help" ]] ; then freepkgHelp else freepkgHelp exit 1 fi exit 0