#!/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"
cols=$(tput cols 2>/dev/null || stty size 2>/dev/null | cut -d' ' -f2)
if [ ${cols:-0} -lt 10 ]; then
progressbar=0
fi
if [ $progressbar -eq 1 ]; then
total=$(wc -l candidates | cut -d ' ' -f 1)
width=$(( $cols - 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 -u > $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 <