Environment/depfile: fix bug with Git hash versions (attempt #2) (#37560)

Co-authored-by: Harmen Stoppels <me@harmenstoppels.nl>
This commit is contained in:
Peter Scheibel 2023-07-17 04:17:32 -07:00 committed by GitHub
parent 63b88c4b75
commit 31431f967a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 91 additions and 12 deletions

View File

@ -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

View File

@ -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",
[ [