diff --git a/EESSI-pilot-install-software.sh b/EESSI-pilot-install-software.sh index 2830754b29..035f851d61 100755 --- a/EESSI-pilot-install-software.sh +++ b/EESSI-pilot-install-software.sh @@ -429,6 +429,52 @@ fi $EB SciPy-bundle-2021.05-foss-2021a.eb --robot check_exit_code $? "${ok_msg}" "${fail_msg}" +# CUDA support + +cuda_version="11.3.1" + +# Need recent version of EasyBuild +echo ">> Installing EasyBuild 4.7.0..." +ok_msg="EasyBuild v4.7.0 installed" +fail_msg="EasyBuild v4.7.0 failed to install" +$EB --from-pr 17065 --include-easyblocks-from-pr 2893 --try-amend=use_pip=1 +check_exit_code $? "${ok_msg}" "${fail_msg}" + +LMOD_IGNORE_CACHE=1 module swap EasyBuild/4.7.0 +check_exit_code $? "Swapped to EasyBuild/4.7.0" "Couldn't swap to EasyBuild/4.7.0" + +# install p7zip (to be able to unpack RPMs) +p7zip_ec="p7zip-17.04-GCCcore-10.3.0.eb" +echo ">> Installing $p7zip_ec..." +ok_msg="$p7zip_ec installed, off to a good (?) start!" +fail_msg="Failed to install $p7zip_ec, woopsie..." +$EB $p7zip_ec --robot +check_exit_code $? "${ok_msg}" "${fail_msg}" + +# install CUDA (uses eb_hooks.py to only install runtime) +cuda_ec="CUDA-${cuda_version}.eb" +echo ">> Installing $cuda_ec..." +ok_msg="$cuda_ec installed, off to a good (?) start!" +fail_msg="Failed to install $cuda_ec, woopsie..." +$EB $cuda_ec --robot +check_exit_code $? "${ok_msg}" "${fail_msg}" + +# Add the host_injections CUDA so we can actually build CUDA apps +# (which unbreaks the symlinks from the runtime installation) +echo ">> Re-installing CUDA $cuda_version under host_injections (to un-break symlinks in EESSI installation)..." +"${TOPDIR}"/gpu_support/cuda_utils/install_cuda_host_injections.sh ${cuda_version} +ok_msg="CUDA $cuda_version (re)installed under host_injections!" +fail_msg="Failed to install CUDA $cuda_version under host_injections, woopsie..." +check_exit_code $? "${ok_msg}" "${fail_msg}" + +# install CUDA samples (requires EESSI support for CUDA) +cuda_samples_ec="CUDA-Samples-11.3-GCC-10.3.0-CUDA-11.3.1.eb" +echo ">> Installing $cuda_samples_ec..." +ok_msg="$cuda_samples_ec installed, off to a good (?) start!" +fail_msg="Failed to install $cuda_samples_ec, woopsie..." +$EB $cuda_samples_ec --robot --from-pr=16914 +check_exit_code $? "${ok_msg}" "${fail_msg}" + ### add packages here echo ">> Creating/updating Lmod cache..." diff --git a/eb_hooks.py b/eb_hooks.py index df7742f999..c7358d5f13 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -8,51 +8,40 @@ from easybuild.tools.systemtools import AARCH64, POWER, X86_64, get_cpu_architecture, get_cpu_features from easybuild.tools.toolchain.compiler import OPTARCH_GENERIC -EESSI_RPATH_OVERRIDE_ATTR = 'orig_rpath_override_dirs' - - -def get_eessi_envvar(eessi_envvar): - """Get an EESSI environment variable from the environment""" - - eessi_envvar_value = os.getenv(eessi_envvar) - if eessi_envvar_value is None: - raise EasyBuildError("$%s is not defined!", eessi_envvar) - - return eessi_envvar_value - - -def get_rpath_override_dirs(software_name): - # determine path to installations in software layer via $EESSI_SOFTWARE_PATH - eessi_software_path = get_eessi_envvar('EESSI_SOFTWARE_PATH') - eessi_pilot_version = get_eessi_envvar('EESSI_PILOT_VERSION') - - # construct the rpath override directory stub - rpath_injection_stub = os.path.join( - # Make sure we are looking inside the `host_injections` directory - eessi_software_path.replace(eessi_pilot_version, os.path.join('host_injections', eessi_pilot_version), 1), - # Add the subdirectory for the specific software - 'rpath_overrides', - software_name, - # We can't know the version, but this allows the use of a symlink - # to facilitate version upgrades without removing files - 'system', - ) - - # Allow for libraries in lib or lib64 - rpath_injection_dirs = [os.path.join(rpath_injection_stub, x) for x in ('lib', 'lib64')] - - return rpath_injection_dirs +EESSI_RPATH_OVERRIDE_ATTR = "orig_rpath_override_dirs" + +CUDA_ENABLED_TOOLCHAINS = [ + "fosscuda", + "gcccuda", + "gimpic", + "giolfc", + "gmklc", + "golfc", + "gomklc", + "gompic", + "goolfc", + "iccifortcuda", + "iimklc", + "iimpic", + "intelcuda", + "iomklc", + "iompic", + "nvompic", + "nvpsmpic", +] def parse_hook(ec, *args, **kwargs): """Main parse hook: trigger custom functions based on software name.""" # determine path to Prefix installation in compat layer via $EPREFIX - eprefix = get_eessi_envvar('EPREFIX') + eprefix = get_eessi_envvar("EPREFIX") if ec.name in PARSE_HOOKS: PARSE_HOOKS[ec.name](ec, eprefix) + ec = inject_gpu_property(ec) + def pre_configure_hook(self, *args, **kwargs): """Main pre-configure hook: trigger custom functions based on software name.""" @@ -74,19 +63,21 @@ def pre_prepare_hook(self, *args, **kwargs): # update the relevant option (but keep the original value so we can reset it later) if hasattr(self, EESSI_RPATH_OVERRIDE_ATTR): - raise EasyBuildError("'self' already has attribute %s! Can't use pre_prepare hook.", - EESSI_RPATH_OVERRIDE_ATTR) + raise EasyBuildError( + "'self' already has attribute %s! Can't use pre_prepare hook.", EESSI_RPATH_OVERRIDE_ATTR + ) - setattr(self, EESSI_RPATH_OVERRIDE_ATTR, build_option('rpath_override_dirs')) + setattr(self, EESSI_RPATH_OVERRIDE_ATTR, build_option("rpath_override_dirs")) if getattr(self, EESSI_RPATH_OVERRIDE_ATTR): # self.EESSI_RPATH_OVERRIDE_ATTR is (already) a colon separated string, let's make it a list orig_rpath_override_dirs = [getattr(self, EESSI_RPATH_OVERRIDE_ATTR)] - rpath_override_dirs = ':'.join(orig_rpath_override_dirs + mpi_rpath_override_dirs) + rpath_override_dirs = ":".join(orig_rpath_override_dirs + mpi_rpath_override_dirs) else: - rpath_override_dirs = ':'.join(mpi_rpath_override_dirs) - update_build_option('rpath_override_dirs', rpath_override_dirs) - print_msg("Updated rpath_override_dirs (to allow overriding MPI family %s): %s", - mpi_family, rpath_override_dirs) + rpath_override_dirs = ":".join(mpi_rpath_override_dirs) + update_build_option("rpath_override_dirs", rpath_override_dirs) + print_msg( + "Updated rpath_override_dirs (to allow overriding MPI family %s): %s", mpi_family, rpath_override_dirs + ) def post_prepare_hook(self, *args, **kwargs): @@ -94,30 +85,78 @@ def post_prepare_hook(self, *args, **kwargs): if hasattr(self, EESSI_RPATH_OVERRIDE_ATTR): # Reset the value of 'rpath_override_dirs' now that we are finished with it - update_build_option('rpath_override_dirs', getattr(self, EESSI_RPATH_OVERRIDE_ATTR)) + update_build_option("rpath_override_dirs", getattr(self, EESSI_RPATH_OVERRIDE_ATTR)) print_msg("Resetting rpath_override_dirs to original value: %s", getattr(self, EESSI_RPATH_OVERRIDE_ATTR)) delattr(self, EESSI_RPATH_OVERRIDE_ATTR) +def pre_configure_hook(self, *args, **kwargs): + """Main pre-configure hook: trigger custom functions based on software name.""" + if self.name in PRE_CONFIGURE_HOOKS: + PRE_CONFIGURE_HOOKS[self.name](self, *args, **kwargs) + + +def post_package_hook(self, *args, **kwargs): + """Main post-package hook: trigger custom functions based on software name.""" + if self.name in POST_PACKAGE_HOOKS: + POST_PACKAGE_HOOKS[self.name](self, *args, **kwargs) + + +# Functions used by hooks + + +def get_eessi_envvar(eessi_envvar): + """Get an EESSI environment variable from the environment""" + + eessi_envvar_value = os.getenv(eessi_envvar) + if eessi_envvar_value is None: + raise EasyBuildError("$%s is not defined!", eessi_envvar) + + return eessi_envvar_value + + +def get_rpath_override_dirs(software_name): + # determine path to installations in software layer via $EESSI_SOFTWARE_PATH + eessi_software_path = get_eessi_envvar("EESSI_SOFTWARE_PATH") + eessi_pilot_version = get_eessi_envvar("EESSI_PILOT_VERSION") + + # construct the rpath override directory stub + rpath_injection_stub = os.path.join( + # Make sure we are looking inside the `host_injections` directory + eessi_software_path.replace(eessi_pilot_version, os.path.join("host_injections", eessi_pilot_version), 1), + # Add the subdirectory for the specific software + "rpath_overrides", + software_name, + # We can't know the version, but this allows the use of a symlink + # to facilitate version upgrades without removing files + "system", + ) + + # Allow for libraries in lib or lib64 + rpath_injection_dirs = [os.path.join(rpath_injection_stub, x) for x in ("lib", "lib64")] + + return rpath_injection_dirs + + def cgal_toolchainopts_precise(ec, eprefix): """Enable 'precise' rather than 'strict' toolchain option for CGAL on POWER.""" - if ec.name == 'CGAL': + if ec.name == "CGAL": if get_cpu_architecture() == POWER: # 'strict' implies '-mieee-fp', which is not supported on POWER # see https://github.com/easybuilders/easybuild-framework/issues/2077 - ec['toolchainopts']['strict'] = False - ec['toolchainopts']['precise'] = True - print_msg("Tweaked toochainopts for %s: %s", ec.name, ec['toolchainopts']) + ec["toolchainopts"]["strict"] = False + ec["toolchainopts"]["precise"] = True + print_msg("Tweaked toochainopts for %s: %s", ec.name, ec["toolchainopts"]) else: raise EasyBuildError("CGAL-specific hook triggered for non-CGAL easyconfig?!") def fontconfig_add_fonts(ec, eprefix): """Inject --with-add-fonts configure option for fontconfig.""" - if ec.name == 'fontconfig': + if ec.name == "fontconfig": # make fontconfig aware of fonts included with compat layer - with_add_fonts = '--with-add-fonts=%s' % os.path.join(eprefix, 'usr', 'share', 'fonts') - ec.update('configopts', with_add_fonts) + with_add_fonts = "--with-add-fonts=%s" % os.path.join(eprefix, "usr", "share", "fonts") + ec.update("configopts", with_add_fonts) print_msg("Added '%s' configure option for %s", with_add_fonts, ec.name) else: raise EasyBuildError("fontconfig-specific hook triggered for non-fontconfig easyconfig?!") @@ -125,29 +164,23 @@ def fontconfig_add_fonts(ec, eprefix): def ucx_eprefix(ec, eprefix): """Make UCX aware of compatibility layer via additional configuration options.""" - if ec.name == 'UCX': - ec.update('configopts', '--with-sysroot=%s' % eprefix) - ec.update('configopts', '--with-rdmacm=%s' % os.path.join(eprefix, 'usr')) - print_msg("Using custom configure options for %s: %s", ec.name, ec['configopts']) + if ec.name == "UCX": + ec.update("configopts", "--with-sysroot=%s" % eprefix) + ec.update("configopts", "--with-rdmacm=%s" % os.path.join(eprefix, "usr")) + print_msg("Using custom configure options for %s: %s", ec.name, ec["configopts"]) else: raise EasyBuildError("UCX-specific hook triggered for non-UCX easyconfig?!") -def pre_configure_hook(self, *args, **kwargs): - """Main pre-configure hook: trigger custom functions based on software name.""" - if self.name in PRE_CONFIGURE_HOOKS: - PRE_CONFIGURE_HOOKS[self.name](self, *args, **kwargs) - - def libfabric_disable_psm3_x86_64_generic(self, *args, **kwargs): """Add --disable-psm3 to libfabric configure options when building with --optarch=GENERIC on x86_64.""" - if self.name == 'libfabric': + if self.name == "libfabric": if get_cpu_architecture() == X86_64: - generic = build_option('optarch') == OPTARCH_GENERIC - no_avx = 'avx' not in get_cpu_features() + generic = build_option("optarch") == OPTARCH_GENERIC + no_avx = "avx" not in get_cpu_features() if generic or no_avx: - self.cfg.update('configopts', '--disable-psm3') - print_msg("Using custom configure options for %s: %s", self.name, self.cfg['configopts']) + self.cfg.update("configopts", "--disable-psm3") + print_msg("Using custom configure options for %s: %s", self.name, self.cfg["configopts"]) else: raise EasyBuildError("libfabric-specific hook triggered for non-libfabric easyconfig?!") @@ -158,10 +191,10 @@ def metabat_preconfigure(self, *args, **kwargs): - take into account that zlib is a filtered dependency, and that there's no libz.a in the EESSI compat layer """ - if self.name == 'MetaBAT': - configopts = self.cfg['configopts'] + if self.name == "MetaBAT": + configopts = self.cfg["configopts"] regex = re.compile(r"\$EBROOTZLIB/lib/libz.a") - self.cfg['configopts'] = regex.sub('$EPREFIX/usr/lib64/libz.so', configopts) + self.cfg["configopts"] = regex.sub("$EPREFIX/usr/lib64/libz.so", configopts) else: raise EasyBuildError("MetaBAT-specific hook triggered for non-MetaBAT easyconfig?!") @@ -171,24 +204,103 @@ def wrf_preconfigure(self, *args, **kwargs): Pre-configure hook for WRF: - patch arch/configure_new.defaults so building WRF with foss toolchain works on aarch64 """ - if self.name == 'WRF': + if self.name == "WRF": if get_cpu_architecture() == AARCH64: pattern = "Linux x86_64 ppc64le, gfortran" repl = "Linux x86_64 aarch64 ppc64le, gfortran" - self.cfg.update('preconfigopts', "sed -i 's/%s/%s/g' arch/configure_new.defaults && " % (pattern, repl)) - print_msg("Using custom preconfigopts for %s: %s", self.name, self.cfg['preconfigopts']) + self.cfg.update("preconfigopts", "sed -i 's/%s/%s/g' arch/configure_new.defaults && " % (pattern, repl)) + print_msg("Using custom preconfigopts for %s: %s", self.name, self.cfg["preconfigopts"]) else: raise EasyBuildError("WRF-specific hook triggered for non-WRF easyconfig?!") +def cuda_postpackage(self, *args, **kwargs): + """Delete CUDA files we are not allowed to ship and replace them with a symlink to a possible installation under host_injections.""" + print_msg("Replacing CUDA stuff we cannot ship with symlinks...") + # read CUDA EULA + eula_path = os.path.join(self.installdir, "EULA.txt") + tmp_buffer = [] + with open(eula_path) as infile: + copy = False + for line in infile: + if line.strip() == "2.6. Attachment A": + copy = True + continue + elif line.strip() == "2.7. Attachment B": + copy = False + continue + elif copy: + tmp_buffer.append(line) + # create whitelist without file extensions, they're not really needed and they only complicate things + whitelist = ['EULA'] + file_extensions = [".so", ".a", ".h", ".bc"] + for tmp in tmp_buffer: + for word in tmp.split(): + if any(ext in word for ext in file_extensions): + whitelist.append(word.split(".")[0]) + whitelist = list(set(whitelist)) + # Do some quick checks for things we should or shouldn't have in the list + if "nvcc" in whitelist: + raise EasyBuildError("Found 'nvcc' in whitelist: %s" % whitelist) + if "libcudart" not in whitelist: + raise EasyBuildError("Did not find 'libcudart' in whitelist: %s" % whitelist) + # iterate over all files in the CUDA path + for root, dirs, files in os.walk(self.installdir): + for filename in files: + # we only really care about real files, i.e. not symlinks + if not os.path.islink(os.path.join(root, filename)): + # check if the current file is part of the whitelist + basename = filename.split(".")[0] + if basename not in whitelist: + # if it is not in the whitelist, delete the file and create a symlink to host_injections + source = os.path.join(root, filename) + target = source.replace("versions", "host_injections") + os.remove(source) + # Using os.symlink requires the existence of the target directory, so we use os.system + system_command="ln -s %s %s" % (target, source) + if os.system(system_command) != 0: + raise EasyBuildError("Failed to create symbolic link: %s" % system_command) + + +def inject_gpu_property(ec): + ec_dict = ec.asdict() + # Check if CUDA is in the dependencies, if so add the GPU Lmod tag + if ( + "CUDA" in [dep[0] for dep in iter(ec_dict["dependencies"])] + or ec_dict["toolchain"]["name"] in CUDA_ENABLED_TOOLCHAINS + ): + ec.log.info("[parse hook] Injecting gpu as Lmod arch property and envvar with CUDA version") + key = "modluafooter" + value = 'add_property("arch","gpu")' + cuda_version = 0 + for dep in iter(ec_dict["dependencies"]): + # Make CUDA a build dependency only (rpathing saves us from link errors) + if "CUDA" in dep[0]: + cuda_version = dep[1] + ec_dict["dependencies"].remove(dep) + ec_dict["builddependencies"].append(dep) if dep not in ec_dict["builddependencies"] else ec_dict[ + "builddependencies" + ] + value = "\n".join([value, 'setenv("EESSICUDAVERSION","%s")' % cuda_version]) + if key in ec_dict: + if not value in ec_dict[key]: + ec[key] = "\n".join([ec_dict[key], value]) + else: + ec[key] = value + return ec + PARSE_HOOKS = { - 'CGAL': cgal_toolchainopts_precise, - 'fontconfig': fontconfig_add_fonts, - 'UCX': ucx_eprefix, + "CGAL": cgal_toolchainopts_precise, + "fontconfig": fontconfig_add_fonts, + "UCX": ucx_eprefix, } PRE_CONFIGURE_HOOKS = { - 'libfabric': libfabric_disable_psm3_x86_64_generic, - 'MetaBAT': metabat_preconfigure, - 'WRF': wrf_preconfigure, + "libfabric": libfabric_disable_psm3_x86_64_generic, + "MetaBAT": metabat_preconfigure, + "WRF": wrf_preconfigure, +} + +POST_PACKAGE_HOOKS = { + "CUDA": cuda_postpackage, } diff --git a/eessi-2021.12.yml b/eessi-2021.12.yml index 210bbb2845..dc80a010ea 100644 --- a/eessi-2021.12.yml +++ b/eessi-2021.12.yml @@ -1,4 +1,14 @@ software: + CUDA: + toolchains: + SYSTEM: + versions: '11.3.1' + CUDA-Samples: + toolchains: + GCC-10.3.0: + versions: + '11.3': + versionsuffix: -CUDA-11.3.1 code-server: toolchains: SYSTEM: @@ -30,7 +40,11 @@ software: gompi-2020a: versions: ['5.6.3'] gompi-2021a: - versions: ['5.7.1'] + versions: [ '5.7.1' ] + p7zip: + toolchains: + GCCcore-10.3.0: + versions: ['17.04'] QuantumESPRESSO: toolchains: foss-2020a: diff --git a/gpu_support/cuda_utils/install_cuda_host_injections.sh b/gpu_support/cuda_utils/install_cuda_host_injections.sh new file mode 100755 index 0000000000..1ddccf4e82 --- /dev/null +++ b/gpu_support/cuda_utils/install_cuda_host_injections.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# Initialise our bash functions +source scripts/utils.sh + +if [[ $# -eq 0 ]] ; then + fatal_error "You must provide the CUDA version as an argument, e.g.:\n $0 11.3.1" +fi +install_cuda_version=$1 +if [[ -z "${EESSI_SOFTWARE_PATH}" ]]; then + fatal_error "This script cannot be used without having first defined EESSI_SOFTWARE_PATH" +else + # As an installation location just use $EESSI_SOFTWARE_PATH but replacing `versions` with `host_injections` + # (CUDA is a binary installation so no need to worry too much about the EasyBuild setup) + cuda_install_parent=${EESSI_SOFTWARE_PATH/versions/host_injections} +fi + +# Only install CUDA if specified version is not found. +# This is only relevant for users, the shipped CUDA installation will +# always be in versions instead of host_injections and have symlinks pointing +# to host_injections for everything we're not allowed to ship +# (existence of easybuild subdir implies a successful install) +if [ -d "${cuda_install_parent}"/software/CUDA/"${install_cuda_version}"/easybuild ]; then + echo_green "CUDA software found! No need to install CUDA again, proceed with testing." +else + # We need to be able write to the installation space so let's make sure we can + if ! create_directory_structure "${cuda_install_parent}"/software/CUDA ; then + fatal_error "No write permissions to directory ${cuda_install_parent}/software/CUDA" + fi + + # we need a directory we can use for temporary storage + if [[ -z "${CUDA_TEMP_DIR}" ]]; then + tmpdir=$(mktemp -d) + else + tmpdir="${CUDA_TEMP_DIR}"/temp + if ! mkdir "$tmpdir" ; then + fatal_error "Could not create directory ${tmpdir}" + fi + fi + + required_space_in_tmpdir=50000 + # Let's see if we have sources and build locations defined if not, we use the temporary space + if [[ -z "${EASYBUILD_BUILDPATH}" ]]; then + export EASYBUILD_BUILDPATH=${tmpdir}/build + required_space_in_tmpdir=$((required_space_in_tmpdir + 5000000)) + fi + if [[ -z "${EASYBUILD_SOURCEPATH}" ]]; then + export EASYBUILD_SOURCEPATH=${tmpdir}/sources + required_space_in_tmpdir=$((required_space_in_tmpdir + 5000000)) + fi + + # The install is pretty fat, you need lots of space for download/unpack/install (~3*5GB), + # need to do a space check before we proceed + avail_space=$(df --output=avail "${cuda_install_parent}"/ | tail -n 1 | awk '{print $1}') + if (( avail_space < 5000000 )); then + fatal_error "Need at least 5GB disk space to install CUDA under ${cuda_install_parent}, exiting now..." + fi + avail_space=$(df --output=avail "${tmpdir}"/ | tail -n 1 | awk '{print $1}') + if (( avail_space < required_space_in_tmpdir )); then + error="Need at least ${required_space_in_tmpdir} disk space under ${tmpdir}.\n" + error="${error}Set the environment variable CUDA_TEMP_DIR to a location with adequate space to pass this check." + error="${error}You can alternatively set EASYBUILD_BUILDPATH and/or EASYBUILD_SOURCEPATH " + error="${error}to reduce this requirement. Exiting now..." + fatal_error "${error}" + fi + + if [[ -z "${EBROOTEASYBUILD}" ]]; then + echo_yellow "Loading EasyBuild module to do actual install" + module load EasyBuild + fi + + # we need the --rebuild option and a (random) dir for the module since we are + # fixing the broken links of the EESSI-shipped installation + extra_args="--rebuild --installpath-modules=${tmpdir}" + + # We don't want hooks used in this install, we need a vanilla CUDA installation + touch "$tmpdir"/none.py + # shellcheck disable=SC2086 # Intended splitting of extra_args + eb --prefix="$tmpdir" ${extra_args} --hooks="$tmpdir"/none.py --installpath="${cuda_install_parent}"/ CUDA-"${install_cuda_version}".eb + ret=$? + if [ $ret -ne 0 ]; then + fatal_error "CUDA installation failed, please check EasyBuild logs..." + else + echo_green "CUDA installation at ${cuda_install_parent}/software/CUDA/${install_cuda_version} succeeded!" + fi + # clean up tmpdir + rm -rf "${tmpdir}" +fi diff --git a/scripts/utils.sh b/scripts/utils.sh index d0da95e87f..f043ba0ca6 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -14,7 +14,7 @@ ANY_ERROR_EXITCODE=1 function fatal_error() { echo_red "ERROR: $1" >&2 if [[ $# -gt 1 ]]; then - exit $2 + exit "$2" else exit "${ANY_ERROR_EXITCODE}" fi @@ -32,11 +32,39 @@ function check_exit_code { fi } +function create_directory_structure() { + # Ensure we are given a single path argument + if [ $# -ne 1 ]; then + echo_red "Function requires a single (relative or absolute) path argument" >&2 + return $ANY_ERROR_EXITCODE + fi + dir_structure="$1" + + # Attempt to create the directory structure + error_message=$(mkdir -p "$dir_structure" 2>&1) + return_code=$? + # If it fails be explicit about the error + if [ ${return_code} -ne 0 ]; then + real_dir=$(realpath -m "$dir_structure") + echo_red "Creating ${dir_structure} (real path ${real_dir}) failed with:\n ${error_message}" >&2 + else + # If we're creating it, our use case is that we want to be able to write there + # (this is a check in case the directory already existed) + if [ ! -w "${dir_structure}" ]; then + real_dir=$(realpath -m "$dir_structure") + echo_red "You do not have (required) write permissions to ${dir_structure} (real path ${real_dir})!" + return_code=$ANY_ERROR_EXITCODE + fi + fi + + return $return_code +} + function get_path_for_tool { tool_name=$1 tool_envvar_name=$2 - which_out=$(which ${tool_name} 2>&1) + which_out=$(which "${tool_name}" 2>&1) exit_code=$? if [[ ${exit_code} -eq 0 ]]; then echo "INFO: found tool ${tool_name} in PATH (${which_out})" >&2 @@ -68,7 +96,7 @@ function get_host_from_url { url=$1 re="(http|https)://([^/:]+)" if [[ $url =~ $re ]]; then - echo ${BASH_REMATCH[2]} + echo "${BASH_REMATCH[2]}" return 0 else echo "" @@ -80,7 +108,7 @@ function get_port_from_url { url=$1 re="(http|https)://[^:]+:([0-9]+)" if [[ $url =~ $re ]]; then - echo ${BASH_REMATCH[2]} + echo "${BASH_REMATCH[2]}" return 0 else echo "" @@ -90,7 +118,7 @@ function get_port_from_url { function get_ipv4_address { hname=$1 - hipv4=$(grep ${hname} /etc/hosts | grep -v '^[[:space:]]*#' | cut -d ' ' -f 1) + hipv4=$(grep "${hname}" /etc/hosts | grep -v '^[[:space:]]*#' | cut -d ' ' -f 1) # TODO try other methods if the one above does not work --> tool that verifies # what method can be used? echo "${hipv4}"