Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

flesh out logic to determine location to binutils commands into a standalone function det_binutils_bin_path in TensorFlow easyblock, and leverage it from jaxlib easyblock #3486

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading