#!/bin/sh
# Copyright (c) 2018, 2022, 2023, 2024 Jonas 'Sortie' Termansen.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# iso-grub-cfg.sh
# Generate GRUB bootloader configuration for release .iso filesystems.

# Note: This file has detailed documentation in release-iso-bootconfig(7).

set -e

this=$(which -- "$0")
thisdir=$(dirname -- "$this")

directory=
mount=false
platform=
version=

dashdash=
previous_option=
for argument do
  if test -n "$previous_option"; then
    eval $previous_option=\$argument
    previous_option=
    continue
  fi

  case $argument in
  *=?*) parameter=$(expr "X$argument" : '[^=]*=\(.*\)' || true) ;;
  *=)   parameter= ;;
  *)    parameter=yes ;;
  esac

  case $dashdash$argument in
  --) dashdash=yes ;;
  --platform) previous_option=platform ;;
  --platform=*) platform=$parameter ;;
  --mount) mount=true ;;
  --version=*) version=$parameter ;;
  --version) previous_option=version ;;
  -*) echo "$0: unrecognized option $argument" >&2
      exit 1 ;;
  *) directory="$argument" ;;
  esac
done

if test -n "$previous_option"; then
  echo "$0: option '$argument' requires an argument" >&2
  exit 1
fi

if test -z "$platform"; then
  echo "$0: platform wasn't set with --platform" >&2
  exit 1
fi

if test -z "$version"; then
  echo "$0: version wasn't set with --version" >&2
  exit 1
fi

if test -z "$directory"; then
  echo "$0: no directory operand supplied" >&2
  exit 1
fi

machine=$(expr x"$platform" : 'x\([^-]*\).*')

maybe_compressed() {
  if [ -e "$1.xz" ]; then
    echo "$1.xz"
  elif [ -e "$1.gz" ]; then
    echo "$1.gz"
  elif [ -e "$1" ]; then
    echo "$1"
  fi
}

human_size() {
  (export LC_ALL=C; du -bh "$1" 2>/dev/null || du -h "$1") |
  sed -E 's/^([^[:space:]]+).*/\1/'
}

portvar() {
  echo "$1" | sed -e 's/-/_/g' -e 's/+/x/g'
}

isinset() {
  (for port in $2; do
     if [ x"$1" = x"$port" ]; then
       echo true
       exit 0
     fi
   done
   echo false
   exit 0)
}

escape_extended_regex() {
  printf "%s\n" "$1" | sed -E -e 's/[[$()*?\+.^{|}]/\\\0/g'
}

list_set_recurse() {
  if [ "$1" = '*' ]; then
    grep -Ev '^all' repository/packages.list
  elif [ ! -e "repository/$1.visited" ]; then
    echo "$1"
    touch "repository/$1.visited"
    for dep in $(tix-vars -d '' "repository/$1.info" RUNTIME_DEPS); do
      list_set_recurse "$dep"
    done
  fi
}

list_set() {
  rm -f repository/*.visited
  list_set_recurse "$1" | LC_ALL=C sort -u
  rm -f repository/*.visited
}

cd "$directory"

sets=$(grep -Ev '^all$' repository/sets.list || true)

kernel=$(maybe_compressed boot/sortix.bin)
if $mount; then
  initrd=$(maybe_compressed boot/sortix.initrd)
  initrds=$initrd
else
  live_initrd=$(maybe_compressed boot/live.tar)
  overlay_initrd=$(maybe_compressed boot/overlay.tar)
  src_initrd=$(maybe_compressed boot/src.tar)
  system_initrd=$(maybe_compressed repository/system.tix.tar)
  initrds="$system_initrd $src_initrd $live_initrd $overlay_initrd"
fi
if $mount; then
  ports=
else
  ports=$(ls repository |
          grep -E '\.tix\.tar\.xz$' |
          sed -E 's/\.tix\.tar\.xz$//' |
          (grep -Ev '^system$' || true))
fi

mkdir -p boot/grub
mkdir -p boot/grub/init

echo "furthermore" > boot/grub/init/furthermore
# TODO: Possibly use a 'try' feature to not warn in case it was already unset.
echo "unset require dhclient" > boot/grub/init/network-no-dhclient
echo "require ntpd optional" > boot/grub/init/local-ntpd
echo "require sshd optional" > boot/grub/init/local-sshd

exec > boot/grub/grub.cfg

cat << EOF
function hook_initialize_terminal {
  insmod all_video
  if loadfont unicode; then
    insmod gfxterm
    terminal_output gfxterm
  fi
}
EOF

for hook in \
advanced_menu_post \
advanced_menu_pre \
initrd_post \
kernel_post \
kernel_pre \
menu_post \
menu_pre \
ports_menu \
ports_menu_post \
ports_menu_pre \
ports_menu_sets \
ports_post \
ports_pre \
tix_menu \
tix_menu_post \
tix_menu_pre \
tix_menu_sets \
; do
  cat << EOF
function hook_$hook {
  true
}
EOF
done
for set in all $sets no; do
  cat << EOF
function hook_ports_set_$set {
  true
}
function hook_tix_set_$set {
  true
}
EOF
done
echo

cat << EOF
insmod part_msdos
insmod ext2
EOF
find . | grep -Eq '\.gz$' && echo "insmod gzio"
find . | grep -Eq '\.xz$' && echo "insmod xzio"

cat << EOF
set version="$version"
set machine="$machine"
set mount=$mount
if \$mount; then
  chain='-- /sbin/init --target=chain'
else
  chain=
fi
set base_menu_title="Sortix \$version for \$machine"
set menu_title="\$base_menu_title"
set title_single_user='live environment'
set title_sysinstall='new installation'
set title_sysupgrade='upgrade existing installation'
set timeout=10
set default="0"
set console=
if [ -e /boot/random.seed ]; then
  no_random_seed=
else
  no_random_seed=--no-random-seed
fi
set enable_src=true
set enable_network_drivers=
set enable_dhclient=true
set enable_gui=true
set enable_ntpd=false
set enable_sshd=false
set kernel_options=
set serial_console=--console=com1

export version
export machine
export mount
export chain
export base_menu_title
export menu_title
export title_single_user
export title_sysinstall
export title_sysupgrade
export timeout
export default
export console
export no_random_seed
export enable_src
export enable_network_drivers
export enable_dhclient
export enable_gui
export enable_ntpd
export enable_sshd
export kernel_options
export serial_console
EOF

if [ -n "$ports" ]; then
  echo
  for port in $ports; do
    printf 'port_%s=true\n' "$(portvar "$port")"
  done
  for port in $ports; do
    printf 'tix_%s=false\n' "$(portvar "$port")"
  done
  echo
  for port in $ports; do
    printf 'export port_%s\n' "$(portvar "$port")"
  done
  for port in $ports; do
    printf 'export tix_%s\n' "$(portvar "$port")"
  done
fi

echo
echo 'function select_ports_set_no {'
for port in $ports; do
  printf "  port_%s=false\n" "$(portvar "$port")"
done
printf "  hook_ports_set_no\n"
echo "}"
for set in $sets; do
  echo
  set_content=$(list_set "$set")
  echo "function select_ports_set_$set {"
  for port in $ports; do
    printf "  port_%s=%s\n" "$(portvar "$port")" "$(isinset "$port" "$set_content")"
  done
  printf "  hook_ports_set_%s\n" "$set"
  echo "}"
done
echo
echo 'function select_ports_set_all {'
for port in $ports; do
  printf "  port_%s=true\n" "$(portvar "$port")"
done
printf "  hook_ports_set_all\n"
echo "}"

echo
echo 'function select_tix_set_no {'
for port in $ports; do
  printf "  tix_%s=false\n" "$(portvar "$port")"
done
printf "  hook_tix_set_no\n"
echo "}"
for set in $sets; do
  echo
  set_content=$(list_set "$set")
  echo "function select_tix_set_$set {"
  for port in $ports; do
    printf "  tix_%s=%s\n" "$(portvar "$port")" "$(isinset "$port" "$set_content")"
  done
  printf "  hook_tix_set_%s\n" "$set"
  echo "}"
done
echo
echo 'function select_tix_set_all {'
for port in $ports; do
  printf "  tix_%s=true\n" "$(portvar "$port")"
done
printf "  hook_tix_set_all\n"
echo "}"
echo

printf "function load_base {\n"
case $platform in
x86_64-*)
  cat << EOF
  if ! cpuid -l; then
    echo "Error: You cannot run this 64-bit operating system because this" \
         "computer has no 64-bit mode."
    read
    exit
  fi
EOF
  ;;
esac
cat << EOF
  hook_kernel_pre
  echo -n "Loading /$kernel ($(human_size $kernel)) ... "
  multiboot /$kernel \$console --firmware=\$grub_platform \$no_random_seed \$enable_network_drivers \$kernel_options \$chain "\$@"
  echo done
  hook_kernel_post
  # TODO: Injecting configuration doesn't work for mounted cdroms.
  if ! \$enable_dhclient; then
    echo -n "Disabling dhclient ... "
    module /boot/grub/init/furthermore --create-to /etc/init/network
    module /boot/grub/init/network-no-dhclient --append-to /etc/init/network
    echo done
  fi
  if \$enable_ntpd; then
    echo -n "Enabling ntpd ... "
    module /boot/grub/init/local-ntpd --append-to /etc/init/local
    module /boot/grub/init/furthermore --create-to /etc/init/time
    module /boot/grub/init/local-ntpd --append-to /etc/init/time
    echo done
  fi
  if \$enable_sshd; then
    echo -n "Enabling sshd ... "
    module /boot/grub/init/local-sshd --append-to /etc/init/local
    echo done
  fi
  if [ \$no_random_seed != --no-random-seed ]; then
    echo -n "Loading /boot/random.seed (256) ... "
    module /boot/random.seed --random-seed
    echo done
  fi
EOF
for initrd in $initrds; do
  if [ "$initrd" = "$src_initrd" ]; then
    cat << EOF
  if \$enable_src; then
    echo -n "Loading /$initrd ($(human_size $initrd)) ... "
    module /$initrd
    echo done
  fi
EOF
  else
    cat << EOF
  echo -n "Loading /$initrd ($(human_size $initrd)) ... "
  module /$initrd
  echo done
EOF
  fi
done
cat << EOF
  hook_initrd_post
}
EOF

echo
cat << EOF
function load_ports {
  hook_ports_pre
EOF
if [ -z "$ports" ]; then
  printf "  true\n"
fi
for port in $ports; do
  tix=repository/$port.tix.tar.xz
  cat << EOF
  if \$tix_$(portvar "$port"); then
    echo -n "Loading /$tix ($(human_size $tix)) ... "
    module --nounzip /$tix --to /$tix
    echo done
  fi
  if \$port_$(portvar "$port"); then
    echo -n "Loading /$tix ($(human_size $tix)) ... "
    module /$tix
    echo done
  fi
EOF
done
cat << EOF
  hook_ports_post
}
EOF

echo
cat << EOF
function load_sortix {
  load_base "\$@"
  load_ports
}
EOF

cat << EOF

if [ -e /boot/grub/hooks.cfg ]; then
  . /boot/grub/hooks.cfg
fi

hook_initialize_terminal

. /boot/grub/main.cfg
EOF

exec > boot/grub/main.cfg

menuentry() {
  echo
  printf "menuentry \"Sortix (%s)\" {\n" "$1"
  if [ -n "$2" ]; then
    printf "  if \$enable_gui; then\n"
    printf "    load_sortix %s-gui\n" "$2"
    printf "  else\n"
    printf "    load_sortix %s\n" "$2"
    printf "  fi\n"
  else
    printf "  load_sortix\n"
  fi
  printf "}\n"
}

cat << EOF
menu_title="\$base_menu_title"

hook_menu_pre
EOF

menuentry "\$title_single_user" '-- /sbin/init --target=single-user'
menuentry "\$title_sysinstall" '-- /sbin/init --target=sysinstall'
menuentry "\$title_sysupgrade" '-- /sbin/init --target=sysupgrade'

cat << EOF

if ! $mount; then
menuentry "Select ports..." {
  configfile /boot/grub/ports.cfg
}
fi

menuentry "Advanced..." {
  configfile /boot/grub/advanced.cfg
}

hook_menu_post
EOF

exec > boot/grub/advanced.cfg

cat << EOF
menuentry "Back..." {
  configfile /boot/grub/main.cfg
}

menu_title="\$base_menu_title - Advanced Options"

hook_advanced_menu_pre

if "\$enable_gui"; then
  menuentry "Disable GUI" {
    enable_gui=false
    configfile /boot/grub/advanced.cfg
  }
else
  menuentry "Enable GUI" {
    enable_gui=true
    configfile /boot/grub/advanced.cfg
  }
fi

if [ "\$console" = "\$serial_console" ]; then
  menuentry "Disable serial console" {
    console=
    configfile /boot/grub/advanced.cfg
  }
else
  menuentry "Enable serial console" {
    console="$serial_console"
    enable_gui=false
    configfile /boot/grub/advanced.cfg
  }
fi

if ! $mount; then
if "\$enable_src"; then
  menuentry "Disable loading source code" {
    enable_src=false
    configfile /boot/grub/advanced.cfg
  }
else
  menuentry "Enable loading source code" {
    enable_src=true
    configfile /boot/grub/advanced.cfg
  }
fi
fi

if [ "\$enable_network_drivers" = --disable-network-drivers ]; then
  menuentry "Enable networking drivers" {
    enable_network_drivers=
    configfile /boot/grub/advanced.cfg
  }
else
  menuentry "Disable networking drivers" {
    enable_network_drivers=--disable-network-drivers
    configfile /boot/grub/advanced.cfg
  }
fi

if ! $mount; then
if \$enable_dhclient; then
  menuentry "Disable DHCP client" {
    enable_dhclient=false
    configfile /boot/grub/advanced.cfg
  }
else
  menuentry "Enable DHCP client" {
    enable_dhclient=true
    configfile /boot/grub/advanced.cfg
  }
fi

if \$enable_ntpd; then
  menuentry "Disable NTP client" {
    enable_ntpd=false
    configfile /boot/grub/advanced.cfg
  }
else
  menuentry "Enable NTP client" {
    enable_ntpd=true
    configfile /boot/grub/advanced.cfg
  }
fi

if \$enable_sshd; then
  menuentry "Disable SSH server" {
    enable_sshd=false
    configfile /boot/grub/advanced.cfg
  }
else
  menuentry "Enable SSH server" {
    enable_sshd=true
    configfile /boot/grub/advanced.cfg
  }
fi

menuentry "Select binary packages..." {
  configfile /boot/grub/tix.cfg
}
fi

hook_advanced_menu_post
EOF

exec > boot/grub/ports.cfg

cat << EOF
menuentry "Back..." {
  configfile /boot/grub/main.cfg
}

menu_title="\$base_menu_title - Ports"

hook_ports_menu_pre

menuentry "Load all ports" {
  select_ports_set_all
  configfile /boot/grub/ports.cfg
}

hook_ports_menu_sets
EOF

for set in $sets; do
  echo
  printf 'menuentry "Load only '"$set"' ports" {\n'
  printf "  select_ports_set_%s\n" "$set"
  printf '  configfile /boot/grub/ports.cfg\n'
  printf '}\n'
done

cat << EOF

menuentry "Load no ports" {
  select_ports_set_no
  configfile /boot/grub/ports.cfg
}

hook_ports_menu

EOF

for port in $ports; do
  cat << EOF
if \$port_$(portvar "$port"); then
  menuentry "$port = true" {
    port_$(portvar "$port")=false
    configfile /boot/grub/ports.cfg
  }
else
  menuentry "$port = false" {
    port_$(portvar "$port")=true
    configfile /boot/grub/ports.cfg
  }
fi
EOF
done

cat << EOF

hook_ports_menu_post
EOF

exec > boot/grub/tix.cfg

cat << EOF
menuentry "Back..." {
  configfile /boot/grub/advanced.cfg
}

menu_title="\$base_menu_title - Binary Packages"

hook_tix_menu_pre

menuentry "Load all binary packages" {
  select_tix_set_all
  configfile /boot/grub/tix.cfg
}

hook_tix_menu_sets
EOF

for set in $sets; do
  echo
  printf 'menuentry "Load only '"$set"' binary packages" {\n'
  printf "  select_tix_set_%s\n" "$set"
  printf '  configfile /boot/grub/tix.cfg\n'
  printf '}\n'
done

cat << EOF

menuentry "Load no binary packages" {
  select_tix_set_no
  configfile /boot/grub/tix.cfg
}

hook_tix_menu

EOF

for port in $ports; do
  cat << EOF
if \$tix_$(portvar "$port"); then
  menuentry "$port = true" {
    tix_$(portvar "$port")=false
    configfile /boot/grub/tix.cfg
  }
else
  menuentry "$port = false" {
    tix_$(portvar "$port")=true
    configfile /boot/grub/tix.cfg
  }
fi
EOF
done

cat << EOF

hook_tix_menu_post
EOF
