Co-authored-by: Harmen Stoppels <me@harmenstoppels.nl>
This commit is contained in:
		@@ -8,6 +8,7 @@
 | 
				
			|||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
from enum import Enum
 | 
					from enum import Enum
 | 
				
			||||||
from typing import List, Optional
 | 
					from typing import List, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -45,8 +46,8 @@ class DepfileNode:
 | 
				
			|||||||
    def __init__(
 | 
					    def __init__(
 | 
				
			||||||
        self, target: spack.spec.Spec, prereqs: List[spack.spec.Spec], buildcache: UseBuildCache
 | 
					        self, target: spack.spec.Spec, prereqs: List[spack.spec.Spec], buildcache: UseBuildCache
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        self.target = target
 | 
					        self.target = MakefileSpec(target)
 | 
				
			||||||
        self.prereqs = prereqs
 | 
					        self.prereqs = list(MakefileSpec(x) for x in prereqs)
 | 
				
			||||||
        if buildcache == UseBuildCache.ONLY:
 | 
					        if buildcache == UseBuildCache.ONLY:
 | 
				
			||||||
            self.buildcache_flag = "--use-buildcache=only"
 | 
					            self.buildcache_flag = "--use-buildcache=only"
 | 
				
			||||||
        elif buildcache == UseBuildCache.NEVER:
 | 
					        elif buildcache == UseBuildCache.NEVER:
 | 
				
			||||||
@@ -89,6 +90,32 @@ def accept(self, node):
 | 
				
			|||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MakefileSpec(object):
 | 
				
			||||||
 | 
					    """Limited interface to spec to help generate targets etc. without
 | 
				
			||||||
 | 
					    introducing unwanted special characters.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _pattern = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, spec):
 | 
				
			||||||
 | 
					        self.spec = spec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def safe_name(self):
 | 
				
			||||||
 | 
					        return self.safe_format("{name}-{version}-{hash}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def spec_hash(self):
 | 
				
			||||||
 | 
					        return self.spec.dag_hash()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def safe_format(self, format_str):
 | 
				
			||||||
 | 
					        unsafe_result = self.spec.format(format_str)
 | 
				
			||||||
 | 
					        if not MakefileSpec._pattern:
 | 
				
			||||||
 | 
					            MakefileSpec._pattern = re.compile(r"[^A-Za-z0-9_.-]")
 | 
				
			||||||
 | 
					        return MakefileSpec._pattern.sub("_", unsafe_result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unsafe_format(self, format_str):
 | 
				
			||||||
 | 
					        return self.spec.format(format_str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MakefileModel:
 | 
					class MakefileModel:
 | 
				
			||||||
    """This class produces all data to render a makefile for specs of an environment."""
 | 
					    """This class produces all data to render a makefile for specs of an environment."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -114,7 +141,7 @@ def __init__(
 | 
				
			|||||||
        self.env_path = env.path
 | 
					        self.env_path = env.path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # These specs are built in the default target.
 | 
					        # These specs are built in the default target.
 | 
				
			||||||
        self.roots = roots
 | 
					        self.roots = list(MakefileSpec(x) for x in roots)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # The SPACK_PACKAGE_IDS variable is "exported", which can be used when including
 | 
					        # The SPACK_PACKAGE_IDS variable is "exported", which can be used when including
 | 
				
			||||||
        # generated makefiles to add post-install hooks, like pushing to a buildcache,
 | 
					        # generated makefiles to add post-install hooks, like pushing to a buildcache,
 | 
				
			||||||
@@ -131,17 +158,19 @@ def __init__(
 | 
				
			|||||||
        # And here we collect a tuple of (target, prereqs, dag_hash, nice_name, buildcache_flag)
 | 
					        # And here we collect a tuple of (target, prereqs, dag_hash, nice_name, buildcache_flag)
 | 
				
			||||||
        self.make_adjacency_list = [
 | 
					        self.make_adjacency_list = [
 | 
				
			||||||
            (
 | 
					            (
 | 
				
			||||||
                self._safe_name(item.target),
 | 
					                item.target.safe_name(),
 | 
				
			||||||
                " ".join(self._install_target(self._safe_name(s)) for s in item.prereqs),
 | 
					                " ".join(self._install_target(s.safe_name()) for s in item.prereqs),
 | 
				
			||||||
                item.target.dag_hash(),
 | 
					                item.target.spec_hash(),
 | 
				
			||||||
                item.target.format("{name}{@version}{%compiler}{variants}{arch=architecture}"),
 | 
					                item.target.unsafe_format(
 | 
				
			||||||
 | 
					                    "{name}{@version}{%compiler}{variants}{arch=architecture}"
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
                item.buildcache_flag,
 | 
					                item.buildcache_flag,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            for item in adjacency_list
 | 
					            for item in adjacency_list
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Root specs without deps are the prereqs for the environment target
 | 
					        # Root specs without deps are the prereqs for the environment target
 | 
				
			||||||
        self.root_install_targets = [self._install_target(self._safe_name(s)) for s in roots]
 | 
					        self.root_install_targets = [self._install_target(s.safe_name()) for s in self.roots]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.jobserver_support = "+" if jobserver else ""
 | 
					        self.jobserver_support = "+" if jobserver else ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -157,7 +186,7 @@ def __init__(
 | 
				
			|||||||
        self.phony_convenience_targets: List[str] = []
 | 
					        self.phony_convenience_targets: List[str] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for node in adjacency_list:
 | 
					        for node in adjacency_list:
 | 
				
			||||||
            tgt = self._safe_name(node.target)
 | 
					            tgt = node.target.safe_name()
 | 
				
			||||||
            self.all_pkg_identifiers.append(tgt)
 | 
					            self.all_pkg_identifiers.append(tgt)
 | 
				
			||||||
            self.all_install_related_targets.append(self._install_target(tgt))
 | 
					            self.all_install_related_targets.append(self._install_target(tgt))
 | 
				
			||||||
            self.all_install_related_targets.append(self._install_deps_target(tgt))
 | 
					            self.all_install_related_targets.append(self._install_deps_target(tgt))
 | 
				
			||||||
@@ -165,9 +194,6 @@ def __init__(
 | 
				
			|||||||
                self.phony_convenience_targets.append(os.path.join("install", tgt))
 | 
					                self.phony_convenience_targets.append(os.path.join("install", tgt))
 | 
				
			||||||
                self.phony_convenience_targets.append(os.path.join("install-deps", tgt))
 | 
					                self.phony_convenience_targets.append(os.path.join("install-deps", tgt))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _safe_name(self, spec: spack.spec.Spec) -> str:
 | 
					 | 
				
			||||||
        return spec.format("{name}-{version}-{hash}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _target(self, name: str) -> str:
 | 
					    def _target(self, name: str) -> str:
 | 
				
			||||||
        # The `all` and `clean` targets are phony. It doesn't make sense to
 | 
					        # The `all` and `clean` targets are phony. It doesn't make sense to
 | 
				
			||||||
        # have /abs/path/to/env/metadir/{all,clean} targets. But it *does* make
 | 
					        # have /abs/path/to/env/metadir/{all,clean} targets. But it *does* make
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,10 +19,12 @@
 | 
				
			|||||||
import spack.cmd.env
 | 
					import spack.cmd.env
 | 
				
			||||||
import spack.config
 | 
					import spack.config
 | 
				
			||||||
import spack.environment as ev
 | 
					import spack.environment as ev
 | 
				
			||||||
 | 
					import spack.environment.depfile as depfile
 | 
				
			||||||
import spack.environment.environment
 | 
					import spack.environment.environment
 | 
				
			||||||
import spack.environment.shell
 | 
					import spack.environment.shell
 | 
				
			||||||
import spack.error
 | 
					import spack.error
 | 
				
			||||||
import spack.modules
 | 
					import spack.modules
 | 
				
			||||||
 | 
					import spack.package_base
 | 
				
			||||||
import spack.paths
 | 
					import spack.paths
 | 
				
			||||||
import spack.repo
 | 
					import spack.repo
 | 
				
			||||||
import spack.util.spack_json as sjson
 | 
					import spack.util.spack_json as sjson
 | 
				
			||||||
@@ -3136,6 +3138,57 @@ def test_environment_depfile_makefile(depfile_flags, expected_installs, tmpdir,
 | 
				
			|||||||
    assert len(specs_that_make_would_install) == len(set(specs_that_make_would_install))
 | 
					    assert len(specs_that_make_would_install) == len(set(specs_that_make_would_install))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_depfile_safe_format():
 | 
				
			||||||
 | 
					    """Test that depfile.MakefileSpec.safe_format() escapes target names."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class SpecLike:
 | 
				
			||||||
 | 
					        def format(self, _):
 | 
				
			||||||
 | 
					            return "abc@def=ghi"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    spec = depfile.MakefileSpec(SpecLike())
 | 
				
			||||||
 | 
					    assert spec.safe_format("{name}") == "abc_def_ghi"
 | 
				
			||||||
 | 
					    assert spec.unsafe_format("{name}") == "abc@def=ghi"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_depfile_works_with_gitversions(tmpdir, mock_packages, monkeypatch):
 | 
				
			||||||
 | 
					    """Git versions may contain = chars, which should be escaped in targets,
 | 
				
			||||||
 | 
					    otherwise they're interpreted as makefile variable assignments."""
 | 
				
			||||||
 | 
					    monkeypatch.setattr(spack.package_base.PackageBase, "git", "repo.git", raising=False)
 | 
				
			||||||
 | 
					    env("create", "test")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    make = Executable("make")
 | 
				
			||||||
 | 
					    makefile = str(tmpdir.join("Makefile"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create an environment with dttop and dtlink1 both at a git version,
 | 
				
			||||||
 | 
					    # and generate a depfile
 | 
				
			||||||
 | 
					    with ev.read("test"):
 | 
				
			||||||
 | 
					        add(f"dttop@{'a' * 40}=1.0 ^dtlink1@{'b' * 40}=1.0")
 | 
				
			||||||
 | 
					        concretize()
 | 
				
			||||||
 | 
					        env("depfile", "-o", makefile, "--make-disable-jobserver", "--make-prefix=prefix")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Do a dry run on the generated depfile
 | 
				
			||||||
 | 
					    out = make("-n", "-f", makefile, output=str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check that all specs are there (without duplicates)
 | 
				
			||||||
 | 
					    specs_that_make_would_install = _parse_dry_run_package_installs(out)
 | 
				
			||||||
 | 
					    expected_installs = [
 | 
				
			||||||
 | 
					        "dtbuild1",
 | 
				
			||||||
 | 
					        "dtbuild2",
 | 
				
			||||||
 | 
					        "dtbuild3",
 | 
				
			||||||
 | 
					        "dtlink1",
 | 
				
			||||||
 | 
					        "dtlink2",
 | 
				
			||||||
 | 
					        "dtlink3",
 | 
				
			||||||
 | 
					        "dtlink4",
 | 
				
			||||||
 | 
					        "dtlink5",
 | 
				
			||||||
 | 
					        "dtrun1",
 | 
				
			||||||
 | 
					        "dtrun2",
 | 
				
			||||||
 | 
					        "dtrun3",
 | 
				
			||||||
 | 
					        "dttop",
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    assert set(specs_that_make_would_install) == set(expected_installs)
 | 
				
			||||||
 | 
					    assert len(specs_that_make_would_install) == len(set(specs_that_make_would_install))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    "picked_package,expected_installs",
 | 
					    "picked_package,expected_installs",
 | 
				
			||||||
    [
 | 
					    [
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user