Source code for spack.build_systems.intel

# Copyright 2013-2023 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_base import InstallError
from spack.util.environment import EnvironmentModifications
from spack.util.executable import Executable
from spack.util.prefix import Prefix
from spack.version import Version, ver

from .generic import Package

# 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(Package): """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. """ #: 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"), "PKG_CONFIG_PATH": os.path.join(self.normalize_path("mkl"), "bin", "pkgconfig"), } 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"]) env.append_path("PKG_CONFIG_PATH", env_mods["PKG_CONFIG_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_base.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] @spack.builder.run_before("install") def configure(self): """Generates the silent.cfg file to pass to installer.sh. See https://software.intel.com/en-us/articles/configuration-file-format """ prefix = self.prefix # Both tokens AND values of the configuration file are validated during # the run of the underlying binary installer. Any unknown token or # unacceptable value will cause that installer to fail. Notably, this # applies to trying to specify a license for a product that does not # require one. # # Fortunately, the validator is a script from a solid code base that is # only lightly adapted to the token vocabulary of each product and # release. Let's get that script so we can preempt its objections. # # Rather than running the script on a trial file and dissecting its # pronouncements, let's brazenly skim it for supported tokens and build # our configuration accordingly. We can do this because the tokens are # quite long and specific. validator_code = open("pset/check.awk", "r").read() # Let's go a little further and distill the tokens (plus some noise). tokenlike_words = set(re.findall(r"[A-Z_]{4,}", validator_code)) # NB: .cfg files generated with the "--duplicate filename" option have # the COMPONENTS string begin with a separator - do not worry about it. components_joined = ";".join(self._filtered_components) nonrpm_db_dir = os.path.join(prefix, "nonrpm-db") config_draft = { # Basics first - these should be accepted in all products. "ACCEPT_EULA": "accept", "PSET_MODE": "install", "CONTINUE_WITH_OPTIONAL_ERROR": "yes", "CONTINUE_WITH_INSTALLDIR_OVERWRITE": "yes", "SIGNING_ENABLED": "no", # Highly variable package specifics: "PSET_INSTALL_DIR": prefix, "NONRPM_DB_DIR": nonrpm_db_dir, "COMPONENTS": components_joined, # Conditional tokens; the first is supported post-2015 only. # Ignore ia32; most recent products don't even provide it. "ARCH_SELECTED": "INTEL64", # was: 'ALL' # 'ism' component -- see uninstall_ism(); also varies by release. "PHONEHOME_SEND_USAGE_DATA": "no", # Ah, as of 2018.2, that somewhat loaded term got replaced by one # in business-speak. We uphold our preference, both out of general # principles and for technical reasons like overhead and non-routed # compute nodes. "INTEL_SW_IMPROVEMENT_PROGRAM_CONSENT": "no", } # Deal with licensing only if truly needed. # NB: Token was 'ACTIVATION' pre ~2013, so basically irrelevant here. if "ACTIVATION_TYPE" in tokenlike_words: config_draft.update(self._determine_license_type) # Write sorted *by token* so the file looks less like a hash dump. f = open("silent.cfg", "w") for token, value in sorted(config_draft.items()): if token in tokenlike_words: f.write("%s=%s\n" % (token, value)) f.close()
[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] @spack.builder.run_after("install") def validate_install(self): # Sometimes the installer exits with an error but doesn't pass a # non-zero exit code to spack. Check for the existence of a 'bin' # directory to catch this error condition. if not os.path.exists(self.prefix.bin): raise InstallError("The installer has failed to install anything.")
[docs] @spack.builder.run_after("install") def configure_rpath(self): if "+rpath" not in self.spec: return # https://software.intel.com/en-us/cpp-compiler-18.0-developer-guide-and-reference-using-configuration-files compilers_bin_dir = self.component_bin_dir("compiler") compilers_lib_dir = self.component_lib_dir("compiler") for compiler_name in "icc icpc ifort".split(): f = os.path.join(compilers_bin_dir, compiler_name) if not os.path.isfile(f): raise InstallError("Cannot find compiler command to configure rpath:\n\t" + f) compiler_cfg = os.path.abspath(f + ".cfg") with open(compiler_cfg, "w") as fh: fh.write("-Xlinker -rpath={0}\n".format(compilers_lib_dir))
[docs] @spack.builder.run_after("install") def configure_auto_dispatch(self): if self._has_compilers: if "auto_dispatch=none" in self.spec: return compilers_bin_dir = self.component_bin_dir("compiler") for compiler_name in "icc icpc ifort".split(): f = os.path.join(compilers_bin_dir, compiler_name) if not os.path.isfile(f): raise InstallError( "Cannot find compiler command to configure " "auto_dispatch:\n\t" + f ) ad = [] for x in IntelPackage.auto_dispatch_options: if "auto_dispatch={0}".format(x) in self.spec: ad.append(x) compiler_cfg = os.path.abspath(f + ".cfg") with open(compiler_cfg, "a") as fh: fh.write("-ax{0}\n".format(",".join(ad)))
[docs] @spack.builder.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] @spack.builder.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] @spack.builder.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)