#!/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. `-s/--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. `-n/--no-install`: don't generate the install/uninstall files.
#   3. `-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/extlinux/extlinux.conf file.
#
# *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.
# *NOTE*: it's assumed that either `update-initramfs` or `dracut` exist. If you
# are using something else, send me a patch.
#####

set -e

##
# Arguments that can be passed.

while [[ $# -gt 0 ]]; do
    case $1 in
    -s | --skip-config)
        SKIP_CONFIG="t"
        shift
        ;;
    -n | --no-install)
        NO_INSTALL="t"
        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.
  -n, --no-install      Don't generate the install/uninstall files.
  -s, --skip-config     Skip the configuration step.
HERE
    exit 0
fi

##
# Initial checks.

# Most of the times, we are safer if we just set the `ARCH` env. 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 in the Linux Kernel."
    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 into a local bundle.

if [ -n "$NO_INSTALL" ]; then
    echo "kbuild (info): done!"
else
    name=$(cat include/config/kernel.release)

    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"

    # Now write the actual install script.
    cat <<SCRIPT >"$INSTALL_PATH/install.sh"
#!/usr/bin/env bash

set -ex

##
# Ensure you are root and dependencies are met.

if [ "\$EUID" -ne 0 ];then
   echo "kbuild (error): run this as root."
   exit 1
fi

if [ ! -f /sbin/update-initramfs ] && [ ! -f /usr/bin/dracut ]; then
   echo "kbuild (error): you need either 'dracut' or 'update-initramfs'."
   exit 1
fi

##
# Copy files into filesystem and generate initramfs.

# Regular installs.
cp -rv usr /
cp -rv boot /

# Some environments like 'update-initramfs' on Ubuntu expect files to be placed
# under a special directory in /usr/lib/firmware. Pull this trick now.
mkdir -p /usr/lib/firmware/$name/device-tree/
cp -rv boot/dtbs/$name/* /usr/lib/firmware/$name/device-tree/

# And run dracut/update-initramfs.
export PATH=/sbin:\$PATH
if command -v dracut &> /dev/null
then
    dracut --kver=$name -f
else
    # 'update-initramfs' might fail when trying to run 'flash' on Ubuntu, which
    # is done as a post update script. This is expected, so ignore it.
    set +e
    update-initramfs -k $name -u
    set -e
fi

##
# Provide hints on how to configure the bootloader.


if [ -z "\$EDITOR" ]; then
   EDITOR=vi
fi

if [ -f /boot/extlinux/extlinux.conf ]; then
   cat <<EXT >>/boot/extlinux/extlinux.conf

###
### NOTE: generated by kbuild.
###

label l1
	menu label Debian GNU/Linux bookworm/sid $name
	linux /vmlinuz-$name
	initrd /initrd.img-$name

	fdtdir /dtbs/$name
	append  root=/dev/nvme0n1p4 rw console=tty0 console=ttyS0,115200 earlycon rootwait debug

label l1r
	menu label Debian GNU/Linux bookworm/sid $name (rescue target)
	linux /vmlinuz-$name
	initrd /initrd.img-$name

	fdtdir /dtbs/$name
	append  root=/dev/nvme0n1p4 rw console=tty0 console=ttyS0,115200 earlycon rootwait single debug
EXT

    \$EDITOR /boot/extlinux/extlinux.conf
else
    # The other possibility is Grub. If that is not available, then do nothing.
    if [ -f /boot/grub/grub.cfg ]; then
        cat <<EXT >/etc/grub.d/40_custom
#!/bin/sh
exec tail -n +3 \\\$0

###
### NOTE: generated by kbuild.
###

menuentry 'Ubuntu on Linux $name' --class ubuntu --class gnu-linux --class gnu --class os {
	load_video
	insmod gzio
	if [ x\\\$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
	insmod part_gpt
	insmod ext2
	search --no-floppy --fs-uuid --set=root b68f5a83-3f28-4178-b0f5-195a309eef3a
	echo	'Loading Linux $name ...'
	linux 	/boot/vmlinuz-$name root=UUID=b68f5a83-3f28-4178-b0f5-195a309eef3a rw console=tty0 console=ttyS0,115200 earlycon rootwait debug
	echo	'Loading initial ramdisk ...'
	initrd 	/boot/initrd.img-$name
	echo	'Loading device tree blob...'
	devicetree	/boot/dtbs/$name/starfive/jh7110-starfive-visionfive-2-v1.3b.dtb
}
EXT
        update-grub
    fi
fi
SCRIPT

    # And the uninstall script.
    cat <<SCRIPT >"$INSTALL_PATH/uninstall.sh"
#!/usr/bin/env bash

set -ex

if [ "\$EUID" -ne 0 ];then
   echo "kbuild (error): run this as root."
   exit 1
fi

if [ ! -f files.txt ]; then
   echo "kbuild (error): there must be a 'files.txt' file."
   exit 1
fi

xargs rm -f < files.txt
rm -r /usr/lib/modules/$name
rm -r /boot/dtbs/$name
rm -r /usr/lib/firmware/$name
rm /boot/initrd.img-$name

if [ -z "\$EDITOR" ]; then
   EDITOR=vi
fi
if [ -f /boot/extlinux/extlinux.conf ]; then
   \$EDITOR /boot/extlinux/extlinux.conf
else
   \$EDITOR /etc/grub.d/40_custom
   update-grub
fi
SCRIPT

    ##
    # 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'."
fi
