#!/usr/bin/env bash
# Copyright (C) 2024-Ω Miquel Sabaté Solà <mikisabate@gmail.com>
#
# 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, either version 3 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, see <http://www.gnu.org/licenses/>.

#####
# Build the Linux Kernel with different install options. After you call this
# script it will:
#
#   1. Call `defconfig`/`oldconfig` depending on whether the `.config` file
#      exists or not.
#   2. Call `menuconfig` so the user can double check the configuration.
#   3. Build the Kernel.
#   4. Copy into a tarball the resulting image, modules, dtbs, and headers. Two
#      scripts are bundled in: install.sh and uninstall.sh.
#
# You can also pass some additional options, check the -h/--help message to read
# more about them.
#
# By default kbuild will produce a tarball so you can install the kernel on a
# target machine. In there you just need to untar the given bundle and then run
# the 'install.sh' script from within the resulting directory. This will:
#
#   1. Copy recursively all the files into their proper paths.
#   2. Make an initramfs for the new image.
#   3. Run your $EDITOR to update the boot loader configuration.
#
# If you provide the '-l/--local' option then the tarball is not produced, and
# the 'install.sh' and 'uninstall.sh' scripts are left in the current
# directory so you can run them on your current machine.
#
# *NOTE*: this script expects the `ARCH` environment variable to be set, and if
# that doesn't match the current architecture, then it will expect to have
# `CROSS_COMPILE` set as well.
#####

set -e

KBUILD_CONFIG_PATH="$HOME/.config/kbuild"

# Helper function to list the available profiles in the system.
list_available_profiles() {
    dirs="$(find $KBUILD_CONFIG_PATH/ -mindepth 1 -maxdepth 1 -type d -printf '%P\n' | sort | awk '{printf "%s%s", (NR==1?"":", "), $0} END {print ""}')"
    if [ -z "$dirs" ]; then
        echo "kbuild (info): there are no available profiles."
    else
        echo "kbuild (info): available profiles: $dirs."
    fi
}

##
# Arguments that can be passed.

while [[ $# -gt 0 ]]; do
    case $1 in
        --skip-config)
            SKIP_CONFIG="t"
            shift
            ;;
        --skip-install)
            NO_INSTALL="t"
            shift
            ;;
        -p | --profile)
            if [ -z "$2" ]; then
                echo "kbuild (error): the 'profile' option expects a value."
                exit 1
            fi

            PROFILE="$2"
            shift 2
            ;;
        -l | --local)
            IS_LOCAL=1
            shift
            ;;
        --list-profiles)
            list_available_profiles
            exit 0
            ;;
        -h | --help)
            SHOW_HELP=1
            shift
            ;;
        -*)
            echo "kbuild (error): unknown option '$1'."
            exit 1
            ;;
        *)
            echo "kbuild (warning): argument '$1' will be ignored."
            shift # past argument
            ;;
    esac
done

# Print the show message if needed.
if [ -n "$SHOW_HELP" ]; then
    cat <<HERE
usage: kbuild [OPTIONS]

Build the Linux Kernel located at the current directory. You may pass the
following options:

  -h, --help            Show this message.
  -p, --profile         The profile to be picked up.
  -l, --local           This is a local build, just generate install/uninstall files.
  --list-profiles       List the profiles that are available on the system.
  --skip-install        Don't generate the install/uninstall files.
  --skip-config         Skip the configuration step.
HERE
    exit 0
fi

##
# Profile select.

# If the configuration directory doesn't exist, create it now.
if [ ! -d "$KBUILD_CONFIG_PATH" ]; then
    echo "kbuild (info): creating the '$KBUILD_CONFIG_PATH' directory."
    mkdir -p $KBUILD_CONFIG_PATH
fi

if [ -z "$PROFILE" ]; then
    echo "kbuild (error): you have to provide a profile."
    list_available_profiles
    exit 1
fi

PROFILE_DIR="$KBUILD_CONFIG_PATH/$PROFILE"
if [ ! -d "$PROFILE_DIR" ]; then
    echo "kbuild (error): given profile does not exist."
    list_available_profiles
    exit 1
fi

##
# Initial checks.

# If this is a local build, then force `ARCH` To match the current system's
# architecture and unset any `CROSS_COMPILE` business.
if [ ! -z "$IS_LOCAL" ]; then
    ARCH="$(uname -m)"
    unset CROSS_COMPILE
fi

# Most of the times, we are safer if we just set the `ARCH` environment
# variable.
if [ -z "$ARCH" ]; then
    echo "kbuild (error): \$ARCH has to be defined."
    exit 1
fi

# Is cross compilation the thing we want?
if [ "$ARCH" != "$(uname -m)" ]; then
    # Confusingly enough, riscv64 is supposed to be passed as 'riscv' to the
    # kernel. Hence, don't take that as being different.
    if [ "$(uname -m)" != "riscv64" ] || [ "$ARCH" != "riscv" ]; then
        # We are indeed cross compiling. If `CROSS_COMPILE` is not set at this
        # point we should raise an error.
        if [ -z "$CROSS_COMPILE" ]; then
            echo "kbuild (error): \$CROSS_COMPILE has to be defined."
            exit 1
        fi
    fi
fi

# To avoid embarrassing moments, let's make sure that we are in the Linux repo.
if [ ! -f "Kbuild" ]; then
    echo "kbuild (error): we are not at the root of the Linux Kernel repository."
    exit 1
fi

##
# .config

if [ -z "$SKIP_CONFIG" ]; then
    if [ -f ".config" ]; then
        echo "kbuild (info): running 'oldconfig'."
        make oldconfig
    else
        echo "kbuild (info): running 'defconfig'."
        make defconfig
    fi

    echo "kbuild (info): running 'menuconfig' to double check."
    make menuconfig
else
    echo "kbuild (info): skipping configuration step..."
fi

##
# Build the kernel.

echo "kbuild (info): building the Linux kernel..."
make -j "$(nproc)"

##
# Installing.

if [ -n "$NO_INSTALL" ]; then
    echo "kbuild (info): done!"
    exit 0
fi

name=$(cat include/config/kernel.release)

if [ ! -z "$IS_LOCAL" ]; then
    # Just generate install/uninstall scripts and go.

    cp "$KBUILD_CONFIG_PATH/$PROFILE/install.sh" install.sh
    sed -i "s/\$name/$name/g" install.sh
    chmod +x install.sh

    cp "$KBUILD_CONFIG_PATH/$PROFILE/uninstall.sh" uninstall.sh
    sed -i "s/\$name/$name/g" uninstall.sh
    chmod +x uninstall.sh

    echo "kbuild (info): generated 'install.sh' and 'uninstall.sh'."

    exit 0
fi

##
# Into a bundle.

if [ -z "$INSTALL_PATH" ]; then
    INSTALL_PATH="buildroot-$name"
fi
mkdir -p "$INSTALL_PATH/usr/lib/modules/$name"
pushd "$INSTALL_PATH" >/dev/null
ln -s usr/lib lib
popd >/dev/null

make modules_install INSTALL_MOD_PATH="$INSTALL_PATH"
make dtbs_install INSTALL_DTBS_PATH="$INSTALL_PATH/boot/dtbs/$name"
make headers_install INSTALL_HDR_PATH="$INSTALL_PATH/usr"

unlink "$INSTALL_PATH/lib"

make install INSTALL_PATH="$INSTALL_PATH/boot" &>/dev/null
cp .config "$INSTALL_PATH/boot/config-$name"

##
# Setup install.sh script.

# Files that are going to be installed. Funnily enough, the pipeline will create
# the "files.txt" first, so it will show on the `find` output. Hence the stupid
# workaround you see of moving it in later.
find "$INSTALL_PATH" -type f -printf "/%P\n" >files.txt
mv files.txt "$INSTALL_PATH/files.txt"

# Copy install/uninstall scripts.
cp "$KBUILD_CONFIG_PATH/$PROFILE/install.sh" "$INSTALL_PATH/install.sh"
sed -i "s/\$name/$name/g" "$INSTALL_PATH/install.sh"
chmod +x "$INSTALL_PATH/install.sh"

cp "$KBUILD_CONFIG_PATH/$PROFILE/uninstall.sh" "$INSTALL_PATH/uninstall.sh"
sed -i "s/\$name/$name/g" "$INSTALL_PATH/uninstall.sh"
chmod +x "$INSTALL_PATH/uninstall.sh"

##
# Create tarball.

tar czf "$INSTALL_PATH.tar.gz" "$INSTALL_PATH"
rm -r "$INSTALL_PATH"

echo "kbuild (info): your build is now ready at '$INSTALL_PATH.tar.gz'."
