Skip to content

Commit 282f068

Browse files
authored
Merge pull request #19 from hiker/linker-lib-flags
Linker flags for common libraries
2 parents a9e1ad5 + f7e40f3 commit 282f068

File tree

7 files changed

+310
-39
lines changed

7 files changed

+310
-39
lines changed

Documentation/source/advanced_config.rst

+8
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ to specify 3rd party libraries at the link stage.
193193
194194
link_exe(state, flags=['-lm', '-lnetcdf'])
195195
196+
Linkers will be pre-configured with flags for common libraries. Where possible,
197+
a library name should be used to include the required flags for linking.
198+
199+
.. code-block::
200+
:linenos:
201+
202+
link_exe(state, libs=['netcdf'])
203+
196204
Path-specific flags
197205
-------------------
198206

source/fab/steps/link.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"""
1010
import logging
1111
from string import Template
12-
from typing import Optional
12+
from typing import List, Optional, Union
1313

1414
from fab.artefacts import ArtefactSet
1515
from fab.steps import step
@@ -33,7 +33,10 @@ def __call__(self, artefact_store):
3333

3434

3535
@step
36-
def link_exe(config, flags=None, source: Optional[ArtefactsGetter] = None):
36+
def link_exe(config,
37+
libs: Union[List[str], None] = None,
38+
flags: Union[List[str], None] = None,
39+
source: Optional[ArtefactsGetter] = None):
3740
"""
3841
Link object files into an executable for every build target.
3942
@@ -49,8 +52,10 @@ def link_exe(config, flags=None, source: Optional[ArtefactsGetter] = None):
4952
The :class:`fab.build_config.BuildConfig` object where we can read
5053
settings such as the project workspace folder or the multiprocessing
5154
flag.
55+
:param libs:
56+
A list of required library names to pass to the linker.
5257
:param flags:
53-
A list of flags to pass to the linker.
58+
A list of additional flags to pass to the linker.
5459
:param source:
5560
An optional :class:`~fab.artefacts.ArtefactsGetter`. It defaults to the
5661
output from compiler steps, which typically is the expected behaviour.
@@ -59,13 +64,15 @@ def link_exe(config, flags=None, source: Optional[ArtefactsGetter] = None):
5964
linker = config.tool_box.get_tool(Category.LINKER, config.mpi)
6065
logger.info(f'Linker is {linker.name}')
6166

62-
flags = flags or []
67+
libs = libs or []
68+
if flags:
69+
linker.add_post_lib_flags(flags)
6370
source_getter = source or DefaultLinkerSource()
6471

6572
target_objects = source_getter(config.artefact_store)
6673
for root, objects in target_objects.items():
6774
exe_path = config.project_workspace / f'{root}'
68-
linker.link(objects, exe_path, openmp=config.openmp, add_libs=flags)
75+
linker.link(objects, exe_path, openmp=config.openmp, libs=libs)
6976
config.artefact_store.add(ArtefactSet.EXECUTABLES, exe_path)
7077

7178

@@ -115,4 +122,5 @@ def link_shared_object(config, output_fpath: str, flags=None,
115122

116123
objects = target_objects[None]
117124
out_name = Template(output_fpath).substitute(output=config.build_output)
118-
linker.link(objects, out_name, openmp=config.openmp, add_libs=flags)
125+
linker.add_post_lib_flags(flags)
126+
linker.link(objects, out_name, openmp=config.openmp)

source/fab/tools/linker.py

+72-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
import os
1111
from pathlib import Path
12-
from typing import cast, List, Optional
12+
from typing import cast, Dict, List, Optional
13+
import warnings
1314

1415
from fab.tools.category import Category
1516
from fab.tools.compiler import Compiler
@@ -51,6 +52,12 @@ def __init__(self, name: Optional[str] = None,
5152
self._compiler = compiler
5253
self.flags.extend(os.getenv("LDFLAGS", "").split())
5354

55+
# Maintain a set of flags for common libraries.
56+
self._lib_flags: Dict[str, List[str]] = {}
57+
# Allow flags to include before or after any library-specific flags.
58+
self._pre_lib_flags: List[str] = []
59+
self._post_lib_flags: List[str] = []
60+
5461
@property
5562
def mpi(self) -> bool:
5663
''':returns: whether the linker supports MPI or not.'''
@@ -66,16 +73,71 @@ def check_available(self) -> bool:
6673

6774
return super().check_available()
6875

76+
def get_lib_flags(self, lib: str) -> List[str]:
77+
'''Gets the standard flags for a standard library
78+
79+
:param lib: the library name
80+
81+
:returns: a list of flags
82+
83+
:raises RuntimeError: if lib is not recognised
84+
'''
85+
try:
86+
return self._lib_flags[lib]
87+
except KeyError:
88+
raise RuntimeError(f"Unknown library name: '{lib}'")
89+
90+
def add_lib_flags(self, lib: str, flags: List[str],
91+
silent_replace: bool = False):
92+
'''Add a set of flags for a standard library
93+
94+
:param lib: the library name
95+
:param flags: the flags to use with the library
96+
:param silent_replace: if set, no warning will be printed when an
97+
existing lib is overwritten.
98+
'''
99+
if lib in self._lib_flags and not silent_replace:
100+
warnings.warn(f"Replacing existing flags for library {lib}: "
101+
f"'{self._lib_flags[lib]}' with "
102+
f"'{flags}'.")
103+
104+
# Make a copy to avoid modifying the caller's list
105+
self._lib_flags[lib] = flags[:]
106+
107+
def remove_lib_flags(self, lib: str):
108+
'''Remove any flags configured for a standard library
109+
110+
:param lib: the library name
111+
'''
112+
try:
113+
del self._lib_flags[lib]
114+
except KeyError:
115+
pass
116+
117+
def add_pre_lib_flags(self, flags: List[str]):
118+
'''Add a set of flags to use before any library-specific flags
119+
120+
:param flags: the flags to include
121+
'''
122+
self._pre_lib_flags.extend(flags)
123+
124+
def add_post_lib_flags(self, flags: List[str]):
125+
'''Add a set of flags to use after any library-specific flags
126+
127+
:param flags: the flags to include
128+
'''
129+
self._post_lib_flags.extend(flags)
130+
69131
def link(self, input_files: List[Path], output_file: Path,
70132
openmp: bool,
71-
add_libs: Optional[List[str]] = None) -> str:
133+
libs: Optional[List[str]] = None) -> str:
72134
'''Executes the linker with the specified input files,
73135
creating `output_file`.
74136
75137
:param input_files: list of input files to link.
76138
:param output_file: output file.
77139
:param openm: whether OpenMP is requested or not.
78-
:param add_libs: additional linker flags.
140+
:param libs: additional libraries to link with.
79141
80142
:returns: the stdout of the link command
81143
'''
@@ -88,7 +150,12 @@ def link(self, input_files: List[Path], output_file: Path,
88150
params = []
89151
# TODO: why are the .o files sorted? That shouldn't matter
90152
params.extend(sorted(map(str, input_files)))
91-
if add_libs:
92-
params += add_libs
153+
154+
if self._pre_lib_flags:
155+
params.extend(self._pre_lib_flags)
156+
for lib in (libs or []):
157+
params.extend(self.get_lib_flags(lib))
158+
if self._post_lib_flags:
159+
params.extend(self._post_lib_flags)
93160
params.extend([self._output_flag, str(output_file)])
94161
return self.run(params)

tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def fixture_mock_linker():
4848
Category.FORTRAN_COMPILER)
4949
mock_linker.run = mock.Mock()
5050
mock_linker._version = (1, 2, 3)
51+
mock_linker.add_lib_flags("netcdf", ["-lnetcdff", "-lnetcdf"])
5152
return mock_linker
5253

5354

tests/unit_tests/steps/test_archive_objects.py

+15-10
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def test_for_exes(self):
5151

5252
# ensure the correct artefacts were created
5353
assert config.artefact_store[ArtefactSet.OBJECT_ARCHIVES] == {
54-
target: set([str(config.build_output / f'{target}.a')]) for target in targets}
54+
target: set([str(config.build_output / f'{target}.a')])
55+
for target in targets}
5556

5657
def test_for_library(self):
5758
'''As used when building an object archive or archiving before linking
@@ -65,32 +66,36 @@ def test_for_library(self):
6566
mock_result = mock.Mock(returncode=0, return_value=123)
6667
with mock.patch('fab.tools.tool.subprocess.run',
6768
return_value=mock_result) as mock_run_command, \
68-
pytest.warns(UserWarning, match="_metric_send_conn not set, cannot send metrics"):
69-
archive_objects(config=config, output_fpath=config.build_output / 'mylib.a')
69+
pytest.warns(UserWarning, match="_metric_send_conn not set, "
70+
"cannot send metrics"):
71+
archive_objects(config=config,
72+
output_fpath=config.build_output / 'mylib.a')
7073

7174
# ensure the correct command line calls were made
7275
mock_run_command.assert_called_once_with([
73-
'ar', 'cr', str(config.build_output / 'mylib.a'), 'util1.o', 'util2.o'],
76+
'ar', 'cr', str(config.build_output / 'mylib.a'), 'util1.o',
77+
'util2.o'],
7478
capture_output=True, env=None, cwd=None, check=False)
7579

7680
# ensure the correct artefacts were created
7781
assert config.artefact_store[ArtefactSet.OBJECT_ARCHIVES] == {
7882
None: set([str(config.build_output / 'mylib.a')])}
7983

80-
def test_incorrect_tool(self):
84+
def test_incorrect_tool(self, mock_c_compiler):
8185
'''Test that an incorrect archive tool is detected
8286
'''
8387

8488
config = BuildConfig('proj', ToolBox(), mpi=False, openmp=False)
8589
tool_box = config.tool_box
86-
cc = tool_box.get_tool(Category.C_COMPILER, config.mpi)
87-
# And set its category to C_COMPILER
90+
cc = mock_c_compiler
91+
# And set its category to be AR
8892
cc._category = Category.AR
89-
# So overwrite the C compiler with the re-categories Fortran compiler
93+
# Now add this 'ar' tool to the tool box
9094
tool_box.add_tool(cc)
9195

9296
with pytest.raises(RuntimeError) as err:
9397
archive_objects(config=config,
9498
output_fpath=config.build_output / 'mylib.a')
95-
assert ("Unexpected tool 'gcc' of type '<class "
96-
"'fab.tools.compiler.Gcc'>' instead of Ar" in str(err.value))
99+
assert ("Unexpected tool 'mock_c_compiler' of type '<class "
100+
"'fab.tools.compiler.CCompiler'>' instead of Ar"
101+
in str(err.value))

tests/unit_tests/steps/test_link.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,20 @@ def test_run(self, tool_box):
3434
linker = Linker("mock_link", "mock_link.exe", "mock-vendor")
3535
# Mark the linker as available to it can be added to the tool box
3636
linker._is_available = True
37+
38+
# Add a custom library to the linker
39+
linker.add_lib_flags('mylib', ['-L/my/lib', '-mylib'])
3740
tool_box.add_tool(linker, silent_replace=True)
3841
mock_result = mock.Mock(returncode=0, stdout="abc\ndef".encode())
3942
with mock.patch('fab.tools.tool.subprocess.run',
4043
return_value=mock_result) as tool_run, \
4144
pytest.warns(UserWarning,
4245
match="_metric_send_conn not "
4346
"set, cannot send metrics"):
44-
link_exe(config, flags=['-fooflag', '-barflag'])
47+
link_exe(config, libs=['mylib'], flags=['-fooflag', '-barflag'])
4548

4649
tool_run.assert_called_with(
4750
['mock_link.exe', '-L/foo1/lib', '-L/foo2/lib', 'bar.o', 'foo.o',
48-
'-fooflag', '-barflag', '-o', 'workspace/foo'],
51+
'-L/my/lib', '-mylib', '-fooflag', '-barflag',
52+
'-o', 'workspace/foo'],
4953
capture_output=True, env=None, cwd=None, check=False)

0 commit comments

Comments
 (0)