diff --git a/.github/workflows/end2end_bwrap.yml b/.github/workflows/end2end_bwrap.yml index 3f98f2cbe9..831b50167e 100644 --- a/.github/workflows/end2end_bwrap.yml +++ b/.github/workflows/end2end_bwrap.yml @@ -54,6 +54,123 @@ jobs: bash -lc "source /tmp/eb_env; $cmd" done + - name: Define bwrap validation helper + shell: bash + run: | + cat > /tmp/check_bwrap_install.sh <<'EOF' + #!/usr/bin/env bash + + check_no_glob_matches() { + local label="$1" + local pattern="$2" + + echo ">>> CHECK: $label" + echo " glob: $pattern" + + if compgen -G "$pattern" > /dev/null; then + echo "FAIL: files/directories matched" + compgen -G "$pattern" | sort + return 1 + fi + + echo "PASS: no files/directories matched" + echo + } + + check_path_exists() { + local label="$1" + local path="$2" + + echo ">>> CHECK: $label" + echo " path: $path" + + if [[ -e "$path" ]]; then + echo "PASS: path exists" + ls -ld "$path" + echo + return 0 + fi + + echo "FAIL: path does not exist" + echo "Parent contents:" + ls -la "$(dirname "$path")" || true + echo + return 1 + } + + check_glob_exists() { + local label="$1" + local pattern="$2" + + echo ">>> CHECK: $label" + echo " glob: $pattern" + + if compgen -G "$pattern" > /dev/null; then + echo "PASS: matches found" + compgen -G "$pattern" | sort | xargs -r ls -l + echo + return 0 + fi + + echo "FAIL: no matches found" + echo + return 1 + } + + main() { + local failures=0 + + echo "=== Host-side EasyBuild path state ===" + find "$HOME/.local/easybuild" -maxdepth 5 -print || true + echo + + echo "=== bwrap install path state ===" + find /tmp/bwrap-installpath -maxdepth 5 -print || true + echo + + check_no_glob_matches "no host UnZip modules" \ + "$HOME/.local/easybuild/modules/all/UnZip/*" || failures=$((failures + 1)) + + check_no_glob_matches "no host bzip2 modules" \ + "$HOME/.local/easybuild/modules/all/bzip2/*" || failures=$((failures + 1)) + + check_no_glob_matches "no host UnZip software files" \ + "$HOME/.local/easybuild/software/UnZip/6.0/*" || failures=$((failures + 1)) + + check_no_glob_matches "no host bzip2 software files" \ + "$HOME/.local/easybuild/software/bzip2/1.0.8/*" || failures=$((failures + 1)) + + check_path_exists "bwrap bzip2 module file exists" \ + "/tmp/bwrap-installpath/modules/all/bzip2/1.0.8.lua" || failures=$((failures + 1)) + + check_path_exists "bwrap UnZip module file exists" \ + "/tmp/bwrap-installpath/modules/all/UnZip/6.0.lua" || failures=$((failures + 1)) + + check_glob_exists "bwrap UnZip installed binaries exist" \ + "/tmp/bwrap-installpath/software/UnZip/*/bin/*" || failures=$((failures + 1)) + + check_glob_exists "bwrap bzip2 installed binaries exist" \ + "/tmp/bwrap-installpath/software/bzip2/*/bin/*" || failures=$((failures + 1)) + + check_glob_exists "bwrap UnZip EasyBuild logs exist" \ + "/tmp/bwrap-installpath/software/UnZip/*/easybuild/*log*" || failures=$((failures + 1)) + + check_glob_exists "bwrap bzip2 EasyBuild logs exist" \ + "/tmp/bwrap-installpath/software/bzip2/*/easybuild/*log*" || failures=$((failures + 1)) + + if [[ "$failures" -ne 0 ]]; then + echo "ERROR: $failures bwrap validation check(s) failed" + return 1 + fi + + echo "All bwrap validation checks passed" + } + + main "$@" + EOF + + chmod +x /tmp/check_bwrap_install.sh + - name: End-to-end test of installing UnZip with EasyBuild in bwrap namespace shell: bash run: | @@ -66,16 +183,8 @@ jobs: # test installation of UnZip + bzip2 dependency eb UnZip-6.0.eb --trace --robot --experimental --bwrap --bwrap-installpath /tmp/bwrap-installpath --filter-deps=binutils - # no module files should be created in configured install path (default: $HOME/.local/easybuild), - # not for UnZip, nor for bzip2 dependency - (test -d $HOME/.local/easybuild/modules/all/UnZip || echo 'no UnZip module dir found: OK') > /tmp/out-UnZip-mod - (test -d $HOME/.local/easybuild/modules/all/bzip2 || echo 'no bzip2 module dir found: OK') > /tmp/out-bzip2-mod + /tmp/check_bwrap_install.sh - # software subdirectories in configured install path should be empty - (test -e $HOME/.local/easybuild/software/UnZip/6.0/* || echo 'UnZip software dir in $HOME/.local/easybuild is empty: OK') > /tmp/out-UnZip-sw-dir - (test -e $HOME/.local/easybuild/software/bzip2/1.0.8/* || echo 'bzip2 software dir in $HOME/.local/easybuild is empty: OK') > /tmp/out-bzip2-sw-dir - - grep 'no UnZip module dir found: OK' /tmp/out-UnZip-mod && grep 'no bzip2 module dir found: OK' /tmp/out-bzip2-mod && grep 'UnZip software dir in .* is empty' /tmp/out-UnZip-sw-dir && grep 'bzip2 software dir in .* is empty' /tmp/out-bzip2-sw-dir && ls -l /tmp/bwrap-installpath/modules/all/{bzip2/1.0.8.lua,UnZip/6.0.lua} && ls -l /tmp/bwrap-installpath/software/{UnZip,bzip2}/*/bin/* && ls -l /tmp/bwrap-installpath/software/{UnZip,bzip2}/*/easybuild/*log* EOF - name: End-to-end test of installing UnZip with EasyBuild in bwrap namespace with overlay @@ -89,22 +198,14 @@ jobs: source /tmp/eb_env # remove previously created install directories - rm -r $HOME/.local/easybuild/software/* /tmp/bwrap-installpath + rm -r $HOME/.local/easybuild/{software,modules}/* /tmp/bwrap-installpath # make install directory read-only to force using overlay - chmod -w $HOME/.local/easybuild/software + chmod -w $HOME/.local/easybuild/{software,modules} # test installation of UnZip + bzip2 dependency eb UnZip-6.0.eb --trace --robot --experimental --bwrap --bwrap-installpath /tmp/bwrap-installpath --filter-deps=binutils - # no module files should be created in configured install path (default: $HOME/.local/easybuild), - # not for UnZip, nor for bzip2 dependency - (test -d $HOME/.local/easybuild/modules/all/UnZip || echo 'no UnZip module dir found: OK') > /tmp/out-UnZip-mod - (test -d $HOME/.local/easybuild/modules/all/bzip2 || echo 'no bzip2 module dir found: OK') > /tmp/out-bzip2-mod - - # software subdirectories in configured install path should be empty - (test -e $HOME/.local/easybuild/software/UnZip/6.0/* || echo 'UnZip software dir in $HOME/.local/easybuild is empty: OK') > /tmp/out-UnZip-sw-dir - (test -e $HOME/.local/easybuild/software/bzip2/1.0.8/* || echo 'bzip2 software dir in $HOME/.local/easybuild is empty: OK') > /tmp/out-bzip2-sw-dir + /tmp/check_bwrap_install.sh - grep 'no UnZip module dir found: OK' /tmp/out-UnZip-mod && grep 'no bzip2 module dir found: OK' /tmp/out-bzip2-mod && grep 'UnZip software dir in .* is empty' /tmp/out-UnZip-sw-dir && grep 'bzip2 software dir in .* is empty' /tmp/out-bzip2-sw-dir && ls -l /tmp/bwrap-installpath/modules/all/{bzip2/1.0.8.lua,UnZip/6.0.lua} && ls -l /tmp/bwrap-installpath/software/{UnZip,bzip2}/*/bin/* && ls -l /tmp/bwrap-installpath/software/{UnZip,bzip2}/*/easybuild/*log* EOF diff --git a/easybuild/tools/bwrap.py b/easybuild/tools/bwrap.py index da2f5957c6..4359d85e8d 100644 --- a/easybuild/tools/bwrap.py +++ b/easybuild/tools/bwrap.py @@ -36,7 +36,7 @@ from easybuild.base import fancylogger from easybuild.tools.build_log import EasyBuildError, print_msg from easybuild.tools.config import install_path, ConfigurationVariables -from easybuild.tools.filetools import mkdir, write_file +from easybuild.tools.filetools import copy_dir, mkdir, write_file from easybuild.tools.utilities import trace_msg @@ -104,10 +104,11 @@ def prepare_bwrap(bwrap_installpath): set_bwrap_info('bwrap_installpath', bwrap_installpath) - installpath_software = install_path(typ='software') + installpath_software = os.path.realpath(install_path(typ='software')) set_bwrap_info('installpath_software', installpath_software) - set_bwrap_info('installpath_modules', install_path(typ='modules')) + installpath_modules = os.path.realpath(install_path(typ='modules')) + set_bwrap_info('installpath_modules', installpath_modules) variables = ConfigurationVariables() bwrap_installpath_software = os.path.join(bwrap_installpath, variables['subdir_software']) @@ -117,34 +118,64 @@ def prepare_bwrap(bwrap_installpath): bwrap_cmd = ['bwrap', '--dev-bind', '/', '/'] + mkdir(bwrap_installpath_modules, parents=True) + + bwrap_workdir = os.path.join(bwrap_installpath, 'workdir') + + try: + mkdir(installpath_modules, parents=True) + # copy installpath_modules to bwrap_installpath_modules to ensure all installed modules are available + # required for building multiple unrelated easyconfigs (e.g. easystacks) + if os.path.exists(installpath_modules): + copy_dir(installpath_modules, bwrap_installpath_modules, dirs_exist_ok=True) + # bind mount the modules installpath + bwrap_cmd.extend([ + '--bind', bwrap_installpath_modules, installpath_modules]) + + except EasyBuildError: + # if we can't create the external modules directory, try to use overlayfs + mkdir(bwrap_workdir, parents=True) + bwrap_cmd.extend([ + '--overlay-src', installpath_modules, + '--overlay', bwrap_installpath_modules, bwrap_workdir, installpath_modules]) + + # store bwrap options in a set to avoid duplicate binds + bwrap_opts = set() + # bind mount all software directories for mod in sorted(get_bwrap_info('modules_to_install')): installdir = os.path.join(os.path.realpath(installpath_software), mod) bwrap_installdir = os.path.join(bwrap_installpath_software, mod) + mkdir(bwrap_installdir, parents=True) + use_overlayfs = False try: mkdir(installdir, parents=True) except EasyBuildError: # if we can't create the external installation directory, try to use overlayfs use_overlayfs = True - mkdir(bwrap_installdir, parents=True) + if use_overlayfs: - bwrap_workdir = os.path.join(bwrap_installpath, 'workdir', mod) - mkdir(bwrap_workdir, parents=True) # go up the tree until we find a directory that exists while not os.path.exists(installdir): installdir = os.path.dirname(installdir) bwrap_installdir = os.path.dirname(bwrap_installdir) - opts = ['--overlay-src', installdir, '--overlay', bwrap_installdir, bwrap_workdir, installdir] + mkdir(bwrap_workdir, parents=True) + bwrap_opts.add(( + '--overlay-src', installdir, + '--overlay', bwrap_installdir, bwrap_workdir, installdir)) else: - opts = ['--bind', bwrap_installdir, installdir] - bwrap_cmd.extend(opts) + bwrap_opts.add(( + '--bind', bwrap_installdir, installdir)) + + for x in bwrap_opts: + bwrap_cmd.extend(x) set_bwrap_info('bwrap_cmd', bwrap_cmd) bwrap_cmd_str = ' '.join(bwrap_cmd) # disable `--bwrap` to prepare for a real installation (in bwrap namespace) - bwrap_eb_options = ['--disable-bwrap', f'--installpath-modules={bwrap_installpath_modules}'] + bwrap_eb_options = ['--disable-bwrap'] set_bwrap_info('bwrap_eb_options', bwrap_eb_options) _log.info(f'Info needed for bwrap: {_bwrap_info}') diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 41a4174650..4cbc6f90df 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -877,13 +877,16 @@ def check_module_path(self): self.set_mod_paths() self.log.debug("self.mod_paths set based on $MODULEPATH: %s" % self.mod_paths) - # determine module path for EasyBuild install path to be included in $MODULEPATH - eb_modpath = os.path.join(install_path(typ='modules'), build_option('suffix_modules_path')) - - # make sure EasyBuild module path is in 1st place - mkdir(eb_modpath, parents=True) - self.prepend_module_path(eb_modpath) - self.log.info("Prepended list of module paths with path used by EasyBuild: %s" % eb_modpath) + # avoid creating the module path when in --bwrap mode (before actually running with bwrap), + # in case the user does not have the necessary write permissions + if not build_option('bwrap'): + # determine module path for EasyBuild install path to be included in $MODULEPATH + eb_modpath = os.path.join(install_path(typ='modules'), build_option('suffix_modules_path')) + + # make sure EasyBuild module path is in 1st place + mkdir(eb_modpath, parents=True) + self.prepend_module_path(eb_modpath) + self.log.info("Prepended list of module paths with path used by EasyBuild: %s" % eb_modpath) # set the module path environment accordingly curr_mod_paths = curr_module_paths()