Source code for spack.bootstrap.clingo

# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Bootstrap concrete specs for clingo

Spack uses clingo to concretize specs. When clingo itself needs to be bootstrapped from sources,
we need to rely on another mechanism to get a concrete spec that fits the current host.

This module contains the logic to get a concrete spec for clingo, starting from a prototype
JSON file for a similar platform.
"""

import pathlib
import sys
from typing import Dict, Optional, Tuple, Type

import spack.vendor.archspec.cpu

import spack.compilers.config
import spack.compilers.libraries
import spack.config
import spack.package_base
import spack.platforms
import spack.repo
import spack.spec
import spack.traverse
import spack.version

from .config import spec_for_current_python


def _select_best_version(
    pkg_cls: Type["spack.package_base.PackageBase"], node: spack.spec.Spec, valid_versions: str
) -> None:
    """Try to attach the best known version to a node"""
    constraint = spack.version.from_string(valid_versions)
    allowed_versions = [v for v in pkg_cls.versions if v.satisfies(constraint)]
    try:
        best_version = spack.package_base.sort_by_pkg_preference(allowed_versions, pkg=pkg_cls)[0]
    except (KeyError, ValueError, IndexError):
        return
    node.versions.versions = [spack.version.from_string(f"={best_version}")]


def _add_compilers_if_missing() -> None:
    arch = spack.spec.ArchSpec.default_arch()
    if not spack.compilers.config.compilers_for_arch(arch):
        spack.compilers.config.find_compilers()


[docs] class ClingoBootstrapConcretizer: def __init__(self, configuration): _add_compilers_if_missing() self.host_platform = spack.platforms.host() self.host_os = self.host_platform.default_operating_system() self.host_target = spack.vendor.archspec.cpu.host().family self.host_architecture = spack.spec.ArchSpec.default_arch() self.host_architecture.target = str(self.host_target) self.host_compiler = self._valid_compiler_or_raise() self.host_python = self.python_external_spec() if str(self.host_platform) == "linux": self.host_libc = self.libc_external_spec() self.external_cmake, self.external_bison = self._externals_from_yaml(configuration) def _valid_compiler_or_raise(self): if str(self.host_platform) == "linux": compiler_name = "gcc" elif str(self.host_platform) == "darwin": compiler_name = "apple-clang" elif str(self.host_platform) == "windows": compiler_name = "msvc" elif str(self.host_platform) == "freebsd": compiler_name = "llvm" else: raise RuntimeError(f"Cannot bootstrap clingo from sources on {self.host_platform}") candidates = [ x for x in spack.compilers.config.CompilerFactory.from_packages_yaml(spack.config.CONFIG) if x.name == compiler_name ] if not candidates: raise RuntimeError( f"Cannot find any version of {compiler_name} to bootstrap clingo from sources" ) candidates.sort(key=lambda x: x.version, reverse=True) best = candidates[0] # Get compilers for bootstrapping from the 'builtin' repository best.namespace = "builtin" # If the compiler does not support C++ 14, fail with a legible error message try: _ = best.package.standard_flag(language="cxx", standard="14") except RuntimeError as e: raise RuntimeError( "cannot find a compiler supporting C++ 14 [needed to bootstrap clingo]" ) from e return candidates[0] def _externals_from_yaml( self, configuration: "spack.config.Configuration" ) -> Tuple[Optional["spack.spec.Spec"], Optional["spack.spec.Spec"]]: packages_yaml = configuration.get("packages") requirements = {"cmake": "@3.20:", "bison": "@2.5:"} selected: Dict[str, Optional["spack.spec.Spec"]] = {"cmake": None, "bison": None} for pkg_name in ["cmake", "bison"]: if pkg_name not in packages_yaml: continue candidates = packages_yaml[pkg_name].get("externals", []) for candidate in candidates: s = spack.spec.Spec(candidate["spec"], external_path=candidate["prefix"]) if not s.satisfies(requirements[pkg_name]): continue if not s.intersects(f"arch={self.host_architecture}"): continue selected[pkg_name] = self._external_spec(s) break return selected["cmake"], selected["bison"]
[docs] def prototype_path(self) -> pathlib.Path: """Path to a prototype concrete specfile for clingo""" parent_dir = pathlib.Path(__file__).parent result = parent_dir / "prototypes" / f"clingo-{self.host_platform}-{self.host_target}.json" if str(self.host_platform) == "linux": # Using aarch64 as a fallback, since it has gnuconfig (x86_64 doesn't have it) if not result.exists(): result = parent_dir / "prototypes" / f"clingo-{self.host_platform}-aarch64.json" elif str(self.host_platform) == "freebsd": result = parent_dir / "prototypes" / f"clingo-{self.host_platform}-amd64.json" elif not result.exists(): raise RuntimeError(f"Cannot bootstrap clingo from sources on {self.host_platform}") return result
[docs] def concretize(self) -> "spack.spec.Spec": # Read the prototype and mark it NOT concrete s = spack.spec.Spec.from_specfile(str(self.prototype_path())) s._mark_concrete(False) # These are nodes in the cmake stack, whose versions are frequently deprecated for # security reasons. In case there is no external cmake on this machine, we'll update # their versions to the most preferred, within the valid range, according to the # repository we know. to_be_updated = { pkg_name: (spack.repo.PATH.get_pkg_class(pkg_name), valid_versions) for pkg_name, valid_versions in { "ca-certificates-mozilla": ":", "openssl": "3:3", "curl": "8:8", "cmake": "3.16:3", "libiconv": "1:1", "ncurses": "6:6", "m4": "1.4", }.items() } # Tweak it to conform to the host architecture + update the version of a few dependencies for node in s.traverse(): # Clear patches, we'll compute them correctly later node.patches.clear() if "patches" in node.variants: del node.variants["patches"] node.architecture.os = str(self.host_os) node.architecture = self.host_architecture if node.name == "gcc-runtime": node.versions = self.host_compiler.versions if node.name in to_be_updated: pkg_cls, valid_versions = to_be_updated[node.name] _select_best_version(pkg_cls=pkg_cls, node=node, valid_versions=valid_versions) # Can't use re2c@3.1 with Python 3.6 if self.host_python.satisfies("@3.6"): s["re2c"].versions.versions = [spack.version.from_string("=2.2")] for edge in spack.traverse.traverse_edges([s], cover="edges"): if edge.spec.name == "python": edge.spec = self.host_python if edge.spec.name == "bison" and self.external_bison: edge.spec = self.external_bison if edge.spec.name == "cmake" and self.external_cmake: edge.spec = self.external_cmake if edge.spec.name == self.host_compiler.name: edge.spec = self.host_compiler if "libc" in edge.virtuals: edge.spec = self.host_libc spack.spec._inject_patches_variant(s) s._finalize_concretization() # Work around the fact that the installer calls Spec.dependents() and # we modified edges inconsistently return s.copy()
[docs] def python_external_spec(self) -> "spack.spec.Spec": """Python external spec corresponding to the current running interpreter""" result = spack.spec.Spec(spec_for_current_python(), external_path=sys.exec_prefix) return self._external_spec(result)
[docs] def libc_external_spec(self) -> "spack.spec.Spec": detector = spack.compilers.libraries.CompilerPropertyDetector(self.host_compiler) result = detector.default_libc() return self._external_spec(result)
def _external_spec(self, initial_spec) -> "spack.spec.Spec": initial_spec.namespace = "builtin" initial_spec.architecture = self.host_architecture for flag_type in spack.spec.FlagMap.valid_compiler_flags(): initial_spec.compiler_flags[flag_type] = [] return spack.spec.parse_with_version_concrete(initial_spec)