#!/bin/bash
# Copyright (C) 2008 Vladimir V. Kamarzin <vvk@altlinux.org>
# Copyright (C) 2020-2023 Vitaly Chikunov <vt@altlinux.org>
#
# Remove all kernels except current
#
# This file 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 2 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. 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; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

export LC_ALL=C
PROG=${0##*/}
message() {
	echo >&2 "$PROG: $*"
}

fatal() {
	message "$@"
	exit 1
}

show_usage()
{
	[ -z "$*" ] || message "$*"
	echo >&2 "Try \`$PROG --help' for more information."
	exit 1
}

show_help()
{
        cat <<EOF
Convenient way to remove old kernels previously installed with update-kernel.

Usage: $PROG [options]

Options:
	-f, -y, --force   do not ask for removal confirmation (default is to ask)
	-n, --dry-run     just simulate removal
	-t, --type        remove old kernels for the specified flavour (un-def, 6.12, etc)
	-a, --all         remove old kernels for all flavours (keep latest for each one)
	-A, --purge       purge other flavours
	--no-backup       do not keep the backup kernel
	-h, --help        show this text and exit

Note: currently booted and backup kernels won't be removed.
The backup kernel is the latest kernel with uptime of more than a day.
All numeric kernels (x.y) are considered to be a single flavour.
EOF
exit 0
}

#parse command line options
TEMP=$(getopt -n "$PROG" -o f,B,y,n,t:,h,a,A,v -l force,no-backup,dry-run,type:,help,all,purge,verbose -- "$@") || show_usage
eval set -- "$TEMP"

all=
dryrun=
force=
kernel_flavour=
nobackuplogic=
verbose=
while :; do
        case "$1" in
                --) shift; break
                        ;;
                -f|-y|--force) force="-y"
                        ;;
		-B) message "Option '-B' is deprecated, please use '--no-backup' instead."
			;&
		--no-backup)
			nobackuplogic=1
			;;
		-n|--dry-run) dryrun="--no-remove"
			;;
		-t|--type) shift ; kernel_flavour="$1"
			;;
		-a|--all) all=yes
			;;
		-A|--purge) all=purge
			;;
		-v|--verbose) verbose=1
			;;
                -h|--help) show_help
        esac
	shift
done

[ $# -eq 0 ] || show_usage "invalid argument -- '$*'"

if [ -n "$verbose" ]; then
	V() { echo >&2 "+ $*"; "$@"; }
else
	V() { "$@"; }
fi

cr=$'\n'
# set colors on tty
if [ -t 1 ]; then
        RED=$'\e[1;31m'
        GREEN=$'\e[1;32m'
	YELLOW=$'\e[1;33m'
        MAGENTA=$'\e[1;35m'
        BRIGHT=$'\e[1m'
        NORM=$'\e[0m'
else
        RED=
        GREEN=
	YELLOW=
        MAGENTA=
        BRIGHT=
        NORM=
fi

uname_r=${DEBUG_UNAME_OVERRIDE-$(uname -r)}

# get running kernel version
current_kernel_package=$(rpmquery -qf "/lib/modules/$uname_r/kernel" 2>/dev/null)
if [ -n "$current_kernel_package" ] ; then
    current_kernel_pkgname=$(rpmquery --queryformat "%{NAME}-%{VERSION}-%{RELEASE}\n" -q "$current_kernel_package")
    echo "Currently booted kernel package: $BRIGHT$current_kernel_pkgname$NORM"
    unset current_kernel_pkgname
else
    echo "Running kernel version: $BRIGHT$uname_r$NORM (package not found)"
fi

# set kernel flavour. if not defined with -t option, use current
current_kernel_flavour="$uname_r"
current_kernel_flavour="${current_kernel_flavour#*-}"
current_kernel_flavour="${current_kernel_flavour%-*}"
all_kernels="$(rpm -qa --qf '%{NAME}\t%{EVR}\n' 'kernel-image-*' | grep -v -w -e domU -e debuginfo -e checkinstall | cut -d- -f3-)"
# all_kernels have already stripped 'kernel-image-' prefix.
if [ -z "$kernel_flavour" ] && grep -qPx '\d+\.\d+' <<<"$current_kernel_flavour"; then
	# If we are booted into one of x.y (new) flavours apply purging logic
	# to the all of stable kernels.
	kernel_flavour=latest
fi
# Determine what flavours to consider for removal.
if [ -n "$all" ] ; then
	flavours="$(echo "$all_kernels" | cut -f1 | sort -u)"
elif [ "$kernel_flavour" = latest ]; then
	# Internal term for stable flavours, excluding -rc kernels. It's safe
	# to add std-def|un-def flavours here, because below it will be checked
	# for presence in the repo anyway. So, they will not be deleted if they
	# are in the repo, but they will be shown as latest.
	flavours=$(echo "$all_kernels" | grep -ve '\.0-alt0\.rc' | cut -f1 | grep -Px 'std-def|un-def|\d+\.\d+' | sort -u)
elif [ -n "$kernel_flavour" ]; then
	flavours="$kernel_flavour"
else
	flavours="$current_kernel_flavour"
fi
unset all_kernels

# Get the latest kernel with uptime >=1 day
if [ -z "$nobackuplogic" ] && [ -f "/var/log/wtmp" ]; then
	good_kernel=$(last -a reboot | awk '$ 10 ~ /+/ {print $10,$11; exit }')
	good_kernel_days=${good_kernel%% *}
	good_kernel=${good_kernel##* }
	good_kernel_days=${good_kernel_days#(}
	good_kernel_days=${good_kernel_days%%+*}
else
	good_kernel=
	good_kernel_days=
fi
good_kernel_package=
if [ -n "$good_kernel" ]; then
	if [ "$good_kernel" != "$uname_r" ]; then
		tver=${good_kernel%%-*}
		trel=${good_kernel##*-}
		tflv=${good_kernel#"$tver"-}
		tflv=${tflv%-"$trel"}
		good_kernel=kernel-image-$tflv-$tver-$trel
		good_kernel_package=$(rpm -q "$good_kernel" 2>/dev/null)
		unset tver trel tflv
		echo "Previous kernel with uptime $good_kernel_days days: $BRIGHT$good_kernel$NORM (backup)"
		if [ -z "$good_kernel_package" ]; then
			echo "${RED}Warning: Package for the backup kernel not found.$NORM"
		fi
	else
		echo "Backup kernel is the same as booted kernel (uptime $good_kernel_days days)."
	fi
else
	echo "${YELLOW}Warning: Backup kernel is not determined.$NORM"
fi

# Speed up consecutive apt-cache runs under a user by configuring session
# persistent Dir::Cache.
APTCACHE=()
if tmpdir=$(mktemp -d); then
	readonly tmpdir
	_atexit() { rm -rf -- "$tmpdir"; }
	trap _atexit EXIT
	eval "$(apt-config shell APTCACHEDIR Dir::Cache/f)"
	[ -w "$APTCACHEDIR" ] || APTCACHE=(-o Dir::Cache="$tmpdir")
else
	fatal "Cannot create temporary directory."
fi

in_pkglist() {
	local flavour=$1
	[ -e "$tmpdir/pkgnames" ] ||
		apt-cache "${APTCACHE[@]}" pkgnames 'kernel-image-' > "$tmpdir/pkgnames"
	grep -P "^kernel-image-\Q$flavour\E\#" < "$tmpdir/pkgnames" > "$tmpdir/pkgnames.$flavour"
	local pkg pkglist=
	while read -r pkg; do
		pkglist=$(apt-cache "${APTCACHE[@]}" policy "$pkg" | grep -w 'pkglist$')
		[ -z "$pkglist" ] || break
	done < "$tmpdir/pkgnames.$flavour"
	echo "${pkglist:+yes}"
}

# Highest stable flavour will be used for newest kernel check.
newest_stable_kernel=
newest_stable_flavour=$(grep -Px '\d+\.\d+' <<<"$flavours" | sort -V | tail -1)
if [ -n "$newest_stable_flavour" ]; then
	newest_stable_kernel=$(rpm -q "kernel-image-$newest_stable_flavour" 2>/dev/null | sort -V | tail -1)
fi

# Sort the kernels
other_flavours=
keep_kernels=
apt_args_list=
pkglisted=
for flavour in $flavours; do
	all_kernels="$(rpm -q "kernel-image-$flavour" 2>/dev/null | sort -V)"
	newest_kernel=$(echo "$all_kernels" | tail -1)
	[ "$all" = "purge" ] || pkglisted=$(in_pkglist "$flavour")
	real_flavour=$flavour
	if grep -qPx '\d+\.\d+' <<<"$flavour"; then
		newest_kernel=$newest_stable_kernel
		[ "$kernel_flavour" = latest ] && flavour=latest
	fi
	for kernel in $all_kernels; do
		reason=
		whydel=
		if [ "$all" = "purge" ] && [ "$kernel_flavour" != "$flavour" ]; then
			# Old kernels belonging to 'latest' flavours will be handled below as non-latest.
			whydel="other flavour"
		elif [ "$all" != "purge" ] && [ -z "$pkglisted" ]; then
			whydel="EOL"
		elif [ "$kernel" = "$newest_kernel" ]; then
			reason="$reason, latest for $real_flavour"
			[ "$kernel_flavour" != "$flavour" ] && other_flavours=y
		fi
		if [ "$kernel" = "$current_kernel_package" ]; then
			reason="$reason, currently booted"
		fi
		if [ "$kernel" = "$good_kernel_package" ]; then
			reason="$reason, with uptime $good_kernel_days days"
		fi
		if [ -n "$reason" ]; then
			keep_kernels="$keep_kernels   $GREEN$kernel$NORM (${reason#, })$cr"
		else
			[ -z "$whydel" ] || whydel=" ($whydel)"
			remove_kernels="$remove_kernels   $MAGENTA$kernel$NORM$whydel$cr"
			apt_args_list="$apt_args_list $(rpm -q --queryformat '%{NAME}=%{EPOCH}:%{VERSION}-%{RELEASE}\n' "$kernel" \
				| sed -e "s,(none):,,g")"
		fi
	done
done
howmuch=$(echo "$keep_kernels" | grep -c .)
if [ "$howmuch" -gt 0 ]; then
	[ "$howmuch" -eq 1 ] && phrase="this kernel" || phrase="these $BRIGHT$howmuch$NORM kernels"
	echo "Keeping $phrase (with the reason why):"
	printf "%s" "$keep_kernels"
	[ -n "$other_flavours" ] && echo "Specify -A to remove all kernels of other flavours than $kernel_flavour."
	echo
fi

if [ -z "$apt_args_list" ] ; then
	echo "Nothing to remove."
	exit 0
fi

howmuch=$(echo "$remove_kernels" | grep -c .)
[ "$howmuch" -eq 1 ] && phrase="this kernel" || phrase="these $BRIGHT$howmuch$NORM kernels"
echo "Will be removing $phrase:"
echo "$remove_kernels"

if [ "$UID" != "0" ]; then
	echo >&2 "${RED}Warning: This program requires root privileges.$NORM"
fi

echo -n "Confirm" ${dryrun:+--dry-run} "uninstall action for $phrase [Y/n]? "
if [ -n "$force" ]; then
	echo "yes"
else
	while true; do
		read -r || { echo "Aborting"; exit 1; }
		shopt -s nocasematch
		case "$REPLY" in
			n|no|q) exit 0 ;;
			y|ye|yes|'') break ;;
			*) ;;
		esac
		echo -n "[Y/n] "
		shopt -u nocasematch
	done
fi


# shellcheck disable=SC2086
V apt-get -y $dryrun remove $apt_args_list

# Mask non-zero apt exit code on dry run:
if [ -n "$dryrun" ]; then
    exit 0
fi
