Skip to content

Commit

Permalink
Add util to check executables/shared libs for missing shared libs (#890)
Browse files Browse the repository at this point in the history
* Add util to check executables/shared libs for missing shared libs

* add ldd_check.py to CI

* add ignore option and ignore libifcore.so

* add ldd_check.py to utils docs

* fix doco and bug for ldd_check.py

* Update ldd_check.py
  • Loading branch information
AlexanderRichert-NOAA authored Dec 8, 2023
1 parent 70b8072 commit 8daa0de
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ubuntu-ci-x86_64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions doc/source/Utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

------------------------------
Expand Down
86 changes: 86 additions & 0 deletions util/ldd_check.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 8daa0de

Please sign in to comment.