# -*- shell-script -*- functions for making spkg-install scripts a little easier to write,
# eliminating duplication.  All Sage helper functions begin with sdh_ (for
# Sage-distribution helper).  Consult the below documentation for the list of
# available helper functions.
#
# This documentation is also repeated in the Sage docs in
# src/doc/en/developer/packaging.rst, so if anything here changes, or
# if you add anything, please modify that file accordingly.
#
# - sdh_die MESSAGE
#
#    Exit the build script with the error code of the last command if it was
#    non-zero, or with 1 otherwise, and print an error message.
#    Typically used like:
#
#        command || sdh_die "Command failed"
#
#    This function can also (if not given any arguments) read the error message
#    from stdin.  In particular this is useful in conjunction with a heredoc to
#    write multi-line error messages:
#
#        command || sdh_die << _EOF_
#        Command failed.
#        Reason given.
#        _EOF_
#
# - sdh_check_vars [VARIABLE ...]
#
#    Check that one or more variables are defined and non-empty, and exit with
#    an error if any are undefined or empty. Variable names should be given
#    without the '$' to prevent unwanted expansion.
#
# - sdh_guard
#
#    Wrapper for `sdh_check_vars` that checks some common variables without
#    which many/most packages won't build correctly (SAGE_ROOT, SAGE_LOCAL,
#    SAGE_SHARE). This is important to prevent installation to unintended
#    locations.
#
# - sdh_configure [...]
#
#    Runs `./configure --prefix="$SAGE_LOCAL" --libdir="$SAGE_LOCAL/lib"`
#    --disable-static, (for autoconf'd projects with extra
#    --disable-maintainer-mode --disable-dependency-tracking) Additional
#    arguments to `./configure` may be given as arguments.
#
# - sdh_make [...]
#
#    Runs `$MAKE` with the default target.  Additional arguments to `make` may
#    be given as arguments.
#
# - sdh_make_install [...]
#
#    Runs `$MAKE install` with DESTDIR correctly set to a temporary install
#    directory, for staged installations.  Additional arguments to `make` may
#    be given as arguments.  If $SAGE_DESTDIR is not set then the command is
#    run with $SAGE_SUDO, if set.
#
# - sdh_pip_install [--no-deps] [--build-isolation] [...]
#
#    Builds a wheel using `pip wheel` with the given options [...], then installs
#    the wheel.  Unless the special option --build-isolation is given,
#    the wheel is built using the option --no-build-isolation.
#    If the special option --no-deps is given, it is passed to pip install.
#    If $SAGE_DESTDIR is not set then the command is run with $SAGE_SUDO, if
#    set.
#
# - sdh_pip_uninstall [...]
#
#    Runs `pip uninstall` with the given arguments.  If unsuccessful, it displays a warning.
#
# - eval sdh_prefix_args PREFIX [...]
#
#    Helper function for transforming build options so that they can be passed
#    through "pip".
#
# - sdh_cmake [...]
#
#    Runs `cmake` in the current directory with the given arguments, as well as
#    additional arguments passed to cmake (assuming packages are using the
#    GNUInstallDirs module) so that `CMAKE_INSTALL_PREFIX` and
#    `CMAKE_INSTALL_LIBDIR` are set correctly.
#
# - sdh_install [-T] SRC [SRC...] DEST
#
#    Copies one or more files or directories given as SRC (recursively in the
#    case of directories) into the destination directory DEST, while ensuring
#    that DEST and all its parent directories exist.  DEST should be a path
#    under $SAGE_LOCAL, generally.  For DESTDIR installs the $SAGE_DESTDIR path
#    is automatically prepended to the destination.
#
#    The -T option treats DEST as a normal file instead (e.g. for copying a
#    file to a different filename).  All directory components are still created
#    in this case.
#
# - sdh_preload_lib EXECUTABLE SONAME
#
#    (Linux only--no-op on other platforms.)  Check shared libraries loaded by
#    EXECUTABLE (may be a program or another library) for a library starting
#    with SONAME, and if found appends SONAME to the LD_PRELOAD environment
#    variable.  See https://trac.sagemath.org/24885.

set -o allexport


# Utility function to get the terminal width in columns
# Returns 80 by default if nothing else works
_sdh_cols() {
    local cols="${COLUMNS:-$(tput cols 2>/dev/null)}"
    if [ "$?" -ne 0 -o -z "$cols" ]; then
        # If we can't get the terminal width any other way just default to 80
        cols=80
    fi
    echo $cols
}


# Utility function to print a terminal-width horizontal rule using the given
# character (or '-' by default)
_sdh_hr() {
    local char="${1:--}"
    printf '%*s\n' $(_sdh_cols) '' | tr ' ' "${char}"
}


sdh_die() {
    local ret=$?
    local msg

    if [ $ret -eq 0 ]; then
        # Always return non-zero, but if the last command run returned non-zero
        # then return its exact error code
        ret=1
    fi

    if [ $# -gt 0 ]; then
        msg="$*"
    else
        msg="$(cat -)"
    fi

    _sdh_hr >&2 '*'
    echo "$msg" | fmt -s -w $(_sdh_cols) >&2
    _sdh_hr >&2 '*'
    exit $ret
}


sdh_check_vars() {
    while [ -n "$1" ]; do
        [ -n "$(eval "echo "\${${1}+isset}"")" ] || sdh_die << _EOF_
${1} undefined ... exiting
Maybe run 'sage --buildsh'?
_EOF_
        shift
    done
}


sdh_guard() {
    sdh_check_vars SAGE_ROOT SAGE_LOCAL SAGE_INST_LOCAL SAGE_SHARE
}


sdh_configure() {
    echo "Configuring $PKG_NAME"
    # Run all configure scripts with bash to work around bugs with
    # non-portable scripts.
    # See https://trac.sagemath.org/ticket/24491
    if [ -z "$CONFIG_SHELL" ]; then
        export CONFIG_SHELL=`command -v bash`
    fi
    if [ "$UNAME" = "CYGWIN" ]; then
        # TODO: To use --disable-static for all packages on Cygwin, need
        # #30814: Cygwin: Fix remaining packages to build shared libraries, using AM_LDFLAGS=-no-undefined
        DISABLE_STATIC=
    else
        DISABLE_STATIC=--disable-static
    fi
    ./configure --prefix="$SAGE_INST_LOCAL" --libdir="$SAGE_INST_LOCAL/lib" $DISABLE_STATIC --disable-maintainer-mode --disable-dependency-tracking "$@"
    if [ $? -ne 0 ]; then # perhaps it is a non-autoconf'd project
      ./configure --prefix="$SAGE_INST_LOCAL" --libdir="$SAGE_INST_LOCAL/lib" $DISABLE_STATIC "$@"
      if [ $? -ne 0 ]; then
        if [ -f "$(pwd)/config.log" ]; then
            sdh_die <<_EOF_
Error configuring $PKG_NAME
See the file
    $(pwd)/config.log
for details.
_EOF_
        fi
        sdh_die "Error configuring $PKG_NAME"
      fi
    fi
}


sdh_make() {
    echo "Building $PKG_NAME"
    ${MAKE:-make} "$@" || sdh_die "Error building $PKG_NAME"
}


sdh_make_check() {
    echo "Checking $PKG_NAME"
    ${MAKE:-make} check "$@" || sdh_die "Failures checking $PKG_NAME"
}

sdh_make_install() {
    echo "Installing $PKG_NAME"
    if [ -n "$SAGE_DESTDIR" ]; then
        local sudo=""
    else
        local sudo="$SAGE_SUDO"
    fi
    $sudo ${MAKE:-make} install DESTDIR="$SAGE_DESTDIR" "$@" || \
        sdh_die "Error installing $PKG_NAME"
}

sdh_setup_bdist_wheel() {
    # Trac #32046: Most uses of this function can be replaced by sdh_pip_install
    mkdir -p dist
    rm -f dist/*.whl
    BDIST_DIR="$(mktemp -d)"
    python3 setup.py --no-user-cfg \
         bdist_wheel --bdist-dir "$BDIST_DIR" \
         "$@" || sdh_die "Error building a wheel for $PKG_NAME"
}

sdh_prefix_args () {
    prefix="$1"
    shift
    while [ $# -gt 0 ]; do
        # Quoted quotes because the result is to be run through eval
        echo "$prefix" \"$1\"
        shift
    done
}

sdh_pip_install() {
    echo "Installing $PKG_NAME"
    mkdir -p dist
    rm -f dist/*.whl
    install_options=""
    # pip has --no-build-isolation but no flag that turns the default back on...
    build_isolation_option="--no-build-isolation --no-binary :all:"
    while [ $# -gt 0 ]; do
        case "$1" in
            --build-isolation)
                # If a package requests build isolation, we allow it to provision
                # its build environment using the stored wheels.
                # We pass --find-links and remove the --no-binary option.
                # The SPKG needs to declare "setuptools_wheel" as a dependency.
                build_isolation_option="--find-links=$SAGE_SPKG_WHEELS"
                ;;
            --no-deps)
                install_options="$install_options $1"
                ;;
            *)
                break
                ;;
        esac
        shift
    done
    python3 -m pip wheel --wheel-dir=dist --verbose --no-deps --no-index --isolated --ignore-requires-python $build_isolation_option "$@" || \
        sdh_die "Error building a wheel for $PKG_NAME"

    sdh_store_and_pip_install_wheel $install_options .
}

sdh_pip_editable_install() {
    echo "Installing $PKG_NAME (editable mode)"
    python3 -m pip install --verbose --no-deps --no-index --no-build-isolation --isolated --editable "$@" || \
        sdh_die "Error installing $PKG_NAME"
}

sdh_store_wheel() {
    if [ -n "$SAGE_DESTDIR" ]; then
        local sudo=""
    else
        local sudo="$SAGE_SUDO"
    fi
    if [ "$*" != "." ]; then
        sdh_die "Error: sdh_store_wheel requires . as only argument"
    fi
    wheel=""
    for w in dist/*.whl; do
        if [ -n "$wheel" ]; then
            sdh_die "Error: more than one wheel found after building"
        fi
        if [ -f "$w" ]; then
            wheel="$w"
        fi
    done
    if [ -z "$wheel" ]; then
        sdh_die "Error: no wheel found after building"
    fi

    mkdir -p "${SAGE_DESTDIR}${SAGE_SPKG_WHEELS}" && \
        $sudo mv "$wheel" "${SAGE_DESTDIR}${SAGE_SPKG_WHEELS}/" || \
        sdh_die "Error storing $wheel"
    wheel="${SAGE_DESTDIR}${SAGE_SPKG_WHEELS}/${wheel##*/}"
}

sdh_store_and_pip_install_wheel() {
    local pip_options=""
    while [ $# -gt 0 ]; do
        case $1 in
            -*) pip_options="$pip_options $1"
                ;;
            *)
                break
                ;;
        esac
        shift
    done
    sdh_store_wheel "$@"
    if [ -n "$SAGE_SUDO" ]; then
        # Trac #29585: Do the SAGE_DESTDIR staging of the wheel installation
        # ONLY if SAGE_SUDO is set (in that case, we still do the staging so
        # that we do not invoke pip as root).
        if [ -n "$SAGE_DESTDIR" ]; then
            # --no-warn-script-location: Suppress a warning caused by --root
            local sudo=""
            local root="--root=$SAGE_DESTDIR --no-warn-script-location"
        else
            # Trac #32361: Of course, this can only be done for normal packages,
            # whose installation goes through sage-spkg.
            # For script packages, we do have to invoke pip as root.
            local sudo="$SAGE_SUDO"
            local root=""
        fi
    else
        local sudo=""
        local root=""
    fi
    # Trac #32659: pip no longer reinstalls local wheels if the version is the same.
    # Because neither (1) applying patches nor (2) local changes (in the case
    # of sage-conf, sage-setup, etc.) bump the version number, we need to
    # override this behavior.  The pip install option --force-reinstall does too
    # much -- it also reinstalls all dependencies, which we do not want.
    wheel_basename="${wheel##*/}"
    distname="${wheel_basename%%-*}"
    $sudo sage-pip-uninstall "$distname"
    if [ $? -ne 0 ]; then
        echo "(ignoring error)" >&2
    fi
    $sudo sage-pip-install $root $pip_options "$wheel" || \
        sdh_die "Error installing ${wheel##*/}"
    if [ -n "${SAGE_PKG_DIR}" ]; then
        # Record name of installed distribution name for uninstallation.
        wheel=${wheel##*/}
        echo "${wheel%%-*}" >> ${SAGE_PKG_DIR}/spkg-piprm-requirements.txt
    fi
}

sdh_pip_uninstall() {
    sage-pip-uninstall "$@"
    if [ $? -ne 0 ]; then
        echo "Warning: pip exited with status $?" >&2
    fi
}

sdh_cmake() {
    echo "Configuring $PKG_NAME with cmake"
    cmake . -DCMAKE_INSTALL_PREFIX="${SAGE_INST_LOCAL}" \
            -DCMAKE_INSTALL_LIBDIR=lib \
            "$@"
    if [ $? -ne 0 ]; then
        if [ -f "$(pwd)/CMakeFiles/CMakeOutput.log" ]; then
            sdh_die <<_EOF_
Error configuring $PKG_NAME with cmake
See the file
    $(pwd)/CMakeFiles/CMakeOutput.log
for details.
_EOF_
        fi
        sdh_die "Error configuring $PKG_NAME with cmake"
    fi
}


sdh_install() {
    local T=0
    local src=()

    if [ "$1" = "-T" ]; then
        T=1
        shift
    fi

    while [ $# -gt 1 ]; do
        if [ ! \( -e "$1" -o -L "$1" \) ]; then
            sdh_die "Error: source file/directory $1 does not exist"
        fi
        src+=("$1")
        shift
    done

    local dest="$1"

    if [ -z "$src" ]; then
        sdh_die "Error: no source file(s) for sdh_install given"
    fi

    if [ -z "$dest" ]; then
        sdh_die "Error: destination for sdh_install not given"
    fi

    # Prefix SAGE_DESTDIR to the destination for DESTDIR installs
    dest="${SAGE_DESTDIR}$dest"

    if [ $T -eq 0 -a -e "$dest" -a ! -d "$dest" ]; then
        sdh_die "Error: destination $dest for sdh_install exists and is not a directory"
    fi

    local destdir="$dest"
    if [ $T -eq 1 ]; then
        destdir="$(dirname $dest)"
    fi

    if [ ! -d "$destdir" ]; then
        mkdir -p "$destdir" || exit $?
    fi

    for s in "${src[@]}"; do
        echo "$s -> $dest"
        cp -R -p "$s" "$dest" || exit $?
    done
}


sdh_preload_lib() {
    local executable="$1"
    local soname="$2"
    if [ "$UNAME" != "Linux" ]; then
        return 0
    fi

    local ldlibs="$(ldd $(which $executable))"
    if [ $? -ne 0 ]; then
        sdh_die "Could not get shared library dependencies for $executable"
    fi

    local lib=$(echo "$ldlibs" | sed -n 's/\s*'$soname'.* => \(.\+\) .*/\1/p')
    if [ -n "$lib" ]; then
        if [ -n "$LD_PRELOAD" ]; then
            export LD_PRELOAD="$LD_PRELOAD:$lib"
        else
            export LD_PRELOAD="$lib"
        fi
    fi
}

set +o allexport
