#!/bin/bash
#
# izmod
#
# Tool for compiling, installing and updating out of tree
# Linux kernel modules.
#
# Copyright 2021 Felipe Sanchez <izto@asic-linux.com.mx>
#
#    This program 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: exclusively version 2 of the License.
#
#    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
#
# See the file COPYING for details.
#

DATE="20210101"
VERSION="1.0.0"

DEBUG=0

######################################################
#                  Useful functions                  #
######################################################

display_version () {
   echo "$(basename $0) $VERSION, $DATE"
}

display_usage () {
cat << EOF
Usage: $(basename $0) [options] command <module> ...

Options:

   -k version Kernel version
   -f file    Kernel configuration file
   -s file    Module symvers file
   -R         Force rebuilding of module package
   -r         Force reinstallation of module package
   -V         Display the version number

Commands:

   install
   uninstall
   load
   unload
   genpkg
   info
   help

EOF
}


make_tempfile() {
   # Temporary file
   local retfile=`mktemp ${TMPBASE}/$(basename $0).XXXXXX`
   if [ $? -gt 0 ]; then
      error_exit "Error while creating tmpfile at $TMPBASE"
      tmp_cleanup
      exit 1
   fi
   echo "$retfile" >> "$TMP_LIST"
   echo $retfile
}


tmp_cleanup () {
   local t
   while read t; do
      rm -f "$t"
   done < "$TMP_LIST"
   rm -f "$TMP_LIST"
}

error_exit() {
   local exitval

   exitval=100
   [[ "$2" ]] && exitval="$2"

   tmsg >&2
   tmsg "$1" >&2
   tmsg >&2

   tmp_cleanup
   exit $exitval
}


#
# tmsg show a tagged message
#
tmsg () {
  echo -e "[$(basename $0)] $@"
}

#
# get_envdir $envdir
#
# For earch file f in the directory $envdir add a variable named f to the
# environment with the contents of file f as a value.
#

get_envdir() {
   local envdir=$1

   if [[ -d "$envdir" ]]; then
      for v in $('ls' -1 $envdir); do
         if [[ -s $v ]]; then
            [[ $v =~ ^\. || $v =~ ^= ]] || read -d"" -r "$v" < "${envdir}/${v}"
         else
            unset $v
         fi
      done
   fi
}



#######################################################
#                      Defaults                       #
#######################################################


# Temp file location
[[ "$TMPBASE" ]] || TMPBASE="/tmp"

# Transient info dir
[[ "$IZLIBDIR" ]] || IZLIBDIR="/var/lib/izmod"

# Path to local izmod library
[[ "$LDIR" ]] || LDIR="/var/lib/izmod"

# Path to make executable
[[ "$MAKE" ]] || MAKE=/usr/bin/make

# Path to GAR
[[ "$GARDIR" ]] || GARDIR=/usr/gar

# Loading system backend.
# At the moment only gar is supported.
# A future alternative might be DKMS.
[[ "$LOAD_BACKEND" ]] || LOAD_BACKEND="gar"


#######################################################
#             Process command line options            #
#######################################################

while getopts ":Vk:f:s:rR" opt; do
  case $opt in
    V)
      display_version
      exit 0
      ;;
    k)
      KVER="$OPTARG"
      ;;
    f)
      KCONFIG="$OPTARG"
      ;;
    s)
      KSYMVERS="$OPTARG"
      ;;
    R)
      FORCE_REBUILD=1
      ;;
    r)
      REINSTALL="--reinstall"
      ;;
    \?)
      echo "$(basename $0): Invalid flag: -$OPTARG" >&2
      display_usage
      exit 1
      ;;
    :)
      echo "$(basename $0): Option -$OPTARG requires an argument" >&2
      exit 1
      ;;
  esac
done

shift $(($OPTIND-1))

# List of tempfiles
set -e
TMP_LIST=$(mktemp ${TMPBASE}/$(basename $0).XXXXXX)
set +e

# Error log
ERRFILE=$(make_tempfile)

#####################################################
#                Positional arguments               #
#####################################################

command="$1"
modname="$2"

shift 2
rest="$@"

#########################################
#            Sanity checks              #
#########################################



gen_cfmd5 () {
   (echo -n "$base_kver"; cat "$KCONFIG") | md5sum | awk '{print $1}'
}

#
# process_all
#
# process_all [izmod_function]
#
process_all () {
   local modspresent
   local m

   # If modname is "all" then try to run [izmod_function] on all known modules
   if [[ $modname == "all" ]]; then
      if ls "${IZLIBDIR}/installed"/* &> /dev/null; then
         find "${IZLIBDIR}/installed" -type f | while read m; do
            modname=$(basename "$m")
            # The default is izmod_install if no function is provided
            if [[ "$1" ]]; then
               "$1"
            else
               izmod_install
            fi
         done
      else
         tmsg "There are no installed modules listed in ${IZLIBDIR}/installed."
      fi
      tmp_cleanup
      exit 0
   fi
}

izmod_install () {
   local cfmd5

   process_all izmod_install

   cfmd5=$(gen_cfmd5)
   if [[ $FORCE_REBUILD -eq 1 ]]; then
      izmod_genpkg
   else
      # Try installing it from the slapt-get repositories
      tmsg
      tmsg "Trying to install ${modname}-module-${cfmd5} from the system repositories"
      echo
      if ! slapt-get -i -y $REINSTALL "${modname}-module-${cfmd5}"; then
   
         # If that fails, try building it
         echo
         tmsg "Unable to install the module package from the system repositories."
         tmsg "I will attempt to build it."
         izmod_genpkg

      fi
   fi
   # If we reach this place it means the module was
   # correctly installed. Record its name for the future.
   set -e
   mkdir -p "${IZLIBDIR}/installed"
   touch "${IZLIBDIR}/installed/${modname}"
   set +e
}

izmod_uninstall () {
   local p
   local pkgname
   local cfmd5

   process_all izmod_uninstall

   cfmd5=$(gen_cfmd5)
   pkgname="${modname}-module-${cfmd5}"

   tmsg "Trying to remove the package: ${pkgname}"

   # There could be multiple versions of the same module
   # package for this specific kernel build (represented
   # by the md5 hash) so we try and remove them all.
   find -L /var/log/packages/ -name "${pkgname}*" | while read p; do
      removepkg "$p" || error_exit "Error while removing ${pkgname}"
   done

   # Unregister the module name from our installed modules database
   rm -f "${IZLIBDIR}/installed/${modname}"

   tmsg "The '${modname}' module has been uninstalled."
   tmsg
   tmsg "If the module is no longer in use you may unload by running:"
   tmsg "     $(basename $0) unload ${modname}"
}

izmod_load () {
   # No call to process_all because izmod_install does call it
   izmod_install
   echo
   tmsg "Loading module $modname"
   echo
   modprobe -v "$modname" || error_exit "Could not load the module '$modname'."
   echo
   tmsg "The $modname module was successfully loaded"
   echo
}

izmod_unload () {
   process_all izmod_unload
   echo
   tmsg "Unloading module $modname"
   modprobe -r -q "$modname"
   tmsg "The $modname module was successfully unloaded"
   echo
}

izmod_genpkg () {
   
   local garmod
   local kvars
   local pkgname
   local pkgpath

   process_all izmod_genpkg

   if [[ -z "$KSYMVERS" ]]; then
      tmsg
      tmsg "Warning: No symvers file supplied." >&2
      KSYMVERS="NOT-SUPPLIED"
   fi

   # izmod requires an izmod/<modname>-module
   # directory in the GAR system.
   garmod="${GARDIR}/izmod/izmod-meta-${modname}"
   kvars="KERNEL_VERSION=${base_kver} KERNEL_CONFIG_FILE=${KCONFIG} KERNEL_MODSYMVERS=${KSYMVERS}"

   # Building the package requires GAR:
   if ! [[ -d "$GARDIR" ]]; then
      ( tmsg "The GAR system must be installed in order to build the module"
        tmsg 
        tmsg "You can try installing it with:"
        tmsg
        tmsg "  slapt-get -i iztaci-gar"
        tmsg ) >&2
      error_exit "fatal: The GAR build system could not be found at ($GARDIR)" 111
   fi

   # It also requires the gar package dir for this module exists
   if ! [[ -d "$garmod" ]]; then
      # If there is no package dir, we might have a meta-package that installs it:
      tmsg "I could not find the GAR package directory for module '${modname}'."
      tmsg "I will attempt to install a meta-package that will provide it."
      echo
      if ! slapt-get -y -i "izmod-${modname}"; then
         ( tmsg
           tmsg "I could not find a suitable meta-package."
           tmsg 
           tmsg "Consult izmod's documentation for instructions on"
           tmsg "how to create a package directory for izmod-${modname}."
           tmsg
           tmsg ) >&2
      fi
   fi

   if ! [[ -d "$garmod" ]]; then
      error_exit "fatal: There is no GAR package directory for izmod-meta-${modname}" 111
   fi

   pkgname=$(cd "${garmod}"; "$MAKE" ${kvars} valueof-IZTACI_PACKAGE_NAME)

   if [[ $FORCE_REBUILD -eq 1 ]]; then
      rm -f "${garmod}/${pkgname}"
   fi

   # Attempt to build the package if it's not already built
   if [[ -f "${garmod}/${pkgname}" ]]; then
      pkgpath="${garmod}/${pkgname}"
   else
      tmsg
      tmsg "Running" "$MAKE" -C "${garmod}" ${kvars} clean package garchive
      tmsg
      echo
      "$MAKE" -C "${garmod}" ${kvars} clean package garchive && pkgpath="${garmod}/${pkgname}"
   fi
     
   # Do we have a package now?
   if [[ -f "$pkgpath" ]]; then
      tmsg "Installing package: $pkgname"
      echo
      installpkg "$pkgpath"
   else
      error_exit "Could not install the module package for $modname."
   fi

}

izmod_getpkg () {
   echo getpkg
}

izmod_info () {
   process_all izmod_info
   echo
   echo "Kernel version:        $KVER"
   echo "Base kernel version:   $base_kver"
   echo "Local kernel version:  $local_kver"
   echo "Kernel config file:    $KCONFIG"
   echo "Package name:          ${modname}-module-$(gen_cfmd5)"
   echo
}


# We will hold the references to temporary files here
tmpfiles=("xxx")

#####################################################
#                      Main loop                    #
#####################################################


main () {

   echo "###########################################################################"
   echo -n "                           "
   display_version
   echo "###########################################################################"
   echo

   if [[ "$command" == "help" ]]; then
      display_usage 
      exit 0
   fi

   # We must have the kernel version, configuration
   # file and optionally a symvers file.
   
   # Kernel version
   if [[ -z "$KVER" ]]; then
      # By default, use the current kernel version
      KVER=$(uname -r)
      tmsg "Kernel version not specified:"
      tmsg "using running kernel version ($KVER)"

      # If we have no explicit path to the kernel configuration
      # file then we will try to use /proc/config.gz
      if [[ -z "$KCONFIG" ]]; then
         modprobe configs &> /dev/null
         if [[ -f /proc/config.gz ]]; then
            KCONFIG=$(make_tempfile)
            set -e
            zcat /proc/config.gz >> "$KCONFIG"
            set +e
            tmsg "Kernel configuration file not specified:"
            tmsg "using running kernel configuration file (/proc/config.gz)"
         else
            error_exit "You didn't supply the path to the kernel configuration file and /proc/config.gz does not exist."
         fi
      fi
   fi

   [[ -z "$KVER"    ]] && error_exit "Please supply the kernel version."
   [[ -z "$KCONFIG" ]] && error_exit "Please supply the path to the kernel configuration file."
   [[ -f "$KCONFIG" ]] || error_exit "The kernel configuration file ($KCONFIG) is not readable."

   # We need the base kernel version to pass it to the build process.
   # The vanilla kernel versions have the form "a.b.c". The localversion
   # is appended to "c": a.b.clocal".
   # We will get the base by removing the localversion from "c".

   # First, get the local version string from the configuration file.
   local_kver=$(grep CONFIG_LOCALVERSION= "$KCONFIG" | cut -f2 -d\")

   # Beujolais!
   base_kver=$(echo "$KVER" | cut -f1,2 -d.).$(echo "$KVER" | cut -f3- -d. | sed -r "s|${local_kver}$||g")

   case "$command" in
      install|update)
         izmod_install $rest
         ;;
      uninstall|remove)
         izmod_uninstall $rest
         ;;
      load)
         izmod_load $rest
         ;;
      unload)
         izmod_unload $rest
         ;;
      genpkg|pkg)
         izmod_genpkg $rest
         ;;
      info)
         izmod_info $rest
         ;;
      help)
         display_usage
         ;;
      "")
         display_usage
         error_exit "You must supply a command" 111
         ;;
      *)
         printf "\n*** %s\n\n" "${command}: unknown command" >&2
         exit 1
         ;;
   esac
}

main
retval=$?

rm -f $ERRFILE
tmp_cleanup

exit $retval
