#!/usr/bin/env bash

if [ -z "${BASH_VERSINFO[0]}" ] || [ "${BASH_VERSINFO[0]}" -lt 4 ] ; then
    echo "Error: Bash version 4 or newer is required for the kvm-unit-tests"
    exit 1
fi

# Return the default CPU type to compile for
function get_default_processor()
{
    local arch="$1"

    case "$arch" in
    "arm")
        echo "cortex-a15"
        ;;
    "arm64")
        echo "cortex-a57"
        ;;
    *)
        echo "$arch"
        ;;
    esac
}

# Return the default CPU type to run on
function get_default_qemu_cpu()
{
    local arch="$1"

    case "$arch" in
    "arm")
        echo "cortex-a15"
        ;;
    "arm64" | "riscv32" | "riscv64")
        echo "max"
        ;;
    esac
}

srcdir=$(cd "$(dirname "$0")"; pwd)
prefix=/usr/local
cc=gcc
cflags=
ld=ld
objcopy=objcopy
objdump=objdump
readelf=readelf
ar=ar
addr2line=addr2line
arch=$(uname -m | sed -e 's/i.86/i386/;s/arm64/aarch64/;s/arm.*/arm/;s/ppc64.*/ppc64/')
host=$arch
cross_prefix=
endian=""
pretty_print_stacks=yes
environ_default=yes
u32_long=
wa_divide=
target=
errata_force=0
erratatxt="$srcdir/errata.txt"
host_key_document=
gen_se_header=
enable_dump=no
page_size=
earlycon=
console=
efi=
efi_direct=
target_cpu=

# Enable -Werror by default for git repositories only (i.e. developer builds)
if [ -e "$srcdir"/.git ]; then
    werror=-Werror
else
    werror=
fi

usage() {
    [ "$arch" = "aarch64" ] && arch="arm64"
    [ -z "$processor" ] && processor=$(get_default_processor $arch)
    cat <<-EOF
	Usage: $0 [options]

	Options include:
	    --arch=ARCH            architecture to compile for ($arch). ARCH can be one of:
	                           arm, arm64, i386, ppc64, riscv32, riscv64, s390x, x86_64
	    --processor=PROCESSOR  processor to compile for ($processor)
	    --target-cpu=CPU       the CPU model to run on. If left unset, the run script
	                           selects the best value based on the host system and the
	                           test configuration.
	    --target=TARGET        target platform that the tests will be running on (qemu or
	                           kvmtool, default is qemu) (arm/arm64 and riscv32/riscv64 only)
	    --cross-prefix=PREFIX  cross compiler prefix
	    --cc=CC                c compiler to use ($cc)
	    --cflags=FLAGS         extra options to be passed to the c compiler
	    --ld=LD                ld linker to use ($ld)
	    --prefix=PREFIX        where to install things ($prefix)
	    --endian=ENDIAN        endianness to compile for (little or big, ppc64 only)
	    --[enable|disable]-pretty-print-stacks
	                           enable or disable pretty stack printing (enabled by default)
	    --[enable|disable]-default-environ
	                           enable or disable the generation of a default environ when
	                           no environ is provided by the user (enabled by default)
	    --erratatxt=FILE       specify a file to use instead of errata.txt. Use
	                           '--erratatxt=' to ensure no file is used.
	    --host-key-document=HOST_KEY_DOCUMENT
	                           Specify the machine-specific host-key document for creating
	                           a PVM image with 'genprotimg' (s390x only)
	    --gen-se-header=GEN_SE_HEADER
	                           Provide an executable to generate a PV header
	                           requires --host-key-document. (s390x-snippets only)
	    --[enable|disable]-dump
	                           Allow PV guests to be dumped. Requires at least z16.
	                           (s390x only)
	    --page-size=PAGE_SIZE
	                           Specify the page size (translation granule). PAGE_SIZE can be
	                           4k [default], 16k, 64k for arm64.
	                           4k [default], 64k for ppc64.
	    --earlycon=EARLYCON
	                           Specify the UART name, type and address used for the earlycon (optional).
	                           The specified address will overwrite the UART address set by
	                           the --target option. EARLYCON can be one of (case sensitive):
	               uart[8250],mmio,ADDR
	                           Specify an 8250 compatible UART at address ADDR. Supported
	                           register stride is 8 bit only.
	               pl011,ADDR
	               pl011,mmio32,ADDR
	                           Specify a PL011 compatible UART at address ADDR. Supported
	                           register stride is 32 bit only.
	                           (arm/arm64 and riscv32/riscv64 only)
	    --console=CONSOLE
	                           Specify the device used for output (optional).
	               sbi         Use SBI DBCN (riscv only)
	    --[enable|disable]-efi Boot and run from UEFI (disabled by default, x86_64 and arm64 only)
	    --[enable|disable]-werror
	                           Select whether to compile with the -Werror compiler flag
	    --[enable|disable]-efi-direct
	                           Select whether to run EFI tests directly with QEMU's -kernel
	                           option. When not enabled, tests will be placed in an EFI file
	                           system and run from the UEFI shell. Ignored when efi isn't enabled
	                           and defaults to enabled when efi is enabled for riscv64.
	                           (arm64 and riscv64 only)
EOF
    exit 1
}

optno=1
argc=$#
while [[ $optno -le $argc ]]; do
    opt="$1"; shift
    optno=$(( $optno + 1 ))
    arg=
    if [[ "$opt" = *=* ]]; then
	arg="${opt#*=}"
	opt="${opt%%=*}"
    fi
    case "$opt" in
	--prefix)
	    prefix="$arg"
	    ;;
        --arch)
	    arch="$arg"
	    ;;
        --processor)
	    processor="$arg"
	    ;;
	--target-cpu)
	    target_cpu="$arg"
	    ;;
	--target)
	    target="$arg"
	    ;;
	--cross-prefix)
	    cross_prefix="$arg"
	    ;;
	--endian)
	    endian="$arg"
	    ;;
	--cc)
	    cc="$arg"
	    cc_selected=yes
	    ;;
	--cflags)
	    cflags="$arg"
	    ;;
	--ld)
	    ld="$arg"
	    ;;
	--enable-pretty-print-stacks)
	    pretty_print_stacks=yes
	    ;;
	--disable-pretty-print-stacks)
	    pretty_print_stacks=no
	    ;;
	--enable-default-environ)
	    environ_default=yes
	    ;;
	--disable-default-environ)
	    environ_default=no
	    ;;
	--erratatxt)
	    erratatxt=
	    [ "$arg" ] && erratatxt=$(eval realpath "$arg")
	    ;;
	--host-key-document)
	    host_key_document="$arg"
	    ;;
	--gen-se-header)
	    gen_se_header="$arg"
	    ;;
	--enable-dump)
	    enable_dump=yes
	    ;;
	--disable-dump)
	    enable_dump=no
	    ;;
	--page-size)
	    page_size="$arg"
	    ;;
	--earlycon)
	    earlycon="$arg"
	    ;;
	--console)
	    console="$arg"
	    ;;
	--enable-efi)
	    efi=y
	    ;;
	--disable-efi)
	    efi=n
	    ;;
	--enable-efi-direct)
	    efi_direct=y
	    ;;
	--disable-efi-direct)
	    efi_direct=n
	    ;;
	--enable-werror)
	    werror=-Werror
	    ;;
	--disable-werror)
	    werror=
	    ;;
	--help)
	    usage
	    ;;
	*)
	    echo "Unknown option '$opt'"
	    echo
	    usage
	    ;;
    esac
done

if [ -z "$cc_selected" ] && [ "$cross_prefix" ]; then
    cc="$cross_prefix$cc"
fi

if [ -z "$efi" ] || [ "$efi" = "n" ]; then
    [ "$efi_direct" = "y" ] && efi_direct=
fi

if [ -n "$host_key_document" ] && [ ! -f "$host_key_document" ]; then
    echo "Host key document doesn't exist at the specified location."
    exit 1
fi

if [ "$erratatxt" ] && [ ! -f "$erratatxt" ]; then
    echo "erratatxt: $erratatxt does not exist or is not a regular file"
    exit 1
fi

arch_name=$arch
[ "$arch" = "aarch64" ] && arch="arm64"
[ "$arch" = "powerpc" ] && arch="ppc64"
[ "$arch_name" = "arm64" ] && arch_name="aarch64"

if [ "$arch" = "riscv" ]; then
    echo "riscv32 or riscv64 must be specified"
    exit 1
fi

if [ -z "$target" ]; then
    target="qemu"
else
    if [ "$arch" != "arm" ] && [ "$arch" != "arm64" ] &&
       [ "$arch" != "riscv32" ] && [ "$arch" != "riscv64" ]; then
        echo "--target is not supported for $arch"
        usage
    fi
fi

if [ "$efi" ] && [ "$arch" != "x86_64" ] &&
   [ "$arch" != "arm64" ] && [ "$arch" != "riscv64" ]; then
    echo "--[enable|disable]-efi is not supported for $arch"
    usage
fi

if [ "$efi" ] && [ "$arch" = "riscv64" ] && [ -z "$efi_direct" ]; then
    efi_direct=y
fi

if [ -z "$page_size" ]; then
    if [ "$arch" = "arm" ] || [ "$arch" = "arm64" ]; then
        page_size="4096"
    elif [ "$arch" = "ppc64" ]; then
        page_size="65536"
    fi
else
    if [ "${page_size: -1}" = "K" ] || [ "${page_size: -1}" = "k" ]; then
        page_size=$(( ${page_size%?} * 1024 ))
    fi

    if [ "$arch" = "arm64" ]; then
        if [ "$page_size" != "4096" ] && [ "$page_size" != "16384" ] &&
               [ "$page_size" != "65536" ]; then
            echo "arm64 doesn't support page size of $page_size"
            usage
        fi
        if [ "$efi" = 'y' ] && [ "$page_size" != "4096" ]; then
            echo "efi must use 4K pages"
            exit 1
        fi
    elif [ "$arch" = "ppc64" ]; then
        if [ "$page_size" != "4096" ] && [ "$page_size" != "65536" ]; then
            echo "ppc64 doesn't support page size of $page_size"
            usage
        fi
    else
        echo "--page-size is not supported for $arch"
        usage
    fi
fi

if [ "$earlycon" ]; then
    IFS=, read -r name type_addr addr <<<"$earlycon"
    if [ "$name" != "uart" ] && [ "$name" != "uart8250" ] && [ "$name" != "pl011" ]; then
        echo "unknown earlycon name: $name"
        usage
    fi

    if [ "$name" = "pl011" ]; then
        if [ -z "$addr" ]; then
            addr=$type_addr
        else
            if [ "$type_addr" != "mmio32" ]; then
                echo "unknown $name earlycon type: $type_addr"
                usage
            fi
        fi
    else
        if [ "$type_addr" != "mmio" ]; then
            echo "unknown $name earlycon type: $type_addr"
            usage
        fi
    fi

    if [ -z "$addr" ]; then
        echo "missing $name earlycon address"
        usage
    fi
    if [[ $addr =~ ^0(x|X)[0-9a-fA-F]+$ ]] || [[ $addr =~ ^[0-9]+$ ]]; then
        uart_early_addr=$addr
    else
        echo "invalid $name earlycon address: $addr"
        usage
    fi
fi

# $arch will have changed when cross-compiling.
[ -z "$processor" ] && processor=$(get_default_processor $arch)

if [ "$arch" = "i386" ] || [ "$arch" = "x86_64" ]; then
    testdir=x86
    arch_libdir=x86
elif [ "$arch" = "arm" ] || [ "$arch" = "arm64" ]; then
    testdir=arm
    arch_libdir=$arch
    if [ "$target" = "qemu" ]; then
        : "${uart_early_addr:=0x9000000}"
    elif [ "$target" = "kvmtool" ]; then
        : "${uart_early_addr:=0x1000000}"
    else
        echo "--target must be one of 'qemu' or 'kvmtool'!"
        usage
    fi
elif [ "$arch" = "ppc64" ]; then
    testdir=powerpc
    arch_libdir=powerpc
    firmware="$testdir/boot_rom.bin"
    if [ "$endian" != "little" ] && [ "$endian" != "big" ]; then
        echo "You must provide endianness (big or little)!"
        usage
    fi
elif [ "$arch" = "riscv32" ] || [ "$arch" = "riscv64" ]; then
    testdir=riscv
    arch_libdir=riscv
    : "${uart_early_addr:=0x10000000}"
    if [ "$target" != "qemu" ] && [ "$target" != "kvmtool" ]; then
        echo "--target must be one of 'qemu' or 'kvmtool'!"
        usage
    fi
elif [ "$arch" = "s390x" ]; then
    testdir=s390x
    arch_libdir=s390x
else
    echo "arch $arch is not supported!"
    arch=
    usage
fi
if [ ! -d "$srcdir/$testdir" ]; then
    echo "$srcdir/$testdir does not exist!"
    exit 1
fi
if [ ! -d "$srcdir/lib/$arch_libdir" ]; then
    echo "$srcdir/lib/$arch_libdir does not exist!"
    exit 1
fi

if [ "$efi" = "y" ] && [ -f "$srcdir/$testdir/efi/run" ]; then
    ln -fs "$srcdir/$testdir/efi/run" $testdir-run
elif [ -f "$srcdir/$testdir/run" ]; then
    ln -fs "$srcdir/$testdir/run" $testdir-run
fi

testsubdir=$testdir
if [ "$efi" = "y" ]; then
    testsubdir=$testdir/efi
fi

# check if uint32_t needs a long format modifier
cat << EOF > lib-test.c
__UINT32_TYPE__
EOF
u32_long=$("$cc" $cflags -E lib-test.c | grep -v '^#' | grep -q long && echo yes)
rm -f lib-test.c

# check if slash can be used for division
if [ "$arch" = "i386" ] || [ "$arch" = "x86_64" ]; then
  cat << EOF > lib-test.S
foo:
    movl (8 / 2), %eax
EOF
  wa_divide=$("$cc" $cflags -c lib-test.S >/dev/null 2>&1 || echo yes)
  rm -f lib-test.{o,S}
fi

# warn if enhanced getopt is unavailable
getopt -T > /dev/null
if [ $? -ne 4 ]; then
    echo "Without enhanced getopt you won't be able to use run_tests.sh."
    echo "Add it to your PATH?"
fi

# Are we in a separate build tree? If so, link the Makefile
# and shared stuff so that 'make' and run_tests.sh work.
if test ! -e Makefile; then
    echo "linking Makefile..."
    ln -s "$srcdir/Makefile" .

    echo "linking tests..."
    mkdir -p $testsubdir
    ln -sf "$srcdir/$testdir/run" $testdir/
    if test "$testdir" != "$testsubdir"; then
        ln -sf "$srcdir/$testsubdir/run" $testsubdir/
    fi
    ln -sf "$srcdir/$testdir/unittests.cfg" $testdir/
    ln -sf "$srcdir/run_tests.sh"

    if [ -d "$srcdir/$testdir/snippets" ]; then
        mkdir -p "$testdir/snippets/c"
    fi

    echo "linking scripts..."
    ln -sf "$srcdir/scripts"
fi

# link lib/asm for the architecture
rm -f lib/asm
asm="asm-generic"
if [ -d "$srcdir/lib/$arch_libdir/asm" ]; then
	asm="$srcdir/lib/$arch_libdir/asm"
	mkdir -p "lib/$arch_libdir"
else
	echo "$srcdir/lib/$arch_libdir/asm does not exist"
	exit 1
fi
ln -sf "$asm" lib/asm
mkdir -p lib/generated lib/libfdt

# create the config
cat <<EOF > config.mak
# Shellcheck does not see these are used
# shellcheck disable=SC2034
# Shellcheck can give pointless quoting warnings for some commands
# shellcheck disable=SC2209
SRCDIR=$srcdir
PREFIX=$prefix
HOST=$host
ARCH=$arch
ARCH_NAME=$arch_name
ARCH_LIBDIR=$arch_libdir
PROCESSOR=$processor
TARGET_CPU=$target_cpu
DEFAULT_QEMU_CPU=$(get_default_qemu_cpu $arch)
CC=$cc
CFLAGS=$cflags
LD=$cross_prefix$ld
OBJCOPY=$cross_prefix$objcopy
OBJDUMP=$cross_prefix$objdump
READELF=$cross_prefix$readelf
AR=$cross_prefix$ar
ADDR2LINE=$cross_prefix$addr2line
TEST_DIR=$testdir
TEST_SUBDIR=$testsubdir
FIRMWARE=$firmware
ENDIAN=$endian
PRETTY_PRINT_STACKS=$pretty_print_stacks
ENVIRON_DEFAULT=$environ_default
ERRATATXT=$erratatxt
U32_LONG_FMT=$u32_long
WA_DIVIDE=$wa_divide
GENPROTIMG=${GENPROTIMG-genprotimg}
HOST_KEY_DOCUMENT=$host_key_document
CONFIG_DUMP=$enable_dump
CONFIG_EFI=$efi
EFI_DIRECT=$efi_direct
CONFIG_WERROR=$werror
GEN_SE_HEADER=$gen_se_header
EOF
if [ "$arch" = "arm" ] || [ "$arch" = "arm64" ] ||
   [ "$arch" = "riscv32" ] || [ "$arch" = "riscv64" ]; then
    echo "TARGET=$target" >> config.mak
fi

cat <<EOF > lib/config.h
#ifndef _CONFIG_H_
#define _CONFIG_H_
/*
 * Generated file. DO NOT MODIFY.
 *
 */

EOF
if [ "$arch" = "arm" ] || [ "$arch" = "arm64" ]; then
cat <<EOF >> lib/config.h

#define CONFIG_UART_EARLY_BASE ${uart_early_addr}
#define CONFIG_ERRATA_FORCE ${errata_force}

EOF
fi

if [ "$arch" = "arm" ] || [ "$arch" = "arm64" ] || [ "$arch" = "ppc64" ]; then
cat <<EOF >> lib/config.h

#define CONFIG_PAGE_SIZE _AC(${page_size}, UL)

EOF
elif [ "$arch" = "riscv32" ] || [ "$arch" = "riscv64" ]; then
    echo "#define CONFIG_UART_EARLY_BASE ${uart_early_addr}" >> lib/config.h
    [ "$console" = "sbi" ] && echo "#define CONFIG_SBI_CONSOLE" >> lib/config.h
    echo >> lib/config.h
fi
echo "#endif" >> lib/config.h
