ASP-based solver: improve reusing nodes with gcc-runtime (#42408)
* ASP-based solver: improve reusing nodes with gcc-runtime This PR skips emitting dependency constraints on "gcc-runtime", for concrete specs that are considered for reuse. Instead, an appropriate version of gcc-runtime is recomputed considering also the concrete nodes from reused specs. This ensures that root nodes in a DAG have always a runtime that is at a version greater or equal than their dependencies. * Add unit-test for view with multiple runtimes * Select latest version of runtimes in views * Construct result keeping track of latest * Keep ordering stable, just in case
This commit is contained in:
parent
13daa1b692
commit
157d47fc5a
3 changed files with 90 additions and 6 deletions
|
@ -626,14 +626,13 @@ def view(self, new: Optional[str] = None) -> SimpleFilesystemView:
|
||||||
new: If a string, create a FilesystemView rooted at that path. Default None. This
|
new: If a string, create a FilesystemView rooted at that path. Default None. This
|
||||||
should only be used to regenerate the view, and cannot be used to access specs.
|
should only be used to regenerate the view, and cannot be used to access specs.
|
||||||
"""
|
"""
|
||||||
root = new if new else self._current_root
|
path = new if new else self._current_root
|
||||||
if not root:
|
if not path:
|
||||||
# This can only be hit if we write a future bug
|
# This can only be hit if we write a future bug
|
||||||
raise SpackEnvironmentViewError(
|
raise SpackEnvironmentViewError(
|
||||||
"Attempting to get nonexistent view from environment. "
|
f"Attempting to get nonexistent view from environment. View root is at {self.root}"
|
||||||
f"View root is at {self.root}"
|
|
||||||
)
|
)
|
||||||
return self._view(root)
|
return self._view(path)
|
||||||
|
|
||||||
def _view(self, root: str) -> SimpleFilesystemView:
|
def _view(self, root: str) -> SimpleFilesystemView:
|
||||||
"""Returns a view object for a given root dir."""
|
"""Returns a view object for a given root dir."""
|
||||||
|
@ -678,7 +677,9 @@ def specs_for_view(self, concrete_roots: List[Spec]) -> List[Spec]:
|
||||||
|
|
||||||
# Filter selected, installed specs
|
# Filter selected, installed specs
|
||||||
with spack.store.STORE.db.read_transaction():
|
with spack.store.STORE.db.read_transaction():
|
||||||
return [s for s in specs if s in self and s.installed]
|
result = [s for s in specs if s in self and s.installed]
|
||||||
|
|
||||||
|
return self._exclude_duplicate_runtimes(result)
|
||||||
|
|
||||||
def regenerate(self, concrete_roots: List[Spec]) -> None:
|
def regenerate(self, concrete_roots: List[Spec]) -> None:
|
||||||
specs = self.specs_for_view(concrete_roots)
|
specs = self.specs_for_view(concrete_roots)
|
||||||
|
@ -765,6 +766,16 @@ def regenerate(self, concrete_roots: List[Spec]) -> None:
|
||||||
msg += str(e)
|
msg += str(e)
|
||||||
tty.warn(msg)
|
tty.warn(msg)
|
||||||
|
|
||||||
|
def _exclude_duplicate_runtimes(self, nodes):
|
||||||
|
all_runtimes = spack.repo.PATH.packages_with_tags("runtime")
|
||||||
|
runtimes_by_name = {}
|
||||||
|
for s in nodes:
|
||||||
|
if s.name not in all_runtimes:
|
||||||
|
continue
|
||||||
|
current_runtime = runtimes_by_name.get(s.name, s)
|
||||||
|
runtimes_by_name[s.name] = max(current_runtime, s, key=lambda x: x.version)
|
||||||
|
return [x for x in nodes if x.name not in all_runtimes or runtimes_by_name[x.name] == x]
|
||||||
|
|
||||||
|
|
||||||
def _create_environment(path):
|
def _create_environment(path):
|
||||||
return Environment(path)
|
return Environment(path)
|
||||||
|
|
|
@ -1786,6 +1786,11 @@ def _spec_clauses(
|
||||||
dep = dspec.spec
|
dep = dspec.spec
|
||||||
|
|
||||||
if spec.concrete:
|
if spec.concrete:
|
||||||
|
# GCC runtime is solved again by clingo, even on concrete specs, to give
|
||||||
|
# the possibility to reuse specs built against a different runtime.
|
||||||
|
if dep.name == "gcc-runtime":
|
||||||
|
continue
|
||||||
|
|
||||||
# We know dependencies are real for concrete specs. For abstract
|
# We know dependencies are real for concrete specs. For abstract
|
||||||
# specs they just mean the dep is somehow in the DAG.
|
# specs they just mean the dep is somehow in the DAG.
|
||||||
for dtype in dt.ALL_FLAGS:
|
for dtype in dt.ALL_FLAGS:
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
import spack.repo
|
import spack.repo
|
||||||
import spack.solver.asp
|
import spack.solver.asp
|
||||||
import spack.spec
|
import spack.spec
|
||||||
|
from spack.environment.environment import ViewDescriptor
|
||||||
from spack.version import Version
|
from spack.version import Version
|
||||||
|
|
||||||
pytestmark = [
|
pytestmark = [
|
||||||
|
@ -19,6 +20,17 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _concretize_with_reuse(*, root_str, reused_str):
|
||||||
|
reused_spec = spack.spec.Spec(reused_str).concretized()
|
||||||
|
setup = spack.solver.asp.SpackSolverSetup(tests=False)
|
||||||
|
driver = spack.solver.asp.PyclingoDriver()
|
||||||
|
result, _, _ = driver.solve(
|
||||||
|
setup, [spack.spec.Spec(f"{root_str} ^{reused_str}")], reuse=[reused_spec]
|
||||||
|
)
|
||||||
|
root = result.specs[0]
|
||||||
|
return root, reused_spec
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def runtime_repo(config):
|
def runtime_repo(config):
|
||||||
repo = os.path.join(spack.paths.repos_path, "compiler_runtime.test")
|
repo = os.path.join(spack.paths.repos_path, "compiler_runtime.test")
|
||||||
|
@ -60,3 +72,59 @@ def test_external_nodes_do_not_have_runtimes(runtime_repo, mutable_config, tmp_p
|
||||||
assert a.dependencies("gcc-runtime")
|
assert a.dependencies("gcc-runtime")
|
||||||
assert a.dependencies("b")
|
assert a.dependencies("b")
|
||||||
assert not b.dependencies("gcc-runtime")
|
assert not b.dependencies("gcc-runtime")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"root_str,reused_str,expected,nruntime",
|
||||||
|
[
|
||||||
|
# The reused runtime is older than we need, thus we'll add a more recent one for a
|
||||||
|
("a%gcc@10.2.1", "b%gcc@4.5.0", {"a": "gcc-runtime@10.2.1", "b": "gcc-runtime@4.5.0"}, 2),
|
||||||
|
# The root is compiled with an older compiler, thus we'll reuse the runtime from b
|
||||||
|
("a%gcc@4.5.0", "b%gcc@10.2.1", {"a": "gcc-runtime@10.2.1", "b": "gcc-runtime@10.2.1"}, 1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_reusing_specs_with_gcc_runtime(root_str, reused_str, expected, nruntime, runtime_repo):
|
||||||
|
"""Tests that we can reuse specs with a "gcc-runtime" leaf node. In particular, checks
|
||||||
|
that the semantic for gcc-runtimes versions accounts for reused packages too.
|
||||||
|
"""
|
||||||
|
root, reused_spec = _concretize_with_reuse(root_str=root_str, reused_str=reused_str)
|
||||||
|
|
||||||
|
assert f"{expected['b']}" in reused_spec
|
||||||
|
runtime_a = root.dependencies("gcc-runtime")[0]
|
||||||
|
assert runtime_a.satisfies(expected["a"])
|
||||||
|
runtime_b = root["b"].dependencies("gcc-runtime")[0]
|
||||||
|
assert runtime_b.satisfies(expected["b"])
|
||||||
|
|
||||||
|
runtimes = [x for x in root.traverse() if x.name == "gcc-runtime"]
|
||||||
|
assert len(runtimes) == nruntime
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"root_str,reused_str,expected,not_expected",
|
||||||
|
[
|
||||||
|
# Ensure that, whether we have multiple runtimes in the DAG or not,
|
||||||
|
# we always link only the latest version
|
||||||
|
("a%gcc@10.2.1", "b%gcc@4.5.0", ["gcc-runtime@10.2.1"], ["gcc-runtime@4.5.0"]),
|
||||||
|
("a%gcc@4.5.0", "b%gcc@10.2.1", ["gcc-runtime@10.2.1"], ["gcc-runtime@4.5.0"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_views_can_handle_duplicate_runtime_nodes(
|
||||||
|
root_str, reused_str, expected, not_expected, runtime_repo, tmp_path, monkeypatch
|
||||||
|
):
|
||||||
|
"""Tests that an environment is able to select the latest version of a runtime node to be
|
||||||
|
linked in a view, in case more than one compatible version is in the DAG.
|
||||||
|
"""
|
||||||
|
root, reused_spec = _concretize_with_reuse(root_str=root_str, reused_str=reused_str)
|
||||||
|
|
||||||
|
# Mock the installation status to allow selecting nodes for the view
|
||||||
|
monkeypatch.setattr(spack.spec.Spec, "installed", True)
|
||||||
|
nodes = list(root.traverse())
|
||||||
|
|
||||||
|
view = ViewDescriptor(str(tmp_path), str(tmp_path))
|
||||||
|
candidate_specs = view.specs_for_view(nodes)
|
||||||
|
|
||||||
|
for x in expected:
|
||||||
|
assert any(node.satisfies(x) for node in candidate_specs)
|
||||||
|
|
||||||
|
for x in not_expected:
|
||||||
|
assert all(not node.satisfies(x) for node in candidate_specs)
|
||||||
|
|
Loading…
Reference in a new issue