cp2k/tools/toolchain/scripts/tool_kit.sh
2026-07-01 17:46:21 +02:00

748 lines
23 KiB
Bash

# A set of tools used in the toolchain installer, intended to be used
# by sourcing this file inside other scripts.
# TODO: Review and if possible fix shellcheck errors.
# shellcheck disable=all
# shellcheck shell=bash
SYS_INCLUDE_PATH=${SYS_INCLUDE_PATH:-"/usr/local/include:/usr/include"}
SYS_LIB_PATH=${SYS_LIB_PATH:-"/usr/local/lib64:/usr/local/lib:/usr/lib64:/usr/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/aarch64-linux-gnu:/lib64:/lib"}
INCLUDE_PATHS=${INCLUDE_PATHS:-"CPATH SYS_INCLUDE_PATH"}
LIB_PATHS=${LIB_PATHS:-"LIBRARY_PATH LD_LIBRARY_PATH LD_RUN_PATH SYS_LIB_PATH"}
time_start=$(date +%s)
# report timing
report_timing() {
time_stop=$(date +%s)
printf "Step %s took %0.2f seconds.\n" "$1" $((time_stop - time_start))
}
# report a warning message with script name and line number
report_warning() {
if [ $# -gt 1 ]; then
local __lineno=", line $1"
local __message="$2"
else
local __lineno=''
local __message="$1"
fi
echo "WARNING: (${SCRIPT_NAME}${__lineno}) $__message" >&2
}
# report an error message with script name and line number
report_error() {
if [ $# -gt 1 ]; then
local __lineno=", line $1"
local __message="$2"
else
local __lineno=''
local __message="$1"
fi
echo "ERROR: (${SCRIPT_NAME}${__lineno}) $__message" >&2
return 1
}
# error handler for line trap from set -e
error_handler() {
report_error "$1" "Non-zero exit code detected."
}
# source a file if it exists, otherwise do nothing
load() {
if [ -f "$1" ]; then
source "$1"
fi
}
# Take excerpt of ${LOG_LINES} lines from tail of a file, always reporting
# its absolute path at the top, and then exit with non-zero code
tail_excerpt() {
local __filename=$(real_path "$1")
if [ -n "${LOG_LINES}" ]; then
local __lines="${LOG_LINES}"
elif [ $# -gt 1 ]; then
local __lines="$2"
fi
tail -v -n "${__lines}" "${__filename}"
exit 1
}
# A more portable command that will give the full path, removing
# symlinks, of a given path. This is more portable than readlink -f
# which does not work on Mac OS X
# Former name is "realpath", renamed to avoid accidentally overriding
# the native GNU coreutils command when this script is sourced
# https://www.gnu.org/software/coreutils/manual/html_node/realpath-invocation.html
real_path() {
local __path="$1"
if [ "x$__path" = x ]; then
return 0
fi
local __basename=$(basename "$__path")
if [ -e "$__path" ]; then
echo $(
cd "$(dirname "$__path")"
pwd -P
)/"$__basename"
return 0
else
return 1
fi
}
# given a list, outputs a list with duplicated items filtered out
unique() (
# given a list, outputs a list with duplicated items filtered out.
# If -d <delimiter> option exists, then output the list delimited
# by <delimiter>; note that this option does not effect the input.
local __result=''
local __delimiter=' '
local __item=''
if [ "$1" = "-d" ]; then
shift
__delimiter="$1"
shift
fi
# It is essential that we quote $@, which makes it equivalent to
# "$1" "$2" ... So this works if any of the arguments contains
# space. And we use \n to separate the fields in the
# __result for now, so that fields that contain spaces are
# correctly grepped.
for __item in "$@"; do
if [ x"$__result" = x ]; then
__result="${__item}"
# Note that quoting $__result after echo is essential to
# retain the \n in the variable from the output of echo. Also
# remember grep only works on a line by line basis, so if
# items are delimited by newlines, then for grep search it
# should be delimited by ^ and $ (beginning and end of line)
elif ! (echo "$__result" |
grep -s -q -e "^$__item\$"); then
__result="${__result}
${__item}"
fi
done
__result="$(echo "$__result" | paste -s -d "$__delimiter" -)"
# quoting $__result below is again essential for correct
# behaviour if IFS is set to be the same $__delimiter in the
# parent shell calling this macro
echo "$__result"
)
# reverse a list
reverse() (
# given a list, output a list with reversed order. If -d
# <delimiter> option exists, then output the list delimited by
# <delimiter>; note that this option does not effect the input.
local __result=''
local __delimiter=' '
local __item=''
if [ "$1" = "-d" ]; then
shift
__delimiter="$1"
shift
fi
for __item in "$@"; do
if [ x"$__result" = x ]; then
__result="$__item"
else
__result="${__item}${__delimiter}${__result}"
fi
done
echo "$__result"
)
# get the number of processes available for compilation
get_nprocs() {
if [ -n "${NPROCS_OVERWRITE}" ]; then
echo ${NPROCS_OVERWRITE} | sed 's/^0*//'
elif $(command -v nproc > /dev/null 2>&1); then
echo $(nproc --all)
elif $(command -v sysctl > /dev/null 2>&1); then
echo $(sysctl -n hw.ncpu)
else
echo 1
fi
}
# convert a list of paths to -L<dir> ... used by ld
paths_to_ld() {
# need to define the POSIX default IFS values here, cannot just do
# __ifs=$IFS first, because IFS can be unset, and if so __ifs will
# becomes an empty string (null) and NOT unset, so later when IFS
# is set to __ifs it becomes null rather than unset, and thus
# causing wrong behaviour. So if IFS is unset, __ifs should be
# the POSIX default value. Further more, due to shell
# automatically remove the tailing "\n" in a string during
# variable assignment, we need to add x after \n and then remove
# it.
local __paths=$@
local __name=''
local __raw_path=''
local __dir=''
local __lib_dirs=''
# set default IFS first
local __ifs=$(printf " \t\nx")
__ifs="${__ifs%x}"
[ "$IFS" ] && __ifs="$IFS"
for __name in $__paths; do
eval __raw_path=\$"$__name"
# change internal field separator to :
IFS=':'
# loop over all dirs in path, and filter out duplications
for __dir in $__raw_path; do
if ! [ x"$__dir" = x ]; then
if ! [[ "$__lib_dirs" =~ (^|[[:space:]])"-L'$__dir'"($|[[:space:]]) ]]; then
__lib_dirs="$__lib_dirs -L'$__dir'"
fi
fi
done
IFS="$__ifs"
done
echo $__lib_dirs
}
# Find a file from directories given in a list of paths, each has the
# same format as env variable PATH. If the file is found, then echoes
# the full path of the file. If the file is not found, then echoes
# __FALSE__. The file name can also contain wildcards that are
# acceptable for bash, and in that case the full path of the first
# matching file will be echoed.
find_in_paths() {
local __target=$1
shift
local __paths=$@
local __name=''
local __raw_path=''
local __dir=''
local __file=''
local __files=''
# use the IFS variable to take care of possible spaces in file/dir names
local __ifs="$(printf " \t\nx")"
__ifs="${__ifs%x}"
[ "$IFS" ] && __ifs="$IFS"
for __name in $__paths; do
eval __raw_path=\$"$__name"
# fields in paths are separated by :
IFS=':'
for __dir in $__raw_path; do
# files in possible glob expansion are to be delimited by "\n\b"
IFS="$(printf "\nx")"
IFS="${IFS%x}"
for __file in $__dir/$__target; do
if [ -e "$__file" ]; then
echo $(real_path "$__file")
# must remember to change IFS back when exiting
IFS="$__ifs"
return 0
fi
done
IFS=':'
done
IFS=$__ifs
done
echo "__FALSE__"
}
# search through a list of given paths, try to find the required file
# or directory, and if found then add full path of dirname file, or
# directory, to the -I include list for CFLAGS and append to a user
# specified variable (__cflags_name). If not found, then nothing is
# done. If the option -p is present, then if the search target is a
# directory, then the parent directory of the directory is used for -I
# instead. The search target accepts bash wildcards, and in this case
# the first match will be used.
add_include_from_paths() {
local __parent_dir_only=false
if [ $1 = "-p" ]; then
__parent_dir_only=true
shift
fi
local __cflags_name=$1
shift
local __search_target=$1
shift
local __paths=$@
local __found_target=""
local __cflags=""
__found_target="$(find_in_paths "$__search_target" \
$__paths)"
if [ "$__found_target" != "__FALSE__" ]; then
if [ -f "$__found_target" ] || $__parent_dir_only; then
__found_target="$(dirname "$__found_target")"
fi
echo "Found include directory $__found_target"
eval __cflags=\$"${__cflags_name}"
__cflags="${__cflags} -I${__found_target}"
# remove possible duplicates
__cflags="$(unique $__cflags)"
# must escape all quotes again before the last eval, as
# otherwise all quotes gets interpreted by the shell when
# assigning to variable because eval will reduce one escape
# level
__cflags="${__cflags//'/\\'/}"
eval $__cflags_name=\"$__cflags\"
fi
}
# search through a list of given paths, try to find the required file
# or directory, and if found then add full path of dirname file, or
# directory, to the -L library list (including -Wl,-rpath) for LDFLAGS
# and append to a user specified variable (__ldflags_name). If not
# found, then nothing is done. If the option -p is present, then if
# the search target is a directory, then the parent directory of the
# directory is used for -L instead. The search target accepts bash
# wildcards, and in this case the first match will be used.
add_lib_from_paths() {
local __parent_dir_only=false
if [ $1 = "-p" ]; then
__parent_dir_only=true
shift
fi
local __ldflags_name=$1
shift
local __search_target=$1
shift
local __paths=$@
local __found_target=""
local __ldflags=""
__found_target="$(find_in_paths "$__search_target" \
$__paths)"
if [ "$__found_target" != "__FALSE__" ]; then
if [ -f "$__found_target" ] || $__parent_dir_only; then
__found_target="$(dirname "$__found_target")"
fi
echo "Found lib directory $__found_target"
eval __ldflags=\$"${__ldflags_name}"
__ldflags="${__ldflags} -L${__found_target} -Wl,-rpath,${__found_target}"
# remove possible duplicates
__ldflags="$(unique $__ldflags)"
# must escape all quotes again before the last eval, as
# otherwise all quotes gets interpreted by the shell when
# assigning to variable because eval will reduce one escape
# level
__ldflags="${__ldflags//'/\\'/}"
eval $__ldflags_name=\"$__ldflags\"
fi
}
# check if environment variable is assigned and non-empty
# https://serverfault.com/questions/7503/how-to-determine-if-a-bash-variable-is-empty
require_env() {
local __env_var_name=$1
local __env_var="$(eval echo \"\$$__env_var_name\")"
if [ -z "${__env_var+set}" ]; then
report_error "requires environment variable $__env_var_name to work"
fi
}
# check if a command is available
check_command() {
local __command=${1}
if [ $# -eq 1 ]; then
local __package=${1}
elif [ $# -gt 1 ]; then
local __package=${2}
fi
if $(command -v ${__command} > /dev/null 2>&1); then
echo "path to ${__command} is $(real_path $(command -v ${__command}))"
else
report_error "Cannot find ${__command}, please check if the package ${__package} is installed or in system search path"
fi
}
# Check pkg-config package
check_pkgconfig() {
local pkg="$1"
if $(pkg-config --exists "${pkg}"); then
echo "pkg-config found ${pkg} under $(pkg-config --variable=prefix "${pkg}")"
else
report_error "Cannot find pkg-config package ${pkg}"
fi
}
# check if directory exists
check_dir() {
local __dir=$1
if [ -d "$__dir" ]; then
echo "Found directory $__dir"
else
report_error "Cannot find $__dir"
fi
}
# check if a command has been installed correctly
check_install() {
local __command=${1}
if [ $# -eq 1 ]; then
local __package=${1}
elif [ $# -gt 1 ]; then
local __package=${2}
fi
if $(command -v ${__command} > /dev/null 2>&1); then
echo "$(basename ${__command}) is installed as $(command -v ${__command})"
else
report_error "cannot find ${__command}, please check if the package ${__package} has been installed correctly"
fi
}
# check if a library can be found by ld, library names should in the
# format -lname, which would then referred to libname.a or libname.so
# by ld
check_lib() {
local __libname="${1#-l}"
if [ $# -eq 1 ]; then
local __package=lib"$__libname"
elif [ $# -gt 1 ]; then
local __package=$2
fi
# Note that LD_LIBRARY_PATH is NOT used by ld during linking
# stage, and is only used for searching to the shared libraries
# required by the executable AFTER it has already been compiled, to
# override its internal search paths built into the binary when it
# was compiled. Here, we explicitly include the commonly defined
# library search paths---including LD_LIBRARY_PATH---in the -L
# search paths of ld. This is the only way ld can include
# non-standard directories in its search path. If we use gcc
# instead of ld for linker then we can use LIBRARY_PATH, which IS
# used during link stage. However, I think using ld is more
# general, as in most systems LIBRARY_PATH is rarely defined, and
# we would have to rely on gcc.
local __search_engine="ld -o /dev/null"
local __search_paths="$LIB_PATHS"
# convert a list of paths to -L<dir> list used by ld
__search_engine="$__search_engine $(paths_to_ld $__search_paths)"
# needed the eval to interpret the quoted directories correctly (somehow)
if (eval $__search_engine -l$__libname 2>&1 | grep -q -s "\-l$__libname"); then
# if library not found, ld will return error message
# containing the library name
report_error \
"ld cannot find -l$__libname, please check if $__package is installed or in system search path"
else
# if library is found, then ld will return error message about
# not able to find _start or _main symbol
echo "lib$__libname is found in ld search path"
fi
}
# check if a module is available for the current version of gfortran,
# returns 0 if available and 1 if not
check_gfortran_module() {
local __module_name=$1
local __FC=${FC:-gfortran}
cat << EOF | $__FC -c -o /dev/null -xf95 -ffree-form - > /dev/null 2>&1
PROGRAM check_gfortran_module
USE ${__module_name}
IMPLICIT NONE
PRINT *, "PASS"
END PROGRAM check_gfortran_module
EOF
}
# check if a flag is allowed for the current version of
# gfortran. returns 0 if allowed and 1 if not
check_gfortran_flag() {
local __flag=$1
local __FC=${FC:-gfortran}
# no need to do a full compilation, just -E -cpp would do for
# checking flags
cat << EOF | $__FC -E -cpp $__flag -xf95 -ffree-form - > /dev/null 2>&1
PROGRAM test_code
IMPLICIT NONE
PRINT *, "PASS"
END PROGRAM test_code
EOF
}
# check if a flag is allowed for the current version of
# gcc. returns 0 if allowed and 1 if not
check_gcc_flag() {
local __flag=$1
local __CC=${CC:-gcc}
# no need to do a full compilation, just -E -cpp would do for
# checking flags
cat << EOF | $__CC -E -cpp $__flag -xc - > /dev/null 2>&1
#include <stdio.h>
int main() {
printf("PASS\n");
}
EOF
}
# check if a flag is allowed for the current version of
# g++. returns 0 if allowed and 1 if not
check_gxx_flag() {
local __flag=$1
local __CXX=${CXX:-g++}
# no need to do a full compilation, just -E -cpp would do for
# checking flags
cat << EOF | $__CXX -E -cpp $__flag -xc - > /dev/null 2>&1
#include <stdio.h>
int main() {
printf("PASS\n");
}
EOF
}
# given a list of flags, only print out what is allowed by the current
# version of gfortran
allowed_gfortran_flags() {
local __flags=$@
local __flag=''
local __result=''
for __flag in $__flags; do
if (check_gfortran_flag $__flag); then
[ -z "$__result" ] && __result="$__flag" || __result="$__result $__flag"
fi
done
echo $__result
}
# given a list of flags, only print out what is allowed by the current
# version of gcc
allowed_gcc_flags() {
local __flags=$@
local __flag=''
local __result=''
for __flag in $__flags; do
if (check_gcc_flag $__flag); then
[ -z "$__result" ] && __result="$__flag" || __result="$__result $__flag"
fi
done
echo $__result
}
# given a list of flags, only print out what is allowed by the current
# version of g++
allowed_gxx_flags() {
local __flags=$@
local __flag=''
local __result=''
for __flag in $__flags; do
if (check_gxx_flag $__flag); then
[ -z "$__result" ] && __result="$__flag" || __result="$__result $__flag"
fi
done
echo $__result
}
# remove a directory to a given path
remove_path() {
local __path_name=$1
local __directory=$2
local __path="$(eval echo \$$__path_name)"
# must remove all the middle ones first before treating two ends,
# otherwise there can be cases where not all __directory are
# removed.
__path=${__path//:$__directory:/:}
__path=${__path#$__directory:}
__path=${__path%:$__directory}
__path=$(echo "$__path" | sed "s#^$__directory\$##g")
eval $__path_name=\"$__path\"
export $__path_name
}
# prepend a directory to a given path
prepend_path() {
# prepend directory to $path_name and then export path_name. If
# the directory already exists in path, bring the directory to the
# front of the list.
# $1 is path name
# $2 is directory
remove_path "$1" "$2"
eval $1=\"$2\${$1:+\":\$$1\"}\"
eval export $1
}
# append a directory to a given path
append_path() {
# append directory to $path_name and then export path_name. If
# the directory already exists in path, bring the directory to the
# back of the list.
# $1 is path name
# $2 is directory
remove_path "$1" "$2"
eval $1=\"\${$1:+\"\$$1:\"}$2\"
eval export $1
}
# helper routine for reading --enable=* input options
read_enable() {
local __input_var="${1#*=}"
case $__input_var in
"$1")
# if there is no "=" then treat as "yes"
echo "__TRUE__"
;;
yes)
echo "__TRUE__"
;;
no)
echo "__FALSE__"
;;
*)
echo "__INVALID__"
;;
esac
}
# helper routine for reading --with=* input options
read_with() {
local __input_var="${1#--with*=}"
case $__input_var in
"${1}")
# if there is no "=" then treat as "install"
if [ ${#} -gt 1 ]; then
echo "${2}"
else
echo "__INSTALL__"
fi
;;
install)
echo "__INSTALL__"
;;
system)
echo "__SYSTEM__"
;;
no)
echo "__DONTUSE__"
;;
*)
echo "${__input_var//\~/$HOME}"
;;
esac
}
# get checksum command based on OS type, inspired by part of gcc package
# (gcc-14.3.0/contrib/download_prerequisites)
get_checksum_cmd() {
local __os=$(uname)
local __chksum="sha256sum"
case $__os in
"Darwin" | "FreeBSD" | "DragonFly" | "AIX")
__chksum='shasum -a 256'
;;
"OpenBSD")
__chksum='sha256'
;;
*)
__chksum='sha256sum'
;;
esac
echo "$__chksum"
}
# helper routine to check integrity of files
# intended to be used as condition test in an if-then-else construct, because
# no extra actions are taken for missing file or failed match in this form
# usage: checksum sha256 filename
checksum() {
local __sha256="$1"
local __filename="$2"
local __shasum_command=$(get_checksum_cmd)
echo "$__sha256 $__filename" | ${__shasum_command} --check
}
# downloader for the package tars, includes checksum
download_pkg_from_urlpath() {
# usage: download_pkg_from_url_path sha256 filename urlpath [outfile]
local __sha256="$1"
local __filename="$2"
local __url="$3/${__filename}"
local __outfile="${4:-${__filename}}"
local __command="wget ${DOWNLOADER_FLAGS} --quiet ${__url} -O ${__outfile}"
echo "${__command}"
# download
if ! eval "${__command}"; then
report_error "failed to download ${__url}"
fi
# checksum
if checksum "${__sha256}" "${__outfile}"; then
echo "Checksum of $__filename Ok"
else
rm -vf "${__outfile}"
report_error "Checksum of $__filename could not be verified, abort."
fi
}
# download from CP2K.org
download_pkg_from_cp2k_org() {
# usage: download_pkg_from_cp2k_org sha256 filename
download_pkg_from_urlpath "$1" "$2" https://www.cp2k.org/static/downloads
}
# retrieve package under current directory with filename and checksum verification
# if file exists and checksum is correct, only print a message
# if file exists but checksum is incorrect, delete and re-download from cp2k.org
# if file does not exist, download from cp2k.org
retrieve_package() {
local __sha256="$1"
local __filename="$2"
if ! [ -f "${__filename}" ]; then
download_pkg_from_cp2k_org "${__sha256}" "${__filename}"
else
if ! checksum "${__sha256}" "${__filename}"; then
echo "$__filename is found but checksum is wrong; delete and re-download"
rm -vf "${__filename}"
download_pkg_from_cp2k_org "${__sha256}" "${__filename}"
else
echo "$__filename is found and checksum is right"
fi
fi
}
# verify the checksums inside the given checksum file
verify_checksums() {
local __checksum_file=$1
local __shasum_command=$(get_checksum_cmd)
${__shasum_command} --check "${__checksum_file}" > /dev/null 2>&1
}
# write a checksum file $1 containing checksums for each given file $2, $3, ... (plus the $VERSION_FILE)
write_checksums() {
local __checksum_file=$1
shift # remove output file from arguments to be able to pass them along properly quoted
local __shasum_command=$(get_checksum_cmd)
${__shasum_command} "${VERSION_FILE}" "$@" > "${__checksum_file}"
}
# generate a filtered toolchain.env
write_toolchain_env() {
local __installdir=$1
# run the following in a subshell to not affect the currently running shell
# we do not need to achieve complete filtering, it is sufficient to
# remove problematic variables (TERM/TERMCAP/COLORTERM) which may trigger
# 'too many arguments' (since the environment vars are stored in the same memory block as command line arguments)
# or which may not be valid anymore the next time the user runs the toolchain scripts,
# like the proxy vars which may affect fetching tarballs
(
unset COLORTERM DISPLAY EDITOR LESS LESSOPEN LOGNAME LS_COLORS PAGER
unset TERM TERMCAP USER
unset ftp_proxy http_proxy no_proxy
unset GPG_AGENT_INFO SSH_AGENT_PID SSH_AUTH_SOCK SSH_CLIENT SSH_CONNECTION SSH_TTY
unset LS_COLORS LS_OPTIONS
unset STY WINDOW XAUTHORITY
unset XDG_CURRENT_DESKTOP XDG_RUNTIME_DIR XDG_SEAT XDG_SESSION_CLASS XDG_SESSION_DESKTOP XDG_SESSION_ID XDG_SESSION_TYPE XDG_VTNR XDG_CONFIG_DIRS XDG_DATA_DIRS
unset DBUS_SESSION_BUS_ADDRESS
export -p
) > "${__installdir}/toolchain.env"
}
# Write a setup file without containing flags unnecessay for building and running CP2K
filter_setup() {
local source_file="$1"
local target_file="$2"
# Check if setup_xxx file exists
if [[ ! -f "$source_file" ]]; then
report_error "File '$source_file' does not exist."
fi
local filename=$(basename "$source_file")
echo "# ==================== Setup for ${filename#*_} ==================== #" >> "$target_file"
grep -v -E 'CPATH|FLAGS|_LIBS|_INCLUDES' "$source_file" >> "$target_file"
}