# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import glob
import inspect
import os
import re
import sys
import tempfile
import xml.etree.ElementTree as ElementTree
import llnl.util.tty as tty
from llnl.util.filesystem import (
HeaderList,
LibraryList,
ancestor,
filter_file,
find_headers,
find_libraries,
find_system_libraries,
install,
)
import spack.error
from spack.build_environment import dso_suffix
from spack.package import InstallError, PackageBase, run_after
from spack.util.environment import EnvironmentModifications
from spack.util.executable import Executable
from spack.util.prefix import Prefix
from spack.version import Version, ver
# A couple of utility functions that might be useful in general. If so, they
# should really be defined elsewhere, unless deemed heretical.
# (Or na"ive on my part).
[docs]def debug_print(msg, *args):
'''Prints a message (usu. a variable) and the callers' names for a couple
of stack frames.
'''
# https://docs.python.org/2/library/inspect.html#the-interpreter-stack
stack = inspect.stack()
_func_name = 3
tty.debug("%s.%s:\t%s" % (stack[2][_func_name], stack[1][_func_name], msg),
*args)
[docs]def raise_lib_error(*args):
'''Bails out with an error message. Shows args after the first as one per
line, tab-indented, useful for long paths to line up and stand out.
'''
raise InstallError("\n\t".join(str(i) for i in args))
def _expand_fields(s):
'''[Experimental] Expand arch-related fields in a string, typically a
filename.
Supported fields and their typical expansions are::
{platform} linux, mac
{arch} intel64 (including on Mac)
{libarch} intel64, empty on Mac
{bits} 64
'''
# Python-native string formatting requires arg list counts to match the
# replacement field count; optional fields are far easier with regexes.
_bits = '64'
_arch = 'intel64' # TBD: ia32
if 'linux' in sys.platform: # NB: linux2 vs. linux
s = re.sub('{platform}', 'linux', s)
s = re.sub('{libarch}', _arch, s)
elif 'darwin' in sys.platform:
s = re.sub('{platform}', 'mac', s)
s = re.sub('{libarch}', '', s) # no arch dirs are used (as of 2018)
# elif 'win' in sys.platform: # TBD
# s = re.sub('{platform}', 'windows', s)
s = re.sub('{arch}', _arch, s)
s = re.sub('{bits}', _bits, s)
return s
[docs]class IntelPackage(PackageBase):
"""Specialized class for licensed Intel software.
This class provides two phases that can be overridden:
1. :py:meth:`~.IntelPackage.configure`
2. :py:meth:`~.IntelPackage.install`
They both have sensible defaults and for many packages the
only thing necessary will be to override setup_run_environment
to set the appropriate environment variables.
"""
#: Phases of an Intel package
phases = ['configure', 'install']
#: This attribute is used in UI queries that need to know the build
#: system base class
build_system_class = 'IntelPackage'
#: A dict that maps Spack version specs to release years, needed to infer
#: the installation directory layout for pre-2016 versions in the family of
#: Intel packages.
#
# Like any property, it can be overridden in client packages, should older
# versions ever be added there. The initial dict here contains the
# packages defined in Spack as of 2018-04. Keys could conceivably overlap
# but preferably should not - only the first key in hash traversal order
# that satisfies self.spec will be used.
version_years = {
# intel-daal is versioned 2016 and later, no divining is needed
'intel-ipp@9.0:9': 2016,
'intel-mkl@11.3.0:11.3': 2016,
'intel-mpi@5.1:5': 2016,
}
# Below is the list of possible values for setting auto dispatch functions
# for the Intel compilers. Using these allows for the building of fat
# binaries that will detect the CPU SIMD capabilities at run time and
# activate the appropriate extensions.
auto_dispatch_options = ('COMMON-AVX512', 'MIC-AVX512', 'CORE-AVX512',
'CORE-AVX2', 'CORE-AVX-I', 'AVX', 'SSE4.2',
'SSE4.1', 'SSSE3', 'SSE3', 'SSE2')
@property
def license_required(self):
# The Intel libraries are provided without requiring a license as of
# version 2017.2. Trying to specify one anyway will fail. See:
# https://software.intel.com/en-us/articles/free-ipsxe-tools-and-libraries
return self._has_compilers or self.version < ver('2017.2')
#: Comment symbol used in the license.lic file
license_comment = '#'
#: Environment variables that Intel searches for a license file
license_vars = ['INTEL_LICENSE_FILE']
#: URL providing information on how to acquire a license key
license_url = 'https://software.intel.com/en-us/articles/intel-license-manager-faq'
#: Location where Intel searches for a license file
@property
def license_files(self):
dirs = ['Licenses']
if self._has_compilers:
dirs.append(self.component_bin_dir('compiler'))
for variant, component_suite_dir in {
'+advisor': 'advisor',
'+inspector': 'inspector',
'+itac': 'itac',
'+vtune': 'vtune_profiler',
}.items():
if variant in self.spec:
dirs.append(self.normalize_path(
'licenses', component_suite_dir, relative=True))
files = [os.path.join(d, 'license.lic') for d in dirs]
return files
#: Components to install (list of name patterns from pset/mediaconfig.xml)
# NB: Renamed from plain components() for coding and maintainability.
@property
def pset_components(self):
# Do not detail single-purpose client packages.
if not self._has_compilers:
return ['ALL']
# tty.warn('DEBUG: installing ALL components')
# return ['ALL']
# Always include compilers and closely related components.
# Pre-2016 compiler components have different names - throw in all.
# Later releases have overlapping minor parts that differ by "edition".
# NB: The spack package 'intel' is a subset of
# 'intel-parallel-studio@composer' without the lib variants.
c = ' intel-icc intel-ifort' \
' intel-ccomp intel-fcomp intel-comp-' \
' intel-compilerproc intel-compilerprof intel-compilerpro-' \
' intel-psxe intel-openmp'
additions_for = {
'cluster': ' intel-icsxe',
'professional': ' intel-ips-',
'composer': ' intel-compxe',
}
if self._edition in additions_for:
c += additions_for[self._edition]
for variant, components_to_add in {
'+daal': ' intel-daal', # Data Analytics Acceleration Lib
'+gdb': ' intel-gdb', # Integrated Performance Primitives
'+ipp': ' intel-ipp intel-crypto-ipp',
'+mkl': ' intel-mkl', # Math Kernel Library
'+mpi': ' intel-mpi intel-imb', # MPI runtime, SDK, benchm.
'+tbb': ' intel-tbb', # Threading Building Blocks
'+advisor': ' intel-advisor',
'+clck': ' intel_clck', # Cluster Checker
'+inspector': ' intel-inspector',
'+itac': ' intel-itac intel-ta intel-tc'
' intel-trace-analyzer intel-trace-collector',
# Trace Analyzer and Collector
'+vtune': ' intel-vtune'
# VTune, ..-profiler since 2020, ..-amplifier before
}.items():
if variant in self.spec:
c += components_to_add
debug_print(c)
return c.split()
# ---------------------------------------------------------------------
# Utilities
# ---------------------------------------------------------------------
@property
def _filtered_components(self):
'''Expands the list of desired component patterns to the exact names
present in the given download.
'''
c = self.pset_components
if 'ALL' in c or 'DEFAULTS' in c: # No filter needed
return c
# mediaconfig.xml is known to contain duplicate components.
# If more than one copy of the same component is used, you
# will get an error message about invalid components.
# Use sets to prevent duplicates and for efficient traversal.
requested = set(c)
confirmed = set()
# NB: To get a reasonable overview in pretty much the documented way:
#
# grep -E '<Product|<Abbr|<Name>..[a-z]' pset/mediaconfig.xml
#
# https://software.intel.com/en-us/articles/configuration-file-format
#
xmltree = ElementTree.parse('pset/mediaconfig.xml')
for entry in xmltree.getroot().findall('.//Abbr'): # XPath expression
name_present = entry.text
for name_requested in requested:
if name_present.startswith(name_requested):
confirmed.add(name_present)
return list(confirmed)
@property
def intel64_int_suffix(self):
'''Provide the suffix for Intel library names to match a client
application's desired int size, conveyed by the active spec variant.
The possible suffixes and their meanings are:
``ilp64`` all of int, long, and pointer are 64 bit,
`` lp64`` only long and pointer are 64 bit; int will be 32bit.
'''
if '+ilp64' in self.spec:
return 'ilp64'
else:
return 'lp64'
@property
def _has_compilers(self):
return self.name in ['intel', 'intel-parallel-studio']
@property
def _edition(self):
if self.name == 'intel-parallel-studio':
return self.version[0] # clearer than .up_to(1), I think.
elif self.name == 'intel':
return 'composer'
else:
return ''
@property
def version_yearlike(self):
'''Return the version in a unified style, suitable for Version class
conditionals.
'''
# Input data for this routine: self.version
# Returns: YYYY.Nupdate[.Buildseq]
#
# Specifics by package:
#
# Package Format of self.version
# ------------------------------------------------------------
# 'intel-parallel-studio' <edition>.YYYY.Nupdate
# 'intel' YY.0.Nupdate (some assigned ad-hoc)
# Recent lib packages YYYY.Nupdate.Buildseq
# Early lib packages Major.Minor.Patch.Buildseq
# ------------------------------------------------------------
#
# Package Output
# ------------------------------------------------------------
# 'intel-parallel-studio' YYYY.Nupdate
# 'intel' YYYY.Nupdate
# Recent lib packages YYYY.Nupdate.Buildseq
# Known early lib packages YYYY.Minor.Patch.Buildseq (*)
# Unknown early lib packages (2000 + Major).Minor.Patch.Buildseq
# ----------------------------------------------------------------
#
# (*) YYYY is taken from @property "version_years" (a dict of specs)
#
try:
if self.name == 'intel':
# Has a "Minor" version element, but it is always set as 0. To
# be useful for comparisons, drop it and get YYYY.Nupdate.
v_tail = self.version[2:] # coerced just fine via __getitem__
else:
v_tail = self.version[1:]
except IndexError:
# Hmm - this happens on "spack install intel-mkl@11".
# I thought concretization picks an actual version??
return self.version # give up
if self.name == 'intel-parallel-studio':
return v_tail
v_year = self.version[0]
if v_year < 2000:
# Shoehorn Major into release year until we know better.
v_year += 2000
for spec, year in self.version_years.items():
if self.spec.satisfies(spec):
v_year = year
break
return ver('%s.%s' % (v_year, v_tail))
# ---------------------------------------------------------------------
# Directory handling common to all Intel components
# ---------------------------------------------------------------------
# For reference: classes using IntelPackage, as of Spack-0.11:
#
# intel/ intel-ipp/ intel-mpi/
# intel-daal/ intel-mkl/ intel-parallel-studio/
#
# Not using class IntelPackage:
# intel-gpu-tools/ intel-mkl-dnn/ intel-tbb/
#
[docs] def normalize_suite_dir(self, suite_dir_name, version_globs=['*.*.*']):
'''Returns the version-specific and absolute path to the directory of
an Intel product or a suite of product components.
Parameters:
suite_dir_name (str):
Name of the product directory, without numeric version.
- Examples::
composer_xe, parallel_studio_xe, compilers_and_libraries
The following will work as well, even though they are not
directly targets for Spack installation::
advisor_xe, inspector_xe, vtune_amplifier_xe,
performance_snapshots (new name for vtune as of 2018)
These are single-component products without subordinate
components and are normally made available to users by a
toplevel psxevars.sh or equivalent file to source (and thus by
the modulefiles that Spack produces).
version_globs (list): Suffix glob patterns (most specific
first) expected to qualify suite_dir_name to its fully
version-specific install directory (as opposed to a
compatibility directory or symlink).
'''
# See ./README-intel.rst for background and analysis of dir layouts.
d = self.prefix
# Distinguish between product installations that were done external to
# Spack (integrated via packages.yaml) and Spack-internal ones. The
# resulting prefixes may differ in directory depth and specificity.
unversioned_dirname = ''
if suite_dir_name and suite_dir_name in d:
# If e.g. MKL was installed outside of Spack, it is likely just one
# product or product component among possibly many other Intel
# products and their releases that were installed in sibling or
# cousin directories. In such cases, the prefix given to Spack
# will inevitably be a highly product-specific and preferably fully
# version-specific directory. This is what we want and need, and
# nothing more specific than that, i.e., if needed, convert, e.g.:
# .../compilers_and_libraries*/* -> .../compilers_and_libraries*
d = re.sub('(%s%s.*?)%s.*' %
(os.sep, re.escape(suite_dir_name), os.sep), r'\1', d)
# The Intel installer scripts try hard to place compatibility links
# named like this in the install dir to convey upgrade benefits to
# traditional client apps. But such a generic name can be trouble
# when given to Spack: the link target is bound to change outside
# of Spack's purview and when it does, the outcome of subsequent
# builds of dependent packages may be affected. (Though Intel has
# been remarkably good at backward compatibility.)
# I'm not sure if Spack's package hashing includes link targets.
if d.endswith(suite_dir_name):
# NB: This could get tiresome without a seen++ test.
# tty.warn('Intel product found in a version-neutral directory'
# ' - future builds may not be reproducible.')
#
# Simply doing realpath() would not be enough, because:
# compilers_and_libraries -> compilers_and_libraries_2018
# which is mostly a staging directory for symlinks (see next).
unversioned_dirname = d
else:
# By contrast, a Spack-internal MKL installation will inherit its
# prefix from install.sh of Intel's package distribution, where it
# means the high-level installation directory that is specific to
# the *vendor* (think of the default "/opt/intel"). We must now
# step down into the *product* directory to get the usual
# hierarchy. But let's not do that in haste ...
#
# For a Spack-born install, the fully-qualified release directory
# desired above may seem less important since product upgrades
# won't land in the same parent. However, only the fully qualified
# directory contains the regular files for the compiler commands:
#
# $ ls -lF <HASH>/compilers_and_libraries*/linux/bin/intel64/icc
#
# <HASH>/compilers_and_libraries_2018.1.163/linux/bin/intel64/icc*
# A regular file in the actual release directory. Bingo!
#
# <HASH>/compilers_and_libraries_2018/linux/bin/intel64/icc -> ...
# A symlink - no good. Note that "compilers_and_libraries_2018/"
# is itself a directory (not symlink) but it merely holds a
# compatibility dir hierarchy with lots of symlinks into the
# release dir.
#
# <HASH>/compilers_and_libraries/linux/bin/intel64/icc -> ...
# Ditto.
#
# Now, the Spack packages for MKL and MPI packges use version
# triplets, but the one for intel-parallel-studio does not.
# So, we can't have it quite as easy as:
# d = Prefix(d.append('compilers_and_libraries_' + self.version))
# Alright, let's see what we can find instead:
unversioned_dirname = os.path.join(d, suite_dir_name)
if unversioned_dirname:
for g in version_globs:
try_glob = unversioned_dirname + g
debug_print('trying %s' % try_glob)
matching_dirs = sorted(glob.glob(try_glob))
# NB: Python glob() returns results in arbitrary order - ugh!
# NB2: sorted() is a shortcut that is NOT number-aware.
if matching_dirs:
debug_print('found %d:' % len(matching_dirs),
matching_dirs)
# Take the highest and thus presumably newest match, which
# better be the sole one anyway.
d = matching_dirs[-1]
break
if not matching_dirs:
# No match -- return a sensible value anyway.
d = unversioned_dirname
debug_print(d)
return Prefix(d)
[docs] def normalize_path(self, component_path, component_suite_dir=None,
relative=False):
'''Returns the absolute or relative path to a component or file under a
component suite directory.
Intel's product names, scope, and directory layout changed over the
years. This function provides a unified interface to their directory
names.
Parameters:
component_path (str): a component name like 'mkl', or 'mpi', or a
deeper relative path.
component_suite_dir (str): _Unversioned_ name of the expected
parent directory of component_path. When absent or `None`, an
appropriate default will be used. A present but empty string
`""` requests that `component_path` refer to `self.prefix`
directly.
Typical values: `compilers_and_libraries`, `composer_xe`,
`parallel_studio_xe`.
Also supported: `advisor`, `inspector`, `vtune`. The actual
directory name for these suites varies by release year. The
name will be corrected as needed for use in the return value.
relative (bool): When True, return path relative to self.prefix,
otherwise, return an absolute path (the default).
'''
# Design note: Choosing the default for `component_suite_dir` was a bit
# tricky since there better be a sensible means to specify direct
# parentage under self.prefix (even though you normally shouldn't need
# a function for that). I chose "" to allow that case be represented,
# and 'None' or the absence of the kwarg to represent the most relevant
# case for the time of writing.
#
# In the 2015 releases (the earliest in Spack as of 2018), there were
# nominally two separate products that provided the compilers:
# "Composer" as lower tier, and "Parallel Studio" as upper tier. In
# Spack, we justifiably retcon both as "intel-parallel-studio@composer"
# and "...@cluster", respectively. Both of these use the older
# "composer_xe" dir layout, as do their virtual package personas.
#
# All other "intel-foo" packages in Spack as of 2018-04 use the
# "compilers_and_libraries" layout, including the 2016 releases that
# are not natively versioned by year.
cs = component_suite_dir
if cs is None and component_path.startswith('ism'):
cs = 'parallel_studio_xe'
v = self.version_yearlike
# Glob variants to complete component_suite_dir.
# Helper var for older MPI versions - those are reparented, with each
# version in their own version-named dir.
standalone_glob = '[1-9]*.*.*'
# Most other components; try most specific glob first.
# flake8 is far too opinionated about lists - ugh.
normalize_kwargs = {
'version_globs': [
'_%s' % self.version,
'_%s.*' % v.up_to(2), # should be: YYYY.Nupdate
'_*.*.*', # last resort
]
}
for rename_rule in [
# cs given as arg, in years, dir actually used, [version_globs]
[None, ':2015', 'composer_xe'],
[None, '2016:', 'compilers_and_libraries'],
['advisor', ':2016', 'advisor_xe'],
['inspector', ':2016', 'inspector_xe'],
['vtune_profiler', ':2017', 'vtune_amplifier_xe'],
['vtune', ':2017', 'vtune_amplifier_xe'], # alt.
['vtune_profiler', ':2019', 'vtune_amplifier'],
['itac', ':', 'itac', [os.sep + standalone_glob]],
]:
if cs == rename_rule[0] and v.satisfies(ver(rename_rule[1])):
cs = rename_rule[2]
if len(rename_rule) > 3:
normalize_kwargs = {'version_globs': rename_rule[3]}
break
d = self.normalize_suite_dir(cs, **normalize_kwargs)
# Help find components not located directly under d.
# NB: ancestor() not well suited if version_globs may contain os.sep .
parent_dir = re.sub(os.sep + re.escape(cs) + '.*', '', d)
reparent_as = {}
if cs == 'compilers_and_libraries': # must qualify further
d = os.path.join(d, _expand_fields('{platform}'))
elif cs == 'composer_xe':
reparent_as = {'mpi': 'impi'}
# ignore 'imb' (MPI Benchmarks)
for nominal_p, actual_p in reparent_as.items():
if component_path.startswith(nominal_p):
dirs = glob.glob(
os.path.join(parent_dir, actual_p, standalone_glob))
debug_print('reparent dirs: %s' % dirs)
# Brazenly assume last match is the most recent version;
# convert back to relative of parent_dir, and re-assemble.
rel_dir = dirs[-1].split(parent_dir + os.sep, 1)[-1]
component_path = component_path.replace(nominal_p, rel_dir, 1)
d = parent_dir
d = os.path.join(d, component_path)
if relative:
d = os.path.relpath(os.path.realpath(d), parent_dir)
debug_print(d)
return d
[docs] def component_bin_dir(self, component, **kwargs):
d = self.normalize_path(component, **kwargs)
if component == 'compiler': # bin dir is always under PARENT
d = os.path.join(ancestor(d), 'bin', _expand_fields('{libarch}'))
d = d.rstrip(os.sep) # cosmetics, when {libarch} is empty
# NB: Works fine even with relative=True, e.g.:
# composer_xe/compiler -> composer_xe/bin/intel64
elif component == 'mpi':
d = os.path.join(d, _expand_fields('{libarch}'), 'bin')
else:
d = os.path.join(d, 'bin')
debug_print(d)
return d
[docs] def component_lib_dir(self, component, **kwargs):
'''Provide directory suitable for find_libraries() and
SPACK_COMPILER_EXTRA_RPATHS.
'''
d = self.normalize_path(component, **kwargs)
if component == 'mpi':
d = os.path.join(d, _expand_fields('{libarch}'), 'lib')
else:
d = os.path.join(d, 'lib', _expand_fields('{libarch}'))
d = d.rstrip(os.sep) # cosmetics, when {libarch} is empty
if component == 'tbb': # must qualify further for abi
d = os.path.join(d, self._tbb_abi)
debug_print(d)
return d
[docs] def component_include_dir(self, component, **kwargs):
d = self.normalize_path(component, **kwargs)
if component == 'mpi':
d = os.path.join(d, _expand_fields('{libarch}'), 'include')
else:
d = os.path.join(d, 'include')
debug_print(d)
return d
@property
def file_to_source(self):
'''Full path of file to source for initializing an Intel package.
A client package could override as follows:
` @property`
` def file_to_source(self):`
` return self.normalize_path("apsvars.sh", "vtune_amplifier")`
'''
vars_file_info_for = {
# key (usu. spack package name) -> [rel_path, component_suite_dir]
# Extension note: handle additions by Spack name or ad-hoc keys.
'@early_compiler': ['bin/compilervars', None],
'intel-parallel-studio': ['bin/psxevars', 'parallel_studio_xe'],
'intel': ['bin/compilervars', None],
'intel-daal': ['daal/bin/daalvars', None],
'intel-ipp': ['ipp/bin/ippvars', None],
'intel-mkl': ['mkl/bin/mklvars', None],
'intel-mpi': ['mpi/{libarch}/bin/mpivars', None],
}
key = self.name
if self.version_yearlike.satisfies(ver(':2015')):
# Same file as 'intel' but 'None' for component_suite_dir will
# resolve differently. Listed as a separate entry to serve as
# example and to avoid pitfalls upon possible refactoring.
key = '@early_compiler'
f, component_suite_dir = vars_file_info_for[key]
f = _expand_fields(f) + '.sh'
# TODO?? win32 would have to handle os.sep, '.bat' (unless POSIX??)
f = self.normalize_path(f, component_suite_dir)
return f
# ---------------------------------------------------------------------
# Threading, including (WIP) support for virtual 'tbb'
# ---------------------------------------------------------------------
@property
def openmp_libs(self):
'''Supply LibraryList for linking OpenMP'''
if '%intel' in self.spec:
# NB: Hunting down explicit library files may be the Spack way of
# doing things, but be aware that "{icc|ifort} --help openmp"
# steers us towards options instead: -qopenmp-link={dynamic,static}
omp_libnames = ['libiomp5']
omp_libs = find_libraries(
omp_libnames,
root=self.component_lib_dir('compiler'),
shared=('+shared' in self.spec))
# Note about search root here: For MKL, the directory
# "$MKLROOT/../compiler" will be present even for an MKL-only
# product installation (as opposed to one being ghosted via
# packages.yaml), specificially to provide the 'iomp5' libs.
elif '%gcc' in self.spec:
with self.compiler.compiler_environment():
omp_lib_path = Executable(self.compiler.cc)(
'--print-file-name', 'libgomp.%s' % dso_suffix, output=str)
omp_libs = LibraryList(omp_lib_path.strip())
elif '%clang' in self.spec:
with self.compiler.compiler_environment():
omp_lib_path = Executable(self.compiler.cc)(
'--print-file-name', 'libomp.%s' % dso_suffix, output=str)
omp_libs = LibraryList(omp_lib_path.strip())
if len(omp_libs) < 1:
raise_lib_error('Cannot locate OpenMP libraries:', omp_libnames)
debug_print(omp_libs)
return omp_libs
@property
def _gcc_executable(self):
'''Return GCC executable'''
# Match the available gcc, as it's done in tbbvars.sh.
gcc_name = 'gcc'
# but first check if -gcc-name is specified in cflags
for flag in self.spec.compiler_flags['cflags']:
if flag.startswith('-gcc-name='):
gcc_name = flag.split('-gcc-name=')[1]
break
debug_print(gcc_name)
return Executable(gcc_name)
@property
def tbb_headers(self):
# Note: TBB is included as
# #include <tbb/task_scheduler_init.h>
return HeaderList([
self.component_include_dir('tbb') + '/dummy.h'])
@property
def tbb_libs(self):
'''Supply LibraryList for linking TBB'''
# TODO: When is 'libtbbmalloc' needed?
tbb_lib = find_libraries(
['libtbb'], root=self.component_lib_dir('tbb'))
# NB: Like icc with -qopenmp, so does icpc steer us towards using an
# option: "icpc -tbb"
# TODO: clang(?)
gcc = self._gcc_executable # must be gcc, not self.compiler.cc
with self.compiler.compiler_environment():
cxx_lib_path = gcc(
'--print-file-name', 'libstdc++.%s' % dso_suffix, output=str)
libs = tbb_lib + LibraryList(cxx_lib_path.rstrip())
debug_print(libs)
return libs
@property
def _tbb_abi(self):
'''Select the ABI needed for linking TBB'''
gcc = self._gcc_executable
with self.compiler.compiler_environment():
matches = re.search(r'(gcc|LLVM).* ([0-9]+\.[0-9]+\.[0-9]+).*',
gcc('--version', output=str), re.I | re.M)
abi = ''
if sys.platform == 'darwin':
pass
elif matches:
# TODO: Confirm that this covers clang (needed on Linux only)
gcc_version = Version(matches.groups()[1])
if gcc_version >= ver('4.7'):
abi = 'gcc4.7'
elif gcc_version >= ver('4.4'):
abi = 'gcc4.4'
else:
abi = 'gcc4.1' # unlikely, one hopes.
# Alrighty then ...
debug_print(abi)
return abi
# ---------------------------------------------------------------------
# Support for virtual 'blas/lapack/scalapack'
# ---------------------------------------------------------------------
@property
def blas_libs(self):
# Main magic here.
# For reference, see The Intel Math Kernel Library Link Line Advisor:
# https://software.intel.com/en-us/articles/intel-mkl-link-line-advisor/
mkl_integer = 'libmkl_intel_' + self.intel64_int_suffix
if self.spec.satisfies('threads=openmp'):
if '%intel' in self.spec:
mkl_threading = 'libmkl_intel_thread'
elif '%gcc' in self.spec or '%clang' in self.spec:
mkl_threading = 'libmkl_gnu_thread'
threading_engine_libs = self.openmp_libs
elif self.spec.satisfies('threads=tbb'):
mkl_threading = 'libmkl_tbb_thread'
threading_engine_libs = self.tbb_libs
elif self.spec.satisfies('threads=none'):
mkl_threading = 'libmkl_sequential'
threading_engine_libs = LibraryList([])
else:
raise_lib_error('Cannot determine MKL threading libraries.')
mkl_libnames = [mkl_integer, mkl_threading, 'libmkl_core']
mkl_libs = find_libraries(
mkl_libnames,
root=self.component_lib_dir('mkl'),
shared=('+shared' in self.spec))
debug_print(mkl_libs)
if len(mkl_libs) < 3:
raise_lib_error('Cannot locate core MKL libraries:', mkl_libnames,
'in:', self.component_lib_dir('mkl'))
# The Intel MKL link line advisor recommends these system libraries
system_libs = find_system_libraries(
'libpthread libm libdl'.split(),
shared=('+shared' in self.spec))
debug_print(system_libs)
return mkl_libs + threading_engine_libs + system_libs
@property
def lapack_libs(self):
return self.blas_libs
@property
def scalapack_libs(self):
# Intel MKL does not directly depend on MPI but the BLACS library
# which underlies ScaLapack does. It comes in several personalities;
# we must supply a personality matching the MPI implementation that
# is active for the root package that asked for ScaLapack.
spec_root = self.spec.root
if sys.platform == 'darwin' and '^mpich' in spec_root:
# The only supported choice for MKL 2018 on Mac.
blacs_lib = 'libmkl_blacs_mpich'
elif '^openmpi' in spec_root:
blacs_lib = 'libmkl_blacs_openmpi'
elif '^mpich@1' in spec_root:
# Was supported only up to 2015.
blacs_lib = 'libmkl_blacs'
elif ('^mpich@2:' in spec_root or
'^cray-mpich' in spec_root or
'^mvapich2' in spec_root or
'^intel-mpi' in spec_root or
'^intel-oneapi-mpi' in spec_root or
'^intel-parallel-studio' in spec_root):
blacs_lib = 'libmkl_blacs_intelmpi'
elif '^mpt' in spec_root:
blacs_lib = 'libmkl_blacs_sgimpt'
else:
raise_lib_error('Cannot find a BLACS library for the given MPI.')
int_suff = '_' + self.intel64_int_suffix
scalapack_libnames = [
'libmkl_scalapack' + int_suff,
blacs_lib + int_suff,
]
sca_libs = find_libraries(
scalapack_libnames,
root=self.component_lib_dir('mkl'),
shared=('+shared' in self.spec))
debug_print(sca_libs)
if len(sca_libs) < 2:
raise_lib_error(
'Cannot locate ScaLapack/BLACS libraries:', scalapack_libnames)
# NB: ScaLapack is installed as "cluster" components within MKL or
# MKL-encompassing products. But those were *optional* for the ca.
# 2015/2016 product releases, which was easy to overlook, and I have
# been bitten by that. Thus, complain early because it'd be a sore
# disappointment to have missing ScaLapack libs show up as a link error
# near the end phase of a client package's build phase.
return sca_libs
# ---------------------------------------------------------------------
# Support for virtual 'mpi'
# ---------------------------------------------------------------------
@property
def mpi_compiler_wrappers(self):
'''Return paths to compiler wrappers as a dict of env-like names
'''
# Intel comes with 2 different flavors of MPI wrappers:
#
# * mpiicc, mpiicpc, and mpiifort are hardcoded to wrap around
# the Intel compilers.
# * mpicc, mpicxx, mpif90, and mpif77 allow you to set which
# compilers to wrap using I_MPI_CC and friends. By default,
# wraps around the GCC compilers.
#
# In theory, these should be equivalent as long as I_MPI_CC
# and friends are set to point to the Intel compilers, but in
# practice, mpicc fails to compile some applications while
# mpiicc works.
bindir = self.component_bin_dir('mpi')
if self.compiler.name == 'intel':
wrapper_vars = {
# eschew Prefix objects -- emphasize the command strings.
'MPICC': os.path.join(bindir, 'mpiicc'),
'MPICXX': os.path.join(bindir, 'mpiicpc'),
'MPIF77': os.path.join(bindir, 'mpiifort'),
'MPIF90': os.path.join(bindir, 'mpiifort'),
'MPIFC': os.path.join(bindir, 'mpiifort'),
}
else:
wrapper_vars = {
'MPICC': os.path.join(bindir, 'mpicc'),
'MPICXX': os.path.join(bindir, 'mpicxx'),
'MPIF77': os.path.join(bindir, 'mpif77'),
'MPIF90': os.path.join(bindir, 'mpif90'),
'MPIFC': os.path.join(bindir, 'mpif90'),
}
# debug_print("wrapper_vars =", wrapper_vars)
return wrapper_vars
[docs] def mpi_setup_dependent_build_environment(
self, env, dependent_spec, compilers_of_client={}):
'''Unified back-end for setup_dependent_build_environment() of
Intel packages that provide 'mpi'.
Parameters:
env, dependent_spec: same as in
setup_dependent_build_environment().
compilers_of_client (dict): Conveys spack_cc, spack_cxx, etc.,
from the scope of dependent packages; constructed in caller.
'''
# See also: setup_dependent_package()
wrapper_vars = {
'I_MPI_CC': compilers_of_client['CC'],
'I_MPI_CXX': compilers_of_client['CXX'],
'I_MPI_F77': compilers_of_client['F77'],
'I_MPI_F90': compilers_of_client['F90'],
'I_MPI_FC': compilers_of_client['FC'],
# NB: Normally set by the modulefile, but that is not active here:
'I_MPI_ROOT': self.normalize_path('mpi'),
}
# CAUTION - SIMILAR code in:
# var/spack/repos/builtin/packages/mpich/package.py
# var/spack/repos/builtin/packages/openmpi/package.py
# var/spack/repos/builtin/packages/mvapich2/package.py
#
# On Cray, the regular compiler wrappers *are* the MPI wrappers.
if 'platform=cray' in self.spec:
# TODO: Confirm
wrapper_vars.update({
'MPICC': compilers_of_client['CC'],
'MPICXX': compilers_of_client['CXX'],
'MPIF77': compilers_of_client['F77'],
'MPIF90': compilers_of_client['F90'],
})
else:
compiler_wrapper_commands = self.mpi_compiler_wrappers
wrapper_vars.update({
'MPICC': compiler_wrapper_commands['MPICC'],
'MPICXX': compiler_wrapper_commands['MPICXX'],
'MPIF77': compiler_wrapper_commands['MPIF77'],
'MPIF90': compiler_wrapper_commands['MPIF90'],
})
# Ensure that the directory containing the compiler wrappers is in the
# PATH. Spack packages add `prefix.bin` to their dependents' paths,
# but because of the intel directory hierarchy that is insufficient.
env.prepend_path('PATH', os.path.dirname(wrapper_vars['MPICC']))
for key, value in wrapper_vars.items():
env.set(key, value)
debug_print("adding to build env:", wrapper_vars)
# ---------------------------------------------------------------------
# General support for child packages
# ---------------------------------------------------------------------
@property
def headers(self):
result = HeaderList([])
if '+mpi' in self.spec or self.provides('mpi'):
result += find_headers(
['mpi'],
root=self.component_include_dir('mpi'),
recursive=False)
if '+mkl' in self.spec or self.provides('mkl'):
result += find_headers(
['mkl_cblas', 'mkl_lapacke'],
root=self.component_include_dir('mkl'),
recursive=False)
if '+tbb' in self.spec or self.provides('tbb'):
result += self.tbb_headers
debug_print(result)
return result
@property
def libs(self):
result = LibraryList([])
if '+tbb' in self.spec or self.provides('tbb'):
result = self.tbb_libs + result
if '+mkl' in self.spec or self.provides('blas'):
result = self.blas_libs + result
if '+mkl' in self.spec or self.provides('lapack'):
result = self.lapack_libs + result
if '+mpi' in self.spec or self.provides('mpi'):
# If prefix is too general, recursive searches may get files from
# supported but inappropriate sub-architectures like 'mic'.
libnames = ['libmpifort', 'libmpi']
if 'cxx' in self.spec.last_query.extra_parameters:
libnames = ['libmpicxx'] + libnames
result = find_libraries(
libnames,
root=self.component_lib_dir('mpi'),
shared=True, recursive=True) + result
# Intel MPI since 2019 depends on libfabric which is not in the
# lib directory but in a directory of its own which should be
# included in the rpath
if self.version_yearlike >= ver('2019'):
d = ancestor(self.component_lib_dir('mpi'))
if '+external-libfabric' in self.spec:
result += self.spec['libfabric'].libs
else:
result += find_libraries(['libfabric'],
os.path.join(d, 'libfabric', 'lib'))
if '^mpi' in self.spec.root and ('+mkl' in self.spec or
self.provides('scalapack')):
result = self.scalapack_libs + result
debug_print(result)
return result
[docs] def setup_run_environment(self, env):
"""Adds environment variables to the generated module file.
These environment variables come from running:
.. code-block:: console
$ source parallel_studio_xe_2017/bin/psxevars.sh intel64
[and likewise for MKL, MPI, and other components]
"""
f = self.file_to_source
tty.debug("sourcing " + f)
# All Intel packages expect at least the architecture as argument.
# Some accept more args, but those are not (yet?) handled here.
args = (_expand_fields('{arch}'),)
# On Mac, the platform is *also required*, at least as of 2018.
# I am not sure about earlier versions.
# if sys.platform == 'darwin':
# args = ()
env.extend(EnvironmentModifications.from_sourcing_file(f, *args))
if self.spec.name in ('intel', 'intel-parallel-studio'):
# this package provides compilers
# TODO: fix check above when compilers are dependencies
env.set('CC', self.prefix.bin.icc)
env.set('CXX', self.prefix.bin.icpc)
env.set('FC', self.prefix.bin.ifort)
env.set('F77', self.prefix.bin.ifort)
env.set('F90', self.prefix.bin.ifort)
[docs] def setup_dependent_build_environment(self, env, dependent_spec):
# NB: This function is overwritten by 'mpi' provider packages:
#
# var/spack/repos/builtin/packages/intel-mpi/package.py
# var/spack/repos/builtin/packages/intel-parallel-studio/package.py
#
# They call _setup_dependent_env_callback() as well, but with the
# dictionary kwarg compilers_of_client{} present and populated.
# Handle everything in a callback version.
self._setup_dependent_env_callback(env, dependent_spec)
def _setup_dependent_env_callback(
self, env, dependent_spec, compilers_of_client={}):
# Expected to be called from a client's
# setup_dependent_build_environment(),
# with args extended to convey the client's compilers as needed.
if '+mkl' in self.spec or self.provides('mkl'):
# Spack's env philosophy demands that we replicate some of the
# settings normally handled by file_to_source ...
#
# TODO: Why is setup_run_environment()
# [which uses file_to_source()]
# not called as a matter of course upon entering the current
# function? (guarding against multiple calls notwithstanding)
#
# Use a local dict to facilitate debug_print():
env_mods = {
'MKLROOT': self.normalize_path('mkl'),
'SPACK_COMPILER_EXTRA_RPATHS': self.component_lib_dir('mkl'),
'CMAKE_PREFIX_PATH': self.normalize_path('mkl'),
'CMAKE_LIBRARY_PATH': self.component_lib_dir('mkl'),
'CMAKE_INCLUDE_PATH': self.component_include_dir('mkl'),
}
env.set('MKLROOT', env_mods['MKLROOT'])
env.append_path('SPACK_COMPILER_EXTRA_RPATHS',
env_mods['SPACK_COMPILER_EXTRA_RPATHS'])
env.append_path('CMAKE_PREFIX_PATH', env_mods['CMAKE_PREFIX_PATH'])
env.append_path('CMAKE_LIBRARY_PATH',
env_mods['CMAKE_LIBRARY_PATH'])
env.append_path('CMAKE_INCLUDE_PATH',
env_mods['CMAKE_INCLUDE_PATH'])
debug_print("adding/modifying build env:", env_mods)
if '+mpi' in self.spec or self.provides('mpi'):
if compilers_of_client:
self.mpi_setup_dependent_build_environment(
env, dependent_spec, compilers_of_client)
# We could forego this nonce function and inline its code here,
# but (a) it sisters mpi_compiler_wrappers() [needed twice]
# which performs dizzyingly similar but necessarily different
# actions, and (b) function code leaves a bit more breathing
# room within the suffocating corset of flake8 line length.
else:
raise InstallError('compilers_of_client arg required for MPI')
[docs] def setup_dependent_package(self, module, dep_spec):
# https://spack.readthedocs.io/en/latest/spack.html#spack.package.PackageBase.setup_dependent_package
# Reminder: "module" refers to Python module.
# Called before the install() method of dependents.
if '+mpi' in self.spec or self.provides('mpi'):
compiler_wrapper_commands = self.mpi_compiler_wrappers
self.spec.mpicc = compiler_wrapper_commands['MPICC']
self.spec.mpicxx = compiler_wrapper_commands['MPICXX']
self.spec.mpif77 = compiler_wrapper_commands['MPIF77']
self.spec.mpifc = compiler_wrapper_commands['MPIFC']
debug_print(("spec '%s' received .mpi* properties:" % self.spec),
compiler_wrapper_commands)
# ---------------------------------------------------------------------
# Specifics for installation phase
# ---------------------------------------------------------------------
@property
def global_license_file(self):
"""Returns the path where a Spack-global license file should be stored.
All Intel software shares the same license, so we store it in a
common 'intel' directory."""
return os.path.join(self.global_license_dir, 'intel', 'license.lic')
@property
def _determine_license_type(self):
'''Provide appropriate license tokens for the installer (silent.cfg).
'''
# See:
# ./README-intel.rst, section "Details for licensing tokens".
# ./build_systems/README-intel.rst, section "Licenses"
#
# Ideally, we just tell the installer to look around on the system.
# Thankfully, we neither need to care nor emulate where it looks:
license_type = {'ACTIVATION_TYPE': 'exist_lic', }
# However (and only), if the spack-internal Intel license file has been
# populated beyond its templated explanatory comments, proffer it to
# the installer instead:
f = self.global_license_file
if os.path.isfile(f):
# The file will have been created upon self.license_required AND
# self.license_files having been populated, so the "if" is usually
# true by the time the present function runs; ../hooks/licensing.py
with open(f) as fh:
if re.search(r'^[ \t]*[^' + self.license_comment + '\n]',
fh.read(), re.MULTILINE):
license_type = {
'ACTIVATION_TYPE': 'license_file',
'ACTIVATION_LICENSE_FILE': f,
}
debug_print(license_type)
return license_type
[docs] def install(self, spec, prefix):
'''Runs Intel's install.sh installation script. Afterwards, save the
installer config and logs to <prefix>/.spack
'''
# prepare
tmpdir = tempfile.mkdtemp(prefix='spack-intel-')
install_script = Executable('./install.sh')
install_script.add_default_env('TMPDIR', tmpdir)
# Need to set HOME to avoid using ~/intel
install_script.add_default_env('HOME', prefix)
# perform
install_script('--silent', 'silent.cfg')
# preserve config and logs
dst = os.path.join(self.prefix, '.spack')
install('silent.cfg', dst)
for f in glob.glob('%s/intel*log' % tmpdir):
install(f, dst)
[docs] @run_after('install')
def filter_compiler_wrappers(self):
if (('+mpi' in self.spec or self.provides('mpi')) and
'~newdtags' in self.spec):
bin_dir = self.component_bin_dir('mpi')
for f in 'mpif77 mpif90 mpigcc mpigxx mpiicc mpiicpc ' \
'mpiifort'.split():
f = os.path.join(bin_dir, f)
filter_file('-Xlinker --enable-new-dtags', ' ', f, string=True)
[docs] @run_after('install')
def uninstall_ism(self):
# The "Intel(R) Software Improvement Program" [ahem] gets installed,
# apparently regardless of PHONEHOME_SEND_USAGE_DATA.
#
# https://software.intel.com/en-us/articles/software-improvement-program
# https://software.intel.com/en-us/forums/intel-c-compiler/topic/506959
# Hubert H. (Intel) Mon, 03/10/2014 - 03:02 wrote:
# "... you can also uninstall the Intel(R) Software Manager
# completely: <installdir>/intel/ism/uninstall.sh"
f = os.path.join(self.normalize_path('ism'), 'uninstall.sh')
if os.path.isfile(f):
tty.warn('Uninstalling "Intel Software Improvement Program"'
'component')
uninstall = Executable(f)
uninstall('--silent')
# TODO? also try
# ~/intel/ism/uninstall --silent
debug_print(os.getcwd())
return
@property
def base_lib_dir(self):
"""Provide the library directory located in the base of Intel installation.
"""
d = self.normalize_path('')
d = os.path.join(d, 'lib')
debug_print(d)
return d
[docs] @run_after('install')
def modify_LLVMgold_rpath(self):
"""Add libimf.so and other required libraries to the RUNPATH of LLVMgold.so.
These are needed explicitly at dependent link time when
`ld -plugin LLVMgold.so` is called by the compiler.
"""
if self._has_compilers:
LLVMgold_libs = find_libraries('LLVMgold', self.base_lib_dir,
shared=True, recursive=True)
# Ignore ia32 entries as they mostly ignore throughout the rest
# of the file.
# The first entry in rpath preserves the original, the seconds entry
# is the location of libimf.so. If this relative location is changed
# in compiler releases, then we need to search for libimf.so instead
# of this static path.
for lib in LLVMgold_libs:
if not self.spec.satisfies('^patchelf'):
raise spack.error.SpackError(
'Attempting to patch RPATH in LLVMgold.so.'
+ '`patchelf` dependency should be set in package.py'
)
patchelf = Executable('patchelf')
rpath = ':'.join([patchelf('--print-rpath', lib, output=str).strip(),
'$ORIGIN/../compiler/lib/intel64_lin'])
patchelf('--set-rpath', rpath, lib)
# Check that self.prefix is there after installation
run_after('install')(PackageBase.sanity_check_prefix)