Skip to content

Commit

Permalink
flesh out logic to determine location to binutils commands into a sta…
Browse files Browse the repository at this point in the history
…ndalone function det_binutils_bin_path in TensorFlow easyblock, and leverage it from jaxlib easyblock
  • Loading branch information
boegel committed Oct 14, 2024
1 parent 5cdeaef commit 38a825c
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 35 deletions.
6 changes: 2 additions & 4 deletions easybuild/easyblocks/j/jaxlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
77 changes: 46 additions & 31 deletions easybuild/easyblocks/t/tensorflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down Expand Up @@ -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: <prefix>/RPATH_WRAPPERS_SUBDIR/<util>_folder/<util>
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(',')
Expand Down Expand Up @@ -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: <prefix>/RPATH_WRAPPERS_SUBDIR/<util>_folder/<util>
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
Expand Down
50 changes: 50 additions & 0 deletions test/easyblocks/easyblock_specific.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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."""
Expand Down

0 comments on commit 38a825c

Please sign in to comment.