summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMassimiliano Culpo <massimiliano.culpo@gmail.com>2024-04-03 14:18:47 +0200
committerHarmen Stoppels <harmenstoppels@gmail.com>2024-04-22 15:18:06 +0200
commit34146c197a6addcfda45c54d72d0ddb6871d392c (patch)
tree680e74eb55a2224c42a565dd19539ae1d41b5b13 /lib
parent209a3bf3026f9d75262dd1e92c04008876b4fbae (diff)
downloadspack-34146c197a6addcfda45c54d72d0ddb6871d392c.tar.gz
spack-34146c197a6addcfda45c54d72d0ddb6871d392c.tar.bz2
spack-34146c197a6addcfda45c54d72d0ddb6871d392c.tar.xz
spack-34146c197a6addcfda45c54d72d0ddb6871d392c.zip
Add libc dependency to compiled packages and runtime deps
This commit differentiate linux from other platforms by using libc compatibility as a criterion for deciding which buildcaches / binaries can be reused. Other platforms still use OS compatibility. On linux a libc is injected by all compilers as an implicit external, and the compatibility criterion is that a libc is compatible with all other libcs with the same name and a version that is lesser or equal. Some concretization unit tests use libc when run on linux.
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/solver/asp.py93
-rw-r--r--lib/spack/spack/solver/concretize.lp15
-rw-r--r--lib/spack/spack/solver/libc_compatibility.lp37
-rw-r--r--lib/spack/spack/solver/os_compatibility.lp16
-rw-r--r--lib/spack/spack/test/concretize.py43
5 files changed, 160 insertions, 44 deletions
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index a77e92a51d..631dd835fc 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -283,6 +283,27 @@ def all_compilers_in_config(configuration):
return spack.compilers.all_compilers_from(configuration)
+def compatible_libc(candidate_libc_spec):
+ """Returns a list of libc specs that are compatible with the one passed as argument"""
+ result = set()
+ for compiler in all_compilers_in_config(spack.config.CONFIG):
+ libc = compiler.default_libc()
+ if not libc:
+ continue
+ if (
+ libc.name == candidate_libc_spec.name
+ and libc.version >= candidate_libc_spec.version
+ and libc.external_path == candidate_libc_spec.external_path
+ ):
+ result.add(libc)
+ return sorted(result)
+
+
+def using_libc_compatibility() -> bool:
+ """Returns True if we are currently using libc compatibility"""
+ return spack.platforms.host().name == "linux"
+
+
def extend_flag_list(flag_list, new_flags):
"""Extend a list of flags, preserving order and precedence.
@@ -566,16 +587,16 @@ def _spec_with_default_name(spec_str, name):
return spec
-def _external_config_with_implictit_externals():
+def _external_config_with_implicit_externals(configuration):
# Read packages.yaml and normalize it, so that it will not contain entries referring to
# virtual packages.
- packages_yaml = _normalize_packages_yaml(spack.config.get("packages"))
+ packages_yaml = _normalize_packages_yaml(configuration.get("packages"))
# Add externals for libc from compilers on Linux
- if spack.platforms.host().name != "linux":
+ if not using_libc_compatibility():
return packages_yaml
- for compiler in all_compilers_in_config():
+ for compiler in all_compilers_in_config(configuration):
libc = compiler.default_libc()
if libc:
entry = {"spec": f"{libc} %{compiler.spec}", "prefix": libc.external_path}
@@ -801,10 +822,16 @@ class PyclingoDriver:
self.control.load(os.path.join(parent_dir, "heuristic.lp"))
if spack.config.CONFIG.get("concretizer:duplicates:strategy", "none") != "none":
self.control.load(os.path.join(parent_dir, "heuristic_separate.lp"))
- self.control.load(os.path.join(parent_dir, "os_compatibility.lp"))
self.control.load(os.path.join(parent_dir, "display.lp"))
if not setup.concretize_everything:
self.control.load(os.path.join(parent_dir, "when_possible.lp"))
+
+ # Binary compatibility is based on libc on Linux, and on the os tag elsewhere
+ if using_libc_compatibility():
+ self.control.load(os.path.join(parent_dir, "libc_compatibility.lp"))
+ else:
+ self.control.load(os.path.join(parent_dir, "os_compatibility.lp"))
+
timer.stop("load")
# Grounding is the first step in the solve -- it turns our facts
@@ -1572,7 +1599,7 @@ class SpackSolverSetup:
def external_packages(self):
"""Facts on external packages, from packages.yaml and implicit externals."""
- packages_yaml = _external_config_with_implictit_externals()
+ packages_yaml = _external_config_with_implicit_externals(spack.config.CONFIG)
self.gen.h1("External packages")
for pkg_name, data in packages_yaml.items():
@@ -1845,6 +1872,15 @@ class SpackSolverSetup:
if dep.name == "gcc-runtime":
continue
+ # LIBC is also solved again by clingo, but in this case the compatibility
+ # is not encoded in the parent node - so we need to emit explicit facts
+ if "libc" in dspec.virtuals:
+ for x in compatible_libc(dep):
+ clauses.append(
+ fn.attr("compatible_libc", spec.name, x.name, x.version)
+ )
+ continue
+
# We know dependencies are real for concrete specs. For abstract
# specs they just mean the dep is somehow in the DAG.
for dtype in dt.ALL_FLAGS:
@@ -2316,6 +2352,10 @@ class SpackSolverSetup:
self.gen = ProblemInstanceBuilder()
compiler_parser = CompilerParser(configuration=spack.config.CONFIG).with_input_specs(specs)
+ # Only relevant for linux
+ for libc in compiler_parser.allowed_libcs:
+ self.gen.fact(fn.allowed_libc(libc.name, libc.version))
+
if not allow_deprecated:
self.gen.fact(fn.deprecated_versions_not_allowed())
@@ -2445,18 +2485,35 @@ class SpackSolverSetup:
def define_runtime_constraints(self):
"""Define the constraints to be imposed on the runtimes"""
recorder = RuntimePropertyRecorder(self)
- # TODO: Use only available compilers ?
+
for compiler in self.possible_compilers:
- compiler_with_different_cls_names = {"oneapi": "intel-oneapi-compilers"}
+ compiler_with_different_cls_names = {
+ "oneapi": "intel-oneapi-compilers",
+ "clang": "llvm",
+ }
compiler_cls_name = compiler_with_different_cls_names.get(
compiler.spec.name, compiler.spec.name
)
try:
compiler_cls = spack.repo.PATH.get_pkg_class(compiler_cls_name)
+ if hasattr(compiler_cls, "runtime_constraints"):
+ compiler_cls.runtime_constraints(spec=compiler.spec, pkg=recorder)
except spack.repo.UnknownPackageError:
+ pass
+
+ # Inject libc from available compilers, on Linux
+ if not compiler.available:
continue
- if hasattr(compiler_cls, "runtime_constraints"):
- compiler_cls.runtime_constraints(spec=compiler.spec, pkg=recorder)
+
+ if using_libc_compatibility():
+ libc = compiler.compiler_obj.default_libc()
+ if libc:
+ recorder("*").depends_on(
+ "libc", when=f"%{compiler.spec}", type="link", description="Add libc"
+ )
+ recorder("*").depends_on(
+ str(libc), when=f"%{compiler.spec}", type="link", description="Add libc"
+ )
recorder.consume_facts()
@@ -2833,7 +2890,19 @@ class CompilerParser:
def __init__(self, configuration) -> None:
self.compilers: Set[KnownCompiler] = set()
+ self.allowed_libcs = set()
for c in all_compilers_in_config(configuration):
+ if using_libc_compatibility():
+ libc = c.default_libc()
+ if not libc:
+ warnings.warn(
+ f"cannot detect libc from {c.spec}. The compiler will not be used "
+ f"during concretization."
+ )
+ continue
+
+ self.allowed_libcs.add(libc)
+
target = c.target if c.target != "any" else None
candidate = KnownCompiler(
spec=c.spec, os=c.operating_system, target=target, available=True, compiler_obj=c
@@ -3199,7 +3268,7 @@ class SpecBuilder:
def external_spec_selected(self, node, idx):
"""This means that the external spec and index idx has been selected for this package."""
- packages_yaml = _external_config_with_implictit_externals()
+ packages_yaml = _external_config_with_implicit_externals(spack.config.CONFIG)
spec_info = packages_yaml[node.pkg]["externals"][int(idx)]
self._specs[node].external_path = spec_info.get("prefix", None)
self._specs[node].external_modules = spack.spec.Spec._format_module_list(
@@ -3514,7 +3583,7 @@ class Solver:
def _reusable_specs(self, specs):
reusable_specs = []
if self.reuse:
- packages = spack.config.get("packages")
+ packages = _external_config_with_implicit_externals(spack.config.CONFIG)
# Specs from the local Database
with spack.store.STORE.db.read_transaction():
reusable_specs.extend(
diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp
index 70b3645810..ba1da1cfe6 100644
--- a/lib/spack/spack/solver/concretize.lp
+++ b/lib/spack/spack/solver/concretize.lp
@@ -1023,14 +1023,6 @@ error(100, "Cannot select '{0} os={1}' (operating system '{1}' is not buildable)
attr("node_os", node(X, Package), OS),
not buildable_os(OS).
-% can't have dependencies on incompatible OS's
-error(100, "{0} and dependency {1} have incompatible operating systems 'os={2}' and 'os={3}'", Package, Dependency, PackageNodeOS, DependencyOS)
- :- depends_on(node(X, Package), node(Y, Dependency)),
- attr("node_os", node(X, Package), PackageNodeOS),
- attr("node_os", node(Y, Dependency), DependencyOS),
- not os_compatible(PackageNodeOS, DependencyOS),
- build(node(X, Package)).
-
% give OS choice weights according to os declarations
node_os_weight(PackageNode, Weight)
:- attr("node", PackageNode),
@@ -1043,13 +1035,6 @@ os_compatible(OS, OS) :- os(OS).
% Transitive compatibility among operating systems
os_compatible(OS1, OS3) :- os_compatible(OS1, OS2), os_compatible(OS2, OS3).
-% We can select only operating systems compatible with the ones
-% for which we can build software. We need a cardinality constraint
-% since we might have more than one "buildable_os(OS)" fact.
-:- not 1 { os_compatible(CurrentOS, ReusedOS) : buildable_os(CurrentOS) },
- attr("node_os", Package, ReusedOS),
- internal_error("Reused OS incompatible with build OS").
-
% If an OS is set explicitly respect the value
attr("node_os", PackageNode, OS) :- attr("node_os_set", PackageNode, OS), attr("node", PackageNode).
diff --git a/lib/spack/spack/solver/libc_compatibility.lp b/lib/spack/spack/solver/libc_compatibility.lp
new file mode 100644
index 0000000000..28c7c57fda
--- /dev/null
+++ b/lib/spack/spack/solver/libc_compatibility.lp
@@ -0,0 +1,37 @@
+% Copyright 2013-2024 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)
+
+%=============================================================================
+% Libc compatibility rules for reusing solves.
+%
+% These rules are used on Linux
+%=============================================================================
+
+% A package cannot be reused if the libc is not compatible with it
+:- provider(node(X, LibcPackage), node(0, "libc")),
+ attr("version", node(X, LibcPackage), LibcVersion),
+ attr("hash", node(R, ReusedPackage), Hash),
+ % Libc packages can be reused without the "compatible_libc" attribute
+ ReusedPackage != LibcPackage,
+ not attr("compatible_libc", node(R, ReusedPackage), LibcPackage, LibcVersion).
+
+% Check whether the DAG has any built package
+has_built_packages() :- build(X), not external(X).
+
+% A libc is needed in the DAG
+:- has_built_packages(), not provider(_, node(0, "libc")).
+
+% The libc must be chosen among available ones
+:- has_built_packages(),
+ provider(node(X, LibcPackage), node(0, "libc")),
+ attr("node", node(X, LibcPackage)),
+ attr("version", node(X, LibcPackage), LibcVersion),
+ not allowed_libc(LibcPackage, LibcVersion).
+
+% A built node must depend on libc
+:- build(PackageNode),
+ provider(LibcNode, node(0, "libc")),
+ not external(PackageNode),
+ not depends_on(PackageNode, LibcNode).
diff --git a/lib/spack/spack/solver/os_compatibility.lp b/lib/spack/spack/solver/os_compatibility.lp
index df312acf6d..86d255ee36 100644
--- a/lib/spack/spack/solver/os_compatibility.lp
+++ b/lib/spack/spack/solver/os_compatibility.lp
@@ -7,8 +7,24 @@
% OS compatibility rules for reusing solves.
% os_compatible(RecentOS, OlderOS)
% OlderOS binaries can be used on RecentOS
+%
+% These rules are used on every platform, but Linux
%=============================================================================
% macOS
os_compatible("monterey", "bigsur").
os_compatible("bigsur", "catalina").
+
+% can't have dependencies on incompatible OS's
+error(100, "{0} and dependency {1} have incompatible operating systems 'os={2}' and 'os={3}'", Package, Dependency, PackageNodeOS, DependencyOS)
+ :- depends_on(node(X, Package), node(Y, Dependency)),
+ attr("node_os", node(X, Package), PackageNodeOS),
+ attr("node_os", node(Y, Dependency), DependencyOS),
+ not os_compatible(PackageNodeOS, DependencyOS),
+ build(node(X, Package)).
+
+% We can select only operating systems compatible with the ones
+% for which we can build software. We need a cardinality constraint
+% since we might have more than one "buildable_os(OS)" fact.
+:- not 1 { os_compatible(CurrentOS, ReusedOS) : buildable_os(CurrentOS) },
+ attr("node_os", Package, ReusedOS).
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
index db67523150..070e79da0f 100644
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -13,6 +13,7 @@ import archspec.cpu
import llnl.util.lang
+import spack.compiler
import spack.compilers
import spack.concretize
import spack.config
@@ -67,6 +68,24 @@ def check_concretize(abstract_spec):
return concrete
+@pytest.fixture(scope="function", autouse=True)
+def binary_compatibility(monkeypatch, request):
+ """Selects whether we use OS compatibility for binaries, or libc compatibility."""
+ if spack.platforms.real_host().name != "linux":
+ return
+
+ if "mock_packages" not in request.fixturenames:
+ # Only builtin.mock has a mock glibc package
+ return
+
+ if "database" in request.fixturenames or "mutable_database" in request.fixturenames:
+ # Databases have been created without glibc support
+ return
+
+ monkeypatch.setattr(spack.solver.asp, "using_libc_compatibility", lambda: True)
+ monkeypatch.setattr(spack.compiler.Compiler, "default_libc", lambda x: Spec("glibc@=2.28"))
+
+
@pytest.fixture(
params=[
# no_deps
@@ -1452,6 +1471,8 @@ class TestConcretize:
):
s = Spec(spec_str).concretized()
for node in s.traverse():
+ if node.name == "glibc":
+ continue
assert node.satisfies(expected_os)
@pytest.mark.regression("22718")
@@ -1764,7 +1785,8 @@ class TestConcretize:
for s in result.specs:
concrete_specs.update(s.traverse())
- assert len(concrete_specs) == expected
+ libc_offset = 1 if spack.solver.asp.using_libc_compatibility() else 0
+ assert len(concrete_specs) == expected + libc_offset
@pytest.mark.parametrize(
"specs,expected_spec,occurances",
@@ -1884,29 +1906,16 @@ class TestConcretize:
result_spec = result.specs[0]
num_specs = len(list(result_spec.traverse()))
+ libc_offset = 1 if spack.solver.asp.using_libc_compatibility() else 0
criteria = [
- (num_specs - 1, None, "number of packages to build (vs. reuse)"),
+ (num_specs - 1 - libc_offset, None, "number of packages to build (vs. reuse)"),
(2, 0, "version badness"),
]
for criterion in criteria:
- assert criterion in result.criteria
+ assert criterion in result.criteria, result_spec
assert result_spec.satisfies("^b@1.0")
- @pytest.mark.regression("31169")
- @pytest.mark.only_clingo("Use case not supported by the original concretizer")
- def test_not_reusing_incompatible_os(self):
- root_spec = Spec("b")
- s = root_spec.concretized()
- wrong_os = s.copy()
- wrong_os.architecture = spack.spec.ArchSpec("test-ubuntu2204-x86_64")
- with spack.config.override("concretizer:reuse", True):
- solver = spack.solver.asp.Solver()
- setup = spack.solver.asp.SpackSolverSetup()
- result, _, _ = solver.driver.solve(setup, [root_spec], reuse=[wrong_os])
- concrete_spec = result.specs[0]
- assert concrete_spec.satisfies("os={}".format(s.architecture.os))
-
@pytest.mark.only_clingo("Use case not supported by the original concretizer")
def test_reuse_succeeds_with_config_compatible_os(self):
root_spec = Spec("b")