diff --git a/easybuild/easyblocks/j/jaxlib.py b/easybuild/easyblocks/j/jaxlib.py index d4dca3245c..8ed5a4faea 100644 --- a/easybuild/easyblocks/j/jaxlib.py +++ b/easybuild/easyblocks/j/jaxlib.py @@ -37,6 +37,7 @@ import easybuild.tools.environment as env from easybuild.easyblocks.generic.pythonpackage import PythonPackage +from easybuild.easyblocks.tensorflow import det_binutils_bin_path from easybuild.framework.easyconfig import CUSTOM from easybuild.tools.build_log import EasyBuildError from easybuild.tools.filetools import apply_regex_substitutions, which @@ -68,12 +69,9 @@ def configure_step(self): super(EB_jaxlib, self).configure_step() - binutils_root = get_software_root('binutils') - if not binutils_root: - raise EasyBuildError("Failed to determine installation prefix for binutils") config_env_vars = { # This is the binutils bin folder: https://github.com/tensorflow/tensorflow/issues/39263 - 'GCC_HOST_COMPILER_PREFIX': os.path.join(binutils_root, 'bin'), + 'GCC_HOST_COMPILER_PREFIX': det_binutils_bin_path(), } # Collect options for the build script diff --git a/easybuild/easyblocks/t/tensorflow.py b/easybuild/easyblocks/t/tensorflow.py index c72aec1ea1..b6c12838eb 100644 --- a/easybuild/easyblocks/t/tensorflow.py +++ b/easybuild/easyblocks/t/tensorflow.py @@ -40,6 +40,7 @@ import easybuild.tools.environment as env import easybuild.tools.toolchain as toolchain +from easybuild.base import fancylogger from easybuild.easyblocks.generic.pythonpackage import PythonPackage, det_python_version from easybuild.easyblocks.python import EXTS_FILTER_PYTHON_PACKAGES from easybuild.framework.easyconfig import CUSTOM @@ -51,7 +52,7 @@ from easybuild.tools.modules import get_software_root, get_software_version, get_software_libdir from easybuild.tools.run import run_cmd from easybuild.tools.systemtools import AARCH64, X86_64, get_cpu_architecture, get_os_name, get_os_version -from easybuild.tools.toolchain.toolchain import RPATH_WRAPPERS_SUBDIR +from easybuild.tools.toolchain.toolchain import RPATH_WRAPPERS_SUBDIR, Toolchain CPU_DEVICE = 'cpu' @@ -82,6 +83,49 @@ KNOWN_BINUTILS = ('ar', 'as', 'dwp', 'ld', 'ld.bfd', 'ld.gold', 'nm', 'objcopy', 'objdump', 'strip') +_log = fancylogger.getLogger('easyblocks.tensorflow') + + +def det_binutils_bin_path(): + """ + Determine location where binutils' ld command is installed. + + This may be an RPATH wrapper script (when EasyBuild is configured with --rpath). + In that case, symlinks for all binutils commands are collectively created in a new directory. + """ + ld_path = which('ld', on_error=ERROR) + binutils_bin_path = os.path.dirname(ld_path) + if Toolchain.is_rpath_wrapper(ld_path): + # binutils binaries may be expected to all be in a single path, + # but newer EasyBuild puts each in its own subfolder; + # This new layout is: /RPATH_WRAPPERS_SUBDIR/_folder/ + rpath_wrapper_root = os.path.dirname(os.path.dirname(ld_path)) + if os.path.basename(rpath_wrapper_root) == RPATH_WRAPPERS_SUBDIR: + # Add symlinks to each binutils binary into a single folder + new_rpath_wrapper_dir = os.path.join(tempfile.mkdtemp(), RPATH_WRAPPERS_SUBDIR) + binutils_root = get_software_root('binutils') + if binutils_root: + _log.debug("Using binutils dependency at %s to gather binutils files.", binutils_root) + binutils_files = next(os.walk(os.path.join(binutils_root, 'bin')))[2] + else: + # binutils might be filtered (--filter-deps), so recursively gather files in the rpath wrapper dir + binutils_files = {f for (_, _, files) in os.walk(rpath_wrapper_root) for f in files} + # And add known ones + binutils_files.update(KNOWN_BINUTILS) + _log.info("Found %s to be an rpath wrapper. Adding symlinks for binutils (%s) to %s.", + ld_path, ', '.join(binutils_files), new_rpath_wrapper_dir) + mkdir(new_rpath_wrapper_dir) + for file in binutils_files: + # use `which` to take rpath wrappers where available + # Ignore missing ones if binutils was filtered (in which case we used a heuristic) + path = which(file, on_error=ERROR if binutils_root else WARN) + if path: + symlink(path, os.path.join(new_rpath_wrapper_dir, file)) + binutils_bin_path = new_rpath_wrapper_dir + + return binutils_bin_path + + def split_tf_libs_txt(valid_libs_txt): """Split the VALID_LIBS entry from the TF file into single names""" entries = valid_libs_txt.split(',') @@ -477,36 +521,7 @@ def configure_step(self): bazel_max = 64 if get_bazel_version() < '3.0.0' else 128 self.cfg['parallel'] = min(self.cfg['parallel'], bazel_max) - # determine location where binutils' ld command is installed - # note that this may be an RPATH wrapper script (when EasyBuild is configured with --rpath) - ld_path = which('ld', on_error=ERROR) - self.binutils_bin_path = os.path.dirname(ld_path) - if self.toolchain.is_rpath_wrapper(ld_path): - # TF expects all binutils binaries in a single path but newer EB puts each in its own subfolder - # This new layout is: /RPATH_WRAPPERS_SUBDIR/_folder/ - rpath_wrapper_root = os.path.dirname(os.path.dirname(ld_path)) - if os.path.basename(rpath_wrapper_root) == RPATH_WRAPPERS_SUBDIR: - # Add symlinks to each binutils binary into a single folder - new_rpath_wrapper_dir = os.path.join(self.wrapper_dir, RPATH_WRAPPERS_SUBDIR) - binutils_root = get_software_root('binutils') - if binutils_root: - self.log.debug("Using binutils dependency at %s to gather binutils files.", binutils_root) - binutils_files = next(os.walk(os.path.join(binutils_root, 'bin')))[2] - else: - # binutils might be filtered (--filter-deps), so recursively gather files in the rpath wrapper dir - binutils_files = {f for (_, _, files) in os.walk(rpath_wrapper_root) for f in files} - # And add known ones - binutils_files.update(KNOWN_BINUTILS) - self.log.info("Found %s to be an rpath wrapper. Adding symlinks for binutils (%s) to %s.", - ld_path, ', '.join(binutils_files), new_rpath_wrapper_dir) - mkdir(new_rpath_wrapper_dir) - for file in binutils_files: - # use `which` to take rpath wrappers where available - # Ignore missing ones if binutils was filtered (in which case we used a heuristic) - path = which(file, on_error=ERROR if binutils_root else WARN) - if path: - symlink(path, os.path.join(new_rpath_wrapper_dir, file)) - self.binutils_bin_path = new_rpath_wrapper_dir + self.binutils_bin_path = det_binutils_bin_path() # filter out paths from CPATH and LIBRARY_PATH. This is needed since bazel will pull some dependencies that # might conflict with dependencies on the system and/or installed with EB. For example: protobuf diff --git a/test/easyblocks/easyblock_specific.py b/test/easyblocks/easyblock_specific.py index e17822318e..cc2e820ad6 100644 --- a/test/easyblocks/easyblock_specific.py +++ b/test/easyblocks/easyblock_specific.py @@ -41,6 +41,7 @@ from easybuild.base.testing import TestCase from easybuild.easyblocks.generic.cmakemake import det_cmake_version from easybuild.easyblocks.generic.toolchain import Toolchain +from easybuild.easyblocks.tensorflow import det_binutils_bin_path from easybuild.framework.easyblock import EasyBlock, get_easyblock_instance from easybuild.framework.easyconfig.easyconfig import process_easyconfig from easybuild.tools import config @@ -51,6 +52,7 @@ from easybuild.tools.modules import modules_tool from easybuild.tools.options import set_tmpdir from easybuild.tools.py2vs3 import StringIO +from easybuild.tools.toolchain.toolchain import RPATH_WRAPPERS_SUBDIR class EasyBlockSpecificTest(TestCase): @@ -359,6 +361,54 @@ def test_symlink_dist_site_packages(self): self.assertTrue(os.path.isdir(lib64_site_path)) self.assertFalse(os.path.islink(lib64_site_path)) + def test_det_binutils_bin_path(self): + """Test det_binutils_bin_path provided by TensorFlow easyblock.""" + binutils_bin_path = det_binutils_bin_path() + self.assertTrue(os.path.join(binutils_bin_path, 'ld')) + self.assertFalse(RPATH_WRAPPERS_SUBDIR in binutils_bin_path) + + wrappers_dir = os.path.join(self.tmpdir, 'fake_wrappers', RPATH_WRAPPERS_SUBDIR) + + # put fake wrappers in place for a couple of binutils command + binutils_cmds = ('as', 'ld', 'nm', 'objdump') + for cmd in binutils_cmds: + wrapper_dir = os.path.join(wrappers_dir, '%s.wrapper' % cmd) + os.environ['PATH'] = wrapper_dir + ':' + os.getenv('PATH') + wrapper = os.path.join(wrapper_dir, cmd) + wrapper_txt = "CMD=%s; rpath_args.py $CMD" % cmd + write_file(wrapper, wrapper_txt) + adjust_permissions(wrapper, stat.S_IXUSR) + + # if $EBROOTBINUTILS is set, binutils commands to consider is determined by contents of $EBROOTBINUTILS/bin + binutils_root = os.path.join(self.tmpdir, 'binutils_root') + for cmd in binutils_cmds[:2]: + cmd_path = os.path.join(binutils_root, 'bin', cmd) + write_file(cmd_path, '#!/bin/bash\necho %s' % cmd) + adjust_permissions(cmd_path, stat.S_IXUSR) + os.environ['EBROOTBINUTILS'] = binutils_root + + binutils_bin_path = det_binutils_bin_path() + self.assertEqual(os.path.basename(binutils_bin_path), RPATH_WRAPPERS_SUBDIR) + self.assertEqual(sorted(os.listdir(binutils_bin_path)), ['as', 'ld']) + for cmd in binutils_cmds[:2]: + cmd_path = os.path.join(binutils_bin_path, cmd) + self.assertTrue(os.path.islink(cmd_path)) + expected_target = os.path.join(wrappers_dir, '%s.wrapper' % cmd, cmd) + self.assertEqual(os.path.realpath(cmd_path), os.path.realpath(expected_target)) + + del os.environ['EBROOTBINUTILS'] + + # if $EBROOTBINUTILS is not set, a pre-defined list of known binutils commands is used (KNOWN_BINUTILS constant) + binutils_bin_path = det_binutils_bin_path() + self.assertEqual(os.path.basename(binutils_bin_path), RPATH_WRAPPERS_SUBDIR) + found_cmds = os.listdir(binutils_bin_path) + self.assertTrue(all(x in found_cmds for x in binutils_cmds)) + for cmd in binutils_cmds: + cmd_path = os.path.join(binutils_bin_path, cmd) + self.assertTrue(os.path.islink(cmd_path)) + expected_target = os.path.join(wrappers_dir, '%s.wrapper' % cmd, cmd) + self.assertEqual(os.path.realpath(cmd_path), os.path.realpath(expected_target)) + def suite(): """Return all easyblock-specific tests."""