#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-or-later
#
# This file is part of nvme.
# Copyright (c) 2026 SUSE LLC
#
# Authors: Daniel Wagner <dwagner@suse.de>

set -e

usage() {
    echo "Usage: build.sh [-b [release|debug]] "
    echo "                [-c [gcc|clang]]"
    echo "                [-m [meson|muon]"
    echo "                [--e2e-controller /dev/nvme0]"
    echo "                [--e2e-ns1 /dev/nvme0n1]"
    echo "                [--e2e-log-dir dir]"
    echo "                [--e2e-log-level level]"
    echo "                [--e2e-nvme-bin path]"
    echo "                [--plugin-tests plugin1,plugin2]"
    echo "                [config]"
    echo ""
    echo "CI build script."
    echo ""
    echo " -a                   run static analysis with clang analyzer"
    echo " -b [release]|debug   build type"
    echo " -c [gcc]|clang       compiler to use"
    echo " -m [meson]|muon      use meson or muon"
    echo " -p                   enable coverage report"
    echo " -s                   run tests with ASan+UBSan (asanubsan setup)"
    echo " -t [arm]|ppc64le|s390x  cross compile target"
    echo " -x                   run tests with valgrind (valgrind setup)"
    echo ""
    echo "options for the 'tests' config (mirror the e2e-* meson options):"
    echo " --e2e-controller dev    controller device for e2e tests, e.g. /dev/nvme0"
    echo "                         (required when running the 'tests' config)"
    echo " --e2e-ns1 dev           namespace device for e2e tests, e.g. /dev/nvme0n1"
    echo "                         (required when running the 'tests' config)"
    echo " --e2e-log-dir dir       log directory for e2e tests"
    echo " --e2e-log-level level   log verbosity for e2e tests"
    echo " --e2e-nvme-bin path     nvme binary to exercise in e2e tests"
    echo " --plugin-tests list     comma-separated list of plugin test suites to"
    echo "                         run against real hardware, e.g. micron,ocp"
    echo ""
    echo "configs with meson:"
    echo "  [default]           default settings"
    echo "  libdbus             build with libdbus"
    echo "  liburing            build with liburing"
    echo "  fallback            download all dependencies"
    echo "                      and build them as shared libraries"
    echo "  cross               use cross toolchain to build"
    echo "  distro              build libnvme and nvme-cli separately"
    echo "  docs                build all documentation"
    echo "  man_docs            build man documentation only"
    echo "  html_docs           build html documentation only"
    echo "  rst_docs            build rst documentation only"
    echo "  static              build a static binary"
    echo "  minimal_static      build a static binary without fabrics support"
    echo "  nofabrics           build without fabrics support, run unit tests"
    echo "  libnvme             build only libnvme"
    echo "  tests               build for nightly build"
    echo ""
    echo "configs with muon:"
    echo "  [default]           minimal static build"
}

BUILDTOOL=meson
MESON=meson
BUILDTYPE=release
CROSS_TARGET=arm
CC=${CC:-"gcc"}
SCAN_BUILD=""

use_coverage=0
use_valgrind=0
use_asan=0
use_analyzer=0

E2E_CONTROLLER=""
E2E_NS1=""
E2E_LOG_DIR=""
E2E_LOG_LEVEL=""
E2E_NVME_BIN=""
PLUGIN_TESTS=""

ARGS=()
while [[ $# -gt 0 ]]; do
    case "$1" in
        -a)
            if ! command -v clang > /dev/null 2>&1; then
                echo "Error: clang is not found; please install clang."
                exit 1
            fi

            if ! command -v scan-build >/dev/null 2>&1; then
                echo "Error: scan-build is not found; please install clang-analyzer."
                exit 1
            fi

            use_analyzer=1
            CC=clang
            SCAN_BUILD=scan-build
            shift
            ;;
        -b)
            BUILDTYPE="$2"
            shift 2
            ;;
        -c)
            CC="$2"
            shift 2
            ;;
        -m)
            BUILDTOOL="$2"
            shift 2
            ;;
        -p)
            use_coverage=1
            shift
            ;;
        -s)
            use_asan=1
            shift
            ;;
        -t)
            CROSS_TARGET="$2"
            shift 2
            ;;
        -x)
            use_valgrind=1
            shift
            ;;
        --e2e-controller)
            E2E_CONTROLLER="$2"
            shift 2
            ;;
        --e2e-ns1)
            E2E_NS1="$2"
            shift 2
            ;;
        --e2e-log-dir)
            E2E_LOG_DIR="$2"
            shift 2
            ;;
        --e2e-log-level)
            E2E_LOG_LEVEL="$2"
            shift 2
            ;;
        --e2e-nvme-bin)
            E2E_NVME_BIN="$2"
            shift 2
            ;;
        --plugin-tests)
            PLUGIN_TESTS="$2"
            shift 2
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        -*)
            usage
            exit 1
            ;;
        *)
            ARGS+=("$1")
            shift
            ;;
    esac
done
set -- "${ARGS[@]}"

CONFIG=${1:-"default"}

if [[ "${CONFIG}" != "tests" ]]; then
    if [[ -n "${E2E_CONTROLLER}" || -n "${E2E_NS1}" || -n "${E2E_LOG_DIR}" || \
          -n "${E2E_LOG_LEVEL}" || -n "${E2E_NVME_BIN}" || -n "${PLUGIN_TESTS}" ]]; then
        echo "error: --e2e-* and --plugin-tests are only valid with the 'tests' config" >&2
        exit 1
    fi
fi

cd "$(git rev-parse --show-toplevel)" || exit 1

BUILDDIR="$(pwd)/.build-ci"
TOOLDIR="$(pwd)/.build-tools"

fn_exists() { declare -F "$1" > /dev/null; }

config_meson_default() {
    local extra_args=()
    if [ "${use_asan:-0}" -eq 1 ]; then
        extra_args+=(-Db_sanitize=address,undefined)
    fi

    CC="${CC}" ${SCAN_BUILD} "${MESON}" setup \
        --werror                                \
        --buildtype="${BUILDTYPE}"              \
		-Dioctl-tests=true						\
        "${extra_args[@]}"                      \
        "${BUILDDIR}"
}

config_meson_musl() {
	local cflags=(
		-U_GNU_SOURCE
		-idirafter /usr/include
		-idirafter /usr/include/x86_64-linux-gnu
	)
	local cflags_str="${cflags[*]}"

    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype="${BUILDTYPE}"              \
        -Dc_args="${cflags_str}"                \
        -Ddefault_library=static                \
        -Djson-c=disabled                       \
        -Dopenssl=disabled                      \
        -Dkeyutils=disabled                     \
        -Dpython=disabled                       \
        "${BUILDDIR}"
}

config_meson_libdbus() {
    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype="${BUILDTYPE}"              \
		-Ddeprecated-cmds=enabled				\
        -Dlibdbus=enabled                       \
        --prefix="${BUILDDIR}/usr"              \
        "${BUILDDIR}"
}

config_meson_liburing() {
    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype="${BUILDTYPE}"              \
        -Dliburing=enabled                      \
        --prefix="${BUILDDIR}/usr"              \
        "${BUILDDIR}"
}

config_meson_fallback() {
    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype="${BUILDTYPE}"              \
        --wrap-mode=forcefallback               \
        --default-library=both                  \
        -Dlibdbus=enabled                       \
        -Ddbus:werror=false                     \
        -Dopenssl:werror=false                  \
        "${BUILDDIR}"
}

config_meson_cross() {
    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype="${BUILDTYPE}"              \
        --cross-file=.github/cross/ubuntu-cross-${CROSS_TARGET}.txt \
        -Dpython=disabled               \
        -Dopenssl=disabled              \
        "${BUILDDIR}"
}

config_meson_docs() {
    CC="${CC}" "${MESON}" setup                 \
        -Ddocs=all                              \
        -Ddocs-build=true                       \
		-Ddeprecated-cmds=enabled				\
        --prefix=/tmp/usr                       \
        "${BUILDDIR}"
}

config_meson_man_docs() {
    CC="${CC}" "${MESON}" setup                 \
        -Ddocs=man                              \
        -Ddocs-build=true                       \
		-Ddeprecated-cmds=enabled				\
        --prefix=/tmp/usr                       \
        "${BUILDDIR}"
}

config_meson_html_docs() {
    CC="${CC}" "${MESON}" setup                 \
        -Ddocs=html                             \
        -Ddocs-build=true                       \
		-Ddeprecated-cmds=enabled				\
        --prefix=/tmp/usr                       \
        "${BUILDDIR}"
}

config_meson_rst_docs() {
    CC="${CC}" "${MESON}" setup                 \
        -Ddocs=rst                              \
        -Ddocs-build=true                       \
		-Ddeprecated-cmds=enabled				\
        --prefix=/tmp/usr                       \
        "${BUILDDIR}"
}

config_meson_static() {
    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype=release                     \
        --default-library=static                \
        --wrap-mode=forcefallback               \
        --prefix=/usr                           \
        -Dc_link_args="-static"                 \
        -Dkeyutils=disabled                     \
        -Dliburing=disabled                     \
        -Dpython=disabled                       \
        -Dopenssl=disabled                      \
        -Dtests=false                           \
        -Dexamples=false                        \
        "${BUILDDIR}"
}

config_meson_minimal_static() {
	local cflags=(
		-U_GNU_SOURCE
		-idirafter /usr/include
		-idirafter /usr/include/x86_64-linux-gnu
		-Oz
		-flto
		-ffunction-sections
		-fdata-sections
		-fno-unwind-tables
		-fno-asynchronous-unwind-tables
		-fno-stack-protector
	)
	local ldflags=(
		-flto
		-Wl,--gc-sections
		-s
		-Wl,--build-id=none
		-static
	)
	local cflags_str="${cflags[*]}"
	local ldflags_str="${ldflags[*]}"

	CC=musl-gcc

    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype=release                     \
        --default-library=static                \
        --prefix=/usr                           \
        -Dplugins="sed,lm,feat,zns,fdp"         \
        -Dc_args="${cflags_str}"                \
        -Dc_link_args="${ldflags_str}"          \
        -Dfabrics=disabled                      \
        -Dmi=disabled                           \
        -Djson-c=disabled                       \
        -Dopenssl=disabled                      \
        -Dtests=false                           \
        -Dexamples=false                        \
        "${BUILDDIR}"
}

config_meson_nofabrics() {
    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype="${BUILDTYPE}"              \
        -Dfabrics=disabled                      \
        -Dmi=disabled                           \
        -Dpython=disabled                       \
        -Djson-c=disabled                       \
        -Dlibkmod=disabled                      \
        -Dopenssl=disabled                      \
        -Dkeyutils=disabled                     \
        "${BUILDDIR}"
}

config_meson_tests() {
    # e2e tests are destructive and refuse to guess a target device, so
    # --e2e-controller/--e2e-ns1 must be passed on the command line (meson
    # setup errors out on its own if they're left unset). The remaining
    # options are optional and mirror the other e2e-*/plugin-tests meson
    # options.
    local extra_args=()
    [ -n "${E2E_CONTROLLER}" ] && extra_args+=(-De2e-controller="${E2E_CONTROLLER}")
    [ -n "${E2E_NS1}" ] && extra_args+=(-De2e-ns1="${E2E_NS1}")
    [ -n "${E2E_LOG_DIR}" ] && extra_args+=(-De2e-log-dir="${E2E_LOG_DIR}")
    [ -n "${E2E_LOG_LEVEL}" ] && extra_args+=(-De2e-log-level="${E2E_LOG_LEVEL}")
    [ -n "${E2E_NVME_BIN}" ] && extra_args+=(-De2e-nvme-bin="${E2E_NVME_BIN}")
    [ -n "${PLUGIN_TESTS}" ] && extra_args+=(-Dplugin-tests="${PLUGIN_TESTS}")

    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype="${BUILDTYPE}"              \
        -De2e-tests=true                        \
        "${extra_args[@]}"                      \
        "${BUILDDIR}"
}

config_meson_libnvme() {
    CC="${CC}" "${MESON}" setup                 \
        --werror                                \
        --buildtype="${BUILDTYPE}"              \
        -Dnvme=disabled                         \
        -Dlibnvme=enabled                       \
        "${BUILDDIR}"
}

build_meson() {
    if [ "${use_analyzer:-0}" -eq 1 ]; then
        "${SCAN_BUILD}" -o "${BUILDDIR}/scan-results" \
            -analyze-headers                          \
            --force-analyze-debug-code                \
            --status-bugs                             \
            "${MESON}" compile -C "${BUILDDIR}"
    else
        "${MESON}" compile                            \
            -C "${BUILDDIR}"
    fi
}

build_meson_docs() {
    "${MESON}" compile                          \
        -C "${BUILDDIR}"                        \
		docs
}

build_meson_man_docs() {
    "${MESON}" compile                          \
        -C "${BUILDDIR}"                        \
		docs
}

build_meson_html_docs() {
    "${MESON}" compile                          \
        -C "${BUILDDIR}"                        \
		docs
}

build_meson_rst_docs() {
    "${MESON}" compile                          \
        -C "${BUILDDIR}"                        \
		docs
}

test_meson() {
    local args=(-C "${BUILDDIR}")

    if [ "${use_valgrind:-0}" -eq 1 ]; then
        if command -v valgrind >/dev/null 2>&1; then
            args+=(--setup valgrind)
        else
            echo "Warning: valgrind requested but not found; running without it." >&2
        fi
    elif [ "${use_asan:-0}" -eq 1 ]; then
        args+=(--setup asanubsan)
    fi

    "${MESON}" test "${args[@]}"
}

test_meson_docs() {
	true
}

test_meson_man_docs() {
	true
}

test_meson_html_docs() {
	true
}

test_meson_rst_docs() {
	true
}

install_meson_docs() {
    "${MESON}" install                          \
        -C "${BUILDDIR}"
}

tools_build_samurai() {
    if [ ! -d "${TOOLDIR}"/samurai ]; then
        git clone --depth 1 https://github.com/michaelforney/samurai.git \
            "${TOOLDIR}/samurai"
    fi

    if [[ -f "${TOOLDIR}/samurai/samu" ]]; then
        return
    fi

    pushd "${TOOLDIR}/samurai" || exit 1
    CC="${CC}" make
    popd || exit 1
}

tools_build_muon() {
    if [ ! -d "${TOOLDIR}/muon" ]; then
        git clone --depth 1 https://git.sr.ht/~lattis/muon \
            "${TOOLDIR}/muon"
    fi

    if [[ -f "${TOOLDIR}/build-muon/muon" ]]; then
        return
    fi

    pushd "${TOOLDIR}/muon" || exit 1

    CC="${CC}" CFLAGS="${CFLAGS} -std=c99" ninja="${SAMU}" ./bootstrap.sh stage1

    CC="${CC}" ninja="${SAMU}" stage1/muon-bootstrap setup    \
        -Dprefix="${TOOLDIR}"                                 \
        -Dsamurai=disabled                                    \
        "${TOOLDIR}/build-muon"
    "${SAMU}" -C "${TOOLDIR}/build-muon"
    MUON="${BUILDDIR}/build-tools/.build-muon/muon"

    # "${TOOLDIR}/build-muon/muon" \
    #    -C "${TOOLDIR}/build-muon" test

    popd || exit 1
}

config_muon_default() {
    # wrap_mode=forcefallback depends on git being available

    CC="${CC}" CFLAGS="${CFLAGS}" ninja="${SAMU}"       \
        "${MUON}" setup                                 \
        -Ddefault_library=static                        \
        -Dc_link_args="-static"                         \
        -Dwrap_mode=forcefallback                       \
        -Djson-c=disabled                       \
        -Dpython=disabled                       \
        -Dopenssl=disabled                      \
        -Dkeyutils=disabled                     \
        -Djson-c=disabled                               \
        "${BUILDDIR}"
}

build_muon() {
    "${SAMU}" -C "${BUILDDIR}"
}

test_muon() {
    ninja="${SAMU}" "${MUON}" -C "${BUILDDIR}" test
    ldd "${BUILDDIR}/nvme" 2>&1 | grep 'not a dynamic executable' || exit 1
}

_install_libnvme() {
    local LBUILDDIR="${BUILDDIR}/.build-libnvme"

    CC="${CC}" "${MESON}" setup     \
        --prefix="${BUILDDIR}/usr"  \
        --buildtype="${BUILDTYPE}"  \
        -Dnvme=disabled             \
        "${LBUILDDIR}"

    "${MESON}" compile              \
        -C "${LBUILDDIR}"

    "${MESON}" install              \
        -C "${LBUILDDIR}"
}

config_meson_distro() {
    _install_libnvme

    PKG_CONFIG_PATH="${BUILDDIR}/usr/lib64/pkgconfig" \
    CC="${CC}" ${MESON} setup                         \
        --prefix="${BUILDDIR}/usr"                    \
        --werror                                      \
        --buildtype="${BUILDTYPE}"                    \
        --force-fallback-for=                         \
        -Dlibnvme=disabled                            \
        "${BUILDDIR}"
}

build_meson_distro() {
    build_meson
}

test_meson_distro() {
    test_meson
}

if [[ "${use_coverage}" -eq 1 && "${BUILDTOOL}" != "meson" ]]; then
     echo "error: coverage reporting (-p) is only supported with the meson build tool" >&2
     exit 1
fi

if [[ "${BUILDTOOL}" == "muon" ]]; then
    SAMU="$(which samu 2> /dev/null)" || true
    if [[ -z "${SAMU}" ]]; then
        tools_build_samurai
        SAMU="${TOOLDIR}/samurai/samu"
    fi

    MUON="$(which muon 2> /dev/null)" || true
    if [[ -z "${MUON}" ]]; then
        tools_build_muon
        MUON="${TOOLDIR}/build-muon/muon"
    fi
fi

echo "samu: ${SAMU}"
echo "muon: ${MUON}"

rm -rf "${BUILDDIR}"

config_"${BUILDTOOL}"_"${CONFIG}"
if [[ "${use_coverage}" -eq 1 ]]; then
    "${MESON}" setup --reconfigure "${BUILDDIR}" -Db_coverage=true
fi
fn_exists "build_${BUILDTOOL}_${CONFIG}" && "build_${BUILDTOOL}_${CONFIG}" || build_"${BUILDTOOL}"
fn_exists "test_${BUILDTOOL}_${CONFIG}" && "test_${BUILDTOOL}_${CONFIG}" || test_"${BUILDTOOL}"
if [[ "${use_coverage}" -eq 1 ]]; then
    gcovr -r . "${BUILDDIR}" --xml-pretty -o coverage.xml
fi
fn_exists "install_${BUILDTOOL}_${CONFIG}" && "install_${BUILDTOOL}_${CONFIG}" || true;
