diff options
author | Greg Becker <becker33@llnl.gov> | 2024-05-06 01:33:33 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-06 10:33:33 +0200 |
commit | 1f31c3374ccfb843cc36e6c967fc61c8b832f10f (patch) | |
tree | dd4c3d36bb6f5b2b85cf8a31bc8bbc9a48c77de3 /lib | |
parent | 27aeb6e293c7fa3331e940bbc389bcdc4117bdd3 (diff) | |
download | spack-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.rst | 14 | ||||
-rw-r--r-- | lib/spack/spack/build_systems/compiler.py | 144 | ||||
-rw-r--r-- | lib/spack/spack/compilers/__init__.py | 59 | ||||
-rw-r--r-- | lib/spack/spack/package.py | 1 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/compiler.py | 30 | ||||
-rw-r--r-- | lib/spack/spack/test/conftest.py | 2 |
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) |