-
Notifications
You must be signed in to change notification settings - Fork 203
/
toolchain.py
1166 lines (960 loc) · 51.3 KB
/
toolchain.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# #
# Copyright 2012-2022 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
# with support of Ghent University (http://ugent.be/hpc),
# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
#
# https://github.com/easybuilders/easybuild
#
# EasyBuild is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation v2.
#
# EasyBuild is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with EasyBuild. If not, see <http://www.gnu.org/licenses/>.
# #
"""
The toolchain module with the abstract Toolchain class.
Creating a new toolchain should be as simple as possible.
Toolchain terminology
---------------------
Toolchain: group of development related utilities (eg compiler) and libraries (eg MPI, linear algebra)
-> eg tc=Toolchain()
Toolchain options : options passed to the toolchain through the easyconfig file
-> eg tc.options
Options : all options passed to an executable
Flags: specific subset of options, typically involved with compilation
-> eg tc.variables.CFLAGS
LinkOptions: specific subset of options, typically involved with linking
-> eg tc.variables.LIBBLAS
TooclchainVariables: list of environment variables that are set when the toolchain is initialised
and the toolchain options have been parsed.
-> eg tc.variables['X'] will be available as os.environ['X']
:author: Stijn De Weirdt (Ghent University)
:author: Kenneth Hoste (Ghent University)
"""
import copy
import os
import stat
import sys
import tempfile
from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError, dry_run_msg, print_warning
from easybuild.tools.config import build_option, install_path
from easybuild.tools.environment import setvar
from easybuild.tools.filetools import adjust_permissions, find_eb_script, read_file, which, write_file
from easybuild.tools.module_generator import dependencies_for
from easybuild.tools.modules import get_software_root, get_software_root_env_var_name
from easybuild.tools.modules import get_software_version, get_software_version_env_var_name
from easybuild.tools.systemtools import LINUX, get_os_type
from easybuild.tools.toolchain.options import ToolchainOptions
from easybuild.tools.toolchain.toolchainvariables import ToolchainVariables
from easybuild.tools.utilities import nub, trace_msg
_log = fancylogger.getLogger('tools.toolchain', fname=False)
# name/version for dummy toolchain
# if name==DUMMY_TOOLCHAIN_NAME and version==DUMMY_TOOLCHAIN_VERSION, do not load dependencies
# NOTE: use of 'dummy' toolchain is deprecated, replaced by 'system' toolchain (which always loads dependencies)
DUMMY_TOOLCHAIN_NAME = 'dummy'
DUMMY_TOOLCHAIN_VERSION = 'dummy'
SYSTEM_TOOLCHAIN_NAME = 'system'
CCACHE = 'ccache'
F90CACHE = 'f90cache'
RPATH_WRAPPERS_SUBDIR = 'rpath_wrappers'
# available capabilities of toolchains
# values match method names supported by Toolchain class (except for 'cuda')
TOOLCHAIN_CAPABILITY_BLAS_FAMILY = 'blas_family'
TOOLCHAIN_CAPABILITY_COMP_FAMILY = 'comp_family'
TOOLCHAIN_CAPABILITY_CUDA = 'cuda'
TOOLCHAIN_CAPABILITY_LAPACK_FAMILY = 'lapack_family'
TOOLCHAIN_CAPABILITY_MPI_FAMILY = 'mpi_family'
TOOLCHAIN_CAPABILITIES = [
TOOLCHAIN_CAPABILITY_BLAS_FAMILY,
TOOLCHAIN_CAPABILITY_COMP_FAMILY,
TOOLCHAIN_CAPABILITY_CUDA,
TOOLCHAIN_CAPABILITY_LAPACK_FAMILY,
TOOLCHAIN_CAPABILITY_MPI_FAMILY,
]
def is_system_toolchain(tc_name):
"""Return whether toolchain with specified name is a system toolchain or not."""
return tc_name in [DUMMY_TOOLCHAIN_NAME, SYSTEM_TOOLCHAIN_NAME]
def env_vars_external_module(name, version, metadata):
"""
Determine $EBROOT* and/or $EBVERSION* environment variables that can be set for external module,
based on the provided name, version and metadata.
"""
env_vars = {}
# define $EBROOT env var for install prefix, picked up by get_software_root
prefix = metadata.get('prefix')
if prefix is not None:
# the prefix can be specified in a number of ways
# * name of environment variable (+ optional relative path to combine it with; format: <name>/<relpath>
# * filepath (assumed if environment variable is not defined)
parts = prefix.split(os.path.sep)
env_var = parts[0]
if env_var in os.environ:
prefix = os.environ[env_var]
rel_path = os.path.sep.join(parts[1:])
if rel_path:
prefix = os.path.join(prefix, rel_path, '')
_log.debug("Derived prefix for software named %s from $%s (rel path: %s): %s",
name, env_var, rel_path, prefix)
else:
_log.debug("Using specified path as prefix for software named %s: %s", name, prefix)
env_vars[get_software_root_env_var_name(name)] = prefix
# define $EBVERSION env var for software version, picked up by get_software_version
if version is not None:
env_vars[get_software_version_env_var_name(name)] = version
return env_vars
class Toolchain(object):
"""General toolchain class"""
OPTIONS_CLASS = ToolchainOptions
VARIABLES_CLASS = ToolchainVariables
NAME = None
VERSION = None
SUBTOOLCHAIN = None
TOOLCHAIN_FAMILY = None
# list of class 'constants' that should be restored for every new instance of this class
CLASS_CONSTANTS_TO_RESTORE = None
CLASS_CONSTANT_COPIES = {}
# class method
def _is_toolchain_for(cls, name):
"""see if this class can provide support for toolchain named name"""
# TODO report later in the initialization the found version
if name:
if hasattr(cls, 'NAME') and name == cls.NAME:
return True
else:
return False
else:
# is no name is supplied, check whether class can be used as a toolchain
return hasattr(cls, 'NAME') and cls.NAME
_is_toolchain_for = classmethod(_is_toolchain_for)
def __init__(self, name=None, version=None, mns=None, class_constants=None, tcdeps=None, modtool=None,
hidden=False):
"""
Toolchain constructor.
:param name: toolchain name
:param version: toolchain version
:param mns: module naming scheme to use
:param class_constants: toolchain 'constants' to define
:param tcdeps: list of toolchain 'dependencies' (i.e., the toolchain components)
:param modtool: ModulesTool instance to use
:param hidden: bool indicating whether toolchain is hidden or not
"""
self.base_init()
self.dependencies = []
self.toolchain_dep_mods = []
self.cached_compilers = set()
if name is None:
name = self.NAME
if name is None:
raise EasyBuildError("Toolchain init: no name provided")
self.name = name
if self.name == DUMMY_TOOLCHAIN_NAME:
self.log.deprecated("Use of 'dummy' toolchain is deprecated, use 'system' toolchain instead", '5.0',
silent=build_option('silent'))
self.name = SYSTEM_TOOLCHAIN_NAME
if version is None:
version = self.VERSION
if version is None:
raise EasyBuildError("Toolchain init: no version provided")
self.version = version
self.modules = []
self.vars = None
self._init_class_constants(class_constants)
self.tcdeps = tcdeps
# toolchain instances are created before initiating build options sometimes, e.g. for --list-toolchains
self.dry_run = build_option('extended_dry_run', default=False)
hidden_toolchains = build_option('hide_toolchains', default=None) or []
self.hidden = hidden or (name in hidden_toolchains)
self.modules_tool = modtool
self.use_rpath = False
self.mns = mns
self.mod_full_name = None
self.mod_short_name = None
self.init_modpaths = None
if not self.is_system_toolchain():
# sometimes no module naming scheme class instance can/will be provided, e.g. with --list-toolchains
if self.mns is not None:
tc_dict = self.as_dict()
self.mod_full_name = self.mns.det_full_module_name(tc_dict)
self.mod_short_name = self.mns.det_short_module_name(tc_dict)
self.init_modpaths = self.mns.det_init_modulepaths(tc_dict)
def is_system_toolchain(self):
"""Return boolean to indicate whether this toolchain is a system(/dummy) toolchain."""
return is_system_toolchain(self.name)
def set_minimal_build_env(self):
"""Set up a minimal build environment, by setting (only) the $CC and $CXX environment variables."""
# this is only relevant when using a system toolchain,
# for proper toolchains these variables will get set via the call to set_variables()
minimal_build_env_raw = build_option('minimal_build_env')
minimal_build_env = {}
for key_val in minimal_build_env_raw.split(','):
parts = key_val.split(':')
if len(parts) == 2:
key, val = parts
minimal_build_env[key] = val
else:
raise EasyBuildError("Incorrect mapping in --minimal-build-env value: '%s'", key_val)
env_vars = {}
for key, val in minimal_build_env.items():
# for key environment variables like $CC and $CXX we are extra careful,
# by making sure the specified command is actually available
if key in ['CC', 'CXX']:
warning_msg = None
if os.path.isabs(val):
if os.path.exists(val):
self.log.info("Specified path for $%s exists: %s", key, val)
env_vars.update({key: val})
else:
warning_msg = "Specified path '%s' does not exist"
else:
cmd_path = which(val)
if cmd_path:
self.log.info("Found compiler command %s at %s, so setting $%s in minimal build environment",
val, cmd_path, key)
env_vars.update({key: val})
else:
warning_msg = "'%s' command not found in $PATH" % val
if warning_msg:
print_warning(warning_msg + ", not setting $%s in minimal build environment" % key, log=self.log)
else:
# no checking for environment variables other than $CC or $CXX
env_vars.update({key: val})
# set specified environment variables, but print a warning
# if we're redefining anything that was already set to a *different* value
for key, new_value in env_vars.items():
curr_value = os.getenv(key)
if curr_value and curr_value != new_value:
print_warning("$%s was defined as '%s', but is now set to '%s' in minimal build environment",
key, curr_value, new_value)
setvar(key, new_value)
def base_init(self):
"""Initialise missing class attributes (log, options, variables)."""
if not hasattr(self, 'log'):
self.log = fancylogger.getLogger(self.__class__.__name__, fname=False)
if not hasattr(self, 'options'):
self.options = self.OPTIONS_CLASS()
if not hasattr(self, 'variables'):
self.variables_init()
def variables_init(self):
"""Initialise toolchain variables."""
self.variables = self.VARIABLES_CLASS()
if hasattr(self, 'LINKER_TOGGLE_START_STOP_GROUP'):
self.variables.LINKER_TOGGLE_START_STOP_GROUP = self.LINKER_TOGGLE_START_STOP_GROUP
if hasattr(self, 'LINKER_TOGGLE_STATIC_DYNAMIC'):
self.variables.LINKER_TOGGLE_STATIC_DYNAMIC = self.LINKER_TOGGLE_STATIC_DYNAMIC
def _init_class_constants(self, class_constants):
"""Initialise class 'constants'."""
# make sure self.CLASS_CONSTANTS_TO_RESTORE is initialised
if class_constants is None:
self.CLASS_CONSTANTS_TO_RESTORE = []
else:
self.CLASS_CONSTANTS_TO_RESTORE = class_constants[:]
self._copy_class_constants()
self._restore_class_constants()
def _copy_class_constants(self):
"""Copy class constants that needs to be restored again when a new instance is created."""
# this only needs to be done the first time (for this class, taking inheritance into account is key)
key = self.__class__
if key not in self.CLASS_CONSTANT_COPIES:
self.CLASS_CONSTANT_COPIES[key] = {}
for cst in self.CLASS_CONSTANTS_TO_RESTORE:
if hasattr(self, cst):
self.CLASS_CONSTANT_COPIES[key][cst] = copy.deepcopy(getattr(self, cst))
else:
raise EasyBuildError("Class constant '%s' to be restored does not exist in %s", cst, self)
self.log.devel("Copied class constants: %s", self.CLASS_CONSTANT_COPIES[key])
def _restore_class_constants(self):
"""Restored class constants that need to be restored when a new instance is created."""
key = self.__class__
for cst in self.CLASS_CONSTANT_COPIES[key]:
newval = copy.deepcopy(self.CLASS_CONSTANT_COPIES[key][cst])
if hasattr(self, cst):
self.log.devel("Restoring class constant '%s' to %s (was: %s)", cst, newval, getattr(self, cst))
else:
self.log.devel("Restoring (currently undefined) class constant '%s' to %s", cst, newval)
setattr(self, cst, newval)
def get_variable(self, name, typ=str):
"""Get value for specified variable.
typ: indicates what type of return value is expected"""
if typ == str:
res = str(self.variables.get(name, ''))
elif typ == list:
if name in self.variables:
res = self.variables[name].flatten()
else:
res = []
else:
raise EasyBuildError("get_variable: Don't know how to create value of type %s.", typ)
return res
def set_variables(self):
"""Do nothing? Everything should have been set by others
Needs to be defined for super() relations
"""
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')
self.log.devel("set_variables: toolchain variables. Do nothing.")
def generate_vars(self):
"""Convert the variables in simple vars"""
self.vars = {}
for k, v in self.variables.items():
self.vars[k] = str(v)
def show_variables(self, offset='', sep='\n', verbose=False):
"""Pretty print the variables"""
if self.vars is None:
self.generate_vars()
var_names = sorted(self.variables.keys())
res = []
for v in var_names:
res.append("%s=%s" % (v, self.variables[v]))
if verbose:
res.append("# type %s" % (type(self.variables[v])))
res.append("# %s" % (self.variables[v].show_el()))
res.append("# repr %s" % (self.variables[v].__repr__()))
if offset is None:
offset = ''
txt = sep.join(["%s%s" % (offset, x) for x in res])
self.log.debug("show_variables:\n%s", txt)
return txt
def get_software_root(self, names):
"""Try to get the software root for all names"""
return self._get_software_multiple(names, self._get_software_root)
def get_software_version(self, names, required=True):
"""Try to get the software version for all names"""
return self._get_software_multiple(names, self._get_software_version, required=required)
def _get_software_multiple(self, names, function, required=True):
"""Execute function of each of names"""
if isinstance(names, (str,)):
names = [names]
res = []
for name in names:
res.append(function(name, required=required))
return res
def _get_software_root(self, name, required=True):
"""Try to get the software root for name"""
root = get_software_root(name)
if root is None:
if required:
raise EasyBuildError("get_software_root software root for %s was not found in environment", name)
else:
self.log.debug("get_software_root software root %s for %s was found in environment", root, name)
return root
def _get_software_version(self, name, required=True):
"""Try to get the software version for name"""
version = get_software_version(name)
if version is None:
if required:
raise EasyBuildError("get_software_version software version for %s was not found in environment", name)
else:
self.log.debug("get_software_version software version %s for %s was found in environment", version, name)
return version
def as_dict(self, name=None, version=None):
"""Return toolchain specification as a dictionary."""
if name is None:
name = self.name
if version is None:
version = self.version
return {
'name': name,
'version': version,
'toolchain': {'name': SYSTEM_TOOLCHAIN_NAME, 'version': ''},
'versionsuffix': '',
'parsed': True, # pretend this is a parsed easyconfig file, as may be required by det_short_module_name
'hidden': self.hidden,
'full_mod_name': self.mod_full_name,
'short_mod_name': self.mod_short_name,
SYSTEM_TOOLCHAIN_NAME: True,
}
def det_short_module_name(self):
"""Determine module name for this toolchain."""
if self.mod_short_name is None:
raise EasyBuildError("Toolchain module name was not set yet")
return self.mod_short_name
def _toolchain_exists(self):
"""
Verify if there exists a toolchain by this name and version
"""
# short-circuit to returning module name for this (non-system) toolchain
if self.is_system_toolchain():
self.log.devel("_toolchain_exists: system toolchain always exists, returning True")
return True
else:
if self.mod_short_name is None:
raise EasyBuildError("Toolchain module name was not set yet")
# check whether a matching module exists if self.mod_short_name contains a module name
return self.modules_tool.exist([self.mod_full_name], skip_avail=True)[0]
def set_options(self, options):
""" Process toolchain options """
for opt in options.keys():
# Only process supported opts
if opt in self.options:
self.options[opt] = options[opt]
else:
# used to be warning, but this is a severe error imho
known_opts = ','.join(self.options.keys())
raise EasyBuildError("Undefined toolchain option %s specified (known options: %s)", opt, known_opts)
def get_dependency_version(self, dependency):
""" Generate a version string for a dependency on a module using this toolchain """
# add toolchain to version string (only for non-system toolchain)
if self.is_system_toolchain():
toolchain = ''
else:
toolchain = '-%s-%s' % (self.name, self.version)
# check if dependency is independent of toolchain (i.e. whether is was built with system compiler)
if SYSTEM_TOOLCHAIN_NAME in dependency and dependency[SYSTEM_TOOLCHAIN_NAME]:
toolchain = ''
suffix = dependency.get('versionsuffix', '')
if 'version' in dependency:
version = ''.join([dependency['version'], toolchain, suffix])
self.log.devel("get_dependency_version: version in dependency return %s", version)
return version
else:
toolchain_suffix = ''.join([toolchain, suffix])
matches = self.modules_tool.available(dependency['name'], toolchain_suffix)
# Find the most recent (or default) one
if len(matches) > 0:
version = matches[-1][-1]
self.log.devel("get_dependency_version: version not in dependency return %s", version)
return
else:
raise EasyBuildError("No toolchain version for dependency name %s (suffix %s) found",
dependency['name'], toolchain_suffix)
def _check_dependencies(self, dependencies):
""" Verify if the given dependencies exist and return them """
self.log.debug("_check_dependencies: adding toolchain dependencies %s", dependencies)
# use *full* module name to check existence of dependencies, since the modules may not be available in the
# current $MODULEPATH without loading the prior dependencies in a module hierarchy
# (e.g. OpenMPI module may only be available after loading GCC module);
# when actually loading the modules for the dependencies, the *short* module name is used,
# see _load_dependencies_modules()
dep_mod_names = [dep['full_mod_name'] for dep in dependencies]
# check whether modules exist
self.log.debug("_check_dependencies: MODULEPATH: %s", os.environ['MODULEPATH'])
if self.dry_run:
deps_exist = [True] * len(dep_mod_names)
else:
deps_exist = self.modules_tool.exist(dep_mod_names)
missing_dep_mods = []
deps = []
for dep, dep_mod_name, dep_exists in zip(dependencies, dep_mod_names, deps_exist):
if dep_exists:
deps.append(dep)
self.log.devel("_check_dependencies: added toolchain dependency %s", str(dep))
elif dep['external_module']:
# external modules may be organised hierarchically,
# so not all modules may be directly available for loading;
# we assume here that the required modules are either provided by the toolchain,
# or are listed earlier as dependency
# examples from OpenHPC:
# - openmpi3 module provided by OpenHPC requires that gnu7, gnu8 or intel module is loaded first
# - fftw module provided by OpenHPC requires that compiler + MPI module are loaded first
self.log.info("Assuming non-visible external module %s is available", dep['full_mod_name'])
deps.append(dep)
else:
missing_dep_mods.append(dep_mod_name)
if missing_dep_mods:
raise EasyBuildError("Missing modules for dependencies (use --robot?): %s", ', '.join(missing_dep_mods))
return deps
def add_dependencies(self, dependencies):
"""
[DEPRECATED] Verify if the given dependencies exist, and return them.
This method is deprecated.
You should pass the dependencies to the 'prepare' method instead, via the 'deps' named argument.
"""
self.log.deprecated("use of 'Toolchain.add_dependencies' method", '4.0')
self.dependencies = self._check_dependencies(dependencies)
def is_required(self, name):
"""Determine whether this is a required toolchain element."""
# default: assume every element is required
return True
def definition(self):
"""
Determine toolchain elements for given Toolchain instance.
"""
var_suff = '_MODULE_NAME'
tc_elems = {}
for var in dir(self):
if var.endswith(var_suff) and getattr(self, var) is not None:
tc_elems.update({var[:-len(var_suff)]: getattr(self, var)})
self.log.debug("Toolchain definition for %s: %s", self.as_dict(), tc_elems)
return tc_elems
def is_dep_in_toolchain_module(self, name):
"""Check whether a specific software name is listed as a dependency in the module for this toolchain."""
return any(map(lambda m: self.mns.is_short_modname_for(m, name), self.toolchain_dep_mods))
def _simulated_load_dependency_module(self, name, version, metadata, verbose=False):
"""
Set environment variables picked up by utility functions for dependencies specified as external modules.
:param name: software name
:param version: software version
:param metadata: dictionary with software metadata ('prefix' for software installation prefix)
"""
self.log.debug("Defining $EB* environment variables for software named %s", name)
env_vars = env_vars_external_module(name, version, metadata)
for key in env_vars:
setvar(key, env_vars[key], verbose=verbose)
def _load_toolchain_module(self, silent=False):
"""Load toolchain module."""
tc_mod = self.det_short_module_name()
if self.dry_run:
dry_run_msg("Loading toolchain module...\n", silent=silent)
# load toolchain module, or simulate load of toolchain components if it is not available
if self.modules_tool.exist([tc_mod], skip_avail=True)[0]:
self.modules_tool.load([tc_mod])
dry_run_msg("module load %s" % tc_mod, silent=silent)
else:
# first simulate loads for toolchain dependencies, if required information is available
if self.tcdeps is not None:
for tcdep in self.tcdeps:
modname = tcdep['short_mod_name']
dry_run_msg("module load %s [SIMULATED]" % modname, silent=silent)
# 'use '$EBROOTNAME' as value for dep install prefix (looks nice in dry run output)
deproot = '$%s' % get_software_root_env_var_name(tcdep['name'])
self._simulated_load_dependency_module(tcdep['name'], tcdep['version'], {'prefix': deproot})
dry_run_msg("module load %s [SIMULATED]" % tc_mod, silent=silent)
# use name of $EBROOT* env var as value for $EBROOT* env var (results in sensible dry run output)
tcroot = '$%s' % get_software_root_env_var_name(self.name)
self._simulated_load_dependency_module(self.name, self.version, {'prefix': tcroot})
else:
# make sure toolchain is available using short module name by running 'module use' on module path subdir
if self.init_modpaths:
mod_path_suffix = build_option('suffix_modules_path')
for modpath in self.init_modpaths:
modpath = os.path.join(install_path('mod'), mod_path_suffix, modpath)
if os.path.exists(modpath):
self.modules_tool.prepend_module_path(modpath)
# load modules for all dependencies
self.log.debug("Loading module for toolchain: %s", tc_mod)
trace_msg("loading toolchain module: " + tc_mod)
self.modules_tool.load([tc_mod])
# append toolchain module to list of modules
self.modules.append(tc_mod)
def _load_dependencies_modules(self, silent=False):
"""Load modules for dependencies, and handle special cases like external modules."""
dep_mods = [dep['short_mod_name'] for dep in self.dependencies]
if self.dry_run:
dry_run_msg("\nLoading modules for dependencies...\n", silent=silent)
mods_exist = self.modules_tool.exist(dep_mods)
# load available modules for dependencies, simulate load for others
for dep, dep_mod_exists in zip(self.dependencies, mods_exist):
mod_name = dep['short_mod_name']
if dep_mod_exists:
self.modules_tool.load([mod_name])
dry_run_msg("module load %s" % mod_name, silent=silent)
else:
dry_run_msg("module load %s [SIMULATED]" % mod_name, silent=silent)
# 'use '$EBROOTNAME' as value for dep install prefix (looks nice in dry run output)
if not dep['external_module']:
deproot = '$%s' % get_software_root_env_var_name(dep['name'])
self._simulated_load_dependency_module(dep['name'], dep['version'], {'prefix': deproot})
else:
# load modules for all dependencies
self.log.debug("Loading modules for dependencies: %s", dep_mods)
self.modules_tool.load(dep_mods)
if self.dependencies:
build_dep_mods = [dep['short_mod_name'] for dep in self.dependencies if dep['build_only']]
if build_dep_mods:
trace_msg("loading modules for build dependencies:")
for dep_mod in build_dep_mods:
trace_msg(' * ' + dep_mod)
else:
trace_msg("(no build dependencies specified)")
run_dep_mods = [dep['short_mod_name'] for dep in self.dependencies if not dep['build_only']]
if run_dep_mods:
trace_msg("loading modules for (runtime) dependencies:")
for dep_mod in run_dep_mods:
trace_msg(' * ' + dep_mod)
else:
trace_msg("(no (runtime) dependencies specified)")
# append dependency modules to list of modules
self.modules.extend(dep_mods)
# define $EBROOT* and $EBVERSION* for external modules, if metadata is available
for dep in [d for d in self.dependencies if d['external_module']]:
mod_name = dep['full_mod_name']
metadata = dep['external_module_metadata']
self.log.debug("Metadata for external module %s: %s", mod_name, metadata)
names = metadata.get('name', [])
versions = metadata.get('version', [None] * len(names))
self.log.debug("Defining $EB* environment variables for external module %s using names %s, versions %s",
mod_name, names, versions)
for name, version in zip(names, versions):
self._simulated_load_dependency_module(name, version, metadata, verbose=True)
def _load_modules(self, silent=False):
"""Load modules for toolchain and dependencies."""
if self.modules_tool is None:
raise EasyBuildError("No modules tool defined in Toolchain instance.")
if not self._toolchain_exists() and not self.dry_run:
raise EasyBuildError("No module found for toolchain: %s", self.mod_short_name)
if self.is_system_toolchain():
self.log.info("Loading dependencies using system toolchain...")
self._load_dependencies_modules(silent=silent)
else:
# load the toolchain and dependencies modules
self.log.debug("Loading toolchain module and dependencies...")
self._load_toolchain_module(silent=silent)
self._load_dependencies_modules(silent=silent)
# include list of loaded modules in dry run output
if self.dry_run:
loaded_mods = self.modules_tool.list()
dry_run_msg("\nFull list of loaded modules:", silent=silent)
if loaded_mods:
for i, mod_name in enumerate([m['mod_name'] for m in loaded_mods]):
dry_run_msg(" %d) %s" % (i + 1, mod_name), silent=silent)
else:
dry_run_msg(" (none)", silent=silent)
dry_run_msg('', silent=silent)
def _verify_toolchain(self):
"""Verify toolchain: check toolchain definition against dependencies of toolchain module."""
# determine direct toolchain dependencies
mod_name = self.det_short_module_name()
self.toolchain_dep_mods = dependencies_for(mod_name, self.modules_tool, depth=0)
self.log.debug("List of toolchain dependencies from toolchain module: %s", self.toolchain_dep_mods)
# only retain names of toolchain elements, excluding toolchain name
toolchain_definition = set([e for es in self.definition().values() for e in es if not e == self.name])
# filter out optional toolchain elements if they're not used in the module
for elem_name in toolchain_definition.copy():
if self.is_required(elem_name) or self.is_dep_in_toolchain_module(elem_name):
continue
# not required and missing: remove from toolchain definition
self.log.debug("Removing %s from list of optional toolchain elements.", elem_name)
toolchain_definition.remove(elem_name)
self.log.debug("List of toolchain elements from toolchain definition: %s", toolchain_definition)
if all(map(self.is_dep_in_toolchain_module, toolchain_definition)):
self.log.info("List of toolchain dependency modules and toolchain definition match!")
else:
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 symlink_commands(self, paths):
"""
Create a symlink for each command to binary/script at specified path.
:param paths: dictionary containing one or mappings, each one specified as a tuple:
(<path/to/script>, <list of commands to symlink to the script>)
"""
symlink_dir = tempfile.mkdtemp()
# prepend location to symlinks to $PATH
setvar('PATH', '%s:%s' % (symlink_dir, os.getenv('PATH')))
for (path, cmds) in paths.values():
for cmd in cmds:
cmd_s = os.path.join(symlink_dir, cmd)
if not os.path.exists(cmd_s):
try:
os.symlink(path, cmd_s)
except OSError as err:
raise EasyBuildError("Failed to symlink %s to %s: %s", path, cmd_s, err)
cmd_path = which(cmd)
self.log.debug("which(%s): %s -> %s", cmd, cmd_path, os.path.realpath(cmd_path))
self.log.info("Commands symlinked to %s via %s: %s", path, symlink_dir, ', '.join(cmds))
def compilers(self):
"""Return list of relevant compilers for this toolchain"""
if self.is_system_toolchain():
c_comps = ['gcc', 'g++']
fortran_comps = ['gfortran']
else:
c_comps = [self.COMPILER_CC, self.COMPILER_CXX]
fortran_comps = [self.COMPILER_F77, self.COMPILER_F90, self.COMPILER_FC]
return (c_comps, fortran_comps)
def is_deprecated(self):
"""Return whether or not this toolchain is deprecated."""
return False
def reset(self):
"""Reset this toolchain instance."""
self.variables_init()
def prepare(self, onlymod=None, deps=None, silent=False, loadmod=True,
rpath_filter_dirs=None, rpath_include_dirs=None):
"""
Prepare a set of environment parameters based on name/version of toolchain
- load modules for toolchain and dependencies
- generate extra variables and set them in the environment
:param deps: list of dependencies
:param onlymod: boolean/string to indicate if the toolchain should only load the environment
with module (True) or also set all other variables (False) like compiler CC etc
(If string: comma separated list of variables that will be ignored).
:param silent: keep quiet, or not (mostly relates to extended dry run output)
:param loadmod: whether or not to (re)load the toolchain module, and the modules for the dependencies
:param rpath_filter_dirs: extra directories to include in RPATH filter (e.g. build dir, tmpdir, ...)
:param rpath_include_dirs: extra directories to include in RPATH
"""
# take into account --sysroot configuration setting
self.handle_sysroot()
# do all dependencies have a toolchain version?
if deps is None:
deps = []
self.dependencies = self._check_dependencies(deps)
if not len(deps) == len(self.dependencies):
self.log.debug("dep %s (%s)" % (len(deps), deps))
self.log.debug("tc.dep %s (%s)" % (len(self.dependencies), self.dependencies))
raise EasyBuildError('Not all dependencies have a matching toolchain version')
if loadmod:
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
self.set_minimal_build_env()
else:
trace_msg("defining build environment for %s/%s toolchain" % (self.name, self.version))
if not self.dry_run:
self._verify_toolchain()
# Generate the variables to be set
self.set_variables()
# set the variables
# onlymod can be comma-separated string of variables not to be set
if onlymod is True:
self.log.debug("prepare: do not set additional variables onlymod=%s", onlymod)
self.generate_vars()
else:
self.log.debug("prepare: set additional variables onlymod=%s", onlymod)
# add LDFLAGS and CPPFLAGS from dependencies to self.vars
self._add_dependency_variables()
self.generate_vars()
self._setenv_variables(onlymod, verbose=not silent)
# consider f90cache first, since ccache can also wrap Fortran compilers
for cache_tool in [F90CACHE, CCACHE]:
if build_option('use_%s' % cache_tool):
self.prepare_compiler_cache(cache_tool)
if build_option('rpath'):
if self.options.get('rpath', True):
self.prepare_rpath_wrappers(rpath_filter_dirs, rpath_include_dirs)
self.use_rpath = True
else:
self.log.info("Not putting RPATH wrappers in place, disabled via 'rpath' toolchain option")
def comp_cache_compilers(self, cache_tool):
"""
Determine list of relevant compilers for specified compiler caching tool.
:param cache_tool: name of compiler caching tool
:return: list of names of relevant compilers
"""
c_comps, fortran_comps = self.compilers()
if cache_tool == CCACHE:
# some version of ccache support caching of Fortran compilations,
# but it doesn't work with Fortran modules (https://github.com/ccache/ccache/issues/342),
# and support was dropped in recent ccache versions;
# as a result, we only use ccache for C/C++ compilers
comps = c_comps
elif cache_tool == F90CACHE:
comps = fortran_comps
else:
raise EasyBuildError("Uknown compiler caching tool specified: %s", cache_tool)
# filter out compilers that are already cached;
# Fortran compilers could already be cached by f90cache when preparing for ccache
for comp in comps[:]:
if comp in self.cached_compilers:
self.log.debug("Not caching compiler %s, it's already being cached", comp)
comps.remove(comp)
self.log.info("Using %s for these compiler commands: %s", cache_tool, ', '.join(comps))
return comps
def prepare_compiler_cache(self, cache_tool):
"""
Prepare for using specified compiler caching tool (e.g., ccache, f90cache)
:param cache_tool: name of compiler caching tool to prepare for
"""
compilers = self.comp_cache_compilers(cache_tool)
self.log.debug("Using compiler cache tool '%s' for compilers: %s", cache_tool, compilers)
# set paths that should be used by compiler caching tool
comp_cache_path = build_option('use_%s' % cache_tool)
setvar('%s_DIR' % cache_tool.upper(), comp_cache_path)
setvar('%s_TEMPDIR' % cache_tool.upper(), tempfile.mkdtemp())
cache_path = which(cache_tool)
if cache_path is None:
raise EasyBuildError("%s binary not found in $PATH, required by --use-ccache", cache_tool)
else:
self.symlink_commands({cache_tool: (cache_path, compilers)})
self.cached_compilers.update(compilers)
self.log.debug("Cached compilers (after preparing for %s): %s", cache_tool, self.cached_compilers)
@staticmethod
def is_rpath_wrapper(path):
"""
Check whether command at specified location already is an RPATH wrapper script rather than the actual command
"""
in_rpath_wrappers_dir = os.path.basename(os.path.dirname(os.path.dirname(path))) == RPATH_WRAPPERS_SUBDIR
# need to use binary mode to read the file, since it may be an actual compiler command (which is a binary file)
calls_rpath_args = b'rpath_args.py $CMD' in read_file(path, mode='rb')
return in_rpath_wrappers_dir and calls_rpath_args
def prepare_rpath_wrappers(self, rpath_filter_dirs=None, rpath_include_dirs=None):
"""
Put RPATH wrapper script in place for compiler and linker commands
:param rpath_filter_dirs: extra directories to include in RPATH filter (e.g. build dir, tmpdir, ...)
"""
if get_os_type() == LINUX:
self.log.info("Putting RPATH wrappers in place...")
else:
raise EasyBuildError("RPATH linking is currently only supported on Linux")
if rpath_filter_dirs is None:
rpath_filter_dirs = []
# always include filter for 'stubs' library directory,
# cfr. https://github.com/easybuilders/easybuild-framework/issues/2683
# (since CUDA 11.something the stubs are in $EBROOTCUDA/stubs/lib64)
lib_stubs_patterns = ['.*/lib(64)?/stubs/?', '.*/stubs/lib(64)?/?']
for lib_stubs_pattern in lib_stubs_patterns:
if lib_stubs_pattern not in rpath_filter_dirs:
rpath_filter_dirs.append(lib_stubs_pattern)
# directory where all wrappers will be placed
wrappers_dir = os.path.join(tempfile.mkdtemp(), RPATH_WRAPPERS_SUBDIR)
# must also wrap compilers commands, required e.g. for Clang ('gcc' on OS X)?
c_comps, fortran_comps = self.compilers()
rpath_args_py = find_eb_script('rpath_args.py')
rpath_wrapper_template = find_eb_script('rpath_wrapper_template.sh.in')
# figure out list of patterns to use in rpath filter
rpath_filter = build_option('rpath_filter')
if rpath_filter is None:
rpath_filter = ['/lib.*', '/usr.*']
self.log.debug("No general RPATH filter specified, falling back to default: %s", rpath_filter)
rpath_filter = ','.join(rpath_filter + ['%s.*' % d for d in rpath_filter_dirs])
self.log.debug("Combined RPATH filter: '%s'", rpath_filter)
rpath_include = ','.join(rpath_include_dirs or [])
self.log.debug("Combined RPATH include paths: '%s'", rpath_include)
# create wrappers
for cmd in nub(c_comps + fortran_comps + ['ld', 'ld.gold', 'ld.bfd']):
orig_cmd = which(cmd)
if orig_cmd:
# bail out early if command already is a wrapped;
# this may occur when building extensions
if self.is_rpath_wrapper(orig_cmd):
self.log.info("%s already seems to be an RPATH wrapper script, not wrapping it again!", orig_cmd)
continue
# determine location for this wrapper