Skip to content
Snippets Groups Projects
build.sh 11.92 KiB
#!/bin/bash

. tools/functions.sh

# Default variables
export SQUASHFS="output/squashfs"
export ROOTFS_DIR="output/rootfs"
export ISO_DIR="output/iso"
export BOOT_FOLDER="$ISO_DIR/boot"
export SQUASHFS_IMG="squash.rootfs"
export VERBOSE=false
export OUTPUT="build"
export ISO_NAME="nexus.iso"
export UBUNTU_VERSION="noble"
export CHALLENGE="default_challenge"
export ARCH="amd64"

TMP_KERNEL=$(mktemp /tmp/vmlinuz.XXXXXX)
TMP_INITRD=$(mktemp /tmp/initrd.XXXXXX)
TMP_SQUASHFS_IMG=$(mktemp /tmp/squashfs_img.XXXXXX)

helper() {
  echo "USAGE: $(basename "$0") [options]"
  echo
  echo "Options:"
  echo "  -v, --verbose         Set verbose mode"
  echo "  --env                 Path to an env file with all described parameters"
  echo "  --output              Set the output folder (default: .)"
  echo "  --luks-passphrase     Enable LUKS encryption with passphrase"
  echo "  --luks-keys           List of additional keys"
  echo "  --ram                 Using the system in RAM"
  echo "  --challenge           List of additional keys"
  echo "  --pxe                 Set the url of the pxe and generate an initramfs, kernel and squashfs for the pxe to use
                        (use the url of the folder where the squashfs is stored, not the direct link to the squashfs)"
  echo "  --cache               Path to a folder with a clean fs already downloaded"
  echo "  --version             Version of Ubuntu (e.g., Noble)"
  echo
  echo "Environment Setup Parameters:"
  echo "  SQUASHFS              Path to the SquashFS file (e.g., out/squash.rootfs)"
  echo "  ROOTFS_DIR            Path to the temporary filesystem directory (e.g., out/rootfs)"
  echo "  ISO_DIR               Path to the temporary ISO directory (e.g., out/iso)"
  echo "  SERVER                IP of the nexus server (e.g., 127.0.0.1:1077)"
  echo "  CERT                  Path to the Certificate file    (e.g., ca-cert.pem)"
  echo "  EXAM_USER             Default user for exam client    (e.g., user)"
  echo "  EXAM_PWD              Default password for exam client        (e.g., password)"
  echo
  echo "Example Usage:"
  echo "  $(basename "$0") --env .env"
  echo "  $(basename "$0") --luks-passphrase mypassphrase --cache cache -v"
  exit 1
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --env)
      if [ -f "$2" ]; then
        source "$2" > /dev/null
      else
        echo "Error: File '$2' not found." >&2
        helper
      fi
      shift 2
      ;;
    --output)
      if [ -n "$2" ]; then
        export OUTPUT="$2"
        shift 2
      else
        echo "Error: --output requires an argument" >&2
        helper
      fi
      ;;
    --luks-passphrase)
      if [ -n "$2" ]; then
        LUKS_PASSPHRASE="$2"
        shift 2
      else
        echo "Error: --luks-passphrase requires an argument" >&2
        helper
      fi
      ;;
    --luks-keys)
      if [ -n "$2" ]; then
        ADDITIONAL_KEYS="$2"
        shift 2
      else
        echo "Error: --luks-keys requires an argument" >&2
        helper
      fi
      ;;
    --challenge)
      if [ -n "$2" ]; then
        CHALLENGE="$2"
        shift 2
      else
        echo "Error: --challenge requires an argument" >&2
        helper
      fi
      ;;
    --pxe)
      if [ -n "$2" ]; then
        export PXE_URL="$2"
        shift 2
      else
        echo "Error: --pxe requires an argument" >&2
        helper
      fi
      ;;
    --ram)
      export RAM="squashfs_tmpfs"
      shift
      ;;
    -v|--verbose)
      export VERBOSE=true
      shift
      ;;
    --cache)
      if [ -n "$2" ]; then
        export CACHE_FS="$2"
        shift 2
      else
        echo "Error: --cache requires an argument" >&2
        helper
      fi
      ;;
    --version)
      if [ -n "$2" ]; then
        export UBUNTU_VERSION="$2"
        shift 2
      else
        echo "Error: --version requires an argument" >&2
        helper
      fi
      ;;
    *)
      echo "Unknown option: $1" >&2
      helper
      ;;
  esac
done

check_inside_container
check_environment_var
check_dependencies
CMDLINE="boot=nexus quiet splash $RAM modprobe.blacklist=floppy"

echo "[Cleanup $ROOTFS_DIR]"
ROOTFS_PATH="${ROOTFS_DIR%/}"
child_mounts=$(findmnt -rn -o TARGET | grep -E "^${ROOTFS_PATH}/[^/]+$")

if [ -n "$child_mounts" ]; then
    echo "WARNING: The following mount points are directly under $ROOTFS_DIR:"
    echo "$child_mounts"
    echo
    read -p "Do you want to unmount these mount points? [y/N]: " confirmation
    case "$confirmation" in
        [yY][eE][sS]|[yY])
            while IFS= read -r mount_point; do
                echo "Unmounting $mount_point..."
                if ! umount "$mount_point"; then
                    echo "Error: Failed to unmount $mount_point. Please unmount manually or reboot the system to avoid issues."
                    exit 1
                fi
            done <<< "$child_mounts"
            ;;
        *)
            echo "Cleanup aborted by user."
            exit 1
            ;;
    esac
fi
run_command rm -rf "$ROOTFS_DIR"

echo "[Cleanup $OUTPUT]"
run_command rm -rf "$OUTPUT"

echo "[Create filesystem...]"
mkdir -p "$ROOTFS_DIR"

if [ -z "$CACHE_FS" ]; then
  run_command debootstrap --arch="$ARCH" "$UBUNTU_VERSION" "$ROOTFS_DIR" http://archive.ubuntu.com/ubuntu/
  run_command mount -t proc /proc "$ROOTFS_DIR/proc"
  run_command mount --rbind /sys "$ROOTFS_DIR/sys"
  run_command mount --rbind /dev "$ROOTFS_DIR/dev"
  run_command mount --rbind /run "$ROOTFS_DIR/run"
  run_command mount -t tmpfs tmpfs "$ROOTFS_DIR/tmp"

  echo "[Installing packages...]"
  packages=$(tr '\n' ' ' < config/01-packages_install/packages)
  echo "deb http://archive.ubuntu.com/ubuntu $UBUNTU_VERSION main universe" > "$ROOTFS_DIR/etc/apt/sources.list"
  run_command_chroot apt update
  run_command_chroot apt install -y --no-install-recommends "$packages"
else
  if [ ! -d "$CACHE_FS" ]; then
    mkdir "$CACHE_FS"
    run_command debootstrap --arch="$ARCH" "$UBUNTU_VERSION" "$CACHE_FS" http://archive.ubuntu.com/ubuntu/

    run_command mount -t proc /proc "$CACHE_FS/proc"
    run_command mount --rbind /sys "$CACHE_FS/sys"
    run_command mount --rbind /dev "$CACHE_FS/dev"
    run_command mount --rbind /run "$CACHE_FS/run"
    run_command mount -t tmpfs tmpfs "$CACHE_FS/tmp"

    echo "[Installing packages...]"
    packages=$(tr '\n' ' ' < config/01-packages_install/packages)
    echo "deb http://archive.ubuntu.com/ubuntu $UBUNTU_VERSION main universe" > "$CACHE_FS/etc/apt/sources.list"
    run_command chroot "$CACHE_FS" apt update
    run_command chroot "$CACHE_FS" apt install -y --no-install-recommends "$packages"

    run_command umount -l "$CACHE_FS/proc"
    run_command umount -l "$CACHE_FS/sys"
    run_command umount -l "$CACHE_FS/dev"
    run_command umount -l "$CACHE_FS/run"
    run_command umount -l "$CACHE_FS/tmp"
  else
    echo " [Skip through cache]"
  fi

  run_command cp -r "$CACHE_FS"/. "$ROOTFS_DIR"/

  run_command mount -t proc /proc "$ROOTFS_DIR/proc"
  run_command mount --rbind /sys "$ROOTFS_DIR/sys"
  run_command mount --rbind /dev "$ROOTFS_DIR/dev"
  run_command mount --rbind /run "$ROOTFS_DIR/run"
  run_command mount -t tmpfs tmpfs "$ROOTFS_DIR/tmp"
fi

echo "[Uploading configuration file...]"
run_command cp -rf config/02-customisation/* "$ROOTFS_DIR"

echo "[Checking for required dependencies...]"
if [ ! -d "/opt/limine" ]; then
  echo "Error: /opt/limine not found. Please install the Limine bootloader files in /opt/limine."
  exit 1
fi

if [ ! -f "$ROOTFS_DIR/usr/lib/shim/shimx64.efi.signed.latest" ] || [ ! -f "$ROOTFS_DIR/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed" ]; then
  echo "Error: Required EFI files not found in $ROOTFS_DIR."
  exit 1
fi

if [ -z "$PXE_URL" ]; then
  EFIBOOT=$(mktemp /tmp/efiboot.img.XXXXXX)
  EFIBOOT_MOUNT=$(mktemp -d /tmp/efiboot.XXXXXX)
  echo "[EFI img preparation]"
  run_command mkdir -p "$ISO_DIR/boot/grub"
  run_command "sed 's/CMDLINE/$CMDLINE/g' config/00-bootloader/grub.cfg > $ISO_DIR/boot/grub/grub.cfg"
  SIZE_EFIBOOT=$(get_total_size "$ROOTFS_DIR/usr/lib/shim/shimx64.efi.signed.latest" "$ROOTFS_DIR/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed")
  OFFSET=$(( 150 * 1024 )) # Some space for fat header and grub.cfg file

  run_command fallocate -l $(( $SIZE_EFIBOOT + $OFFSET)) "$EFIBOOT"
  run_command mkfs.vfat "$EFIBOOT"
  run_command mount -o loop "$EFIBOOT" "$EFIBOOT_MOUNT"
  run_command mkdir -p "$EFIBOOT_MOUNT/EFI/BOOT"
  run_command cp config/00-bootloader/grub.efi.cfg "$EFIBOOT_MOUNT/EFI/BOOT/grub.cfg"
  run_command cp "$ROOTFS_DIR/usr/lib/shim/shimx64.efi.signed.latest" "$EFIBOOT_MOUNT/EFI/BOOT/BOOTx64.EFI"
  run_command cp "$ROOTFS_DIR/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed" "$EFIBOOT_MOUNT/EFI/BOOT/grubx64.efi"
  run_command umount "$EFIBOOT_MOUNT"
  run_command rmdir "$EFIBOOT_MOUNT"
fi

echo "[Post-install...]"
for script in config/03-post_install/*.sh; do
  echo " [Running $script...]"
  run_command "$script"
done

echo "[Moving kernel...]"
KERNEL_FILE=$(ls -1 "$ROOTFS_DIR/boot/vmlinuz-"* 2>/dev/null | head -n 1)
if [ -z "$KERNEL_FILE" ]; then
  echo "Error: No kernel file found in $ROOTFS_DIR/boot"
  exit 1
fi
run_command cp "$KERNEL_FILE" "$TMP_KERNEL"
run_command cp "$ROOTFS_DIR/boot/initrd.img" "$TMP_INITRD"
run_command rm -rf "$ROOTFS_DIR/boot"

echo "[Unmount subsystem...]"
run_command umount -l "$ROOTFS_DIR/proc"
run_command umount -l "$ROOTFS_DIR/sys"
run_command umount -l "$ROOTFS_DIR/dev"
run_command umount -l "$ROOTFS_DIR/run"
run_command umount -l "$ROOTFS_DIR/tmp"

echo "[Create squash.rootfs...]"
run_command mksquashfs "$ROOTFS_DIR" "$SQUASHFS"

if [ ! -z "$LUKS_PASSPHRASE" ]; then
  echo "[Encrypt squash.rootfs...]"
  SQUASHFS_SIZE=$(stat -c %s "$SQUASHFS")
  LUKS_HEADER_SIZE=$((1024 * 1024 * 16)) # 16 MiB header LUKS2
  TOTAL_SIZE=$((SQUASHFS_SIZE + LUKS_HEADER_SIZE))
  SECTOR_SIZE=512

  run_command fallocate -l "$TOTAL_SIZE" "$TMP_SQUASHFS_IMG"
  run_command bash -c "cat <<EOF | cryptsetup luksFormat '$TMP_SQUASHFS_IMG' --batch-mode
$LUKS_PASSPHRASE
EOF"

  run_command bash -c "cat <<EOF | cryptsetup luksOpen '$TMP_SQUASHFS_IMG' container
$LUKS_PASSPHRASE
EOF"
  run_command dd if="$SQUASHFS" of=/dev/mapper/container bs=4M
  run_command cryptsetup luksClose container

  if [ ! -z "$ADDITIONAL_KEYS" ]; then
    for key in $ADDITIONAL_KEYS; do
      key_file=$(mktemp)
      echo "$key" > "$key_file"
      run_command bash -c "cat <<EOF | cryptsetup luksAddKey '$TMP_SQUASHFS_IMG'
$LUKS_PASSPHRASE
$key
EOF"
      rm -f "$key_file"
    done
  fi
else
  cp "$SQUASHFS" "$TMP_SQUASHFS_IMG"
fi

mkdir "$OUTPUT"
if [ ! -z "$PXE_URL" ]; then
  run_command cp "$TMP_KERNEL" "$OUTPUT/vmlinuz"
  run_command cp "$TMP_INITRD" "$OUTPUT/initrd"
  run_command cp "$TMP_SQUASHFS_IMG" "$OUTPUT/$SQUASHFS_IMG"
  run_command cp "$ROOTFS_DIR/usr/lib/shim/shimx64.efi.signed.latest" "$OUTPUT/BOOTx64.EFI"
  run_command cp "$ROOTFS_DIR/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed" "$OUTPUT/grubx64.efi"
  CMDLINE="boot=pxe quiet splash $RAM modprobe.blacklist=floppy"
  run_command "sed 's/CMDLINE/$CMDLINE/g' config/00-bootloader/grub.cfg > $OUTPUT/grub.cfg"
else
  echo "[Moving kernel && squashfs from rootfs to iso/boot...]"
  run_command mkdir -p "$BOOT_FOLDER"
  run_command cp "$TMP_KERNEL" "$BOOT_FOLDER/vmlinuz"
  run_command cp "$TMP_INITRD" "$BOOT_FOLDER/initrd"

  echo "[Create iso...]"
  run_command cp "$TMP_SQUASHFS_IMG" "$ISO_DIR/$SQUASHFS_IMG"
  run_command cp /opt/limine/*.bin "$ISO_DIR/"
  run_command cp /opt/limine/*.sys "$ISO_DIR/"
  run_command cp "$EFIBOOT" "$ISO_DIR/efiboot.img"
  run_command "sed 's/CMDLINE/$CMDLINE/g' config/00-bootloader/limine.conf > $ISO_DIR/boot/limine.conf"
  run_command xorriso -as mkisofs -R -r -J -b "limine-bios-cd.bin" \
        -no-emul-boot -boot-load-size 4 -boot-info-table -hfsplus \
        -apm-block-size 2048 --efi-boot "efiboot.img" \
        -efi-boot-part --efi-boot-image --protective-msdos-label \
        -volid "NEXUSCLIENTISO" \
        "$ISO_DIR" -o "$OUTPUT/$ISO_NAME"

  run_command limine bios-install "$OUTPUT/$ISO_NAME"
fi

chown -R 1000:1000 "$OUTPUT"

echo "[Cleanup temporary files...]"
rm -f "$TMP_KERNEL" "$TMP_INITRD" "$TMP_SQUASHFS_IMG" "$EFIBOOT"