#!/bin/sh
#---help---
# Usage:
#     akms (install | build) [options] <srcdir>
#     akms (uninstall | unbuild) [options] <module>[/<version>]
#     akms status [options] [<module>[/<version>]]
#     akms [-h] [--help] [-V]
#
# Actions:
#     install       Build and install module(s) into /lib/modules, unless already
#                   built (or -r is specified) or installed.
#
#     build         Build module(s), unless already built (or -r is specified),
#                   without installing them.
#
#     uninstall     Uninstall module(s) from /lib/modules.
#
#     unbuild       Remove built module(s) version(s) from the state directory.
#
#     status        Show status of modules.
#
# Arguments:
#     <module>      Name of the module to uninstall, unbuild or show in status.
#                   Use 'all' to uninstall/unbuild all modules.
#
#     <srcdir>      Location of the source directory with AKMBUILD to install.
#                   It may be an absolute path, (glob) name of a directory
#                   inside $modules_srcdir (/usr/src by default), or 'all' to
#                   install all modules found in $modules_srcdir.
#
#     <version>     Version of the module to uninstall, unbuild or show in
#                   status. Use 'all' for all versions. Default is 'installed'
#                   for uninstall, not-installed for unbuild, 'all' for status.
#
# Common options:
#     -K --[no]-keep
#          Whether to keep uninstalled modules in /var/lib/akms. Default is to not
#          keep unless overriden in /etc/akms.conf.
#
#     -q --quiet
#          Be quiet, print only warn and error messages.
#
#     -v --verbose
#          Be verbose, print even debug messages.
#
#     -V --version
#          Print program name & version and exit.
#
#     -h --help
#          Print this message and exit.
#
# Options for install and build:
#     -k --kernel <kernel>
#          Specify for which kernel to install the module(s). Use 'all' to install
#          for all installed kernels (based on /usr/share/kernel/). Defaults to
#          the installed kernel with the same flavor as the currently running
#          kernel.
#
#     -r --rebuild
#          Rebuild and install the module(s) even if they are already built.
#
#        --[no-]overlay
#          Whether to install build dependencies into an overlay (OverlayFS)
#          created on top of /, or on the host system. Default is to use overlay
#          unless disabled in /etc/akms.conf.
#
#        --[no-]sandbox
#          Whether to install build dependencies and build modules in a sandbox
#          using bubblewrap. Default is to use sandbox unless disabled in
#          /etc/akms.conf.
#
#        --skip-cleanup
#          Don't unmount and delete root overlay used for building and don't
#          uninstall build dependencies.
#
# Options for uninstall and unbuild:
#     -f --force
#          Uninstall module's files from /lib/modules even if they are different
#          from the ones stored in /var/lib/akms. Unbuild module (remove it from
#          the state dir) even if it's installed (without uninstalling).
#
#     -k --kernel <kernel>
#          Specify from/for which kernel to uninstall/unbuild the module(s). Use
#          'all' to uninstall from all kernels where the module(s) is/are
#          installed; unbuild for all kernels the module(s) is/are built for.
#          Defaults to the installed kernel with the same flavor as the currently
#          running kernel.
#
# Options for status:
#     -k --kernel <kernel>
#          Filter the output for the specified kernel. Defaults to 'all'.
#
#     -s --state <state>
#          Filter modules by state (building, built, installed, failed,
#          corrupted, or all). Defaults to 'all'.
#
# Homepage: https://github.com/jirutka/akms
#---help---

# NOTE: This script intentionally doen't use `-e` because shell ignores `-e`
# when a conditional-like statement is inside the call stack. And that's
# most of this script.
# See http://mywiki.wooledge.org/BashFAQ/105 (applies to all POSIX shells)
set -uo pipefail

readonly PROGNAME='akms'
readonly VERSION='0.3.0'

readonly CFG_FILE='/etc/akms.conf'
readonly KERNELS_SRC_DIR='/usr/src'
readonly MODULES_BASE_DIR='/lib/modules'
readonly SCRIPTS_DIR='/usr/libexec/akms'
readonly STATE_DIR='/var/lib/akms'

readonly AKMBUILD_VARS_REQUIRED='built_modules modname modver'
readonly AKMBUILD_VARS="$AKMBUILD_VARS_REQUIRED makedepends"
readonly APK_VIRT=".$PROGNAME-build"
readonly TRAP_SIGNALS='EXIT HUP INT TERM'

# Defaults that may be overriden by $CFG_FILE.
MAKEFLAGS="-j$(nproc)"

[ -f "$CFG_FILE" ] && . "$CFG_FILE"

readonly BUILD_USER=${build_user:-akms}
readonly BWRAP_OPTS=${bubblewrap_opts:-}
readonly MAKEDEPENDS=${makedepends:-}
readonly MAKEFLAGS
readonly MODULES_DEST_PATH=${modules_dest_path:-/kernel/extra/akms}
readonly MODULES_SRC_DIR=${modules_srcdir:-/usr/src}
readonly TEMP_DIR="${temp_dir:-/tmp/$PROGNAME}/$(date +%s)"

# Global mutable variables.
LOG_ENABLED_LEVELS=' err warn info '


# Prints help message.
help() {
	sed -n '/^#---help---/,/^#---help---/p' "$0" | sed 's/^# \?//; 1d;$d;'
}

# Cleanups resources before exiting the script.
cleanup() {
	set +u
	trap '' $TRAP_SIGNALS  # unset trap to avoid loop

	if ! $OPT_SKIP_CLEANUP && ! [ -d "$TEMP_DIR"/overlay ]; then
		apk info -q --installed "$APK_VIRT" && apk del --no-progress "$APK_VIRT"
	fi

	if [ -d "$TEMP_DIR"/overlay ]; then
		if $OPT_SKIP_CLEANUP; then
			log warn "Root overlay left mounted at $TEMP_DIR/overlay, unmount it and remove manually!"
			return
		fi

		log debug "Unmounting root overlay $TEMP_DIR/overlay"
		umount -f "$TEMP_DIR"/overlay || {
			err "Failed to umount root overlay at $TEMP_DIR/overlay, unmount it and remove manually!"
			return
		}
	fi

	if [ -d "$TEMP_DIR" ]; then
		rm -Rf "$TEMP_DIR"
	fi
}

# Logs the given message to STDERR, if the specified log level is enabled.
#
# $1: log level (err, warn, info, or debug)
# $*: message
# stderr: decorated message
log() {
	local level="$1"; shift

	case "$LOG_ENABLED_LEVELS" in
		*" $level "*) ;;  # continue
		*) return;;
	esac

	local prefix=
	case "$level" in
		warn) prefix='WARNING: ';;
		err) prefix='ERROR: ';;
	esac

	printf "$PROGNAME: $prefix%s\n" "$@" >&2
}
err() {
	log err "$@"
}

# $1: this log level and higer will be enabled (err, warn, info, or debug)
# vars-out: LOG_ENABLED_LEVELS
setup_logger() {
	local level="$1"

	local l levels=
	for l in err warn info debug; do
		levels="$levels $l "
		[ $l = $level ] && break
	done

	readonly LOG_ENABLED_LEVELS=$levels
}

# Converts the given value to 'true', or 'false'.
# $1: bool-like value
# status: 0
# stdout: true or false
to_bool() {
	case "$1" in
		yes | true | 1) echo true;;
		*) echo false;;
	esac
}

# Refer to script akms-runas for usage information.
runas() {
	export BWRAP_OPTS
	"$SCRIPTS_DIR"/akms-runas "$@"
}

# $1: location of the module's state directory
# stdout: space-separated triplet: kernel modname modver
statedir_to_triplet() {
	local statedir="$(readlink -f "$1")"

	echo "${statedir#$STATE_DIR}" | tr / ' '
}

# $1: kernel release (e.g. 5.10.53-rc3-0-lts)
# stdout: space-separated parts: pkgver pkgrel flavor
split_kernel_ver() {
	local kernel="$1"
	local flavor="${kernel##*-}"
	local ver_rel="${kernel%-*}"
	local ver="${ver_rel%-*}"
	local rel="${ver_rel##*-}"

	echo "$ver" "$rel" "$flavor"
}

# stdout: newline-separated kernel releases (e.g. 5.10.53-0-lts)
list_installed_kernels() {
	cat /usr/share/kernel/*/kernel.release 2>/dev/null
}

# Prints the installed kernel release with the same flavor as the currently
# running kernel.
#
# NOTE: After you upgrade kernel, the installed kernel and the running kernel
# are different until the system is restarted. That's why we don't simply use
# `uname -r`.
#
# stdout: kernel release (e.g. 5.10.53-0-lts)
current_installed_kernel() {
	set -- $(split_kernel_ver $(uname -r))
	cat /usr/share/kernel/$3/kernel.release 2>/dev/null
}

# $1: name and version of the package to check in format
#     <pkgname>=<pkgver>-r<pkgrel>
# status: 0 if the package exists, 1 otherwise
# stderr: error output from apk(1)
is_pkg_avail() {
	local name="${1%=*}"
	local ver="${1#*=}"

	apk list -Ua "$name" | grep -qw "$name-$ver"
}

# Mounts a writable overlay (using OverlayFS) on top of / at path $1.
#
# $1: path where to mount the overlay
mount_rootfs_overlay() {
	local mountdir="$1"

	mkdir -p "$mountdir"/upperdir "$mountdir"/workdir
	mount -t overlay overlay \
		-o "lowerdir=/,upperdir=$mountdir/upperdir,workdir=$mountdir/workdir" \
		"$mountdir"
}

# Writes state to the statedir.
#
# $1: new state
# $2: location of the module's statedir; defaults to $statedir
# vars-in: statedir (optional)
write_state() {
	local state="$1"
	local statedir="${2:-$statedir}"

	mkdir -p "$statedir"
	echo "$state" > "$statedir"/state
}

# Reads state from the module's statedir.
#
# $1: location of the module's state directory; defaults to $statedir
# stdout: the state or an empty string if not set
read_state() {
	local statedir="${1:-$statedir}"

	cat "$statedir"/state 2>/dev/null || echo ''
}

# Loads allowed variables defined in the specified AKMBUILD file into the
# current shell and validates that all required variables are set.
# The AKMBUILD is evaluated in a shell started as $BUILD_USER.
#
# $1: location of the AKMBUILD file
# vars-in: kernel (optional), kernel_srcdir (optional)
# env-out: see $AKMBUILD_VARS
load_akmbuild() {
	local akmbuild="$1"

	if ! [ -r "$akmbuild" ]; then
		err "$akmbuild does not exist or not readable!"; return 1
	fi
	if ! sh -n "$akmbuild"; then
		err "$akmbuild is not a valid shell script!"; return 1
	fi

	local var; for var in $AKMBUILD_VARS; do
		eval "$var=''"
	done

	local vars; vars=$(
		runas "$BUILD_USER" \
			kernel_ver="${kernel:-}" \
			kernel_srcdir="${kernel_srcdir:-}" \
			/bin/sh -c ". '$akmbuild'; set" \
		| grep -E "^($(echo $AKMBUILD_VARS | tr ' ' '|'))="
	)
	if [ $? -ne 0 ]; then
		err "Failed to evaluate $akmbuild"; return 1
	fi

	eval "$vars"

	local var; for var in $AKMBUILD_VARS_REQUIRED; do
		if ! eval "test -n \"\${$var:-}\""; then
			err "$akmbuild: $var must be defined!"; return 1
		fi
	done
}

# Finds module state directories (i.e. built or installed modules) based on the
# given criteria.
#
# $1: kernel release (e.g. 5.10.53-0-lts)
# $2: module name or "all"
# $3: module version or "all" (optional)
# status: 1 if no such state directory was found, 0 otherwise
# stdout: newline-separated list of module state directories
find_statedirs() {
	local kernel="$1"
	local modname="$2"
	local modver="${3:-}"

	[ "$kernel" = all ] && kernel=
	[ "$modname" = all ] && modname=
	[ "$modver" = all ] && modver=

	local statedir
	for statedir in "$STATE_DIR"/${kernel:-[0-9]*}/${modname:-[a-z]*}/${modver:-[0-9]*}; do
		[ -d "$statedir" ] || return 1
		readlink -f "$statedir"
	done
}

# Determines the compression format used for modules installed with kernel $1.
#
# $1: kernel release (e.g. 5.10.53-0-lts)
# stdout: "gz", "xz", "zst", or "" (no compression)
determine_modules_compress_format() {
	local kernel="$1"
	local modules_dep="$MODULES_BASE_DIR/$kernel/modules.dep"

	local ext; for ext in zst gz xz; do
		if grep -Fq ".ko.$ext:" "$modules_dep" 2>/dev/null; then
			echo $ext
			return 0
		fi
	done
	echo ''
}

# Compresses the given file $1 using the specified compression format.
# The compressed file will be written at path `$1.$2` and the original file
# will be removed.
#
# $1: path to the uncompressed file
# $2: "gz", "xz", or "zst"
compress() {
	local filepath="$1"
	local format="$2"

	case "$format" in
		gz) gzip -9 "$filepath";;
		xz) xz -9 "$filepath";;
		zst) zstd -q -T0 --rm -19 "$filepath";;
		*) err "Unsupported compression format: $format"; return 1;;
	esac
}


#=================================  Install  ==================================

# $1: specifies module to install by location of the module's source directory,
#     (full path or just a directory name inside $MODULES_SRC_DIR) or "all" for
#     all modules in $MODULES_SRC_DIR
action_install() {
	if [ $# -lt 1 ]; then
		err 'Missing argument: <srcdir> | all'; return 1
	fi

	local srcdirs
	case "$1" in
		.* | /*) srcdirs=$1;;
		all) srcdirs=$(find_srcdirs 'all') || {
			log info "No AKMBUILDs found in $MODULES_SRC_DIR/*"; return 0
		};;
		*) srcdirs=$(find_srcdirs "$1") || {
			err "No AKMBUILD module found in $MODULES_SRC_DIR/$1"; return 1
		};;
	esac

	local kernel="${OPT_KERNEL:-$(current_installed_kernel || uname -r)}"
	[ "$kernel" = all ] && kernel=

	local rc=0 srcdir
	for kernel in ${kernel:-$(list_installed_kernels)}; do
		for srcdir in $srcdirs; do
			build_and_install_module $srcdir $kernel || rc=$?
		done
	done
	exit $rc
}

# $1: name of module to install or "all"
# vars-need: OPT_*
action_build() {
	action_install "$@"
}

# $1: name of the module's source directory (may be a glob) or "all" (optional)
# status: 1 if no source directory was found (and $1 is not "all"), 0 otherwise
# stdout: newline-separated list of module source directories
find_srcdirs() {
	local name="${1:-}"

	[ "$name" = all ] && name=

	local rc=1 srcdir
	for srcdir in "$MODULES_SRC_DIR"/${name:-*}; do
		if [ -f "$srcdir"/AKMBUILD ]; then
			rc=0
			echo $srcdir
		fi
	done
	exit $rc
}

# $1: module source directory with AKMBUILD
# $2: kernel release (e.g. 5.10.53-0-lts)
build_and_install_module() (
	# NOTE: Must not be local!
	srcdir=$1
	kernel=$2

	load_akmbuild "$srcdir/AKMBUILD" || return 1

	statedir="$STATE_DIR/$kernel/$modname/$modver"
	mkdir -p "$statedir"

	state=$(read_state)
	$OPT_REBUILD && state=''

	if [ "$state" != 'built' ] && [ "$state" != 'installed' ]; then
		log info "Building module $modname/$modver for kernel $kernel"

		write_state 'building'
		install_makedepends "$kernel" "$MAKEDEPENDS $makedepends" && build_module || {
			write_state 'failed'; return 1
		}
		write_state 'built'
		state='built'

		[ "$ACTION" = build ] \
			&& log info "Module $modname/$modver for $kernel is built successfully"
	fi

	[ "$ACTION" = build ] && return

	if [ "$state" = 'built' ] || ! verify_installed_files $statedir; then
		log info "Installing module $modname $modver for kernel $kernel"

		install_module $statedir || {
			write_state 'failed'; return 1
		}
		write_state 'installed'
		ln -fs ${statedir##*/} ${statedir%/*}/installed

		/sbin/depmod $kernel || {
			err 'depmod failed'; return 1
		}
		log info "Module $modname/$modver for $kernel installed successfully"

	elif [ "$state" = 'installed' ]; then
		log info "Module $modname/$modver for $kernel is already installed"
	fi
)  # ")" is not a typo!

# $1: kernel release (e.g. 5.10.53-0-lts)
# $2: ws-separated list of additional dependencies to install
install_makedepends() {
	local kernel="$1"
	local makedepends="$2"

	local apk_opts= build_root=
	if [ -d "$TEMP_DIR"/overlay ]; then
		build_root="$TEMP_DIR/overlay"
		apk_opts="--root $build_root"
	fi

	# if any of these files are missing, it means `build-base` needs installing
	if [ ! -f "/usr/bin/readelf" ] || [ ! -f "/usr/bin/file" ] || [ ! -f "/usr/bin/gcc" ] || [ ! -f "/usr/bin/g++" ] || [ ! -f "/usr/bin/make" ] || [ ! -f "/usr/bin/patch" ]; then
		makedepends="$makedepends build-base"
	fi

	# If all $makedepends are installed, empty $makedepends.
	[ "$makedepends" ] \
		&& apk info $apk_opts -q --installed $makedepends \
		&& makedepends=''

	local kdevpkg="$(
		set -- $(split_kernel_ver "$kernel")
		echo linux-$3-dev=$1-r$2
	)"

	# If linux headers for $kernel are installed, empty $kdevpkg.
	[ -d "${build_root}$KERNELS_SRC_DIR/linux-headers-$kernel" ] \
		&& kdevpkg=''

	# If all $makedepends and linux headers are installed, we're done here.
	[ -z "$makedepends" ] && [ -z "$kdevpkg" ] && return

	# Fail fast if kernel dev package is not available.
	if [ "$kdevpkg" ] && ! is_pkg_avail "$kdevpkg"; then
		err "Package $kdevpkg is not available in the repositories!"; return 1
	fi

	if $OPT_OVERLAY && ! [ "$build_root" ]; then
		log debug "Mounting overlay in $TEMP_DIR/overlay"

		install -d -m700 -o $BUILD_USER "$TEMP_DIR" || return 1
		mount_rootfs_overlay "$TEMP_DIR/overlay" || {
			err "Failed to mount OverlayFS on top of / at $TEMP_DIR/overlay"
			return 1
		}

		build_root="$TEMP_DIR/overlay"
		apk_opts="--root $build_root"

		# Unlock the apk database.
		echo '' > "$build_root"/lib/apk/db/lock
		# Ensure that /tmp is writable.
		install -d -m 1777 -o root -g root "$build_root"/tmp
	fi

	log debug "Installing dependencies ${build_root:+"(in overlay)"}: $kdevpkg $makedepends"
	apk add \
		$apk_opts \
		--virtual "$APK_VIRT" \
		--no-progress \
		--no-scripts \
		"$kdevpkg" $makedepends
	if [ $? -ne 0 ]; then
		err 'Failed to install build dependencies'; return 1
	fi
}

# vars-in: modname srcdir kernel statedir built_modules
build_module() {
	local builddir="$statedir/build"
	local kernel_srcdir="$KERNELS_SRC_DIR/linux-headers-$kernel"

	local build_root=
	[ -d "$TEMP_DIR/overlay" ] && build_root="$TEMP_DIR/overlay"

	local runas_sandbox=
	$OPT_SANDBOX && runas_sandbox=true

	rm -Rf "$builddir"
	# NOTE: The module sources must be available in the builddir, see
	#  https://gitlab.alpinelinux.org/alpine/aports/-/issues/16718.
	cp -a "$srcdir" "$builddir" || return 1
	chown -R "$BUILD_USER" "$builddir" || return 1

	runas "$BUILD_USER" \
		${runas_sandbox:+--sandbox "${build_root:-/}" --bind "$builddir" "$builddir"} \
		srcdir="$srcdir" \
		builddir="$builddir" \
		kernel_ver="$kernel" \
		kernel_srcdir="$kernel_srcdir" \
		LOG_LEVEL="$OPT_LOGLEVEL" \
		MAKEFLAGS="$MAKEFLAGS" \
		"$SCRIPTS_DIR"/akms-build
	if [ $? -ne 0 ]; then
		err "Failed to build module $modname/$modver for $kernel" "examine $builddir"
		return 1
	fi

	mkdir -p "$statedir"/modules
	rm -f "$statedir"/modules/* 2>/dev/null ||:

	local comp_format="$(determine_modules_compress_format $kernel)"

	local ko; for ko in $built_modules; do
		if ! [ -f "$builddir/$ko" ]; then
			err "[$modname] Missing file $ko!"; return 1
		fi
		if [ "$comp_format" ]; then
			if compress "$builddir/$ko" "$comp_format"; then
				ko="$ko.$comp_format"
			else
				log warn "[$modname] Unable to compress $ko with $comp_format!"
			fi
		fi
		cp "$builddir/$ko" "$statedir"/modules/ || return 1
	done

	rm -Rf "$builddir"
}

# $1: location of the module's state directory
# vars-in: see $AKMBUILD_VARS
install_module() {
	local statedir="$1"
	local destdir="$MODULES_BASE_DIR/$kernel$MODULES_DEST_PATH"

	if [ -d ${statedir%/*}/installed ]; then
		uninstall_module ${statedir%/*}/installed 'upgrade'
	fi

	local kofile; for kofile in "$statedir"/modules/*; do
		if ! [ -f "$kofile" ]; then
			err "[$modname] No files found in $statedir/modules!"; return 1
		fi
		log debug "[$modname] Installing ${kofile##*/} to $destdir/"
		install -D -m644 -t "$destdir"/ $kofile || return 1
	done
}


#================================  Uninstall  =================================

# $1: name of the module to uninstall or "all"
action_uninstall() {
	if [ $# -lt 1 ]; then
		err "Missing argument: <module>[/<version>] | all"; return 1
	fi

	local modname modver
	case "$1" in
		*/*) modname=${1%%/*}; modver=${1#*/};;
		*) modname=$1; modver='installed';;
	esac
	local kernel="${OPT_KERNEL:-$(current_installed_kernel || uname -r)}"

	local statedirs
	statedirs=$(find_statedirs "$kernel" "$modname" "$modver") || {
		err "No modules found for kernel:$kernel, name:$modname, version:$modver"
		return 1
	}

	local rc=0 statedir
	for statedir in $statedirs; do
		if ! verify_installed_files $statedir && ! $OPT_FORCE; then
			rc=1
			continue
		fi
		uninstall_module $statedir || rc=$?
	done
	exit $rc
}

# $1: location of the module's state directory
# $2: upgrade mode - if not empty, the info message "Uninstalling module ..."
#     will not be logged
uninstall_module() {
	local statedir="$1"
	local upgrade="${2:-}"

	set -- $(statedir_to_triplet "$statedir")
	local kernel="$1" modname="$2" modver="$3"

	local destdir="$MODULES_BASE_DIR/$kernel$MODULES_DEST_PATH"

	[ "$upgrade" ] || log info "Uninstalling module $modname/$modver from kernel $kernel"

	local kofile koname
	for kofile in "$statedir"/modules/*; do
		if ! [ -e "$kofile" ]; then
			err "[$modname] No files found in $statedir/modules!"; return 1
		fi
		koname=${kofile##*/}

		[ -f "$destdir/$koname" ] || continue

		log debug "[$modname] Removing $koname from $destdir"
		rm "$destdir/$koname"
	done

	write_state 'built' "$statedir"
	rm -f ${statedir%/*}/installed

	if ! $OPT_KEEP_UNINSTALLED; then
		log debug "[$modname] Deleting $statedir"
		remove_statedir $statedir
	fi
}


#================================  Unbuild  ===================================

# $1: name and optionally version of the module to unbuild or "all"
action_unbuild() {
	if [ $# -lt 1 ]; then
		err 'Missing argument: <module>[/<version>] | all'; return 1
	fi

	local modname modver
	case "$1" in
		*/*) modname=${1%%/*}; modver=${1#*/};;
		*) modname=$1; modver='';;
	esac
	local kernel="${OPT_KERNEL:-$(current_installed_kernel || uname -r)}"

	local found=false rc=0 statedir
	for statedir in $(find_statedirs "$kernel" "$modname" "$modver"); do
		if ! [ "$modver" ] && [ "$(read_state $statedir)" = 'installed' ]; then
			continue
		fi
		unbuild_module $statedir || rc=$?
		found=true
	done
	if ! $found; then
		err 'No built modules found for given criteria'
		rc=1
	fi
	return $rc
}

# Removes the given state directory, unless the module is installed and --force
# was not specified.
#
# $1: location of the module's state directory
unbuild_module() {
	local statedir="$1"
	local state="$(read_state "$statedir")"

	set -- $(statedir_to_triplet "$statedir")
	local kernel="$1" modname="$2" modver="$3"

	if [ "$state" = 'installed' ]; then
		if ! $OPT_FORCE; then
			log warn "Module $modname/$modver is installed, skipping (use --force to override)"
			return 1
		fi
		rm -f ${statedir%/*}/installed
	fi

	log info "Unbuilding module $modname/$modver for kernel $kernel"

	remove_statedir $statedir
}

# Removes the given state directory. No checks are performed, nothing is logged.
#
# $1: location of the module's state directory
remove_statedir() {
	local statedir="$1"
	local parentdir="${statedir%/*}"

	rm -Rf "$statedir"

	while [ "$parentdir" != "$STATE_DIR" ]; do
		rmdir "$parentdir" 2>/dev/null || break
		parentdir=${parentdir%/*}
	done
}


#=================================  Status  ===================================

# $1: name of the module to show status for or "all" (optional)
# stdout: status of modules
action_status() {
	local modname modver
	case "${1:-}" in
		'') modname='all'; modver='all';;
		*/*) modname=${1%%/*}; modver=${1#*/};;
		*) modname=$1; modver='all';;
	esac

	local found=false state statedir
	for statedir in $(find_statedirs "$OPT_KERNEL" "$modname" "$modver"); do
		state=$(read_state $statedir) || continue

		if [ "$state" = installed ]; then
			verify_installed_files $statedir || state='corrupted'
		fi
		if [ "$OPT_STATE" = 'all' ] || [ "$state" = "$OPT_STATE" ]; then
			printf '%s\t%s\t%s\t%s\n' $(echo ${statedir#$STATE_DIR} | tr / ' ') "$state"
			found=true
		fi
	done
	if ! $found; then
		log info 'No modules found'; return 1
	fi
}

# $1: location of the module's state directory
verify_installed_files() {
	local statedir="$1"

	set -- $(statedir_to_triplet $statedir)
	local kernel="$1" modname="$2" modver="$3"

	local destdir="$MODULES_BASE_DIR/$kernel$MODULES_DEST_PATH"

	local ko koname rc=0
	for ko in $statedir/modules/*; do
		if ! [ -e "$ko" ]; then
			err "[$modname] No files found in $statedir/modules!"; return 1
		fi
		koname=${ko##*/}

		if ! [ -f "$destdir"/$koname ]; then
			log warn "[$modname] File $koname is missing in $destdir!"
			rc=1; continue
		fi
		if ! cmp "$destdir"/$koname $ko 2>/dev/null; then
			log warn "[$modname] File $koname in $destdir is not from $modname-$modver for $kernel!"
			rc=1
		fi
		log debug "[$modname] File $destdir/$koname is OK"
	done
	return $rc
}


#==================================  Main  ====================================

ACTION=
case "${1:-}" in
	'') log err "Missing arguments, see '$PROGNAME -h'"; exit 1;;
	-h | --help) help; exit 0;;
	-*) ;;
	*) ACTION=$1; shift;;
esac

OPT_FORCE=false
OPT_KEEP_UNINSTALLED=$(to_bool "${keep_uninstalled:-false}")
OPT_KERNEL=
OPT_LOGLEVEL=${log_level:-info}
OPT_OVERLAY=$(to_bool "${use_overlayfs:-true}")
OPT_SANDBOX=$(to_bool "${use_sandbox:-true}")
OPT_REBUILD=false
OPT_STATE='all'
OPT_SKIP_CLEANUP=false

opts=$(getopt -n $PROGNAME \
	-o fKk:qrs:vVh \
	-l force,keep,no-keep,kernel:,overlay,no-overlay,sandbox,no-sandbox,quiet,rebuild,skip-cleanup,state:,verbose,version,help \
	-- "$@") || exit 1
eval set -- "$opts"

while [ $# -gt 0 ]; do
	n=1
	case "$1" in
		-f | --force) OPT_FORCE=true;;
		-K | --keep) OPT_KEEP_UNINSTALLED=true;;
		     --no-keep) OPT_KEEP_UNINSTALLED=false;;
		-k | --kernel) OPT_KERNEL=$2; n=2;;
		-q | --quiet) OPT_LOGLEVEL='warn';;
		     --overlay) OPT_OVERLAY=true;;
		     --no-overlay) OPT_OVERLAY=false;;
		     --sandbox) OPT_SANDBOX=true;;
		     --no-sandbox) OPT_SANDBOX=false;;
		-r | --rebuild) OPT_REBUILD=true;;
		     --skip-cleanup) OPT_SKIP_CLEANUP=true;;
		-s | --state) OPT_STATE=$2; n=2;;
		-v | --verbose) OPT_LOGLEVEL='debug';;
		-V | --version) echo "$PROGNAME $VERSION"; exit 0;;
		-h | --help) help; exit 0;;
		--) shift; break;;
	esac
	shift $n
done

if ! [ "$ACTION" ]; then
	ACTION="${1:-}"; shift
fi

setup_logger "$OPT_LOGLEVEL"

readonly ACTION OPT_FORCE OPT_KEEP_UNINSTALLED OPT_KERNEL OPT_LOGLEVEL
readonly OPT_OVERLAY OPT_SANDBOX OPT_REBUILD OPT_STATE OPT_SKIP_CLEANUP

umask 022

case "$ACTION" in
	install | build | uninstall | unbuild)
		if [ "$(id -u)" -ne 0 ]; then
			err 'Must be run as root!'; exit 1
		fi
		trap cleanup $TRAP_SIGNALS
		action_$ACTION "$@"; exit $?
	;;
	status)
		action_$ACTION "$@"; exit $?
	;;
	*)
		err "Unknown action: $ACTION"
		exit 1
	;;
esac
