summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGreg Becker <becker33@llnl.gov>2024-05-06 01:33:33 -0700
committerGitHub <noreply@github.com>2024-05-06 10:33:33 +0200
commit1f31c3374ccfb843cc36e6c967fc61c8b832f10f (patch)
treedd4c3d36bb6f5b2b85cf8a31bc8bbc9a48c77de3 /lib
parent27aeb6e293c7fa3331e940bbc389bcdc4117bdd3 (diff)
downloadspack-1f31c3374ccfb843cc36e6c967fc61c8b832f10f.tar.gz
spack-1f31c3374ccfb843cc36e6c967fc61c8b832f10f.tar.bz2
spack-1f31c3374ccfb843cc36e6c967fc61c8b832f10f.tar.xz
spack-1f31c3374ccfb843cc36e6c967fc61c8b832f10f.zip
External package detection for compilers (#43464)
This creates shared infrastructure for compiler packages to implement the detailed search capabilities from the `spack compiler find` command for the `spack external find` command. After this commit, `spack compiler find` can be replaced with `spack external find --tag compiler`, with the exception of mixed toolchains.
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/docs/getting_started.rst14
-rw-r--r--lib/spack/spack/build_systems/compiler.py144
-rw-r--r--lib/spack/spack/compilers/__init__.py59
-rw-r--r--lib/spack/spack/package.py1
-rw-r--r--lib/spack/spack/test/cmd/compiler.py30
-rw-r--r--lib/spack/spack/test/conftest.py2
6 files changed, 202 insertions, 48 deletions
diff --git a/lib/spack/docs/getting_started.rst b/lib/spack/docs/getting_started.rst
index 25dfc95ee5..9435111c8e 100644
--- a/lib/spack/docs/getting_started.rst
+++ b/lib/spack/docs/getting_started.rst
@@ -478,6 +478,13 @@ prefix, you can add them to the ``extra_attributes`` field. Similarly,
all other fields from the compilers config can be added to the
``extra_attributes`` field for an external representing a compiler.
+Note that the format for the ``paths`` field in the
+``extra_attributes`` section is different than in the ``compilers``
+config. For compilers configured as external packages, the section is
+named ``compilers`` and the dictionary maps language names (``c``,
+``cxx``, ``fortran``) to paths, rather than using the names ``cc``,
+``fc``, and ``f77``.
+
.. code-block:: yaml
packages:
@@ -493,11 +500,10 @@ all other fields from the compilers config can be added to the
- spec: llvm+clang@15.0.0 arch=linux-rhel8-skylake
prefix: /usr
extra_attributes:
- paths:
- cc: /usr/bin/clang-with-suffix
+ compilers:
+ c: /usr/bin/clang-with-suffix
cxx: /usr/bin/clang++-with-extra-info
- fc: /usr/bin/gfortran
- f77: /usr/bin/gfortran
+ fortran: /usr/bin/gfortran
extra_rpaths:
- /usr/lib/llvm/
diff --git a/lib/spack/spack/build_systems/compiler.py b/lib/spack/spack/build_systems/compiler.py
new file mode 100644
index 0000000000..d441b57b2e
--- /dev/null
+++ b/lib/spack/spack/build_systems/compiler.py
@@ -0,0 +1,144 @@
+# 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 itertools
+import os
+import pathlib
+import re
+import sys
+from typing import Dict, List, Sequence, Tuple, Union
+
+import llnl.util.tty as tty
+from llnl.util.lang import classproperty
+
+import spack.compiler
+import spack.package_base
+
+# Local "type" for type hints
+Path = Union[str, pathlib.Path]
+
+
+class CompilerPackage(spack.package_base.PackageBase):
+ """A Package mixin for all common logic for packages that implement compilers"""
+
+ # TODO: how do these play nicely with other tags
+ tags: Sequence[str] = ["compiler"]
+
+ #: Optional suffix regexes for searching for this type of compiler.
+ #: Suffixes are used by some frameworks, e.g. macports uses an '-mp-X.Y'
+ #: version suffix for gcc.
+ compiler_suffixes: List[str] = [r"-.*"]
+
+ #: Optional prefix regexes for searching for this compiler
+ compiler_prefixes: List[str] = []
+
+ #: Compiler argument(s) that produces version information
+ #: If multiple arguments, the earlier arguments must produce errors when invalid
+ compiler_version_argument: Union[str, Tuple[str]] = "-dumpversion"
+
+ #: Regex used to extract version from compiler's output
+ compiler_version_regex: str = "(.*)"
+
+ #: Static definition of languages supported by this class
+ compiler_languages: Sequence[str] = ["c", "cxx", "fortran"]
+
+ def __init__(self, spec: "spack.spec.Spec"):
+ super().__init__(spec)
+ msg = f"Supported languages for {spec} are not a subset of possible supported languages"
+ msg += f" supports: {self.supported_languages}, valid values: {self.compiler_languages}"
+ assert set(self.supported_languages) <= set(self.compiler_languages), msg
+
+ @property
+ def supported_languages(self) -> Sequence[str]:
+ """Dynamic definition of languages supported by this package"""
+ return self.compiler_languages
+
+ @classproperty
+ def compiler_names(cls) -> Sequence[str]:
+ """Construct list of compiler names from per-language names"""
+ names = []
+ for language in cls.compiler_languages:
+ names.extend(getattr(cls, f"{language}_names"))
+ return names
+
+ @classproperty
+ def executables(cls) -> Sequence[str]:
+ """Construct executables for external detection from names, prefixes, and suffixes."""
+ regexp_fmt = r"^({0}){1}({2})$"
+ prefixes = [""] + cls.compiler_prefixes
+ suffixes = [""] + cls.compiler_suffixes
+ if sys.platform == "win32":
+ ext = r"\.(?:exe|bat)"
+ suffixes += [suf + ext for suf in suffixes]
+ return [
+ regexp_fmt.format(prefix, re.escape(name), suffix)
+ for prefix, name, suffix in itertools.product(prefixes, cls.compiler_names, suffixes)
+ ]
+
+ @classmethod
+ def determine_version(cls, exe: Path):
+ version_argument = cls.compiler_version_argument
+ if isinstance(version_argument, str):
+ version_argument = (version_argument,)
+
+ for va in version_argument:
+ try:
+ output = spack.compiler.get_compiler_version_output(exe, va)
+ match = re.search(cls.compiler_version_regex, output)
+ if match:
+ return ".".join(match.groups())
+ except spack.util.executable.ProcessError:
+ pass
+ except Exception as e:
+ tty.debug(
+ f"[{__file__}] Cannot detect a valid version for the executable "
+ f"{str(exe)}, for package '{cls.name}': {e}"
+ )
+
+ @classmethod
+ def compiler_bindir(cls, prefix: Path) -> Path:
+ """Overridable method for the location of the compiler bindir within the preifx"""
+ return os.path.join(prefix, "bin")
+
+ @classmethod
+ def determine_compiler_paths(cls, exes: Sequence[Path]) -> Dict[str, Path]:
+ """Compute the paths to compiler executables associated with this package
+
+ This is a helper method for ``determine_variants`` to compute the ``extra_attributes``
+ to include with each spec object."""
+ # There are often at least two copies (not symlinks) of each compiler executable in the
+ # same directory: one with a canonical name, e.g. "gfortran", and another one with the
+ # target prefix, e.g. "x86_64-pc-linux-gnu-gfortran". There also might be a copy of "gcc"
+ # with the version suffix, e.g. "x86_64-pc-linux-gnu-gcc-6.3.0". To ensure the consistency
+ # of values in the "paths" dictionary (i.e. we prefer all of them to reference copies
+ # with canonical names if possible), we iterate over the executables in the reversed sorted
+ # order:
+ # First pass over languages identifies exes that are perfect matches for canonical names
+ # Second pass checks for names with prefix/suffix
+ # Second pass is sorted by language name length because longer named languages
+ # e.g. cxx can often contain the names of shorter named languages
+ # e.g. c (e.g. clang/clang++)
+ paths = {}
+ exes = sorted(exes, reverse=True)
+ languages = {
+ lang: getattr(cls, f"{lang}_names")
+ for lang in sorted(cls.compiler_languages, key=len, reverse=True)
+ }
+ for exe in exes:
+ for lang, names in languages.items():
+ if os.path.basename(exe) in names:
+ paths[lang] = exe
+ break
+ else:
+ for lang, names in languages.items():
+ if any(name in os.path.basename(exe) for name in names):
+ paths[lang] = exe
+ break
+
+ return paths
+
+ @classmethod
+ def determine_variants(cls, exes: Sequence[Path], version_str: str) -> Tuple:
+ # path determination is separated so it can be reused in subclasses
+ return "", {"compilers": cls.determine_compiler_paths(exes=exes)}
diff --git a/lib/spack/spack/compilers/__init__.py b/lib/spack/spack/compilers/__init__.py
index a52d787f02..8ce9d81120 100644
--- a/lib/spack/spack/compilers/__init__.py
+++ b/lib/spack/spack/compilers/__init__.py
@@ -164,33 +164,56 @@ def _compiler_config_from_package_config(config):
def _compiler_config_from_external(config):
+ extra_attributes_key = "extra_attributes"
+ compilers_key = "compilers"
+ c_key, cxx_key, fortran_key = "c", "cxx", "fortran"
+
+ # Allow `@x.y.z` instead of `@=x.y.z`
spec = spack.spec.parse_with_version_concrete(config["spec"])
- # use str(spec.versions) to allow `@x.y.z` instead of `@=x.y.z`
+
compiler_spec = spack.spec.CompilerSpec(
package_name_to_compiler_name.get(spec.name, spec.name), spec.version
)
- extra_attributes = config.get("extra_attributes", {})
- prefix = config.get("prefix", None)
-
- compiler_class = class_for_compiler_name(compiler_spec.name)
- paths = extra_attributes.get("paths", {})
- compiler_langs = ["cc", "cxx", "fc", "f77"]
- for lang in compiler_langs:
- if paths.setdefault(lang, None):
- continue
+ err_header = f"The external spec '{spec}' cannot be used as a compiler"
- if not prefix:
- continue
+ # If extra_attributes is not there I might not want to use this entry as a compiler,
+ # therefore just leave a debug message, but don't be loud with a warning.
+ if extra_attributes_key not in config:
+ tty.debug(f"[{__file__}] {err_header}: missing the '{extra_attributes_key}' key")
+ return None
+ extra_attributes = config[extra_attributes_key]
- # Check for files that satisfy the naming scheme for this compiler
- bindir = os.path.join(prefix, "bin")
- for f, regex in itertools.product(os.listdir(bindir), compiler_class.search_regexps(lang)):
- if regex.match(f):
- paths[lang] = os.path.join(bindir, f)
+ # If I have 'extra_attributes' warn if 'compilers' is missing, or we don't have a C compiler
+ if compilers_key not in extra_attributes:
+ warnings.warn(
+ f"{err_header}: missing the '{compilers_key}' key under '{extra_attributes_key}'"
+ )
+ return None
+ attribute_compilers = extra_attributes[compilers_key]
- if all(v is None for v in paths.values()):
+ if c_key not in attribute_compilers:
+ warnings.warn(
+ f"{err_header}: missing the C compiler path under "
+ f"'{extra_attributes_key}:{compilers_key}'"
+ )
return None
+ c_compiler = attribute_compilers[c_key]
+
+ # C++ and Fortran compilers are not mandatory, so let's just leave a debug trace
+ if cxx_key not in attribute_compilers:
+ tty.debug(f"[{__file__}] The external spec {spec} does not have a C++ compiler")
+
+ if fortran_key not in attribute_compilers:
+ tty.debug(f"[{__file__}] The external spec {spec} does not have a Fortran compiler")
+
+ # compilers format has cc/fc/f77, externals format has "c/fortran"
+ paths = {
+ "cc": c_compiler,
+ "cxx": attribute_compilers.get(cxx_key, None),
+ "fc": attribute_compilers.get(fortran_key, None),
+ "f77": attribute_compilers.get(fortran_key, None),
+ }
if not spec.architecture:
host_platform = spack.platforms.host()
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
index ac56cf1e1a..d0b7beda1d 100644
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -39,6 +39,7 @@ from spack.build_systems.cached_cmake import (
)
from spack.build_systems.cargo import CargoPackage
from spack.build_systems.cmake import CMakePackage, generator
+from spack.build_systems.compiler import CompilerPackage
from spack.build_systems.cuda import CudaPackage
from spack.build_systems.generic import Package
from spack.build_systems.gnu import GNUMirrorPackage
diff --git a/lib/spack/spack/test/cmd/compiler.py b/lib/spack/spack/test/cmd/compiler.py
index 150fd1af54..2fde7fbc92 100644
--- a/lib/spack/spack/test/cmd/compiler.py
+++ b/lib/spack/spack/test/cmd/compiler.py
@@ -261,15 +261,14 @@ def test_compiler_list_empty(no_compilers_yaml, working_env, compilers_dir):
[
(
{
- "spec": "gcc@=7.7.7 os=foobar target=x86_64",
+ "spec": "gcc@=7.7.7 languages=c,cxx,fortran os=foobar target=x86_64",
"prefix": "/path/to/fake",
"modules": ["gcc/7.7.7", "foobar"],
"extra_attributes": {
- "paths": {
- "cc": "/path/to/fake/gcc",
+ "compilers": {
+ "c": "/path/to/fake/gcc",
"cxx": "/path/to/fake/g++",
- "fc": "/path/to/fake/gfortran",
- "f77": "/path/to/fake/gfortran",
+ "fortran": "/path/to/fake/gfortran",
},
"flags": {"fflags": "-ffree-form"},
},
@@ -285,26 +284,7 @@ def test_compiler_list_empty(no_compilers_yaml, working_env, compilers_dir):
\tmodules = ['gcc/7.7.7', 'foobar']
\toperating system = foobar
""",
- ),
- (
- {
- "spec": "gcc@7.7.7",
- "prefix": "{prefix}",
- "modules": ["gcc/7.7.7", "foobar"],
- "extra_attributes": {"flags": {"fflags": "-ffree-form"}},
- },
- """gcc@7.7.7:
-\tpaths:
-\t\tcc = {compilers_dir}{sep}gcc-8{suffix}
-\t\tcxx = {compilers_dir}{sep}g++-8{suffix}
-\t\tf77 = {compilers_dir}{sep}gfortran-8{suffix}
-\t\tfc = {compilers_dir}{sep}gfortran-8{suffix}
-\tflags:
-\t\tfflags = ['-ffree-form']
-\tmodules = ['gcc/7.7.7', 'foobar']
-\toperating system = debian6
-""",
- ),
+ )
],
)
def test_compilers_shows_packages_yaml(
diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py
index 59d6171e3d..29c10fb2e3 100644
--- a/lib/spack/spack/test/conftest.py
+++ b/lib/spack/spack/test/conftest.py
@@ -1694,7 +1694,7 @@ def mock_executable(tmp_path):
"""Factory to create a mock executable in a temporary directory that
output a custom string when run.
"""
- shebang = "#!/bin/sh\n" if sys.platform != "win32" else "@ECHO OFF"
+ shebang = "#!/bin/sh\n" if sys.platform != "win32" else "@ECHO OFF\n"
def _factory(name, output, subdir=("bin",)):
executable_dir = tmp_path.joinpath(*subdir)