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

add --search-path-linker option to control linker options at build time #4697

Merged
merged 10 commits into from
Dec 9, 2024
1 change: 1 addition & 0 deletions easybuild/tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'rpath_override_dirs',
'required_linked_shared_libs',
'search_path_cpp_headers',
'search_path_linker',
'skip',
'software_commit',
'stop',
Expand Down
6 changes: 4 additions & 2 deletions easybuild/tools/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
from easybuild.tools.run import run_shell_cmd
from easybuild.tools.package.utilities import avail_package_naming_schemes
from easybuild.tools.toolchain.compiler import DEFAULT_OPT_LEVEL, OPTARCH_MAP_CHAR, OPTARCH_SEP, Compiler
from easybuild.tools.toolchain.toolchain import SEARCH_PATH_CPP_HEADERS, DEFAULT_SEARCH_PATH_CPP_HEADERS
from easybuild.tools.toolchain.toolchain import DEFAULT_SEARCH_PATH_CPP_HEADERS, DEFAULT_SEARCH_PATH_LINKER, SEARCH_PATH
from easybuild.tools.toolchain.toolchain import SYSTEM_TOOLCHAIN_NAME
from easybuild.tools.repository.repository import avail_repositories
from easybuild.tools.systemtools import DARWIN, UNKNOWN, check_python_version, get_cpu_architecture, get_cpu_family
Expand Down Expand Up @@ -639,7 +639,9 @@ def config_options(self):
"For more info, use --avail-repositories."),
'strlist', 'store', self.default_repositorypath),
'search-path-cpp-headers': ("Search path used at build time for include directories", 'choice',
'store', DEFAULT_SEARCH_PATH_CPP_HEADERS, [*SEARCH_PATH_CPP_HEADERS]),
'store', DEFAULT_SEARCH_PATH_CPP_HEADERS, [*SEARCH_PATH["cpp_headers"]]),
'search-path-linker': ("Search path used at build time by the linker for libraries", 'choice',
'store', DEFAULT_SEARCH_PATH_LINKER, [*SEARCH_PATH["linker"]]),
'sourcepath': ("Path(s) to where sources should be downloaded (string, colon-separated)",
None, 'store', mk_full_default_path('sourcepath')),
'subdir-modules': ("Installpath subdir for modules", None, 'store', DEFAULT_PATH_SUBDIRS['subdir_modules']),
Expand Down
1 change: 1 addition & 0 deletions easybuild/tools/toolchain/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class Compiler(Toolchain):
'packed-linker-options': (False, "Pack the linker options as comma separated list"), # ScaLAPACK mainly
'rpath': (True, "Use RPATH wrappers when --rpath is enabled in EasyBuild configuration"),
'search-path-cpp-headers': (None, "Search path used at build time for include directories"),
'search-path-linker': (None, "Search path used at build time by the linker for libraries"),
'extra_cflags': (None, "Specify extra CFLAGS options."),
'extra_cxxflags': (None, "Specify extra CXXFLAGS options."),
'extra_fflags': (None, "Specify extra FFLAGS options."),
Expand Down
7 changes: 4 additions & 3 deletions easybuild/tools/toolchain/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@
('PRECFLAGS', 'FP precision flags'),
] + COMPILER_FLAGS,
LibraryList: [
('LIBS', 'Libraries'), # TODO: where are these used? ld?
('FLIBS', 'Fortran libraries'), # TODO: where are these used? gfortran only?
('LIBS', 'Libraries'), # -l options to pass to the linker (C/C++/Fortran)
('FLIBS', 'Fortran libraries'), # linker flags (e.g. -L and -l) for Fortran libraries
],
LinkLibraryPaths: [
('LDFLAGS', 'Flags passed to linker'), # TODO: overridden by command line?
('LDFLAGS', 'Linker flags'),
],
IncludePaths: [
('CPPFLAGS', 'Preprocessor flags'),
Expand All @@ -72,6 +72,7 @@
('C_INCLUDE_PATH', 'Location of C header files'),
('CPLUS_INCLUDE_PATH', 'Location of C++ header files'),
('OBJC_INCLUDE_PATH', 'Location of Objective C header files'),
('LIBRARY_PATH', 'Location of linker files'),
],
CommandFlagList: COMPILER_VARIABLES,
}
Expand Down
82 changes: 49 additions & 33 deletions easybuild/tools/toolchain/toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,22 @@
TOOLCHAIN_CAPABILITY_LAPACK_FAMILY,
TOOLCHAIN_CAPABILITY_MPI_FAMILY,
]
# modes to handle CPP header search paths
# modes to handle header and linker search paths
# see: https://gcc.gnu.org/onlinedocs/cpp/Environment-Variables.html
SEARCH_PATH_CPP_HEADERS_FLAGS = "CPPFLAGS"
SEARCH_PATH_CPP_HEADERS_CPATH = "CPATH"
SEARCH_PATH_CPP_HEADERS_INCLUDE = "INCLUDE_PATHS"
SEARCH_PATH_CPP_HEADERS = {
SEARCH_PATH_CPP_HEADERS_FLAGS: ["CPPFLAGS"],
SEARCH_PATH_CPP_HEADERS_CPATH: ["CPATH"],
SEARCH_PATH_CPP_HEADERS_INCLUDE: ["C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH"],
# supported on Linux by: GCC, GFortran, oneAPI C/C++ Compilers, oneAPI Fortran Compiler, LLVM-based
SEARCH_PATH = {
"cpp_headers": {
"flags": ["CPPFLAGS"],
"cpath": ["CPATH"],
"include_paths": ["C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH"],
},
"linker": {
"flags": ["LDFLAGS"],
"library_path": ["LIBRARY_PATH"],
},
}
DEFAULT_SEARCH_PATH_CPP_HEADERS = SEARCH_PATH_CPP_HEADERS_FLAGS
DEFAULT_SEARCH_PATH_CPP_HEADERS = "flags"
DEFAULT_SEARCH_PATH_LINKER = "flags"


def is_system_toolchain(tc_name):
Expand Down Expand Up @@ -225,6 +230,11 @@ def __init__(self, name=None, version=None, mns=None, class_constants=None, tcde

self.use_rpath = False

self.search_path = {
"cpp_headers": DEFAULT_SEARCH_PATH_CPP_HEADERS,
"linker": DEFAULT_SEARCH_PATH_LINKER,
}

self.mns = mns
self.mod_full_name = None
self.mod_short_name = None
Expand Down Expand Up @@ -372,9 +382,11 @@ def get_variable(self, name, typ=str):
return res

def set_variables(self):
"""Do nothing? Everything should have been set by others
Needs to be defined for super() relations
"""
No generic toolchain variables set.
Post-process variables set by child Toolchain classes.
"""

if self.options.option('packed-linker-options'):
self.log.devel("set_variables: toolchain variables. packed-linker-options.")
self.variables.try_function_on_element('set_packed_linker_options')
Expand Down Expand Up @@ -759,6 +771,27 @@ def _verify_toolchain(self):
raise EasyBuildError("List of toolchain dependency modules and toolchain definition do not match "
"(found %s vs expected %s)", self.toolchain_dep_mods, toolchain_definition)

def _validate_search_path(self):
"""
Validate search path toolchain options.
Toolchain option has precedence over build option
"""
for search_path in self.search_path:
sp_build_opt = f"search_path_{search_path}"
sp_toolchain_opt = sp_build_opt.replace("_", "-")
if self.options.get(sp_toolchain_opt) is not None:
self.search_path[search_path] = self.options.option(sp_toolchain_opt)
elif build_option(sp_build_opt) is not None:
self.search_path[search_path] = build_option(sp_build_opt)

if self.search_path[search_path] not in SEARCH_PATH[search_path]:
raise EasyBuildError(
"Unknown value selected for toolchain option %s: %s. Choose one of: %s",
sp_toolchain_opt, self.search_path[search_path], ", ".join(SEARCH_PATH[search_path])
)

self.log.debug("%s toolchain option set to: %s", sp_toolchain_opt, self.search_path[search_path])

def symlink_commands(self, paths):
"""
Create a symlink for each command to binary/script at specified path.
Expand Down Expand Up @@ -838,7 +871,6 @@ def prepare(self, onlymod=None, deps=None, silent=False, loadmod=True,
self._load_modules(silent=silent)

if self.is_system_toolchain():

# define minimal build environment when using system toolchain;
# this is mostly done to try controlling which compiler commands are being used,
# cfr. https://github.com/easybuilders/easybuild-framework/issues/3398
Expand All @@ -851,6 +883,7 @@ def prepare(self, onlymod=None, deps=None, silent=False, loadmod=True,
self._verify_toolchain()

# Generate the variables to be set
self._validate_search_path()
self.set_variables()

# set the variables
Expand Down Expand Up @@ -1091,24 +1124,7 @@ def _add_dependency_cpp_headers(self, dep_root, extra_dirs=None):
header_dirs = ["include"]
header_dirs = unique_ordered_extend(header_dirs, extra_dirs)

# mode of operation is defined by search-path-cpp-headers option
# toolchain option has precedence over build option
cpp_headers_mode = DEFAULT_SEARCH_PATH_CPP_HEADERS
build_opt = build_option("search_path_cpp_headers")
if self.options.get("search-path-cpp-headers") is not None:
cpp_headers_mode = self.options.option("search-path-cpp-headers")
self.log.debug("search-path-cpp-headers set by toolchain option: %s", cpp_headers_mode)
elif build_opt is not None:
cpp_headers_mode = build_opt
self.log.debug("search-path-cpp-headers set by build option: %s", cpp_headers_mode)

if cpp_headers_mode not in SEARCH_PATH_CPP_HEADERS:
raise EasyBuildError(
"Unknown value selected for option search-path-cpp-headers: %s. Choose one of: %s",
cpp_headers_mode, ", ".join(SEARCH_PATH_CPP_HEADERS)
)

for env_var in SEARCH_PATH_CPP_HEADERS[cpp_headers_mode]:
for env_var in SEARCH_PATH["cpp_headers"][self.search_path["cpp_headers"]]:
self.log.debug("Adding header paths to toolchain variable '%s': %s", env_var, dep_root)
self.variables.append_subdirs(env_var, dep_root, subdirs=header_dirs)

Expand All @@ -1122,9 +1138,9 @@ def _add_dependency_linker_paths(self, dep_root, extra_dirs=None):
lib_dirs = ["lib64", "lib"]
lib_dirs = unique_ordered_extend(lib_dirs, extra_dirs)

env_var = "LDFLAGS"
self.log.debug("Adding lib paths to toolchain variable '%s': %s", env_var, dep_root)
self.variables.append_subdirs(env_var, dep_root, subdirs=lib_dirs)
for env_var in SEARCH_PATH["linker"][self.search_path["linker"]]:
self.log.debug("Adding lib paths to toolchain variable '%s': %s", env_var, dep_root)
self.variables.append_subdirs(env_var, dep_root, subdirs=lib_dirs)

def _setenv_variables(self, donotset=None, verbose=True):
"""Actually set the environment variables"""
Expand Down
50 changes: 46 additions & 4 deletions test/framework/toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -958,9 +958,9 @@ def test_precision_flags(self):
def test_search_path_cpp_headers(self):
"""Test functionality behind search-path-cpp-headers option"""
cpp_headers_mode = {
"CPPFLAGS": ["CPPFLAGS"],
"CPATH": ["CPATH"],
"INCLUDE_PATHS": ["C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH"],
"flags": ["CPPFLAGS"],
"cpath": ["CPATH"],
"include_paths": ["C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH", "OBJC_INCLUDE_PATH"],
}
# test without toolchain option
for build_opt in cpp_headers_mode:
Expand Down Expand Up @@ -994,7 +994,49 @@ def test_search_path_cpp_headers(self):
tc = self.get_toolchain("foss", version="2018a")
tc.set_options({"search-path-cpp-headers": "WRONG_MODE"})
with self.mocked_stdout_stderr():
error_pattern = "Unknown value selected for option search-path-cpp-headers"
error_pattern = "Unknown value selected for toolchain option search-path-cpp-headers"
self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
self.modtool.purge()

def test_search_path_linker(self):
"""Test functionality behind search-path-linker option"""
linker_mode = {
"flags": ["LDFLAGS"],
"library_path": ["LIBRARY_PATH"],
}
# test without toolchain option
for build_opt in linker_mode:
init_config(build_options={"search_path_linker": build_opt, "silent": True})
tc = self.get_toolchain("foss", version="2018a")
with self.mocked_stdout_stderr():
tc.prepare()
for env_var in linker_mode[build_opt]:
assert_fail_msg = (
f"Variable {env_var} required by search-path-linker build option '{build_opt}' "
"not found in toolchain environment"
)
self.assertIn(env_var, tc.variables, assert_fail_msg)
self.modtool.purge()
# test with toolchain option
for build_opt in linker_mode:
init_config(build_options={"search_path_linker": build_opt, "silent": True})
for tc_opt in linker_mode:
tc = self.get_toolchain("foss", version="2018a")
tc.set_options({"search-path-linker": tc_opt})
with self.mocked_stdout_stderr():
tc.prepare()
for env_var in linker_mode[tc_opt]:
assert_fail_msg = (
f"Variable {env_var} required by search-path-linker toolchain option '{tc_opt}' "
"not found in toolchain environment"
)
self.assertIn(env_var, tc.variables, assert_fail_msg)
self.modtool.purge()
# test wrong toolchain option
tc = self.get_toolchain("foss", version="2018a")
tc.set_options({"search-path-linker": "WRONG_MODE"})
with self.mocked_stdout_stderr():
error_pattern = "Unknown value selected for toolchain option search-path-linker"
self.assertErrorRegex(EasyBuildError, error_pattern, tc.prepare)
self.modtool.purge()

Expand Down
Loading