#! /bin/bash

##############################################################################
# How it works:
# * Kernels install modules below /usr/lib/modules/$krel/kernel/.
# * KMPs install modules below /usr/lib/modules/$krel/updates/ or .../extra/.
# * Symbolic links to modules of compatible KMPs are created under
#   /usr/lib/modules/$krel/weak-updates/{updates,extra}/... (the original path
#   below /usr/lib/modules/$other_krel is used).
# * Depmod searches the directories in this order: updates/, extra/,
#   weak-updates/, kernel/ (see /etd/depmod.conf or
#   /etc/depmod.d/00-system.conf for details).
# * Compatibility of a kernel with a KMP is defined as: The KMP is built
#   for the same flavor as the kernel and after adding the KMP modules to
#   the kernel, depmod -e -E Module.symvers reports no errors about
#   missing symbols or different symbol checksums. See the
#   has_unresolved_symbols() function for details.
#
# * At KMP install time (function add_kmp()), we create symbolic links
#   for all kernels that this KMP is compatible with. We skip kernels that
#   already contain symbolic links to a newer KMP of the same name,
#   contain the KMP itself or another version in updates/ or extra/ or
#   have overlapping module names with other KMPs in the respective
#   kernel (this should not happen).
# * At kernel install time (functions add_kernel()), we create symbolic
#   links for each compatible KMP, unless the KMP or a different one with
#   overlapping module names is present in updates/ or extra/ (KMP build
#   against $krel can be installed before a kernel with that version).
#   When multiple KMPs of the same name are compatbile, we chose the one
#   with the highest version number. This is repeated when subsequent
#   subpackages (main or -extra) of that kernel are installed.
# * At KMP removal time (function remove_kmp()), the modules and their
#   symlinks are removed and, where possible, replaced by symlinks to the
#   newest of the remaining compatible version of that KMP.
# * [NOT IMPLEMENTED] When a kernel subpackage is removed, symlinks to
#   KMPs that become incompatible are removed as well. This is not
#   implemented, because removing the main subpackage and only keeping
#   the -base package AND having KMPs installed is not an expected
#   scenario, and implementing this would only slow down kernel updates.
# * When the kernel is removed (function remove_kernel()), it's
#   weak-updates directory is also removed.
#
# naming conventions used in this script:
# $kmp: name-version-release of a kmp, e.g kqemu-kmp-default-1.3.0pre11_2.6.25.16_0.1-7.1
# $kmpshort: name of a kmp, e.g kqemu-kmp-default
# $basename: portion of $kmp up to the "-kmp-" part, e.g kqemu
# $flavor: flavor of a kmp or kernel, e.g default
# $krel: kernel version, as in /usr/lib/modules/$krel
# $module: full path to a module below updates/
# $symlink: full path to a module symlink below weak-updates/
#
# files in $tmpdir:
# krel-$kmp: kernel version for which $kmp was built
# modules-$kmp: list of modules in $kmp (full paths)
# basenames-$kmp: list of basenames of modules in $kmp
# kmps: list of kmps, newest first
#

: "${DRACUT:=/usr/bin/dracut}"
: "${MKOSI_INITRD:=/usr/bin/mkosi-initrd}"

find_lsinitrd() {
    local lsi
    LSINITRD=
    for lsi in /usr/lib/module-init-tools/lsinitrd-quick /usr/bin/lsinitrd; do
	if [[ -x $lsi ]]; then
	    LSINITRD=$lsi
	    break
	fi
    done
    if [[ ! "$LSINITRD" ]]; then
	echo "$0: could not find lsinitrd" >&2
	exit 1
    fi
    dlog "LSINITRD=$LSINITRD"
}

find_depmod() {
    local _d

    [[ ! -x "$DEPMOD" ]] || return
    DEPMOD=
    for _d in /usr/sbin /sbin; do
	if [[ -x ${_d}/depmod ]]; then
	    DEPMOD=${_d}/depmod
	    break;
	fi
    done
    if [[ ! "$DEPMOD" ]]; then
	echo "ERROR: depmod is not installed - aborting" >&2
	exit 1
    fi
    dlog "DEPMOD=$DEPMOD"
}

find_sdbootutil() {
    [[ ! -x "$SDBOOTUTIL" ]] || return
    SDBOOTUTIL=
    if [ -e "/usr/bin/sdbootutil" ] && /usr/bin/sdbootutil is-installed; then
	SDBOOTUTIL="/usr/bin/sdbootutil"
    fi
    dlog "SDBOOTUTIL=$SDBOOTUTIL"
}

find_initrd_generator() {
    INITRD_GENERATOR="$(. /etc/sysconfig/bootloader 2>/dev/null && echo "$INITRD_GENERATOR")"

    # dracut is the default initrd generator
    if [ -z "$INITRD_GENERATOR" ]; then
	INITRD_GENERATOR="dracut"
    elif [ "$INITRD_GENERATOR" = "mkosi" ]; then
	INITRD_GENERATOR="mkosi-initrd"
    fi

    [ "$INITRD_GENERATOR" = "dracut" ] || [ "$INITRD_GENERATOR" = "mkosi-initrd" ] || {
	echo "ERROR: the initrd generator \"$INITRD_GENERATOR\" configured in /etc/sysconfig/bootloader is not supported." >&2
	exit 1
    }

    dlog "INITRD_GENERATOR=$INITRD_GENERATOR"
}

find_usrmerge_boot() {
    local filename=$1
    local kver=$2
    local ext=${3:+."$3"}
    local f

    for f in "/usr/lib/modules/$kver/$filename$ext" "/boot/$filename-$kver$ext"
    do
	if [ -e "$f" ]; then
	    echo "$f"
	    return
	fi
    done
    log "WARNING: find_usrmerge_boot: $filename$ext not found for kernel $kver"
}

log() {
    [ $opt_verbose -gt 0 ] && echo "$@" >&2
    return 0
}

dlog() {
    [ $opt_verbose -gt 1 ] && echo "$@" >&2
    return 0
}

doit() {
    if [ -n "$doit" ]; then
	# override
	"$@"
	return
    fi
    log "$@"
    if [ -z "$opt_dry_run" ]; then
	"$@"
    else
	:
    fi
}

strip_mod_extensions() {
    sed -rn '/^_kernel_$/p;s/\.ko(\.[gx]z|\.zst)?$//p'
}

# Name of the symlink that makes a module available to a given kernel
symlink_to_module() {
    local module=$1 krel=$2

    echo /usr/lib/modules/$krel/weak-updates/${module#/usr/lib/modules/*/}
}

# Is a kmp already present in or linked to from this kernel?
__kmp_is_present() {
    local kmp=$1 krel=$2

    if [ $krel = "$(cat $tmpdir/krel-$kmp)" ]; then
        return 0
    fi
    local module symlink
    while read module; do
	symlink=$(symlink_to_module $module $krel)
	[ $module -ef $symlink -o $module = "$(readlink $symlink)" ] || return 1
    done < $tmpdir/modules-$kmp

    return 0
}

kmp_is_present() {
    __kmp_is_present "$1" "$2"
    local res=$?
    dlog "kmp_is_present: kmp=$1 krel=$2 => $res"
    return $res
}

# Add the modules of a kmp to /usr/lib/modules/$krel
add_kmp_modules() {
    local kmp=$1 krel=$2 basedir=$3

    [ -n "$kmp" ] || return 0

    local module symlink
    while read module; do
	symlink=$(symlink_to_module $module $krel)
	doit mkdir -p ${opt_debug:+-v} $basedir${symlink%/*} || exit 1
	doit ln -sf ${opt_debug:+-v} $module $basedir$symlink || exit 1
	dlog "add_kmp_modules: added $module to $krel"
    done < $tmpdir/modules-$kmp
}

# Remove the modules of a kmp from /usr/lib/modules/$krel
remove_kmp_modules() {
    local kmp=$1 krel=$2 basedir=$3

    [ -n "$kmp" ] || return 0

    local module symlink
    while read module; do
	symlink=$(symlink_to_module $module $krel)
	doit rm -f ${opt_debug:+-v} $basedir$symlink
	dlog "remove_kmp_modules: removed $module from $krel"
    done < $tmpdir/modules-$kmp
}

# Create a temporary working copy of /usr/lib/modules/$1
create_temporary_modules_dir() {
    local modules_dir=/usr/lib/modules/$1 basedir=$2
    local opt_v=${opt_debug:+-v}

    mkdir -p $opt_v $basedir$modules_dir/weak-updates
    ln -s $opt_v $modules_dir/kernel $basedir$modules_dir/kernel

    eval "$(find $modules_dir -path "$modules_dir/modules.*" -prune \
		-o -path "$modules_dir/kernel" -prune \
		-o -type d -printf "mkdir -p $opt_v $basedir%p\n" \
		-o -printf "ln -s $opt_v %p $basedir%p\n"
           )"
}

# Check for unresolved symbols
has_unresolved_symbols() {
    local krel=$1 basedir=$2 output status args sym_errors _f

    if [ ! -e "$tmpdir/symvers-$krel" ]; then
	_f=$(find_usrmerge_boot symvers "$krel" gz)
	if [ -n "$_f" ]; then
	    dlog "has_unresolved_symbols: found $_f"
	    zcat "$_f" >"$tmpdir/symvers-$krel"
	fi
    fi
    if [ -e $tmpdir/symvers-$krel ]; then
	args=(-E $tmpdir/symvers-$krel)
    else
	echo "WARNING: symvers.gz not found for $krel, symbol resolution will be unreliable" >&2
	_f=$(find_usrmerge_boot System.map "$krel")
	if [ -n "$_f" ]; then
	    args=(-F "$_f")
	else
	    echo "WARNING: System.map not found for $krel, symbol resolution may fail" >&2
	fi
    fi
    output="$("$DEPMOD" -b "$basedir" -ae "${args[@]}" $krel 2>&1)"
    status=$?
    if [ $status -ne 0 ]; then
	echo "$output" >&2
	echo "depmod exited with error $status" >&2
	return 0
    fi
    sym_errors=$(echo "$output" | \
	grep -E ' (needs unknown|disagrees about version of) symbol ')
    if [ -n "$sym_errors" ]; then
	[ -z "$opt_debug" ] || echo "$sym_errors" >&2
	return 0
    fi
    dlog "has_unresolved_symbols: no errors found for $krel"
    return 1
}

# KMPs can only be added if none of the module basenames overlap
basenames_are_unique() {
    local kmp=$1 krel=$2 basedir=$3 dir

    for dir in $basedir/usr/lib/modules/$krel/{weak-updates,updates,extra}/; do
        if [ ! -d "$dir" ]; then
            continue
        fi
	local overlap="$(comm -1 -2 $tmpdir/basenames-$kmp \
               <(find "$dir" -not -type d -printf '%f\n' | sort -u))"
        if [ -n "$overlap" ]; then
	    dlog "basenames_are_unique: found name overlap for $kmp in $dir: " $overlap
            return 1
        fi
    done
    dlog "basenames_are_unique: $kmp is unique in $basedir"
    return 0
}

# Can a kmp be replaced by a different version of the same kmp in a kernel?
# Set the old kmp to "" when no kmp is to be removed.
__can_replace_kmp() {
    local old_kmp=$1 new_kmp=$2 krel=$3

    local basedir=$tmpdir/$krel
    local weak_updates=/usr/lib/modules/$krel/weak-updates/

    [ -d "$basedir" ] || \
	create_temporary_modules_dir "$krel" "$basedir"

    # force doit() to execute the commands (in $tmpdir)
    doit=1 remove_kmp_modules "$old_kmp" "$krel" "$basedir"
    if ! basenames_are_unique "$new_kmp" "$krel" "$basedir"; then
	doit=1 add_kmp_modules "$old_kmp" "$krel" "$basedir"
	return 1
    fi
    doit=1 add_kmp_modules "$new_kmp" "$krel" "$basedir"
    if has_unresolved_symbols "$krel" "$basedir"; then
	doit=1 remove_kmp_modules "$new_kmp" "$krel" "$basedir"
	doit=1 add_kmp_modules "$old_kmp" "$krel" "$basedir"
	return 1
    fi
    return 0
}

can_replace_kmp() {
    __can_replace_kmp "$1" "$2" "$3"
    local res=$?
    dlog "can_replace_kmp: old=$1 new=$2 krel=$3 => $res"
    return $res
}

# Figure out which modules a kmp contains
check_kmp() {
    local kmp=$1

    # Make sure all modules are for the same kernel
    set -- $(sed -re 's:^/usr/lib/modules/([^/]+)/.*:\1:' \
		 $tmpdir/modules-$kmp \
	     | sort -u)
    if [ $# -ne 1 ]; then
	echo "Error: package $kmp seems to contain modules for multiple" \
	     "kernel versions" >&2
	return 1
    fi
    echo $1 > $tmpdir/krel-$kmp
    dlog "check_kmp: $kmp contains modules for $1"

    # Make sure none of the modules are in kernel/ or weak-updates/
    if grep -qE -e '^/usr/lib/modules/[^/]+/(kernel|weak-updates)/' \
	    $tmpdir/modules-$kmp; then
	echo "Error: package $kmp must not install modules into " \
	     "kernel/ or weak-updates/" >&2
	return 1
    fi
    sed -e 's:.*/::' $tmpdir/modules-$kmp \
	| sort -u > $tmpdir/basenames-$kmp
    dlog "check_kmp: $kmp contains: " $(cat $tmpdir/basenames-$kmp)
}

# Figure out which kmps there are, and which modules they contain
# set basename to '*' to find all kmps of a given flavor
find_kmps() {
    local basename=$1 flavor=$2
    local kmp

    for kmp in $(rpm -qa --qf '%{n}-%{v}-%{r}\n' --nodigest --nosignature "$basename-kmp-$flavor"); do
	dlog "find_kmps: looking at $kmp"
	if rpm -q --qf '[%{providename}\n]' --nodigest --nosignature "$kmp" | \
	    grep -q '^kmp_in_kernel$'; then
	    # KMP built directly from the kernel spec file (fate#319339)
	    continue
	fi
	rpm -ql --nodigest --nosignature "$kmp" \
	    | grep -Ee '^/usr/lib/modules/[^/]+/.+\.ko(\.[gx]z|\.zst)?$' \
	    > $tmpdir/modules-$kmp
	if [ $? != 0 ]; then
	    echo "WARNING: $kmp does not contain any kernel modules" >&2
	    rm -f $tmpdir/modules-$kmp
	    continue
	fi

	check_kmp $kmp || return 1
    done

    printf "%s\n" $tmpdir/basenames-* \
    | sed -re "s:$tmpdir/basenames-::" \
    | /usr/lib/rpm/rpmsort -r \
    > $tmpdir/kmps

    dlog "find_kmps: kmps found: " $(cat $tmpdir/kmps)
}

__previous_version_of_kmp() {
    local new_kmp=$1 krel=$2
    local module symlink old_kmp
    
    while read module; do
	symlink=$(symlink_to_module $module $krel)
	[ -e "$symlink" ] || continue
	[ -L "$symlink" ] || return

	old_kmp=$(grep -l "$(readlink "$symlink")" $tmpdir/modules-* | sed 's:.*/modules-::' ) || return
	# The package %NAME must be the same
	[ "${old_kmp%-*-*}" == "${new_kmp%-*-*}" ] || return
	# The other kmp must be older
	while read kmp; do
	    [ "$kmp" == "$old_kmp" ] && return
	    [ "$kmp" == "$new_kmp" ] && break
	done <$tmpdir/kmps
    done < $tmpdir/modules-$new_kmp
    echo "$old_kmp"
}

previous_version_of_kmp() {
    local old="$(__previous_version_of_kmp "$1" "$2")"
    local res=$?
    dlog "previous_version_of_kmp: kmp=$1 krel=$2 => $old"
    echo "$old"
    return $res
}

# Create list of dracut configuration files to read, taking into
# account priorities of the user and built-in configuration.
# Copied from dracut.sh as of dracut 059 (GPL-2.0-or-later)
dropindirs_sort() {
    local suffix=$1
    shift
    local -a files
    local f d

    for d in "$@"; do
        for i in "$d/"*"$suffix"; do
            if [[ -e $i ]]; then
                printf "%s\n" "${i##*/}"
            fi
        done
    done | sort -Vu | {
        readarray -t files

        for f in "${files[@]}"; do
            for d in "$@"; do
                if [[ -e "$d/$f" ]]; then
                    printf "%s\n" "$d/$f"
                    continue 2
                fi
            done
        done
    }
}

get_current_initrd() {
    local krel=$1
    local initrd="/boot/initrd-$krel"
    if [ -n "$SDBOOTUTIL" ]; then
	if [ -z "$TRANSACTIONAL_UPDATE" ] && [ -s '/etc/kernel/entry-token' ]; then
	    read -r _ initrd <<<"$(sdbootutil --entry-keys=initrd show-entry $krel)"
	    initrd="/boot/efi$initrd"
	fi
    fi
    echo "$initrd"
}

get_current_basenames() {
    local initrd=$(get_current_initrd $1)
    $LSINITRD "$initrd" |
	sed -rn 's:.*\<usr/lib/modules/.*/::p' |
	strip_mod_extensions
}

DRACUT_CONFFILE=/etc/dracut.conf
DRACUT_CONFDIR=/etc/dracut.conf.d
DRACUT_BUILTIN_CONFDIR=/usr/lib/dracut/dracut.conf.d
GET_DRACUT_DRIVERS=/usr/lib/module-init-tools/get_dracut_drivers

get_dracut_basenames() {
    local setpriv=$(command -v setpriv)
    local conf= cf

    [[ -x "$GET_DRACUT_DRIVERS" && "$setpriv" ]] || {
	echo "$0: unable to parse dracut configuration, skipping it" >&2
	get_current_basenames "$1"
	return
    }

    [[ ! -f "$DRACUT_CONFFILE" ]] ||
	conf="$(cat "$DRACUT_CONFFILE")"

    # Assemble the content of the dracut configuration files.
    # Make sure to put a newline between subsequent conf file contents.
    for cf in $(dropindirs_sort .conf "$DRACUT_CONFDIR" "$DRACUT_BUILTIN_CONFDIR"); do
	conf="$conf
$(cat "$cf")"
    done

    # run get_dracut_drivers with reduced privileges
    get_current_basenames "$1" |
	"$setpriv" --reuid=nobody --regid=nobody --clear-groups --inh-caps=-all \
		   "$GET_DRACUT_DRIVERS" "$conf"
}

get_mkosi_initrd_basenames() {
    # TODO: get drivers from mkosi-initrd conf (KernelModulesInclude=, KernelModulesExclude=)
    get_current_basenames "$1"
}

get_initrd_basenames() {
    local krel=$1

    case "$INITRD_GENERATOR" in
	"dracut")
	    get_dracut_basenames "$krel"
	    ;;
	"mkosi-initrd")
	    get_mkosi_initrd_basenames "$krel"
	    ;;
    esac
}

# test if rebuilding initrd is needed for $krel.
# stdin - list of changed modules ("_kernel_" for the whole kernel)
needs_initrd() {
    local krel=$1

    # Don't generate an initrd for kdump here. It's done automatically with mkdumprd when
    # /etc/init.d/boot.kdump is called to load the kdump kernel. See mkdumprd(8) why
    # it is done this way.
    if [[ "$krel" == *kdump* ]]; then
        return 1
    fi

    if ! [ -f /etc/fstab -a ! -e /.buildenv ]; then
	case "$INITRD_GENERATOR" in
	"dracut")
	    echo "Please run \"$DRACUT -f /boot/initrd-$krel $krel\" as soon as your system is complete." >&2
	    ;;
	"mkosi-initrd")
	    echo "Please run \"$MKOSI_INITRD --kernel-version $krel -O /boot -o initrd-$krel\" \
as soon as your system is complete." >&2
	    ;;
	esac
	return 1
    fi
    # KMPs can force initrd rebuild with %kernel_module_package -b that sets
    # this variable
    if test -n "$KMP_NEEDS_MKINITRD" && \
		! test "$KMP_NEEDS_MKINITRD" -eq 0 2>/dev/null; then
	dlog "needs_initrd: yes, KMP_NEEDS_MKINITRD=$KMP_NEEDS_MKINITRD"
	return 0
    fi

    local changed_basenames=($(strip_mod_extensions | sort -u))
    dlog "needs_initrd: changed_basenames: " $changed_basenames

    if [ "$changed_basenames" = "_kernel_" ]; then
	dlog "needs_initrd: yes, kernel package"
	return 0
    fi
    local initrd="$(get_current_initrd $krel)"
    if [ -z "$SDBOOTUTIL" ] && [ ! -e "$initrd" ]; then
	dlog "needs_initrd: yes, initrd doesn't exist yet"
	return 0
    fi
    local initrd_basenames=($(get_initrd_basenames "$krel" | sort -u))
    dlog "needs_initrd: initrd_basenames: " $initrd_basenames
    local i=($(join <(printf '%s\n' "${changed_basenames[@]}") \
	            <(printf '%s\n' "${initrd_basenames[@]}") ))
    log "changed initrd modules for kernel $krel: ${i[@]-none}"
    if [ ${#i[@]} -gt 0 ]; then
	dlog "needs_initrd: yes, modules changed"
	return 0
    fi
    dlog "needs_initrd: no"
    return 1
}

# run depmod and rebuild initrd for kernel version $krel
# stdin - list of changed modules ("_kernel_" for a whole kernel)
run_depmod_build_initrd() {
    local krel=$1
    local status=0
    local system_map

    if [ -d /usr/lib/modules/$krel ]; then
	system_map=$(find_usrmerge_boot System.map "$krel")
	if [ -n "$system_map" ]; then
	   doit "$DEPMOD" -F "$system_map" -ae "$krel" || return 1
	fi
    fi
    if needs_initrd $krel; then
	local image x
	for x in vmlinuz image vmlinux linux bzImage uImage Image zImage; do
	    image=$(find_usrmerge_boot "$x" "$krel")
	    [ -z "$image" ] || break
	done
	if [ -n "$image" ]; then
	    if [ -n "$INITRD_IN_POSTTRANS" ] || ([ -n "$SDBOOTUTIL" ] && [ -n "$TRANSACTIONAL_UPDATE" ]); then
		mkdir -p /run/regenerate-initrd
		doit touch /run/regenerate-initrd/$krel
	    else
		if [ -n "$SDBOOTUTIL" ] && [ -z "$TRANSACTIONAL_UPDATE" ]; then
		    doit "$SDBOOTUTIL" --no-reuse-initrd add-kernel "$krel"
		elif [ -z "$SDBOOTUTIL" ]; then
		    local initrd="$(get_current_initrd $krel)"
		    case "$INITRD_GENERATOR" in
			"dracut")
			    doit "$DRACUT" -f "$initrd" $krel
			    ;;
			"mkosi-initrd")
			    doit "$MKOSI_INITRD" --kernel-version "$krel" -O "/boot" -o "initrd-$krel"
			    ;;
		    esac
		fi
		status=$?
	    fi
	else
	    echo "WARNING: kernel image for $krel not found!" >&2
	fi
    fi
    return $status
}

walk_kmps() {
    local krel=$1
    local kmps=( $(cat $tmpdir/kmps) )

    while :; do
	[ ${#kmps[@]} -gt 0 ] || break
	local added='' skipped='' n kmp
	for ((n=0; n<${#kmps[@]}; n++)); do
	    kmp=${kmps[n]}
	    [ -n "$kmp" ] || continue

	    dlog "walk_kmps: checking $kmp for $krel"
	    if kmp_is_present $kmp $krel; then
		log "Package $kmp does not need to be added to kernel $krel"
		kmps[n]=''
		continue
	    fi
	    local old_kmp=$(previous_version_of_kmp $kmp $krel)
	    if can_replace_kmp "$old_kmp" $kmp $krel; then
		remove_kmp_modules "$old_kmp" "$krel"
		add_kmp_modules "$kmp" "$krel"
		if [ -z "$old_kmp" ]; then
		    log "Package $kmp added to kernel $krel"
		else
		    log "Package $old_kmp replaced by package $kmp in kernel $krel"
		fi
		added=1
		kmps[n]=''
		continue
	    fi
	    dlog "walk_kmps: skipped $kmp"
	    skipped=1
	done
	[ -n "$added" -a -n "$skipped" ] || break
    done
}

kernel_changed() {
    local krel=$1 flavor=${1##*-}
    local system_map=$(find_usrmerge_boot System.map "$krel")

    if [ -z "$system_map" ]; then
	# this kernel does not exist anymore
	dlog "kernel_changed: kernel removed"
	return 0
    fi
    if [ ! -d /usr/lib/modules/$krel ]; then
	# a kernel without modules - rebuild initrd nevertheless (to mount the
	# root fs, etc).
	dlog "kernel_changed: kernel without modules"
    elif find_kmps '*' $flavor; then
	walk_kmps "$krel"
    fi

    echo "_kernel_" | run_depmod_build_initrd "$krel"
}

add_kernel() {
    local krel=$1

    kernel_changed $krel
}

remove_kernel() {
    local krel=$1

    local dir=/usr/lib/modules/$krel
    if [ -d $dir/weak-updates ]; then
	rm -rf $dir/weak-updates
    fi
    # If there are no KMPs left, remove the empty directory
    rmdir $dir 2>/dev/null
}

add_kernel_modules() {
    local krel=$1
    cat >/dev/null

    kernel_changed $krel
}

remove_kernel_modules() {
    local krel=$1
    cat >/dev/null

    # FIXME: remove KMP symlinks that no longer work
    kernel_changed $krel
}

add_kmp() {
    local kmp=$1 kmpshort=${1%-*-*}
    local basename=${kmpshort%-kmp-*} flavor=${kmpshort##*-}
    local system_map

    # Find the kmp to be added as well as any previous versions
    find_kmps "$basename" "$flavor" || return 1

    local dir krel status
    for dir in /usr/lib/modules/*; do
	krel=${dir#/usr/lib/modules/}
        case "$krel" in
        *-$flavor)
            ;;
        *)
            continue
        esac
	dlog "add_kmp: processing $kmp for $krel"
	[ -d $dir ] || continue
	system_map=$(find_usrmerge_boot System.map "$krel")
	[ -n "$system_map" ] || continue
	if opt_debug=1 has_unresolved_symbols "$krel" "/"; then
	    echo "Warning: /usr/lib/modules/$krel is inconsistent" >&2
	    echo "Warning: weak-updates symlinks might not be created" >&2
	fi

	if kmp_is_present $kmp $krel; then
	    log "Package $kmp does not need to be added to kernel $krel"
	    run_depmod_build_initrd "$krel" <$tmpdir/basenames-$kmp || \
		status=1
	    continue
	fi
	local old_kmp=$(previous_version_of_kmp $kmp $krel)
	if can_replace_kmp "$old_kmp" $kmp $krel; then
	    remove_kmp_modules "$old_kmp" "$krel"
	    add_kmp_modules "$kmp" "$krel"
	    if [ -z "$old_kmp" ]; then
		log "Package $kmp added to kernel $krel"
		run_depmod_build_initrd "$krel" <$tmpdir/basenames-$kmp || \
		    status=1
	    else
		log "Package $old_kmp replaced by package $kmp in kernel $krel"
		cat $tmpdir/basenames-{$old_kmp,$kmp} \
		| run_depmod_build_initrd "$krel" || status=1
	    fi
	else
	    dlog "add_kmp: skipped $kmp"
	fi
    done
    dlog "add_kmp: status=$status"
    return $status
}

remove_kmp() {
    local kmp=$1 kmpshort=${1%-*-*}
    local basename=${kmpshort%-kmp-*} flavor=${kmpshort##*-}

    # Find any previous versions of the same kmp
    find_kmps "$basename" "$flavor" || return 1

    # Read the list of module names from standard input
    # (This kmp may already have been removed!)
    cat > $tmpdir/modules-$kmp
    check_kmp "$kmp" || return 1

    local dir krel status system_map
    for dir in /usr/lib/modules/*; do
	krel=${dir#/usr/lib/modules/}
        case "$krel" in
        *-$flavor)
            ;;
        *)
            continue
        esac
	[ -d $dir ] || continue
	system_map=$(find_usrmerge_boot System.map "$krel")
	[ -n "$system_map" ] || continue
	dlog "remove_kmp: processing $kmp for $krel"
	if kmp_is_present $kmp $krel; then
	    local other_found=0 inconsistent=0

	    if opt_debug=1 has_unresolved_symbols "$krel" "/" \
			>$tmpdir/unresolved-"$krel" 2>&1; then
		inconsistent=1
	    fi

            if [ $krel != "$(cat $tmpdir/krel-$kmp)" ]; then
                remove_kmp_modules "$kmp" "$krel"
            fi

	    local other_kmp
	    while read other_kmp; do
		[ "$kmp" != "$other_kmp" ] || continue

		other_found=1
		dlog "remove_kmp: checking other KMP $other_kmp"
		if can_replace_kmp "" "$other_kmp" "$krel"; then
		    add_kmp_modules "$other_kmp" "$krel"
		    break
		fi
	    done < $tmpdir/kmps
	    if [ -n "$other_kmp" ]; then
		log "Package $kmp replaced by package $other_kmp in kernel $krel"
		cat $tmpdir/basenames-{$kmp,$other_kmp} \
		| run_depmod_build_initrd "$krel" || status=1
	    else
		log "Package $kmp removed from kernel $krel"
		if [ $other_found -eq 1 ]; then
		    log "Weak-updates symlinks to no other $kmpshort package could be created"
		    if [ $inconsistent -eq 1 ]; then
			echo "Warning: /usr/lib/modules/$krel was inconsistent before removal of $kmp" >&2
			[ -s $tmpdir/unresolved-"$krel" ] && \
			    cat $tmpdir/unresolved-"$krel"
		    fi
		fi
		run_depmod_build_initrd "$krel" <$tmpdir/basenames-$kmp || \
		    status=1
	    fi
	    rm -f $tmpdir/unresolved-"$krel"
	fi
    done
    dlog "remove_kmp: status=$status"
    return $status
}

help() {
cat <<EOF
${0##*/} --add-kmp kmp-name-version-release
	To be called in %post of kernel module packages. Creates
	symlinks in compatible kernel's weak-updates/ directory and rebuilds
	the initrd if needed.

${0##*/} --remove-kmp kmp-name < module-list
	To be called in %postun of kernel module packages. Removes
	weak-updates/ symlinks for this KMP. As the KMP doesn't exist in
	the RPM database at this point, the list of modules has to be
	passed on standard input. Rebuilds the initrd if needed.

${0##*/} --add-kernel kernel-release
	To be called in %post of the kernel base package. Adds
	compatibility symlinks for all compatible KMPs and rebuilds the
	initrd if needed.

${0##*/} --remove-kernel kernel-release
	To be called in %postun of the kernel base package. Removes all
	compatibility symlinks.

${0##*/} --add-kernel-modules kernel-release < module-list
        To be called in %post of kernel subpackages that only contain
        modules (i.e. not kernel-*-base). Adds newly available
        compatibity symlinks and rebuilds the initrd if needed.

${0##*/} --remove-kernel-modules kernel-release < module-list
        To be called in %postun of kernel subpackages that only contain
        modules (i.e. not kernel-*-base). Removes no longer working
        compatibity symlinks and rebuilds the initrd if needed.

${0##*/} --verbose ...
	Print commands as they are executed and other information.

${0##*/} --dry-run ...
	Do not perform any changes to the system. Useful together with
	--verbose for debugging.
EOF
}

usage() {
    echo "Usage:"
    help | sed -n 's/^[^[:blank:]]/    &/p'
}

##############################################################################

save_argv=("$@")
options=`getopt -o vh --long add-kernel,remove-kernel,add-kmp,remove-kmp \
                      --long add-kernel-modules,remove-kernel-modules \
		      --long usage,help,verbose,dry-run,debug,logfile: -- "$@"`
if [ $? -ne 0 ]; then
    usage >&2
    exit 1
fi
eval set -- "$options"
mode=
opt_logfile=$WM2_LOGFILE
case $WM2_VERBOSE in
    [0-3])
	opt_verbose=$WM2_VERBOSE
	;;
    *)
	opt_verbose=0
	;;
esac
case $WM2_DEBUG in
    1)
	opt_debug=1
	;;
    *)
	opt_debug=
	;;
esac
while :; do
    case "$1" in
    --add-kernel | --remove-kernel | --add-kernel-modules | \
    --remove-kernel-modules | --add-kmp | --remove-kmp )
	mode="$1"
	;;
    -v | --verbose)
	opt_verbose=$((opt_verbose + 1))
	;;
    --dry-run)
	opt_dry_run=1
	;;
    --debug)
	opt_debug=1
	;;
    --logfile)
	shift
	opt_logfile=$1
	;;
    --usage)
	usage
	exit 0
	;;
    -h | --help)
	help
	exit 0
	;;
    --)
	shift
	break
	;;
    esac
    shift
done

err=
case "$mode" in
"")
    err="Please specify one of the --add-* or --remove-* options"
    ;;
--add-kernel | --remove-kernel)
    if [ $# -gt 1 ]; then
        err="Too many arguments to $mode"
    fi
    [ $# -eq 1 ] || set -- $(uname -r)
    ;;
*)
    if [ $# -ne 1 ]; then
        err="Option $mode requires exactly one argument"
    fi
    ;;
esac
if [ -n "$err" ]; then
    echo "ERROR: $err" >&2
    usage >&2
    exit 1
fi

if [ -n "$opt_logfile" ]; then
    [ "${opt_logfile#/}" != "$opt_logfile" ] || opt_logfile="/var/log/$opt_logfile"
    echo "${0##/*}: appending output to $opt_logfile" >&2
    exec 2>>"$opt_logfile"
fi
if [ $opt_verbose -gt 2 ]; then
    set -x
    # tracing will print everything, no need to print in log()
    opt_verbose=$((opt_verbose - 3))
fi

#unset LANG LC_ALL LC_COLLATE
find_depmod
find_lsinitrd
find_sdbootutil
find_initrd_generator

tmpdir=$(mktemp -d /var/tmp/${0##*/}.XXXXXX)
trap "rm -rf $tmpdir" EXIT

shopt -s nullglob

case $mode in
--add-kernel)
    add_kernel "$1"
    ;;
--remove-kernel)
    remove_kernel "$1"
    ;;
--add-kernel-modules)
    add_kernel_modules "$1"
    ;;
--remove-kernel-modules)
    remove_kernel_modules "$1"
    ;;
--add-kmp)
    add_kmp "$1"
    ;;
--remove-kmp)
    remove_kmp "$1"
esac

# vim:shiftwidth=4 softtabstop=4
