modmanage.in 15.1 KB
Newer Older
1
#!@PMODULES_HOME@/bin/bash
2

3 4
unset CDPATH

5
shopt -s expand_aliases
6

7 8
declare -r bindir=$(cd $(dirname "$0") && pwd)
declare -r libdir=$(cd "${bindir}/../lib" && pwd)
9

10 11
declare PMODULES_VERSION='@PMODULES_VERSION@'

12
source "${libdir}/libpmodules.bash"
13

gsell's avatar
gsell committed
14 15
PATH="${bindir}:${PATH}"

16 17
print_version() {
	echo "
18
Pmodules @PMODULES_VERSION@ using Tcl Environment Modules @MODULES_VERSION@
19 20
Copyright GNU GPL v2
" 1>&2
21 22
}

23 24 25 26 27 28 29 30 31 32 33
usage() {
	local -r prog=$(basename $0)
	print_version
	echo "
Usage: ${prog} [ switches ] [ subcommand ] [subcommand-args ]

Switches:
        --dry-run     do nothing
        --force       force overwrite

Available SubCommands and Args:
34
        init [--src=<src>] [--user=<user>] <dst>
35
                Initialize a new minimal Pmodule environment.
36 37 38

        install <module> [--with=<dep>...]
                Install matching modules
39 40 41

        sync [--delete] [--dst=<dst>] <src>
                Synchronize modules.
42 43
"
}
44

45 46

declare force='no'
47 48
declare dry_run='no'
declare DRY=''
49 50
declare subcommand=''
declare sargs=()
51

52 53
subcommand_help_init() {
	echo "
gsell's avatar
gsell committed
54
init [--src=<src>] [--user=<user>] [--version=<version>] <dst>
55 56 57 58 59
                Initialize a new minimal Pmodule environment in directory
                <dst>. The <user> parameter must only be present if
                 ${prog} is executed as root.
" 1>&2
}
60

61 62
subcommand_help_install() {
	echo "
63
install <module>... [--with=<dep>...] [--release=<release>...] [--src=<src>]
64 65 66 67
                Install matching modules
" 1>&2
}

68 69 70 71 72 73
subcommand_help_sync() {
	echo "
sync [--delete] [--dst=<dst>] <src>
                Synchronize environment modules and configuration files
                from Pmodule environment <src> to Pmodule environment <dst>
                (default: currently active Pmodule environment).
74
                Not yet implemented:
75 76 77 78 79
                If --delete is given, unmarked modules present in <dst>
                will be deleted.
" 1>&2
}

80 81 82 83 84 85 86 87 88
subcommand_help() {
	if [[ $# == 0 ]]; then
	        usage
	elif typeset -F subcommand_help_$1 > /dev/null 2>&1 ; then
	        # help for sub-command
		subcommand_help_$1
	else
		usage
	fi
89 90
}

91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
#
# Derive the relative module installation path
# from the relative modulefile path
#
# $1: relative module file path
#
get_module_prefix() {
	local -a comp=( ${1//\// } )    # split rel.path into components
	local path="${comp[0]}"		# result path
	local -i i
	for ((i=1; i<${#comp[@]}; i+=2)); do
		path+="/${comp[$((-i-1))]}/${comp[$((-i))]}"
	done
	echo "${path}"
}

#
# Derive the relative module release file path
# from the relative module file path
#
# $1: relative 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
#
gsell's avatar
gsell committed
123
# $1: relative modulefile path (something like: Tools/gnuplot/5.0.0)
124 125 126 127 128 129 130 131 132 133 134
# $2: source prefix of Pmodule environment
# $3: target prefix of Pmodule environment
#
sync_module() {
	local -r rel_modulefile=$1
	local -r src_prefix=$2
	local -r target_prefix=$3

	local -r rel_module_prefix=$( get_module_prefix "${rel_modulefile}" )
	local -r rel_releasefile=$( get_releasefile_name "${rel_modulefile}" )

135 136 137 138 139 140
	if [[ ! -d "${target_prefix}/${rel_module_prefix}" ]] || [[ "${force}" == 'yes' ]]; then
		$DRY mkdir -p "${target_prefix}/${rel_module_prefix}" || return $?
		$DRY rsync --links --perms --recursive --delete \
		      "${src_prefix}/${rel_module_prefix}/" \
		      "${target_prefix}/${rel_module_prefix}/" || return $?
	fi
141 142 143 144 145 146 147
	local -r src_modulefile="${src_prefix}/${PSI_MODULES_ROOT}/${rel_modulefile}"
	local -r src_releasefile="${src_prefix}/${PSI_MODULES_ROOT}/${rel_releasefile}"
	local -r target_modulefile="${target_prefix}/${PSI_MODULES_ROOT}/${rel_modulefile}"
	local -r target_releasefile="${target_prefix}/${PSI_MODULES_ROOT}/${rel_releasefile}"

	if [[ -e "${src_modulefile}" ]] || [[ -e "${src_releasefile}" ]]; then
	        local dir=$( dirname "${target_modulefile}" )
148
		$DRY mkdir -p "${dir}" || return $?
149 150 151 152 153 154 155 156 157
	fi
	if [[ -e "${src_modulefile}" ]]; then
	        $DRY rsync --links --perms --recursive \
		     "${src_modulefile}" "${target_modulefile}" || return $?
	fi
	if [[ -e "${src_releasefile}" ]]; then
	        $DRY rsync --links --perms --recursive \
		     "${src_releasefile}" "${target_releasefile}" || return $?
	fi
gsell's avatar
gsell committed
158 159 160 161 162 163 164 165 166
	if [[ -L "${src_modulefile}" ]]; then
	        local canonical_fname=$(readlink -f "${src_modulefile}")
		local src_template=$(dirname "${canonical_fname}")
		local modulename=$(basename "${src_template}")
		local target_template="${target_prefix}/${PSI_TEMPLATES_DIR}/${modulename}"
		$DRY rsync  --links --perms --recursive --delete \
		     "${src_template}/" \
		     "${target_template}"
	fi
167 168
}

169 170 171 172 173 174 175 176 177 178 179
#
# Sync the Pmodules configuration and templates
#
# $1: source prefix of Pmodule environment
# $2: target prefix of Pmodule environment
#
sync_config() {
	src="$1/${PSI_CONFIG_DIR}/"
	dst="$2/${PSI_CONFIG_DIR}/"
	$DRY rsync --recursive --links --perms --delete \
		"${src}" "${dst}" || die 1 "Error: synch operation failed!"
180
	sed -i.bak "s/PMODULES_VERSION=\(.*\)/PMODULES_VERSION=${PMODULES_VERSION}/" "${dst}/environment.bash"
181 182 183 184 185 186 187 188 189
	echo

	src="$1/${PSI_TEMPLATES_DIR}/"
	dst="$2/${PSI_TEMPLATES_DIR}/"
	$DRY rsync --recursive --links --perms --delete \
		"${src}" "${dst}" || die 1 "Error: synch operation failed!"
	echo
}

190 191 192
#
# Delete a module
#
193 194
# $1: relative modulefile path
# $2: target prefix of Pmodule environment
195 196
#
delete_module() {
197
        echo "Not implemented yet!"
198 199
}

200 201
subcommand_init() {
	local src=''
202
	local target_prefixes=()
203 204
	local user=''
	local opts=''
205
	opts=$(get_options -o h -l src: -l user: -l help -l version: -- "$@")
206 207 208 209 210 211
	if [[ $? != 0 ]]; then
		subcommand_help_init
		exit 1
	fi
	eval set --  "${opts}"
	while (($# > 0)); do
212
		case $1 in
213 214 215 216 217 218 219 220
			--src )
				src=$2
				shift
				;;
			--user )
				user=$2
				shift
				;;
221 222 223 224
			--version )
				PMODULES_VERSION=$2
				shift
				;;
225 226 227 228 229 230 231 232 233
			-- )
				:
				;;
			-* | -h | --help )
				echo "$1: illegal option" 1>&2
				subcommand_help_init
				exit 1
				;;
			* )
234
				target_prefixes+=( "$1" )
235 236 237 238
				;;
		esac
		shift
	done
239
	(( ${#target_prefixes[@]} != 0 )) || die 1 "Error: no target directory specified!"
240

gsell's avatar
gsell committed
241 242 243
	# if source is not passed as argument, derive it from script name
	if [[ -z "${src}" ]]; then
	    src=$(cd "${bindir}/../../../.." && pwd) 
244
	fi
gsell's avatar
gsell committed
245 246 247 248 249 250
	[[ -d "${src}" ]] || \
	    die 1 "Error: ${src}: source directory does not exist!"
	[[ -r "${src}/config/profile.bash" ]] || \
	    die 1 "Error: ${src}: shell profile does not exist or is not readable!"
	source "${src}/config/profile.bash"

251 252
	local -i euid=$(id -u)
	if (( euid == 0 )); then
253
	        [[ -n "${user}" ]] || \
254
		    die 1 "Error: --user parameter is required!"
255 256
		id -u "${user}" > /dev/null 2>&1 || \
		    die 1 "Error: Unable to retrieve user id of user '${user}'"
257
	else
258
		[[ -z "${user}" ]] || \
259
		    die 1 "Error: --user option is only allowed if running as root!"
260
	fi
261 262 263

	check_pmodules_env || die 1 "Giving up..."

264
	echo "
265 266 267 268
Attempting to create a minimal module environment from the
environment at '${PSI_PREFIX}'
"

269
	init_pmodules_environment() {
270
		local -r src_prefix="${PSI_PREFIX}"
271
		local -r target_prefix=$1
272 273
		local src=''
		local dst=''
274 275 276 277
		echo "Initializing target directory '${target_prefix}' ..."
		echo
		if [[ -d "${target_prefix}" ]]  && [[ ${force} == no ]]; then
			echo "Warning: ${target_prefix} already exists."
278
			get_YN_answer "Do you really want to re-run the initialization? (y/N) " || \
279
				      die 1 "Abort ..."
280
		fi
gsell's avatar
gsell committed
281
		force='yes'
282 283
		echo "Creating target directory '${target_prefix}'..."
		$DRY mkdir -p "${target_prefix}" || die 1 "Error: make directory failed!"
284
		echo
285

286 287 288
		echo "Syncing configuration ..."
		sync_config "${PSI_PREFIX}" \
		            "${target_prefix}" || die 1 "Error: configuration synchronization failed!"
289

290 291 292 293 294
		dst="${target_prefix}/${PSI_MODULES_ROOT}/"
		echo "Creating root directory '${dst}' for module hierarchy ..."
		$DRY mkdir -p "${dst}"
		echo

gsell's avatar
gsell committed
295
		echo "Syncing Pmodules ${PMODULES_VERSION} from '${src_prefix}' to '${target_prefix}'..."
296
		sync_module "Tools/Pmodules/${PMODULES_VERSION}" \
297
			    "${src_prefix}" \
298
			    "${target_prefix}" || die 1 "Error: sync Pmodules failed!"
299
		echo
300

301 302
		dst="${target_prefix}/${PSI_CONFIG_DIR}/environment.bash"
		echo "Adding installation source '${src_prefix}' to '${dst}'..."
gsell's avatar
gsell committed
303
		sed -i.bak '/PMODULES_INSTALL_SOURCE/d' "${dst}"
304 305 306
		echo "declare -x PMODULES_INSTALL_SOURCE=\"${src_prefix}\"" >> "${dst}"
		echo

307 308 309 310
		if [[ -n "${user}" ]]; then
	                echo "Changing user of new module environment to '${user}'..."
                        $DRY chown -R "${user}" "${target_prefix}" || die 1 "Error: changing owner failed!"
                        echo
311
		fi
312
		echo "New minimal module environment created at '${target_prefix}'."
313
		echo "To use this environment, execute"
314 315
		echo "   sudo ln -fs ${target_prefix} /opt/psi"
		echo "   source /opt/psi/${PSI_CONFIG_DIR}/profile.bash"
316 317 318
	}

	umask 022
319 320
	for target_prefix in "${target_prefixes[@]}"; do
		init_pmodules_environment "${target_prefix}"
321 322
	done

323 324
}

325 326 327 328 329
subcommand_install() {
	local opts=''
	local -a with=()
	local -a releases=()
	local -a module_pattern=()
330 331
	local -r src_prefix="${PMODULES_INSTALL_SOURCE}"
	local -r target_prefix="${PSI_PREFIX}"
332

333
	opts=$(get_options -o hf -l dry-run -l force -l with: -l release: -l help -l src: -- "$@")
334 335 336 337 338 339 340
	if [[ $? != 0 ]]; then
		subcommand_help_install
		exit 1
	fi
	eval set --  "${opts}"
	while (($# > 0)); do
		case $1 in
341 342 343 344 345 346
			--dry-run )
				DRY='echo'
				;;
			--force | -f )
				force='yes'
				;;
347 348
			--release )
				releases+=( "$2" )
349 350 351 352
				shift
				;;
			--src )
				src_prefix="$2"
353 354 355 356 357 358 359 360 361
				shift
				;;
			--with )
				with+=( "$2" )
				shift
				;;
			-- )
				:
				;;
362 363 364 365 366
			-h | --help )
				subcommand_help_install
				exit 1
				;;
			-* )
367 368 369 370 371 372 373 374 375 376
				echo "$1: illegal option" 1>&2
				subcommand_help_init
				exit 1
				;;
			* )
				module_pattern+=( "$1" )
				;;
		esac
		shift
	done
377 378 379 380 381 382 383
	local -A modules_to_install
	local -i n=0
	while read rel_modulefile; do
		modules_to_install["${rel_modulefile}"]+='.'
		let n+=1
	done < <(${PMODULES_HOME}/bin/modulecmd bash search \
				 "${module_pattern[@]}" \
384
				 "${with[@]/#/--with=}" \
385 386
				 "${releases[@]/#/--release=}" \
				 --no-header --print-modulefiles \
gsell's avatar
gsell committed
387
				 --src="${src_prefix}"  2>&1 1>/dev/null)
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
	(( n == 0 )) && die 0 "Nothing to install..."
	echo -e "The following modules will be installed/updated:\n" 1>&2
	for key in "${!modules_to_install[@]}"; do
		echo "    ${key}" 1>&2
	done
	echo 1>&2
	get_YN_answer "Do you want to continue? [n] " || die 1 "Aborting..."
	echo 1>&2
	for rel_modulefile in "${!modules_to_install[@]}"; do
		if [[ -d "${target_prefix}/${rel_modulefile}" ]]; then
			echo "    Updating; ${rel_modulefile}..." 1>&2
		else
			echo "    Installing: ${rel_modulefile}..."
		fi
		sync_module "${rel_modulefile}" \
			    "${src_prefix}" \
			    "${target_prefix}"
	done
	echo -e "\nDone!\n" 1>&2
407 408
}

409
subcommand_sync() {
410 411
        [[ -z "${PSI_PREFIX}" ]] && die 1 "Error: No current module environment is configured!"

412 413 414
	local delete=false
	local opts=''
	local dst_prefix=''
415 416
	local src_prefix=''

417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
	opts=$(get_options -o h -l dst: -l delete -l help -- "$@")
	if [[ $? != 0 ]]; then
		subcommand_help_sync
		exit 1
	fi
	eval set --  "${opts}"
	while (($# > 0)); do
		case $1 in
			--dst )
				dst_prefix="$2"
				shift
				;;
			--delete )
				delete=true
				;;
			-- )
				:
				;;
			-* | -h | --help )
				echo "$1: illegal option" 1>&2
				subcommand_help_init
				exit 1
				;;
			* )
gsell's avatar
gsell committed
441 442
				[[ -n "${src_prefix}" ]] && \
				    die 1 "Error: Only one source is allowed!"
443
				src_prefix="$1"
444 445 446 447 448
				;;
		esac
		shift
	done
	unset -v opts
449

450 451 452 453
	if [[ -z "${dst_prefix}" ]]; then
		dst_prefix="${PSI_PREFIX}"
	fi
	(
gsell's avatar
gsell committed
454 455
		PSI_PREFIX="${dst_prefix}" check_pmodules_env || \
		    die 1 "Error: invalid destination modules environment!"
456
	) || die 1 "Giving up..."
gsell's avatar
gsell committed
457
	
458
	: ${src_prefix:=${PMODULES_INSTALL_SOURCE}} 
459 460 461
	if [[ -z "${src_prefix}" ]]; then
            die 1 "Error: no source module environment was specified!"
        fi
462
	(
gsell's avatar
gsell committed
463 464
		PSI_PREFIX="${src_prefix}" check_pmodules_env || \
		    die 1 "Error: invalid source modules environment!"
465
	) || die 1 "Giving up..."
gsell's avatar
gsell committed
466 467
	[[ "$( cd "$src_prefix"; pwd -P )" == "$( cd "$dst_prefix"; pwd -P )" ]] && \
	    die 1 "Error: source and destination are equal!"
468 469 470
	local modbin=${PMODULES_HOME#"${PSI_PREFIX}/"}/bin/modulecmd.tcl
	local file_type_src=$( file -b "${src_prefix}/${modbin}" 2>&1 || echo err1 )
	local file_type_dst=$( file -b "${dst_prefix}/${modbin}" 2>&1 || echo err2 )
gsell's avatar
gsell committed
471 472
	[[ "${file_type_src}" == "${file_type_dst}" ]] || \
	    die 1 "Error: The file signatures in the source and destination installation do not match!"
473
	unset -v file_type_src file_type_dst
474
	local dialog_script="${PMODULES_HOME}/bin/dialog.bash"
gsell's avatar
gsell committed
475 476
	[[ -r "$dialog_script" ]] || \
	    die 1 "Error: Unable to find dialog script of installation $dialog_script";
477 478 479 480 481

	DIALOG_LIB=1                # use dialog script as a library
	source "$dialog_script"     # dialog functions

	# Redefine module_out to append modules to the selected_modules variable
482
        local -a selected_modules
483 484 485 486 487 488 489 490 491
	function module_out() {
		local -a args=(${modlist[$1]})
		local path=""
		IFS=/
		[[ -n "${args[3]}" ]] && path="/${args[*]:3}"
		unset IFS
		selected_modules+=( "${args[2]}${path}/${args[0]}" )
	}

gsell's avatar
gsell committed
492 493 494 495 496
	module_picker "${dst_prefix}" "${src_prefix}" || {
		# this calls module_out for each selected module,
		#filling up the selected_modules array
		echo "Abort!"
		exit 1
497
	}
498

499
	local -a destination_modules=( $(cd "${dst_prefix}/${PSI_MODULES_ROOT}"; find -L . -type f | while read f; do n=${f##*/}; [[ "${n:0:1}" == "." ]] || echo ${f#./}; done) )
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518

	# redefine set difference, the version in dialog.bash only handles integers
	function set_difference() {  #  $1 \ $2
		local -a operand1=($1)
		local -a operand2=($2)
		local -A members
		local elem
		for elem in "${operand1[@]}"; do
			members[$elem]=1
		done
		for elem in "${operand2[@]}"; do
			unset members[$elem]
		done
		echo ${!members[@]}
	}

	if [[ "$delete" == "true" ]]; then
		local -a modules_delete=( $(set_difference "${destination_modules[*]}" "${selected_modules[*]}") )
		for m in "${modules_delete[@]}"; do
519
                        echo "Deleting module $m ..."
520 521 522 523 524 525 526 527 528 529 530
			delete_module "$m" "$dst_prefix"
		done
		unset modules_delete
	fi

	local -a modules_copy=( $(set_difference "${selected_modules[*]}" "${destination_modules[*]}") )
	if [[ -n $modules_copy ]]; then
		echo "Syncing configuration ..."
		sync_config "$src_prefix" "$dst_prefix" || die 1 "Error: syncing the configuration failed"
        fi
        for m in "${modules_copy[@]}"; do
531
                echo "Copying module $m ..."
532 533 534 535 536
		sync_module "$m" "$src_prefix" "$dst_prefix" || die 1 "Error: syncing of module $m failed!"
        done
	unset modules_copy
}

537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557
while (($# > 0)); do
	case $1 in
		-h | -H | -\? | --help | -help )
			usage
			exit 1
			;;
		-V | --version )
			print_version
			exit 1
			;;
		-f | --force )
			force='yes'
			;;
		--dry-run )
			dry_run='yes'
			DRY='echo'
			;;
		-* )
			echo "$1: unknown switch.\n" 1>&2
			exit 1
			;;
558
		init|install|sync|help )
559 560 561 562 563 564 565 566 567 568
			subcommand="subcommand_$1"
			shift
			sargs=( $* )
			shift $#
			;;
		* )
			echo "$1: unknown sub-command" 1>&2
			exit 1
	esac
	shift
569
done
570

571 572 573 574
if [[ -z ${subcommand} ]]; then
        usage
	exit 1
fi
575 576 577 578 579 580 581
$subcommand "${sargs[@]}"

# Local Variables:
# mode: sh
# sh-basic-offset: 8
# tab-width: 8
# End: