# -*- 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