Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/build/bin/sage-dist-helpers
4052 views
# -*- 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] [--no-build-isolation] [...]
#
#    Builds a wheel using `pip wheel` with the given options [...], then installs
#    the wheel.  Unless the special option --no-build-isolation is given,
#    the wheel is built using 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.
#
# - sdh_cmake [...]
#
#    Runs `cmake` with the given arguments, as well as additional arguments
#    (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://github.com/sagemath/sage/issues/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://github.com/sagemath/sage/issues/24491
    if [ -z "$CONFIG_SHELL" ]; then
        export CONFIG_SHELL=`command -v bash`
    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_build_wheel() {
    mkdir -p dist
    rm -f dist/*.whl
    export PIP_NO_INDEX=1
    install_options=""
    build_options=""
    # pip has --no-build-isolation but no flag that turns the default back on...
    build_isolation_option=""
    # build has --wheel but no flag that turns the default (build sdist and then wheel) back on
    dist_option="--wheel"
    export PIP_FIND_LINKS="$SAGE_SPKG_WHEELS"
    unset PIP_NO_BINARY
    while [ $# -gt 0 ]; do
        case "$1" in
            --build-isolation)
                # Our default after #33789 (Sage 9.7): We allow the package to provision
                # its build environment using the stored wheels.
                # We pass --find-links.
                # The SPKG needs to declare "setuptools" as a dependency.
                build_isolation_option=""
                export PIP_FIND_LINKS="$SAGE_SPKG_WHEELS"
                unset PIP_NO_BINARY
                ;;
            --no-build-isolation)
                # Use --no-binary, so that no wheels from caches are used.
                unset PIP_FIND_LINKS
                export PIP_NO_BINARY=:all:
                build_isolation_option="--no-isolation --skip-dependency-check"
                ;;
            --sdist-then-wheel)
                dist_option=""
                ;;
            --no-deps)
                install_options="$install_options $1"
                ;;
            -C|--config-settings)
                shift
                # Per 'python -m build --help', options which begin with a hyphen
                # must be in the form of "--config-setting=--opt(=value)" or "-C--opt(=value)"
                build_options="$build_options --config-setting=$1"
                ;;
            *)
                break
                ;;
        esac
        shift
    done
    if python3 -m build $dist_option --outdir=dist $build_isolation_option $build_options "$@"; then
        : # successful
    else
        case $build_isolation_option in
            *--no-isolation*)
                sdh_die "Error building a wheel for $PKG_NAME"
                ;;
            *)
                echo >&2 "Warning: building with \"python3 -m build $dist_option --outdir=dist $build_isolation_option $build_options $@\" failed."
                unset PIP_FIND_LINKS
                export PIP_NO_BINARY=:all:
                build_isolation_option="--no-isolation --skip-dependency-check"
                echo >&2 "Retrying with \"python3 -m build $dist_option --outdir=dist $build_isolation_option $build_options $@\"."
                if python3 -m build $dist_option --outdir=dist $build_isolation_option $build_options "$@"; then
                    echo >&2 "Warning: Wheel building needed to use \"$build_isolation_option\" to succeed. This means that a dependencies file in build/pkgs/ needs to be updated. Please report this to [email protected], including the build log of this package."
                else
                    sdh_die "Error building a wheel for $PKG_NAME"
                fi
                ;;
        esac
    fi
    unset PIP_FIND_LINKS
    unset PIP_NO_BINARY
    unset PIP_NO_INDEX
}

sdh_build_and_store_wheel() {
    sdh_build_wheel "$@"
    sdh_store_wheel .
}

sdh_pip_install() {
    echo "Installing $PKG_NAME"
    sdh_build_wheel "$@"
    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_SPKG_WHEELS}/${wheel##*/}"
    if [ -n "${SAGE_SPKG_SCRIPTS}" -a -n "${PKG_BASE}" ]; then
        wheel_basename="${wheel##*/}"
        distname="${wheel_basename%%-*}"
        # Record name and wheel file location
        if [ -n "$SAGE_DESTDIR" ]; then
            echo "Staged wheel file, staged ${SAGE_SPKG_SCRIPTS}/${PKG_BASE}/spkg-requirements.txt"
        else
            echo "Copied wheel file, wrote ${SAGE_SPKG_SCRIPTS}/${PKG_BASE}/spkg-requirements.txt"
        fi
        mkdir -p ${SAGE_DESTDIR}${SAGE_SPKG_SCRIPTS}/${PKG_BASE}
        echo "${distname} @ file://${wheel}" >> "${SAGE_DESTDIR}${SAGE_SPKG_SCRIPTS}/${PKG_BASE}/spkg-requirements.txt"
    fi
    wheel="${SAGE_DESTDIR}${wheel}"
}

sdh_store_and_pip_install_wheel() {
    # The $SAGE_PIP_INSTALL_FLAGS variable is set by sage-build-env-config.
    # We skip sanity checking its contents since you should either let sage
    # decide what it contains, or really know what you are doing.
    local pip_options="${SAGE_PIP_INSTALL_FLAGS}"

    while [ $# -gt 0 ]; do
        case $1 in
            -*) pip_options="$pip_options $1"
                ;;
            *)
                break
                ;;
        esac
        shift
    done
    sdh_store_wheel "$@"

    wheel_basename="${wheel##*/}"
    distname="${wheel_basename%%-*}"

    if [ -d "$SAGE_BUILD_DIR/$PKG_NAME" ]; then
        # Normal package install through sage-spkg;
        # scripts live in the package's build directory
        # until copied to the final destination by sage-spkg.
        script_dir="$SAGE_BUILD_DIR/$PKG_NAME"
    else
        script_dir="$SAGE_SPKG_SCRIPTS/$PKG_BASE"
    fi

    if [ -n "$SAGE_DESTDIR" -a -z "$SAGE_SUDO" ]; then
        # We stage the wheel file and do the actual installation in post.
        echo "sdh_actually_pip_install_wheel $distname $pip_options -r \"\$SAGE_SPKG_SCRIPTS/\$PKG_BASE/spkg-requirements.txt\"" >> "$script_dir"/spkg-pipinst
    else
        if [ -n "$SAGE_DESTDIR" ]; then
            # Issue #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).
            # --no-warn-script-location: Suppress a warning caused by --root
            local sudo=""
            local root="--root=$SAGE_DESTDIR --no-warn-script-location"
        elif [ -n "$SAGE_SUDO" ]; then
            # Issue #32361: For script packages, we do have to invoke pip as root.
            local sudo="$SAGE_SUDO"
            local root=""
        else
            #
            local sudo=""
            local root=""
        fi
        sdh_actually_pip_install_wheel $distname $root $pip_options "$wheel"
    fi
    echo "sdh_pip_uninstall -r \"\$SAGE_SPKG_SCRIPTS/\$PKG_BASE/spkg-requirements.txt\"" >> "$script_dir"/spkg-piprm
}

sdh_actually_pip_install_wheel() {
    distname=$1
    shift
    # Issue #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.
    $sudo sage-pip-uninstall "$distname" 2>&1 | sed '/^WARNING: Skipping .* as it is not installed./d'
    if [ $? -ne 0 ]; then
        echo "(ignoring error)" >&2
    fi
    $sudo sage-pip-install "$@" || \
        sdh_die "Error installing $distname"
}

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