#!/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 and bundle it into a tarball that can then be used on
# another machine. 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 options:
#   1. `--skip-config`: tell to skip the call to `defconfig` and/or
#      `menuconfig`. This is useful if you are just developing over the same
#      `.config` file again and again.
#   2. `--skip-install`: don't generate the install/uninstall files.
#   3. `-p/--profile`: the kind of machine you are building the kernel for.
#   4. `-l/--local`: this is a local build, just generate install/uninstall scripts.
#   5. `-h/--help`: show the help message.
#
# After that just send the resulting tarball to the machine where you want to
# install this kernel. 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.
#
# *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

##
# 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
            ;;
        -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 (values: debian, suse).
  -l, --local           This is a local build, just generatel install/uninstall files.
  --skip-install        Don't generate the install/uninstall files.
  --skip-config         Skip the configuration step.
HERE
    exit 0
fi

##
# Profile select.

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

PROFILE_DIR="$HOME/.config/kbuild/$PROFILE"
if [ ! -d "$PROFILE_DIR" ]; then
    echo "kbuild (error): given profile does not exist."
    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 "$HOME/.config/kbuild/$PROFILE/install.sh" install.sh
    sed -i "s/\$name/$name/g" install.sh
    chmod +x install.sh

    cp "$HOME/.config/kbuild/$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 "$HOME/.config/kbuild/$PROFILE/install.sh" "$INSTALL_PATH/install.sh"
sed -i "s/\$name/$name/g" "$INSTALL_PATH/install.sh"
chmod +x "$INSTALL_PATH/install.sh"

cp "$HOME/.config/kbuild/$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'."
