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