#!/bin/bash # slackupgrade - full upgrade of a Slackware installation # Copyright (C) 2019-2020 Sergey Poznyakoff. # # Slackware-upgrade-system 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, or (at your option) # any later version. # # Slackware-upgrade-system 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. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with slackupgrade. If not, see # . set -e # Configuration directory : ${SLACKUPGRADE_CONFDIR:=/etc/slackupgrade} : ${SLACKUPGRADE_PKGDIR:=/var/slackupgrade} : ${SLACKUPGRADE_MIRRORS_URL:=https://mirrors.slackware.com/slackware} # Slackware root directory rooturl= # Log file name logfile= # 'y', if dry-run mode is requested dry_run= # If 'y', don't ask anything, assume "y" as answer to all quieries assume_y= # Output verbosity: '' - normal, 'v' - verbose, 'q' - quiet verbosity= # If 'y', install all packages install_all= # Names of the series that should be installed in addition to already installed # packages. Ignored, if install_all is set. install_series= # Names of additional packages. install_packages= # Name of the keep-list file. keep_file= # Max. count of backup files to keep max_backup=4 # Display progress bar when downloading progressbar=1 # Operating mode. Possible values: # INCR Incremental mode. Archive for each package is downloaded prior # to its nstallation and removed immediately afterwards. This # ensures minimal disk space requirements. # SAFE Safe mode. All archives are first downloaded to # SLACKUPGRADE_PKGDIR and then installed. Each archive is removed # immediately after installing from it. This requires some 2.5G # of disk space at the beginning, which will be freed by the end # of upgrade. # AUTO Select incremental mode if there is enough disk space. If not, # ask the user if it is OK to proceed in incremental mode. opmode=AUTO # Internal variables remote= tempdir=${TMP:-/tmp}/slackupg.$$ strip_series=0 installed_list=$tempdir/installed.list avail_index=$tempdir/avail.index avail_list=$tempdir/avail.list series_names="a ap d e f k l n t tcl x xap xfce y " onexit_remove_list= function onexit_remove() { onexit_remove_list="$onexit_remove_list${onexit_remove_list:+ }$1" } function tempdir_create() { u=$(umask) umask 077 mkdir $tempdir umask $u } function tempdir_remove() { rm -rf $tempdir $onexit_remove_list } function usage() { cat <>$logfile "$0: $(date): $*" fi } function error() { echo >&2 "$0: $*" if [ -n "$logfile" ]; then echo >>$logfile "$0: $(date): $*" fi } # Abnormal termination: print error message, remove temporary directory # and terminate with status 1. function abend() { error "$@" tempdir_remove exit 1 } function package_file_name() { pkg=$(awk -vname=$1 '$1==name { print $2 }' $avail_index) if [ -z "$pkg" ]; then error "package $name not found in index (should not happen!)" else echo $pkg fi } # check_package_md5sum PKG ARCHIVE # Verifies the MD5 sum of the ARCHIVE file for Slackware package PKG function check_package_md5sum() { if [ -n "$checksums" ]; then awk -vcname="$1" -vdname="$2" \ '$2==cname { print $1 " " dname }' $checksums | \ md5sum --status --check fi } # all_package_names # Lists all packages from the Slackware distribution. function all_package_names() { grep -v '.*/kde[^/]*/' $avail_index | cut -d ' ' -f 1 } # series_package_names S # Lists the names of packages in Slackware series S. function series_package_names() { grep '.*/'"$1/" $avail_index | cut -d ' ' -f 1 } function catfile() { echo $rooturl/${1#./} } # download_curl FILE URL # Downloads file from URL to FILE using curl. function download_curl { curl $CURL_OPTIONS -L -sS -o$1 $2 } # download_wget LOCAL URL # Downloads file from URL to FILE using wget. function download_wget { if ! wget $WGET_OPTIONS -nv -o wget.log -O$1 $2; then grep -i "failed\|error" wget.log /bin/false fi } # dnfunc_init # Initializes the downloader function to wget or curl. function dnfunc_init() { if [ -z "$dnfunc" ]; then if wget --version >/dev/null 2>&1; then dnfunc=download_wget elif curl --version >/dev/null 2>&1; then dnfunc=download_curl else abend "neither curl nor wget is installed" fi fi } # download FILE # Downloads package FILE from the remote server. On success, returns the # full pathname of the downloaded copy. function download() { local name=$SLACKUPGRADE_PKGDIR/$(basename $1) local url=$(catfile $1) if $dnfunc $name $url; then echo $name fi } # getfile PKG [GPG] # Retrieves the package archive file for the package PKG. If second argument # is non-empty, verifies the GPG signature of the file. # If checksums file is available, verifies also the MD5 checksum of the file. # # Returns full pathname of the retrieved file. function getfile() { local name=$(if [ -n "$remote" ]; then download $1 elif [ $strip_series -eq 1 ]; then catfile $(basename $1) else catfile $1 fi) if [ -n "$2" ]; then ascname=$(if [ -n "$remote" ]; then download $1.asc else catfile $1.asc fi) if [ -n "$ascname" ]; then if ! err=$(${GPG:-gpg} --verify $ascname $name 2>&1 >/dev/null) then error "$err" error "gpg verification failed for $name" dropfile $ascname return else dropfile $ascname fi else error "$name: no clearsign signature file found" return fi fi if [ -n "$checksums" ] && ! check_package_md5sum $1 $name; then error "ERROR: $1: checksum failed" name= fi echo $name } # dropfile FILE # Removes FILE, if it has been downloaded from the remote repository, function dropfile() { if [ -n "$remote" ] || [ $strip_series -eq 1 ]; then rm $1 fi } # upgrade_package [OPTIONS] FILE # Runs upgradepkg with the supplied arguments and captures its output to # the log file. In verbose mode, filters parts of it to the stdout. function upgrade_local() { if [ -n "$dry_run" ]; then echo "upgradepkg $@" elif [ "$verbosity" = 'v' ]; then upgradepkg "$@" | \ tee -a $logfile | \ sed -n -e '/^| Upgrading/s/^| //p' else upgradepkg "$@" >> $logfile fi } # upgrade_package NAME [OPTIONS] # Upgrades the package NAME. OPTIONS will be passed to upgradepkg verbatim. function upgrade_package() { local name=$1 shift if [ -n "$dry_run" ]; then echo "upgradepkg $@ $name" else file=$(getfile $name) if [ -n "$file" ]; then upgrade_local $@ $file dropfile $file fi fi } # version_gt A B # Returns true if version A is greater than B. function version_gt() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1" } # backup FILE [DRY_RUN] # Create a backup copy of FILE, named FILE~ # Before, rename each file matching the pattern FILE~, where is a # decimal digit, to FILE~. Rename FILE~ to FILE~1. # # Second argument, if present, enables the dry-run mode. # function backup() { local dir=$(dirname $1) local stem=$(basename $1) local p if [ -n "$2" ]; then p=echo else p= fi if [ -r "$1" ]; then if [ -r "$1~" ]; then find $dir -name "$stem*" -printf '%f\n' |\ sed -n -r -e 's/(.+)~([[:digit:]]+)$/\2 \1/p' |\ sort +0 -1 -n -r |\ while read n stem do if [ -n "$max_backup" ] && [ $n -ge $max_backup ]; then $p rm $dir/${stem}~$n else $p mv $dir/${stem}~$n $dir/${stem}~$((n + 1)) fi done $p mv "$1~" "$1~1" fi $p mv "$1" "$1~" fi } # disk_avail_size DIR # Returns the available disk size (in kilobytes) on the device where # DIR is located function disk_avail_size() { df -k --output=target,avail | \ sed 1d | \ awk -vdir=$1 \ 'BEGIN { dirlen=length(dir) } { len = length($1) if (len <= dirlen && substr(dir, 1, len) == $1) { if (mp_len < len) { mp_len = len mp_avail = $2 } } } END { print mp_avail }' } # Downloads all selected packages to the spool directory. function download_all() { info "downloading packages" if [ ${COLUMNS:-0} -lt 10 ]; then progressbar=0 fi if [ $progressbar -eq 1 ]; then total=$(wc -l candidates | cut -d ' ' -f 1) width=$(( $COLUMNS - 7 )) fi i=0 cat candidates | while read pkg do if [ $progressbar -eq 1 ]; then n=$(( $i * $width / $total )) s=$(printf "%${n}s" ' '|sed 's/./=/g') printf "\r% 3d%% %s>" $(( $i * 100 / $total)) $s fi if [ -z "$dry_run" ]; then file=$(getfile $(package_file_name $pkg)) if [ -z "$file" ]; then abend "failed to download $pkg" fi fi i=$(( $i + 1 )) done || exit $? if [ $progressbar -eq 1 ]; then s=$(printf "%${width}s" ' '|sed 's/./=/g') printf "\r% 3d%% %s|\n" 100 $s fi } # ########## # Main # ########## while getopts "ahIknp:qSs:vy" OPTION do case $OPTION in I) opmode=INCR;; S) opmode=SAFE;; a) install_all=y;; h) usage exit 0;; k) keep_file=$OPTARG;; n) dry_run=y;; p) install_packages="$install_packages $OPTARG";; q) verbosity=q;; v) verbosity=v;; s) install_series="$install_series $OPTARG";; y) assume_y=y;; *) usage >&2 exit 1 esac done shift $(($OPTIND - 1)) case $# in 0) ;; 1) rooturl=$1;; *) error "unexpected arguments" usage >&2 exit 1 esac if [ $(id -u) != "0" ]; then abend "must be root" fi # Sanity check if [ ! -s /etc/os-release ]; then abend "/etc/os-release doesn't exist" fi . /etc/os-release if [ "$ID" != "slackware" ]; then abend "this doesn't seem to be a Slackware installation" fi case "$(uname -m)" in i?86) ARCH=;; x86_64) ARCH=64;; *) abend "architecture $(uname -m) is not yet supported" esac if [ ! -d $SLACKUPGRADE_PKGDIR ]; then mkdir -p $SLACKUPGRADE_PKGDIR || abend "can't create $SLACKUPGRADE_PKGDIR" fi tempdir_create cd $tempdir if [ -z "$rooturl" ]; then dnfunc_init if ! $dnfunc index.html $SLACKUPGRADE_MIRRORS_URL; then abend "exiting" exit 1 fi version_rx=$(echo $VERSION | sed -e 's/\./\\./g') new_version=$(cat index.html |\ tr '<' '\n' |\ sed -n -r \ -e 's/.*^a href="slackware'$ARCH'-([[:digit:].]+)\/?".*/\1/p'|\ sed -n -e "/$version_rx/{" -en -ep -e '}' ) if [ -n "$new_version" ]; then rooturl="$SLACKUPGRADE_MIRRORS_URL/slackware$ARCH-$new_version" info "using $rooturl as distribution top-level URL" else abend "can't find distribution newer than $VERSION; please supply URL if you have any" fi fi # Check if rooturl is local or remote case $rooturl in /*) unset remote ;; http://*|https://*|ftp://*|ftps://*) dnfunc_init remote=1 ;; *) abend "root directory must be absolute file name or URL" esac if [ -z "$remote" ]; then # Check if rooturl exists and contains the necessary files and directories if [ ! -d $rooturl ]; then abend "$rooturl does not exist" fi # Safe mode is meaningless with local repository opmode=INCR fi # Check if rooturl contains all we need info "verifying distribution" # # Download CHECKSUMS.md5. So far it is the only file that is gpg-checked. # For the rest we rely on MD5 sums. checksums=$(getfile CHECKSUMS.md5 gpg) if [ -z "$checksums" ]; then abend "CHECKSUMS.md5 not found in $rooturl" fi onexit_remove $checksums announce=$(tail +13 $checksums | \ sed -n -r\ -e 's/^[0-9a-fA-F]+[[:space:]]+(\.\/ANNOUNCE\.[[:digit:]_]+)$/\1/p') if [ -z "$announce" ]; then abend "ANNOUNCE not found in $rooturl" fi file=$(getfile $announce) if [ -z "$file" ]; then abend "file $announce not found in $rooturl" fi onexit_remove $file newversion=$(echo "$announce" | sed -e 's/\.\/ANNOUNCE\.//' -e 's/_/./g') if [ -z "$newversion" ]; then abend "cannot determine new version" fi # Create list and index of available files tail +13 $checksums | \ sed -r \ -n \ -e 's/^[0-9a-fA-F]+[[:space:]]+(\.\/slackware(64)?\/.*\/(.*)-[^-]+-(i386|x86(_64)?|arm|noarch|fw)-[[:digit:]]+(_.*)?\.t.z)$/\3 \1/p' | \ tee $avail_index | awk '{print $1}' | sort > $avail_list # Initialize log file name logstem=/var/log/slackupgrade-$VERSION-$newversion${dry_run:+.dry_run} logfile=$logstem.log backup $logfile info "planning upgrade of Slackware $VERSION to $newversion${dry_run:+ (DRY RUN MODE)}" remove_report=$logstem.removed backup $remove_report # Check if pkgdir exists and contains the necessary files and directories for series in $series_names do n=$(sed -n -r -e 's/^[^[:space:]]+[[:space:]]+(\.\/slackware(64)?\/'$series'\/.*\.t.z)$/\1/p' $avail_index | head -1) if [ -z "$n" ]; then abend "no files in series $series" fi file=$(getfile $n) if [ -z "$file" ]; then abend "exiting" fi dropfile $file done # Build a list of installed packages ls /var/log/packages |\ sed -r -e 's/-[^-]+-(i386|x86(_64)?|arm|noarch|fw)-[[:digit:]]+(_.*)?//' |\ sort > $installed_list # Build a list of packages to install (if [ -n "$install_all" ]; then all_package_names else comm -1 -2 $installed_list $avail_list fi for s in $install_series do series_package_names $s done if [ -n "$install_packages" ]; then echo $install_packages fi) | sort -u > candidates.$$ # Build a list of packages that should be removed after install comm -2 -3 $installed_list candidates.$$ > remove.list.$$ if [ -s "$keep_file" ]; then grep -v '^#' $keep_file | \ tr -s '\n' | sort -u | comm -2 -3 remove.list.$$ - > remove.list rm remove.list.$$ else mv remove.list.$$ remove.list fi # Create $remove_report and the list of installation candidates. if [ -f "$SLACKUPGRADE_CONFDIR/$VERSION-$newversion.repl" ]; then info "reading $SLACKUPGRADE_CONFDIR/$VERSION-$newversion.repl" # Update candidates and save the replacement map in a temporary. awk '{ sub(/#.*$/,""); sub(/[[:space:]]+$/,"") } /\\$/ { sub(/\\$/,""); p = p $0; next } NF == 0 { if (p) print p; p = ""; next } { print p $0; p="" }' \ $SLACKUPGRADE_CONFDIR/$VERSION-$newversion.repl | \ sort +0 -1 | \ tee rename.$$ | \ join - remove.list | \ cut -d ' ' -f 2- | \ tr ' ' '\n' | \ cat - candidates.$$ | \ sort -u > candidates # Use the temporary to remove the original package names from the report. awk '{print $1}' rename.$$ | \ comm -13 - remove.list > $remove_report # Clean up rm rename.$$ else cp remove.list $remove_report mv candidates.$$ candidates fi # Disable interrupts during critical section trap '' HUP INT QUIT ABRT if [ -s $remove_report -a "$verbosity" != 'q' ]; then (cat <&2 <&2 <> $logfile fi info "see $remove_report for the list of packages that have been removed" fi info "upgrade finished; see $logfile for details" tempdir_remove conffiles=$logstem.new backup $conffiles find /etc /usr/lib*/ /usr/share/vim -name "*.new" 2>/dev/null | \ sort > $conffiles if [ ! -s "$conffiles" ]; then rm $conffiles fi if [ "$verbosity" != 'q' ]; then echo "IMPORTANT!" if [ -s "$conffiles" ]; then cat <