#!/bin/bash
# Build the xcat-genesis-base .deb for ONE Ubuntu codename.
#
# dracut copies the kernel, the kernel modules and every command out of the root it runs
# in, so this must run inside a root of the target codename. builddebs.pl --genesis starts
# it in that codename's <codename>-<arch>-sbuild chroot, one build per codename.
# --expect-codename is what stops a run on the build host: a single build there gives every
# Ubuntu release the build host's kernel.
#
# Parallel to xCAT-genesis-base.spec, which does the same for EL.

set -euo pipefail

DIR=$(readlink -f "$(dirname "$0")")
OS_RELEASE=${OS_RELEASE:-/etc/os-release}
expect_codename=""
outdir=""

while [ $# -gt 0 ]; do
    case "$1" in
        --expect-codename) expect_codename=${2:-}; shift 2 ;;
        --expect-codename=*) expect_codename=${1#*=}; shift ;;
        --outdir)          outdir=${2:-}; shift 2 ;;
        --outdir=*)        outdir=${1#*=}; shift ;;
        -h|--help)
            echo "usage: builddeb-genesis-base [--expect-codename <codename>] [--outdir <dir>]"
            exit 0 ;;
        *) echo "ERROR: unknown argument: $1" >&2; exit 2 ;;
    esac
done

BUILDARCH=$(dpkg --print-architecture)

case "$BUILDARCH" in
    amd64)   TARCH=x86_64 ;;
    ppc64el) TARCH=ppc64 ;;
    *)       echo "ERROR: unsupported architecture: $BUILDARCH" >&2; exit 1 ;;
esac

# The Genesis debs carry the architecture in the package name, so the packages an upgrade has
# to displace carry it too. 2.19 renames the ppc64 debs to ppc64el; dpkg keeps the old package,
# and its copy of the same files, unless the new one replaces it by name.
rewrite_control() {
    local control=$1 arch=$2 superseded
    case "$arch" in
        ppc64el) superseded="xcat-genesis-ppc64, xcat-genesis-base-ppc64" ;;
        *)       superseded="xcat-genesis-$arch" ;;
    esac
    sed -i -e "s/xcat-genesis-base-amd64/xcat-genesis-base-$arch/g" \
           -e "s/xcat-genesis-scripts-amd64/xcat-genesis-scripts-$arch/g" \
           -e "s/xcat-genesis-amd64/$superseded/g" "$control"
}

VERSION=$(cat "$DIR/../Version" 2>/dev/null || echo "2.18.0")
RELEASE=$(cat "$DIR/../Release" 2>/dev/null || echo "snap$(date +%Y%m%d%H%M)")
CODENAME=$(. "$OS_RELEASE" && echo "${VERSION_CODENAME:-}")

if [ -z "$CODENAME" ]; then
    echo "ERROR: $OS_RELEASE names no VERSION_CODENAME" >&2
    exit 1
fi

# The image is only valid for the release whose kernel it carries. Refuse to build a
# codename's image anywhere but in that codename's root.
if [ -n "$expect_codename" ] && [ "$expect_codename" != "$CODENAME" ]; then
    echo "ERROR: this root is $CODENAME, not $expect_codename." >&2
    echo "       The image would carry the $CODENAME kernel and boot under $expect_codename." >&2
    exit 1
fi

echo "Building xcat-genesis-base for $BUILDARCH ($TARCH) on Ubuntu $CODENAME"

export DEBIAN_FRONTEND=noninteractive

echo "Installing build dependencies..."
apt-get update -qq

# XCAT::GenesisBuildRoot holds the package list, so the unit tests can call it.
REQUIRED_PACKAGES=$(perl -I"$DIR/lib" -MXCAT::GenesisBuildRoot=required_packages \
    -e 'print "$_\n" for required_packages(@ARGV)' "$BUILDARCH" "$CODENAME")
apt-get install -y --no-install-recommends $REQUIRED_PACKAGES

# dpkg-architecture comes from dpkg-dev, which the line above installs.
TRIPLET=$(dpkg-architecture -qDEB_HOST_MULTIARCH)

# ncurses-base put its terminfo entries under /lib/terminfo before 24.04, and both dracut's own
# terminfo module and the 97xcat module ask for /usr/share/terminfo. On jammy the build stopped
# on /usr/share/terminfo/l/linux. Give this build root the path they ask for; the chroot is
# thrown away when the build ends.
if [ -d /lib/terminfo ]; then
    mkdir -p /usr/share/terminfo
    cp -an /lib/terminfo/. /usr/share/terminfo/ 2>/dev/null || true
fi

# Set up dracut module
if [ -d /usr/lib/dracut/modules.d ]; then
    DRACUT_PARENT=/usr/lib/dracut/modules.d
elif [ -d /usr/share/dracut/modules.d ]; then
    DRACUT_PARENT=/usr/share/dracut/modules.d
else
    echo "ERROR: cannot find dracut modules directory" >&2
    exit 1
fi

DRACUTMODDIR=$DRACUT_PARENT/97xcat

GENESIS_TMPDIR=$(mktemp -d /tmp/xcat-genesis-deb.XXXXXX)
GENESIS_ROOT=$GENESIS_TMPDIR/opt/xcat/share/xcat/netboot/genesis/$TARCH
GENESIS_FS=$GENESIS_ROOT/fs
DRACUT_IMAGE=$GENESIS_TMPDIR/genesis.rfs

cleanup() {
    rm -rf "$GENESIS_TMPDIR" "$DRACUTMODDIR"
}
trap cleanup EXIT

rm -rf "$DRACUTMODDIR"
mkdir -p "$DRACUTMODDIR"
cp -a "$DIR/dracut_105/ubuntu/." "$DRACUTMODDIR/"
chmod 0755 "$DRACUTMODDIR/module-setup.sh" "$DRACUTMODDIR/xcatroot" "$DRACUTMODDIR/dhclient-script"

if [ "$BUILDARCH" != "amd64" ]; then
    sed -i '/efibootmgr dmidecode/d' "$DRACUTMODDIR/module-setup.sh"
fi

# linux-image-generic pulls exactly one kernel into this root, and this root belongs to
# $CODENAME, so this is the target release's kernel.
KERNELVERSION=$(ls -1 /lib/modules | sort -V | tail -n 1)
if [ -z "$KERNELVERSION" ]; then
    echo "ERROR: no kernel modules found in /lib/modules" >&2
    exit 1
fi

echo "Using kernel $KERNELVERSION"

mkdir -p "$GENESIS_FS/etc/ssh"
mkdir -p /run/rpcbind

echo "Running dracut..."
dracut --compress gzip -m "xcat base" --no-early-microcode -N -f "$DRACUT_IMAGE" "$KERNELVERSION"

echo "Extracting initramfs..."
(cd "$GENESIS_FS" && zcat "$DRACUT_IMAGE" | cpio -dumi)

for script in \
    "$GENESIS_FS/sbin/dhclient-script" \
    "$GENESIS_FS/usr/sbin/dhclient-script" \
    "$GENESIS_FS/sbin/xcatroot"
do
    [ -f "$script" ] && chmod 0755 "$script"
done

for perl_dir in \
    /usr/share/perl5 \
    "/usr/lib/$TRIPLET/perl5" \
    /usr/local/share/perl5 \
    "/usr/local/lib/$TRIPLET/perl5"
do
    if [ -d "$perl_dir" ]; then
        echo "Adding perl library $perl_dir"
        mkdir -p "$GENESIS_FS$perl_dir"
        cp -a "$perl_dir/." "$GENESIS_FS$perl_dir/"
    fi
done

mkdir -p "$GENESIS_FS/lib/udev/rules.d"
if [ -e /usr/lib/udev/rules.d/80-net-name-slot.rules ]; then
    cp /usr/lib/udev/rules.d/80-net-name-slot.rules "$GENESIS_FS/lib/udev/rules.d/"
elif [ -e /lib/udev/rules.d/80-net-name-slot.rules ]; then
    cp /lib/udev/rules.d/80-net-name-slot.rules "$GENESIS_FS/lib/udev/rules.d/"
elif [ -e "$DIR/80-net-name-slot.rules" ]; then
    cp "$DIR/80-net-name-slot.rules" "$GENESIS_FS/lib/udev/rules.d/"
fi

KERNEL_IMAGE=/boot/vmlinuz-$KERNELVERSION
if [ ! -e "$KERNEL_IMAGE" ]; then
    for candidate in \
        "/boot/vmlinux-$KERNELVERSION" \
        "/usr/lib/modules/$KERNELVERSION/vmlinuz" \
        "/lib/modules/$KERNELVERSION/vmlinuz"
    do
        if [ -e "$candidate" ]; then
            KERNEL_IMAGE="$candidate"
            break
        fi
    done
fi
if [ ! -e "$KERNEL_IMAGE" ]; then
    echo "ERROR: cannot find the image of kernel $KERNELVERSION" >&2
    exit 1
fi
echo "Adding kernel $KERNEL_IMAGE"
cp "$KERNEL_IMAGE" "$GENESIS_ROOT/kernel"

# dracut_install reports a missing command and returns, so a hole reaches the .deb with
# nothing in the log but one line. Read the commands back from the module and check them
# against the payload, the way xCAT-genesis-base.spec does for EL.
bash "$DIR/verify-genesis-payload" --commands-from "$DRACUTMODDIR/module-setup.sh" "$GENESIS_FS" \
    usr/sbin/dhclient bin/sh sbin/xcatroot sbin/dhclient-script etc/rsyslog.conf

# What the verifier cannot know: that this image belongs to this chroot's kernel. An image
# built against another root's /lib/modules boots and then finds no driver for its NIC.
if [ ! -d "$GENESIS_FS/lib/modules/$KERNELVERSION" ]; then
    echo "ERROR: the image carries no /lib/modules/$KERNELVERSION" >&2
    ls -1 "$GENESIS_FS/lib/modules" 2>/dev/null >&2 || true
    exit 1
fi
if [ -z "$(find "$GENESIS_FS/lib/modules/$KERNELVERSION" -name '*.ko*' -print -quit)" ]; then
    echo "ERROR: /lib/modules/$KERNELVERSION in the image holds no kernel module" >&2
    exit 1
fi
for stray in "$GENESIS_FS"/lib/modules/*; do
    [ -d "$stray" ] || continue
    if [ "$(basename "$stray")" != "$KERNELVERSION" ]; then
        echo "ERROR: the image carries $(basename "$stray"), not the $KERNELVERSION of this root" >&2
        exit 1
    fi
done

# The 97xcat hooks are what makes the image xCAT's. dracut drops a module whose check()
# fails without a word, and the image then boots to a plain dracut shell.
if [ -z "$(find "$GENESIS_FS" -path '*/hooks/cmdline/*xcat-cmdline.sh' -print -quit)" ]; then
    echo "ERROR: the image carries no 97xcat cmdline hook" >&2
    exit 1
fi

find "$GENESIS_TMPDIR" -type c -delete

# Stage for dpkg-buildpackage
rm -rf "$DIR/opt"
cp -a "$GENESIS_TMPDIR/opt" "$DIR/"

# Adjust control file for target arch
rewrite_control "$DIR/debian/control" "$BUILDARCH"
# debian/dirs names the image directory, which is the rpm architecture.
echo "/opt/xcat/share/xcat/netboot/genesis/$TARCH/" > "$DIR/debian/dirs"

# dch reads debian/control from the current directory, not from the file it writes.
cd "$DIR"

PKG_VERSION="${VERSION}-${RELEASE}~${CODENAME}"
rm -f "$DIR/debian/changelog"
dch --create --package "xcat-genesis-base-$BUILDARCH" \
    --newversion "$PKG_VERSION" --distribution "$CODENAME" \
    --controlmaint -c "$DIR/debian/changelog" \
    "Native Ubuntu build on $CODENAME $BUILDARCH"

echo "Building .deb package..."
dpkg-buildpackage -rfakeroot -uc -us -b

if [ -n "$outdir" ]; then
    mkdir -p "$outdir"
    mv "$DIR/../"xcat-genesis-base-*_*.deb "$outdir/"
    echo "Build complete. .deb files in $outdir:"
    ls -la "$outdir"
else
    echo "Build complete. .deb files:"
    ls -la "$DIR/../"xcat-genesis-base*.deb 2>/dev/null || echo "Check parent directory for .deb files"
fi
