#!/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. `-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.
# *NOTE*: it's assumed that either `update-initramfs` or `dracut` exist. If you
# are using something else, send me a patch.
#####

set -e

# TODO: add a -u/--uuid flag, which can specify: suse-virt, suse-vf2

##
# 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
            ;;
        -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).
  --skip-install        Don't generate the install/uninstall files.
  --skip-config         Skip the configuration step.
HERE
    exit 0
fi

# Hard check on the profile being picked.
case "$PROFILE" in
    "debian" | "suse")
    ;;
    "")
        echo "kbuild (error): you have to profile one of these profiles: debian, suse."
        exit 1
        ;;
    *)
        echo "kbuild (error): uknown profile '$PROFILE'. Possible values: debian, suse."
        exit 1
        ;;
esac

##
# 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 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 into a local bundle.

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

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
SCRIPT

case "$PROFILE" in
    "debian")
        cat <<SCRIPT >>"$INSTALL_PATH/install.sh"
   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
SCRIPT
        ;;
    "suse")
        cat <<SCRIPT >> "$INSTALL_PATH/install.sh"
        cat <<EXT >/etc/grub.d/40_custom
#!/bin/sh
exec tail -n +3 \\\$0

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

menuentry 'openSUSE on Linux $name' --class opensuse --class gnu-linux --class gnu --class os \\\$menuentry_id_option 'gnulinux-simple-22391c4c-ad76-c05c-84b3-535dc2efaf97' {
	load_video
	set gfxpayload=keep
	insmod gzio
	insmod part_gpt
	insmod btrfs
	search --no-floppy --fs-uuid --set=root 22391c4c-ad76-c05c-84b3-535dc2efaf97
	echo	'Loading Linux $name ...'
	linux	/boot/vmlinuz-$name root=UUID=22391c4c-ad76-c05c-84b3-535dc2efaf97 \\\${extra_cmdline} security=selinux selinux=1 loglevel=3 splash=silent systemd.show_status=1
	echo	'Loading initial ramdisk ...'
	initrd	/boot/initrd-$name
}
EXT

# Edit the configuration file we just produced just in case.
\$EDITOR /etc/grub.d/40_custom

# And update grub itself.
grub2-mkconfig -o /boot/grub2/grub.cfg
SCRIPT
        ;;
    *)
        ;;
esac

# 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 -rf /usr/lib/modules/$name
rm -rf /boot/dtbs/$name
rm -rf /usr/lib/firmware/$name
rm -f /boot/initrd-$name
rm -f /boot/initrd.img-$name

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

SCRIPT

case "$PROFILE" in
    "debian")
cat <<SCRIPT >>"$INSTALL_PATH/uninstall.sh"
\$EDITOR /boot/extlinux/extlinux.conf
SCRIPT
        ;;
    "suse")
cat <<SCRIPT >>"$INSTALL_PATH/uninstall.sh"
\$EDITOR /etc/grub.d/40_custom
update-bootloader
SCRIPT
        ;;
esac


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