Files
Pmodules/Pmodules/modmanage.bash.in
T

748 lines
20 KiB
Plaintext
Executable File

#!@BASH@ --noprofile
PATH='/bin:/usr/bin'
unset CDPATH # unset CDPATH, otherwise 'cd' prints the directoy!
unset IFS # use default IFS
shopt -s nullglob
shopt -s extglob
# used for some output only
declare -r CMD='modmanage'
declare mydir=$(cd $(dirname "$0") && pwd)
declare prefix=$(dirname "${mydir}")
declare libdir="${prefix}/lib"
declare libexecdir="${prefix}/libexec"
declare -r bindir="${prefix}/bin"
source "${libdir}/libstd.bash"
source "${libdir}/libpmodules.bash"
_exit () {
std::die 1 "\nInterrupted..."
}
trap '_exit' INT TERM
_err () {
std::info "\nOops: got an error in function '${FUNCNAME[1]}', line ${BASH_LINENO[0]}"
std::die 1 "Aborting ..."
}
trap '_err' ERR
path="/bin:/usr/bin:${bindir}"
[[ $(uname -s) == 'Darwin' ]] && path+=":${libexecdir}"
std::def_cmds "${path}" 'chown' 'dirname' 'mkdir' 'rsync' 'rm' 'getopt' 'find' 'modulecmd'
unset mydir
unset prefix
unset libdir
unset libexecdir
# bindir we still need
declare PMODULES_VERSION='@PMODULES_VERSION@'
##############################################################################
#
# help [module|sub-command]
#
Subcommands[help]='help'
Options[help]='-o hHV\? -l version -l help'
Help[help]='
USAGE:
modmanage [switches] subcommand [subcommand-args]...
SWITCHES:
-h|-H|-?|--help this usage info
-V|--version modules version & configuration options
--debug enable debug output
--dry-run dry run
SUBCOMMANDS:
+ init [switches] TARGET_DIR
+ install [switches] module...
+ search [switches] [string|pattern]...
+ help [subcommand]
'
subcommand_help() {
local -r subcommand='help'
local -a args=()
while (( $# > 0 )); do
case $1 in
-h | -\? | -H | --help )
print_help "${subcommand}"
;;
-V | --version )
print_help 'version'
;;
-- )
shift 1
args+=( "$@" )
break
;;
* )
args+=( "$1" )
;;
esac
shift
done
for arg in "${args[@]}"; do
if [[ -n "${Help[${arg}]}" ]] ; then
print_help "${arg}"
else
std::die 1 "Unknown sub-command -- ${subcommand}"
fi
done
}
##############################################################################
#
# Derive the module installation path from the modulefile path.
# The passed modulefile must be absolute.
#
# Arguments:
# $1: absolute module file path
#
get_module_prefix() {
"${modulecmd}" bash show "$1" 2>&1 \
|awk '/_HOME |_PREFIX / {print $3; exit}'
}
##############################################################################
#
# Derive the module release-file path from the module file-path.
#
# Arguments:
# $1: module file-path
#
get_releasefile_name() {
echo "$(${dirname} "$1")/.release-$(basename "$1")"
}
##############################################################################
#
# Sync a module from one Pmodules environment to another:
# - sync module installation
# - sync modulefile
# - sync release file
#
# Arguments:
# $1: relative modulefile path (something like: Tools/gnuplot/5.0.0)
# $2: source prefix of Pmodule environment
# $3: target prefix of Pmodule environment
#
sync_module() {
local -r rel_modulefile="$1"
local -r src_root="$2"
local -r target_root="$3"
local -r src_prefix=$( get_module_prefix "${src_root}/${rel_modulefile}" )
local -r rel_prefix=${src_prefix#${src_root}/}
local -r target_prefix="${target_root}/${rel_prefix}"
# install/update module
if [[ ! -d "${target_prefix}" ]] || [[ "${force}" == 'yes' ]]; then
${mkdir} -p "${target_prefix}" || exit $?
${rsync} --links --perms --recursive --delete \
"${src_prefix}/" \
"${target_prefix}/" || exit $?
fi
# create modulefile direcrory and install modulefile
local -r src_modulefile="${src_root}/${rel_modulefile}"
local -r target_modulefile="${target_root}/${rel_modulefile}"
${mkdir} -p "$(${dirname} "${target_modulefile}")" || exit $?
if [[ -e "${src_modulefile}" ]]; then
${rsync} --links --perms --recursive \
"${src_modulefile}" "${target_modulefile}" || exit $?
fi
# install release-file
local -r rel_releasefile=$( get_releasefile_name "${rel_modulefile}" )
local -r src_releasefile="${src_root}/${rel_releasefile}"
local -r target_releasefile="${target_root}/${rel_releasefile}"
if [[ -e "${src_releasefile}" ]]; then
${rsync} --links --perms --recursive \
"${src_releasefile}" "${target_releasefile}" || exit $?
fi
}
##############################################################################
#
# initialize a new module environment
#
#
#
Subcommands[init]='init'
Options[init]='-o \?hfy -l src: -l user: -l help -l force -l yes'
Help[init]="
USAGE:
modmanage init [switches] TARGET_DIR
Initialize a new minimal Pmodule environment in TARGET_DIR.
A user must be specified with '--user=<USER>' if the
programm is executed as root.
SWITCHES:
--user <USER>
If this scripts runs with root privileges, a user name
ore ID must be specified.
--force|--yes|-f|-y
re-initialise an already existing Pmodule environment.
"
subcommand_init() {
check_env() {
[[ -n "${PMODULES_ROOT}" ]] &&
[[ -d "${PMODULES_ROOT}" ]] &&
[[ -n "${PMODULES_HOME}" ]] &&
[[ -n "${PMODULES_VERSION}" ]] || \
std::die 1 "
Error: the module environment you are going to use as source has not been
initialized properly!"
}
#.....................................................................
#
# Sync the Pmodules configuration and templates
#
# Arguments:
# $1: source prefix of Pmodule environment
# $2: target prefix of Pmodule environment
#
sync_config() {
src="$1/${PMODULES_CONFIG_DIR}/"
dst="$2/${PMODULES_CONFIG_DIR}/"
${rsync} --links --perms \
"${src}"/profile.{bash,csh,zsh} "${dst}" \
|| return $?
${rsync} --links --perms \
"${src}"/profile.{bash,csh,zsh}-"${PMODULES_VERSION}" "${dst}" \
|| return $?
${rsync} --links --perms \
"${src}/Pmodules.conf" "${dst}" \
|| return $?
${rsync} --links --perms \
"${src}/modbuild.conf" "${dst}" \
|| return $?
}
local target_root=()
local user=''
while (($# > 0)); do
case $1 in
-h | -H | -\? | --help | -help )
print_help "${subcommand}"
;;
--force | --yes | -f | -y )
force='yes'
;;
--user | --user=* )
if [[ "$1" == '--user' ]]; then
user="$2"
shift
else
user="${1#--*=}"
fi
;;
-- )
:
;;
* )
# assign and remove trailing slashes
target_root="${1%%*([\/])}"
;;
esac
shift
done
if [[ -z ${target_root} ]]; then
std::die 1 "Error: no target directory specified!"
fi
local -i euid=$(id -u)
if (( euid == 0 )); then
[[ -n "${user}" ]] || \
std::die 1 "Error: --user parameter is required!"
id -u "${user}" > /dev/null 2>&1 || \
std::die 1 "Error: Unable to retrieve user id of user '${user}'"
else
[[ -z "${user}" ]] || \
std::die 1 "Error: --user option is only allowed if running as root!"
fi
local src_root="$(std::get_abspath "${bindir}/../../../..")"
local config_file="${src_root}/${PMODULES_CONFIG_DIR}/profile.bash"
if [[ -r "${config_file}" ]]; then
source "${config_file}"
fi
check_env || \
std::die 1 "Giving up..."
echo "Creating a minimal Pmodule environment from the environment at"
echo " ${PMODULES_ROOT}"
echo "in"
echo " ${target_root}"
if [[ -d "${target_root}" ]] && [[ ${force} == no ]]; then
echo "Warning: ${target_root} already exists."
std::get_YN_answer \
"Do you really want to re-run the initialization? (y/N) " \
|| std::die 1 "Abort ..."
fi
force='yes'
echo "Creating target directory '${target_root}'..."
${mkdir} -p "${target_root}" || \
std::die 1 "Error: make directory failed!"
echo "Syncing configuration ..."
sync_config "${src_root}" \
"${target_root}" || \
std::die 1 "Error: configuration synchronization failed!"
echo "Syncing Pmodules ${PMODULES_VERSION} from '${src_root}' to '${target_root}'..."
sync_module "Tools/${PMODULES_MODULEFILES_DIR}/Pmodules/${PMODULES_VERSION}" \
"${src_root}" \
"${target_root}" || \
std::die 1 "Error: sync Pmodules failed!"
for d in "${src_root}"/*/${PMODULES_MODULEFILES_DIR}; do
${mkdir} -p "${target_root}/${d#${src_root}/}"
done
if [[ -n "${user}" ]]; then
echo "Changing user of new module environment to '${user}'..."
${chown} -R "${user}" "${target_root}" || \
std::die 1 "Error: changing owner failed!"
echo
fi
echo "SourceRoot=${src_root}" > "${target_root}/${PMODULES_CONFIG_DIR}/modmanage.conf"
echo "New minimal module environment created at '${target_root}'."
echo "To use this environment, execute"
echo " sudo ln -fs ${target_root} /opt/psi"
echo " source /opt/psi/${PMODULES_CONFIG_DIR}/profile.bash"
}
##############################################################################
#
# sub-command 'install'
#
# Arguments:
Subcommands[install]='install'
Options[install]='-o hf -l force -l with: -l help -l src: -l target:'
Help[install]='
USAGE:
modmanage install [switches] <module>...
Install modules
SWITCHES:
--force] | -f
Install module even it already exists
--src <src>
Install from module environment in <src>
--with <module>
Install module(s) in this sub-group only
<module_pattern>
Install modules matching given pattern
'
subcommand_install() {
local -r subcommand='install'
local opts=''
local -a with=()
local -a module_pattern=()
local src_root="${PMODULES_INSTALL_SOURCE}"
local target_root="${PMODULES_ROOT}"
local modulefile=''
local -A modules_to_install
local -A dependencies_to_install
local -A group_map
local -a modulepath=()
#......................................................................
#
set_initial_modulepath() {
local group
for group in "${!GroupDepths[@]}"; do
(( ${GroupDepths[${group}]} == 0 )) || continue
modulepath+=( "${src_root}/${group}/${PMODULES_MODULEFILES_DIR}" )
done
}
#......................................................................
#
create_group_map() {
#
# For the dependency resolution we need to know, whether a
# module - if loaded - adds a hierarchical group to MODULEPATH
# or not.
#
# Examples:
# Loading a compiler adds the hierarchical group for
# this compiler. The command
# module load gcc/10.3.0
# prepends
# <pmodules_root>/Compiler/modulefiles/gcc/10.3.0
# to MODULEPATH.
#
# The dependency files do not convey the information whether
# loading a module extends MODULEPATH or not. What we need to
# know is
# 1) does loading a specific module extends MODULEPATH?
# 2) if yes: what is the hierarchical group?
#
# Example:
# If we know that loading 'gcc/10.3.0' adds a directory
# in the hierarchical group 'Compiler' to MODULEPATH, we
# know that this directory is
# <pmodules_root>/Compiler/modulefiles/gcc/10.3.0
#
# This information we store in the dictionary 'group_map'.
# For concinience reasons we store the string 'src_root/group'.
# So, 'group_map' maps
# module/version -> src_root/group
#
# Example:
# group_map[gcc/10.3.0]="${src_root}/Compiler"
#
local group=''
for group in "${!GroupDepths[@]}"; do
(( ${GroupDepths[${group}]} > 0 )) || continue
local fname=''
while read fname; do
local -a parts=()
std::split_relpath parts "${fname}"
if (( ${#parts[@]} - 2 != ${GroupDepths[${group}]} )); then
std::warn "error in source group ${group}:"
std::warn "modulefile: ${fname}"
continue
fi
if [[ ${#parts[@]} < 4 ]]; then
echo "${group} ${parts[@]}"
fi
local key="${parts[-4]}/${parts[-3]}"
[[ -z "${group_map[${key}]}" ]] || continue
group_map[${key}]="${src_root}/${group}"
done < <(${find} -L "${src_root}/${group}/${PMODULES_MODULEFILES_DIR}" \
\( -type l -o -type f \) \
\! -name ".*" \
-printf "%P\n" \
)
done
}
#......................................................................
#
# Resolve dependencies to given module
#
# Arguments:
# $1 absolute module file name
#
# Notes:
# Following variables from the enclosing function are used:
# modulepath
# group_map (read-only)
#
resolve_dependencies () {
local -r modulefile="$1"
local -- prefix=$(get_module_prefix "${modulefile}")
local -a rdeps=()
local -- rdeps_file="${prefix}/.dependencies"
local -a ideps=()
local -- ideps_file="${prefix}/.install_dependencies"
if [[ -r "${rdeps_file}" ]]; then
mapfile -t rdeps < <(grep -v '^ *#' "${rdeps_file}" )
fi
if [[ -r "${ideps_file}" ]]; then
mapfile -t ideps < <(grep -v '^ *#' "${ideps_file}" )
fi
# loop over all dependecies
local dep
for dep in "${rdeps[@]}" "${ideps}"; do
[[ -n ${dep} ]] || continue
# search module with current modulepath
local modulename=$(${find} "${modulepath[@]}" -path "*/${dep}" \
-print -quit 2>/dev/null)
[[ -n ${modulename} ]] || \
std::die 3 "Oops: required module '${dep}' not found!"
local rel_modulename="${modulename#${src_root}/}"
dependencies_to_install[${rel_modulename}]='.'
resolve_dependencies "${modulename}"
[[ -v group_map[${dep}] ]] || continue
local dir="${group_map[${dep}]}" # = ${src_root}/<group>
dir+="/${rel_modulename##+([!/])/}" # += rel.name with group removed
modulepath+=( "${dir}" )
done
}
#......................................................................
#
# Print list of modules which will be installed and ask user wheter
# he wants to continue or abort.
#
# Arguments:
# none
#
# Notes:
# Following variables from the enclosing function are used:
# target_root (read-only)
# modules_to_install (read-only)
# dependencies_to_install (read-only)
#
print_modules() {
local modulefile
for modulefile in "$@"; do
local -a parts
std::split_relpath parts "${modulefile}"
local s=''
if (( ${#parts[@]} >= 6 )); then
s="(${parts[2]}/${parts[3]}"
for ((i = 4; i < ${#parts[@]}-2; i+=2)); do
s+=" ${parts[i]}/${parts[i+1]}"
done
s+=')'
fi
std::info "%-20s %s" "${parts[-2]}/${parts[-1]}" "$s"
done 2>&1 | sort
}
print_modules_to_install() {
std::info "The following modules will be installed/updated:"
print_modules "${!modules_to_install[@]}"
if (( ${#dependencies_to_install[@]} > 0 )); then
std::info "\nThe following dependencies will be installed/updated:"
print_modules "${!dependencies_to_install[@]}"
fi
std::info ""
std::get_YN_answer "Do you want to continue? [n] " || \
std::die 1 "Aborting..."
std::info ""
}
while (($# > 0)); do
case $1 in
-h | -H | -\? | --help | -help )
print_help "${subcommand}"
;;
--force | -f )
force='yes'
;;
--src | --src=*)
if [[ $1 == --src ]]; then
src_root="$2"
shift
else
src_root="${1#--*=}"
fi
;;
--target | --target=*)
if [[ $1 == --target ]]; then
target_root="$2"
shift
else
target_root="${1#--*=}"
fi
;;
--with | --with=* )
if [[ "$1" == --with ]]; then
with+=( "$2" )
shift
else
with+=( "${1#--*=}" )
fi
;;
-- )
:
;;
* )
module_pattern+=( "$1" )
;;
esac
shift
done
if [[ -z ${src_root} ]]; then
local conf_file="${PMODULES_ROOT}/${PMODULES_CONFIG_DIR}/modmanage.conf"
if [[ -r ${conf_file} ]]; then
source "${conf_file}"
src_root="${SourceRoot}"
fi
fi
[[ -n ${src_root} ]] \
|| std::die 3 "Oops: no installation source given."
[[ -d ${src_root} ]] \
|| std::die 3 "Oops: '${src_root}' is not a valid installation source."
source "${src_root}/${PMODULES_CONFIG_DIR}/profile.bash"
scan_groups "${src_root}"
set_initial_modulepath
create_group_map
# search for to be installed modules and their dependencies
while read modulefile; do
modules_to_install["${modulefile#${src_root}/}"]+='.'
resolve_dependencies "${modulefile}"
done < <("${modulecmd}" bash search \
"${module_pattern[@]}" \
"${with[@]/#/--with=}" \
-a --glob \
--no-header --print-modulefiles \
--src="${src_root}" 2>&1 1>/dev/null)
(( ${#modules_to_install[@]} == 0 )) && \
std::die 0 "No matching modules found ..."
print_modules_to_install
# install/update ...
for modulefile in "${!modules_to_install[@]}" "${!dependencies_to_install[@]}"; do
std::split_relpath parts "${modulefile}"
std::info " ${parts[-2]}/${parts[-1]}"
sync_module "${modulefile}" "${src_root}" "${target_root}"
done
std::info "\nDone!\n"
} # subcommand_install
##############################################################################
#
# sub-command 'search'
#
Subcommands[search]='search'
Options[search]='-o \?h -l with: -l help -l all-dep -l wrap -l glob -l src:'
Help[install]='
USAGE:
modmanage search [switches] <string>...
search modules
SWITCHES:
--src <src>
Search modules in environment <src>.
Default is the source defined in modmanage.conf.
--with <module>
Search module(s) in this sub-group.
<string>
Search modules matching given string.
<pattern>
Search modules matching given shell glob-pattern.
'
subcommand_search() {
local -a args=()
while (($# > 0)); do
case $1 in
-h | -H | -\? | --help | -help )
print_help "${subcommand}"
;;
--src | --src=*)
if [[ $1 == --src ]]; then
src_root="$2"
shift
else
src_root="${1#--*=}"
fi
;;
--with | --with=* )
if [[ "$1" == --with ]]; then
args+=( '--with' "$2" )
shift
else
args+=( "$1" )
fi
;;
--all-deps | --glob | --wrap )
args+=( "$1" )
;;
-- )
:
;;
* )
args+=( "$1" )
;;
esac
shift
done
if [[ -z ${src_root} ]]; then
local conf_file="${PMODULES_ROOT}/${PMODULES_CONFIG_DIR}/modmanage.conf"
if [[ -r ${conf_file} ]]; then
source "${conf_file}"
src_root="${SourceRoot}"
fi
fi
[[ -n ${src_root} ]] \
|| std::die 3 "Oops: no installation source given."
[[ -d ${src_root} ]] \
|| std::die 3 "Oops: '${src_root}' is not a valid installation source."
${modulecmd} bash search --src="${src_root}" --all-release-stages \
"${args[@]}" 2>&1 1>/dev/null
}
declare force='no'
declare subcommand=''
declare -a opts=()
while (($# > 0)); do
case $1 in
-h | -H | -\? | --help | -help )
print_help 'help'
;;
-V | --version )
print_help 'version'
;;
--debug )
set -x
;;
--dry-run )
chown="echo ${chown}"
mkdir="echo ${mkdir}"
rsync="echo ${rsync}"
;;
-* )
opts+=( "$1" )
;;
* )
subcommand="$1"
shift
break
;;
esac
shift || :
done
if [[ -z "${subcommand}" ]]; then
std::die 1 "${CMD}: no sub-command specified.\n"
print_help 'help'
fi
if [[ -z "${Subcommands[${subcommand}]}" ]]; then
std::die 1 "${CMD}: unknown sub-command -- ${subcommand}\n"
fi
if [[ "${subcommand}" != "init" ]] && [[ -z "${PMODULES_ROOT}" ]]; then
std::die 1 "Error: No current module environment configured!"
fi
tmp=$("${getopt}" --name="${CMD}" ${Options[${subcommand}]} -- "${opts[@]}" "$@" ) \
|| print_help "${subcommand}"
eval args=( "$tmp" )
unset tmp
umask 022
subcommand_${Subcommands[$subcommand]} "${args[@]}"
# Local Variables:
# mode: sh
# sh-basic-offset: 8
# tab-width: 8
# End: