diff --git a/.github/workflows/ubuntu-ci-x86_64.yaml b/.github/workflows/ubuntu-ci-x86_64.yaml index 5afece5c1..912c9da95 100644 --- a/.github/workflows/ubuntu-ci-x86_64.yaml +++ b/.github/workflows/ubuntu-ci-x86_64.yaml @@ -145,6 +145,7 @@ jobs: # Next steps: synchronize source and build cache to a central/combined mirror? echo "Next steps ..." + ${SPACK_STACK_DIR}/util/ldd_check.py $SPACK_ENV 2>&1 | tee log.ldd_check spack clean -a spack module tcl refresh -y spack stack setup-meta-modules diff --git a/doc/source/Utilities.rst b/doc/source/Utilities.rst index fad759688..9e8a81a39 100644 --- a/doc/source/Utilities.rst +++ b/doc/source/Utilities.rst @@ -28,6 +28,20 @@ check_permissions.sh The utility located at util/check_permissions.sh can be run inside any spack-stack environment directory intended for multiple users (i.e., on an HPC or cloud platform). It will return errors if the environment directory is inaccessible to non-owning users and groups (i.e., if o+rx not set), as well as if any directories or files have permissions that make them inaccessible to other users. +.. _LDD_Checker: + +------------------------------ +ldd_check.py +------------------------------ + +The util/ldd_check.py utility should be run for new installations to ensure that no shared library or executable that uses shared libraries is missing a shared library dependency. If the script returns a warning for a given file, this may indicate that Spack's RPATH substitution has not been properly applied. In some instances, missing library dependencies may not indicate a problem, such as a library that is intended to be found through $LD_LIBRARY_PATH after, say, a compiler or MPI environment module is loaded. Though these paths should probably also be RPATH-ified, such instances of harmless missing dependencies may be ignored with ldd_check.py's ``--ignore`` option by specifying a Python regular expression to be excluded from consideration (see example below), or can be permanently whitelisted by modifying the ``whitelist`` variable at the top of the ldd_check.py script itself (in which case please submit a PR). The script searches the 'install/' subdirectory of a given path and runs ``ldd`` on all shared objects. The base path to be search can be specified as a lone positional argument, and by default is the current directory. In practice, this should be ``$SPACK_ENV`` for the environment in question. + +.. code-block:: console + + cd $SPACK_ENV && ../../util/ldd_check.py + # - OR - + util/ldd_check.py $SPACK_ENV --ignore '^libfoo.+' # check for missing shared dependencies, but ignore missing libfoo* + .. _Acorn_Utilities: ------------------------------ diff --git a/util/ldd_check.py b/util/ldd_check.py new file mode 100755 index 000000000..af6d3fa77 --- /dev/null +++ b/util/ldd_check.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 + +# Whitelist file patterns (checked with re.match()) that will be satisfied by +# compiler & MPI modules (though arguably these should be added as extra +# rpaths). +whitelist = [ + "^libmkl.+", + "^libifcore.so.*", +] + +######## + +import argparse +import glob +import os +import platform +import re +import subprocess +import sys + +parser = argparse.ArgumentParser(description="Check executables and shared libraries for missing dependencies") + +parser.add_argument( + "path", + nargs="?", + default=os.getcwd(), + help="Spack environment path ($SPACK_ENV) that contains install/ subdirectory (default: current directory)", +) +parser.add_argument( + "--progress", + "-p", + action="store_true", + help="Print progress to stderr", +) +parser.add_argument( + "--ignore", + "-i", + action="append", + help="Ignore pattern (Python re expression, e.g., '^libfoo.+')", +) + +args = parser.parse_args() + +if args.ignore: + whitelist.extend(args.ignore) + +platform = platform.system() +if platform=="Linux": + ldd_cmd = "ldd" + error_pattern = " => not found" + getlibname = lambda line: re.findall("^[^ ]+", line)[0] +elif platform=="Darwin": + print("macOS not yet supported", file=sys.stderr) + sys.exit(1) +else: + print(f"Platform '{platform}' not supported", file=sys.stderr) + sys.exit(1) + +searchpath = os.path.join(args.path, "install") + +bin_list = glob.glob(os.path.join(searchpath, "*/*/*/bin/*")) +dlib_list = glob.glob(os.path.join(searchpath, "*/*/*/lib*/*.{so,dylib}")) + +master_list = bin_list + dlib_list + +assert master_list, "No files found! Check directory and ensure it contains install/ subdirectory" + +iret = 0 + +for i in range(len(master_list)): + file_to_check = master_list[i] + if args.progress: print(f"\rProgress: {i+1}/{len(master_list)}", file=sys.stderr, end="") + p = subprocess.Popen([ldd_cmd, file_to_check], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + raw_output, null = p.communicate() + output = raw_output.decode(sys.stdout.encoding).strip() + if error_pattern in output: + missing_set = set([getlibname(l.strip()) for l in output.split("\n") if error_pattern in l]) + missing_list = [l for l in missing_set if not any([re.match(p, l) for p in whitelist])] + if not missing_list: continue + missing_output = ",".join(sorted(missing_list)) + print(f"\rWARNING: File {file_to_check} contains the following missing libraries: {missing_output}") + iret = 1 + +if args.progress: print() + +sys.exit(iret)