Skip to content

Commit ddb9cae

Browse files
committed
Merge branch '5.0.x' into reprod-tarballs-mac
2 parents 980f618 + b5c1dd9 commit ddb9cae

File tree

26 files changed

+567
-163
lines changed

26 files changed

+567
-163
lines changed

.github/workflows/eb_command.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-20.04
1515
strategy:
1616
matrix:
17-
python: [3.6, 3.7, 3.8, 3.9, '3.10', '3.11', '3.12']
17+
python: [3.6, 3.7, 3.8, 3.9, '3.10', '3.11', '3.12', '3.13']
1818
fail-fast: false
1919
steps:
2020
- uses: actions/checkout@v3

.github/workflows/linting.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-20.04
1414
strategy:
1515
matrix:
16-
python-version: [3.6, 3.7, 3.8, 3.9, '3.10', '3.11', '3.12']
16+
python-version: [3.6, 3.7, 3.8, 3.9, '3.10', '3.11', '3.12', '3.13']
1717
steps:
1818
- uses: actions/checkout@v3
1919

.github/workflows/unit_tests.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ jobs:
4545
modules_tool: ${{needs.setup.outputs.lmod8}}
4646
- python: '3.12'
4747
modules_tool: ${{needs.setup.outputs.lmod8}}
48+
- python: '3.13'
49+
modules_tool: ${{needs.setup.outputs.lmod8}}
4850
# There may be encoding errors in Python 3 which are hidden when an UTF-8 encoding is set
4951
# Hence run the tests (again) with LC_ALL=C and Python 3.6 (or any < 3.7)
5052
- python: 3.6
@@ -152,7 +154,9 @@ jobs:
152154
cd $HOME
153155
# initialize environment for modules tool
154156
if [ -f $HOME/moduleshome ]; then export MODULESHOME=$(cat $HOME/moduleshome); fi
155-
source $(cat $HOME/mod_init); type module
157+
source $(cat $HOME/mod_init)
158+
type module
159+
module --version
156160
# make sure 'eb' is available via $PATH, and that $PYTHONPATH is set (some tests expect that);
157161
# also pick up changes to $PATH set by sourcing $MOD_INIT
158162
export PREFIX=/tmp/$USER/$GITHUB_SHA

easybuild/framework/easyblock.py

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@
7272
from easybuild.tools.build_details import get_build_stats
7373
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, dry_run_msg, dry_run_warning, dry_run_set_dirs
7474
from easybuild.tools.build_log import print_error, print_msg, print_warning
75-
from easybuild.tools.config import CHECKSUM_PRIORITY_JSON, DEFAULT_ENVVAR_USERS_MODULES
75+
from easybuild.tools.config import CHECKSUM_PRIORITY_JSON, DEFAULT_ENVVAR_USERS_MODULES, PYTHONPATH, EBPYTHONPREFIXES
7676
from easybuild.tools.config import FORCE_DOWNLOAD_ALL, FORCE_DOWNLOAD_PATCHES, FORCE_DOWNLOAD_SOURCES
77-
from easybuild.tools.config import EASYBUILD_SOURCES_URL # noqa
77+
from easybuild.tools.config import EASYBUILD_SOURCES_URL # noqa
7878
from easybuild.tools.config import build_option, build_path, get_log_filename, get_repository, get_repositorypath
7979
from easybuild.tools.config import install_path, log_path, package_path, source_paths
8080
from easybuild.tools.environment import restore_env, sanitize_env
@@ -1219,6 +1219,8 @@ def make_devel_module(self, create_in_builddir=False):
12191219
# these should be all the dependencies and we should load them
12201220
recursive_unload = self.cfg['recursive_module_unload']
12211221
depends_on = self.cfg['module_depends_on']
1222+
if depends_on is not None:
1223+
self.log.deprecated("'module_depends_on' easyconfig parameter should not be used anymore", '6.0')
12221224
for key in os.environ:
12231225
# legacy support
12241226
if key.startswith(DEVEL_ENV_VAR_NAME_PREFIX):
@@ -1346,6 +1348,8 @@ def make_module_dep(self, unload_info=None):
13461348
# include load statements for retained dependencies
13471349
recursive_unload = self.cfg['recursive_module_unload']
13481350
depends_on = self.cfg['module_depends_on']
1351+
if depends_on is not None:
1352+
self.log.deprecated("'module_depends_on' easyconfig parameter should not be used anymore", '6.0')
13491353
loads = []
13501354
for dep in deps:
13511355
unload_modules = []
@@ -1390,6 +1394,49 @@ def make_module_description(self):
13901394
"""
13911395
return self.module_generator.get_description()
13921396

1397+
def make_module_pythonpath(self):
1398+
"""
1399+
Add lines for module file to update $PYTHONPATH or $EBPYTHONPREFIXES,
1400+
if they aren't already present and the standard lib/python*/site-packages subdirectory exists
1401+
"""
1402+
lines = []
1403+
if not os.path.isfile(os.path.join(self.installdir, 'bin', 'python')): # only needed when not a python install
1404+
python_subdir_pattern = os.path.join(self.installdir, 'lib', 'python*', 'site-packages')
1405+
candidate_paths = (os.path.relpath(path, self.installdir) for path in glob.glob(python_subdir_pattern))
1406+
python_paths = [path for path in candidate_paths if re.match(r'lib/python\d+\.\d+/site-packages', path)]
1407+
1408+
# determine whether Python is a runtime dependency;
1409+
# if so, we assume it was installed with EasyBuild, and hence is aware of $EBPYTHONPREFIXES
1410+
runtime_deps = [dep['name'] for dep in self.cfg.dependencies(runtime_only=True)]
1411+
1412+
# don't use $EBPYTHONPREFIXES unless we can and it's preferred or necesary (due to use of multi_deps)
1413+
use_ebpythonprefixes = False
1414+
multi_deps = self.cfg['multi_deps']
1415+
1416+
if 'Python' in runtime_deps:
1417+
self.log.info("Found Python runtime dependency, so considering $EBPYTHONPREFIXES...")
1418+
1419+
if build_option('prefer_python_search_path') == EBPYTHONPREFIXES:
1420+
self.log.info("Preferred Python search path is $EBPYTHONPREFIXES, so using that")
1421+
use_ebpythonprefixes = True
1422+
1423+
elif multi_deps and 'Python' in multi_deps:
1424+
self.log.info("Python is listed in 'multi_deps', so using $EBPYTHONPREFIXES instead of $PYTHONPATH")
1425+
use_ebpythonprefixes = True
1426+
1427+
if python_paths:
1428+
# add paths unless they were already added
1429+
if use_ebpythonprefixes:
1430+
path = '' # EBPYTHONPREFIXES are relative to the install dir
1431+
if path not in self.module_generator.added_paths_per_key[EBPYTHONPREFIXES]:
1432+
lines.append(self.module_generator.prepend_paths(EBPYTHONPREFIXES, path))
1433+
else:
1434+
for python_path in python_paths:
1435+
if python_path not in self.module_generator.added_paths_per_key[PYTHONPATH]:
1436+
lines.append(self.module_generator.prepend_paths(PYTHONPATH, python_path))
1437+
1438+
return lines
1439+
13931440
def make_module_extra(self, altroot=None, altversion=None):
13941441
"""
13951442
Set extra stuff in module file, e.g. $EBROOT*, $EBVERSION*, etc.
@@ -1438,6 +1485,9 @@ def make_module_extra(self, altroot=None, altversion=None):
14381485
value, type(value))
14391486
lines.append(self.module_generator.append_paths(key, value, allow_abs=self.cfg['allow_append_abs_path']))
14401487

1488+
# add lines to update $PYTHONPATH or $EBPYTHONPREFIXES
1489+
lines.extend(self.make_module_pythonpath())
1490+
14411491
modloadmsg = self.cfg['modloadmsg']
14421492
if modloadmsg:
14431493
# add trailing newline to prevent that shell prompt is 'glued' to module load message
@@ -2068,20 +2118,33 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
20682118
# if some of the required dependencies are not installed yet, requeue this extension
20692119
elif pending_deps:
20702120

2071-
# make sure all required dependencies are actually going to be installed,
2072-
# to avoid getting stuck in an infinite loop!
2121+
# check whether all required dependency extensions are actually going to be installed;
2122+
# if not, we assume that they are provided by dependencies;
20732123
missing_deps = [x for x in required_deps if x not in all_ext_names]
20742124
if missing_deps:
2075-
raise EasyBuildError("Missing required dependencies for %s are not going to be installed: %s",
2076-
ext.name, ', '.join(missing_deps))
2077-
else:
2078-
self.log.info("Required dependencies missing for extension %s (%s), adding it back to queue...",
2079-
ext.name, ', '.join(pending_deps))
2125+
msg = f"Missing required extensions for {ext.name} not found "
2126+
msg += "in list of extensions being installed, let's assume they are provided by "
2127+
msg += "dependencies and proceed: " + ', '.join(missing_deps)
2128+
self.log.info(msg)
2129+
2130+
msg = f"Pending dependencies for {ext.name} before taking into account missing dependencies: "
2131+
self.log.debug(msg + ', '.join(pending_deps))
2132+
pending_deps = [x for x in pending_deps if x not in missing_deps]
2133+
msg = f"Pending dependencies for {ext.name} after taking into account missing dependencies: "
2134+
self.log.debug(msg + ', '.join(pending_deps))
2135+
2136+
if pending_deps:
2137+
msg = f"Required dependencies not installed yet for extension {ext.name} ("
2138+
msg += ', '.join(pending_deps)
2139+
msg += "), adding it back to queue..."
2140+
self.log.info(msg)
20802141
# purposely adding extension back in the queue at Nth place rather than at the end,
20812142
# since we assume that the required dependencies will be installed soon...
20822143
exts_queue.insert(max_iter, ext)
20832144

2084-
else:
2145+
# list of pending dependencies may be empty now after taking into account required extensions
2146+
# that are not being installed above, so extension may be ready to install
2147+
if not pending_deps:
20852148
tup = (ext.name, ext.version or '')
20862149
print_msg("starting installation of extension %s %s..." % tup, silent=self.silent, log=self.log)
20872150

@@ -3137,15 +3200,21 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
31373200

31383201
fails = []
31393202

3140-
# hard reset $LD_LIBRARY_PATH before running RPATH sanity check
3141-
orig_env = env.unset_env_vars(['LD_LIBRARY_PATH'])
3203+
if build_option('strict_rpath_sanity_check'):
3204+
self.log.info("Unsetting $LD_LIBRARY_PATH since strict RPATH sanity check is enabled...")
3205+
# hard reset $LD_LIBRARY_PATH before running RPATH sanity check
3206+
orig_env = env.unset_env_vars(['LD_LIBRARY_PATH'])
3207+
else:
3208+
self.log.info("Not unsetting $LD_LIBRARY_PATH since strict RPATH sanity check is disabled...")
3209+
orig_env = None
31423210

31433211
ld_library_path = os.getenv('LD_LIBRARY_PATH', '(empty)')
31443212
self.log.debug(f"$LD_LIBRARY_PATH during RPATH sanity check: {ld_library_path}")
31453213
modules_list = self.modules_tool.list()
31463214
self.log.debug(f"List of loaded modules: {modules_list}")
31473215

31483216
not_found_regex = re.compile(r'(\S+)\s*\=\>\s*not found')
3217+
lib_path_regex = re.compile(r'\S+\s*\=\>\s*(\S+)')
31493218
readelf_rpath_regex = re.compile('(RPATH)', re.M)
31503219

31513220
# List of libraries that should be exempt from the RPATH sanity check;
@@ -3191,6 +3260,15 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
31913260
fail_msg = f"Library {match} not found for {path}"
31923261
self.log.warning(fail_msg)
31933262
fails.append(fail_msg)
3263+
3264+
# if any libraries were not found, log whether dependency libraries have an RPATH section
3265+
if fails:
3266+
lib_paths = re.findall(lib_path_regex, out)
3267+
for lib_path in lib_paths:
3268+
self.log.info(f"Checking whether dependency library {lib_path} has RPATH section")
3269+
res = run_shell_cmd(f"readelf -d {lib_path}", fail_on_error=False)
3270+
if res.exit_code:
3271+
self.log.info(f"No RPATH section found in {lib_path}")
31943272
else:
31953273
self.log.debug(f"Output of 'ldd {path}' checked, looks OK")
31963274

@@ -3213,7 +3291,8 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
32133291
else:
32143292
self.log.debug(f"Not sanity checking files in non-existing directory {dirpath}")
32153293

3216-
env.restore_env_vars(orig_env)
3294+
if orig_env:
3295+
env.restore_env_vars(orig_env)
32173296

32183297
return fails
32193298

@@ -4360,7 +4439,9 @@ def build_and_install_one(ecdict, init_env):
43604439
def ensure_writable_log_dir(log_dir):
43614440
"""Make sure we can write into the log dir"""
43624441
if build_option('read_only_installdir'):
4363-
# temporarily re-enable write permissions for copying log/easyconfig to install dir
4442+
# temporarily re-enable write permissions for copying log/easyconfig to install dir,
4443+
# ensuring that we resolve symlinks
4444+
log_dir = os.path.realpath(log_dir)
43644445
if os.path.exists(log_dir):
43654446
adjust_permissions(log_dir, stat.S_IWUSR, add=True, recursive=True)
43664447
else:

easybuild/framework/easyconfig/default.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@
206206
'moduleclass': [MODULECLASS_BASE, 'Module class to be used for this software', MODULES],
207207
'moduleforceunload': [False, 'Force unload of all modules when loading the extension', MODULES],
208208
'moduleloadnoconflict': [False, "Don't check for conflicts, unload other versions instead ", MODULES],
209-
'module_depends_on': [False, 'Use depends_on (Lmod 7.6.1+) for dependencies in generated module '
210-
'(implies recursive unloading of modules).', MODULES],
209+
'module_depends_on': [None, 'Use depends_on (Lmod 7.6.1+) for dependencies in generated module '
210+
'(implies recursive unloading of modules) [DEPRECATED]', MODULES],
211211
'recursive_module_unload': [None, "Recursive unload of all dependencies when unloading module "
212212
"(True/False to hard enable/disable; None implies honoring "
213213
"the --recursive-module-unload EasyBuild configuration setting",

easybuild/framework/easyconfig/easyconfig.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,15 +1105,19 @@ def filter_deps(self, deps):
11051105

11061106
return retained_deps
11071107

1108-
def dependencies(self, build_only=False):
1108+
def dependencies(self, build_only=False, runtime_only=False):
11091109
"""
11101110
Returns an array of parsed dependencies (after filtering, if requested)
11111111
dependency = {'name': '', 'version': '', 'system': (False|True), 'versionsuffix': '', 'toolchain': ''}
11121112
Iterable builddependencies are flattened when not iterating.
11131113
11141114
:param build_only: only return build dependencies, discard others
1115+
:param runtime_only: only return runtime dependencies, discard others
11151116
"""
1116-
deps = self.builddependencies()
1117+
if runtime_only:
1118+
deps = []
1119+
else:
1120+
deps = self.builddependencies()
11171121

11181122
if not build_only:
11191123
# use += rather than .extend to get a new list rather than updating list of build deps in place...

easybuild/framework/easyconfig/templates.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@
103103
'cuda_sm_comma_sep': 'Comma-separated list of sm_* values that correspond with CUDA compute capabilities',
104104
'cuda_sm_space_sep': 'Space-separated list of sm_* values that correspond with CUDA compute capabilities',
105105
'mpi_cmd_prefix': 'Prefix command for running MPI programs (with default number of ranks)',
106+
# can't be a boolean (True/False), must be a string value since it's a string template
107+
'rpath_enabled': "String value indicating whether or not RPATH linking is used ('true' or 'false')",
106108
'software_commit': "Git commit id to use for the software as specified by --software-commit command line option",
107109
'sysroot': "Location root directory of system, prefix for standard paths like /usr/lib and /usr/include"
108110
"as specify by the --sysroot configuration option",
@@ -298,6 +300,9 @@ def template_constant_dict(config, ignore=None, toolchain=None):
298300
# set 'arch' for system architecture based on 'machine' (4th) element of platform.uname() return value
299301
template_values['arch'] = platform.uname()[4]
300302

303+
# set 'rpath' template based on 'rpath' configuration option, using empty string as fallback
304+
template_values['rpath_enabled'] = 'true' if build_option('rpath') else 'false'
305+
301306
# set 'sysroot' template based on 'sysroot' configuration option, using empty string as fallback
302307
template_values['sysroot'] = build_option('sysroot') or ''
303308

easybuild/scripts/findPythonDeps.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,35 @@ def get_dep_tree(package_spec, verbose):
9595

9696
def find_deps(pkgs, dep_tree):
9797
"""Recursively resolve dependencies of the given package(s) and return them"""
98+
MAX_PACKAGES = 1000
9899
res = []
99-
for orig_pkg in pkgs:
100-
pkg = canonicalize_name(orig_pkg)
101-
matching_entries = [entry for entry in dep_tree
102-
if pkg in (entry['package']['package_name'], entry['package']['key'])]
103-
if not matching_entries:
100+
next_pkgs = set(pkgs)
101+
# Don't check any package multiple times to avoid infinite recursion
102+
seen_pkgs = set()
103+
count = 0
104+
while next_pkgs:
105+
cur_pkgs = next_pkgs - seen_pkgs
106+
seen_pkgs.update(cur_pkgs)
107+
next_pkgs = set()
108+
for orig_pkg in cur_pkgs:
109+
count += 1
110+
if count > MAX_PACKAGES:
111+
raise RuntimeError("Aborting after checking %s packages. Possibly cycle detected!" % MAX_PACKAGES)
112+
pkg = canonicalize_name(orig_pkg)
104113
matching_entries = [entry for entry in dep_tree
105-
if orig_pkg in (entry['package']['package_name'], entry['package']['key'])]
106-
if not matching_entries:
107-
raise RuntimeError("Found no installed package for '%s' in %s" % (pkg, dep_tree))
108-
if len(matching_entries) > 1:
109-
raise RuntimeError("Found multiple installed packages for '%s' in %s" % (pkg, dep_tree))
110-
entry = matching_entries[0]
111-
res.append((entry['package']['package_name'], entry['package']['installed_version']))
112-
deps = (dep['package_name'] for dep in entry['dependencies'])
113-
res.extend(find_deps(deps, dep_tree))
114+
if pkg in (entry['package']['package_name'], entry['package']['key'])]
115+
if not matching_entries:
116+
matching_entries = [entry for entry in dep_tree
117+
if orig_pkg in (entry['package']['package_name'], entry['package']['key'])]
118+
if not matching_entries:
119+
raise RuntimeError("Found no installed package for '%s' in %s" % (pkg, dep_tree))
120+
if len(matching_entries) > 1:
121+
raise RuntimeError("Found multiple installed packages for '%s' in %s" % (pkg, dep_tree))
122+
entry = matching_entries[0]
123+
res.append((entry['package']['package_name'], entry['package']['installed_version']))
124+
# Add dependencies to list of packages to check next
125+
# Could call this function recursively but that might exceed the max recursion depth
126+
next_pkgs.update(dep['package_name'] for dep in entry['dependencies'])
114127
return res
115128

116129

easybuild/toolchains/compiler/gcc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class Gcc(Compiler):
8585
COMPILER_UNIQUE_OPTION_MAP['strict'] = []
8686
COMPILER_UNIQUE_OPTION_MAP['precise'] = []
8787
COMPILER_UNIQUE_OPTION_MAP['loose'] = ['fno-math-errno']
88-
COMPILER_UNIQUE_OPTION_MAP['verloose'] = ['fno-math-errno']
88+
COMPILER_UNIQUE_OPTION_MAP['veryloose'] = ['fno-math-errno']
8989

9090
# used when 'optarch' toolchain option is enabled (and --optarch is not specified)
9191
COMPILER_OPTIMAL_ARCHITECTURE_OPTION = {

0 commit comments

Comments
 (0)