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 re
from enum import Enum
from typing import List, Optional
@ -45,8 +46,8 @@ class DepfileNode:
def __init__(
self, target: spack.spec.Spec, prereqs: List[spack.spec.Spec], buildcache: UseBuildCache
):
self.target = target
self.prereqs = prereqs
self.target = MakefileSpec(target)
self.prereqs = list(MakefileSpec(x) for x in prereqs)
if buildcache == UseBuildCache.ONLY:
self.buildcache_flag = "--use-buildcache=only"
elif buildcache == UseBuildCache.NEVER:
@ -89,6 +90,32 @@ def accept(self, node):
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:
"""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
# 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
# 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)
self.make_adjacency_list = [
(
self._safe_name(item.target),
" ".join(self._install_target(self._safe_name(s)) for s in item.prereqs),
item.target.dag_hash(),
item.target.format("{name}{@version}{%compiler}{variants}{arch=architecture}"),
item.target.safe_name(),
" ".join(self._install_target(s.safe_name()) for s in item.prereqs),
item.target.spec_hash(),
item.target.unsafe_format(
"{name}{@version}{%compiler}{variants}{arch=architecture}"
),
item.buildcache_flag,
)
for item in adjacency_list
]
# 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 ""
@ -157,7 +186,7 @@ def __init__(
self.phony_convenience_targets: List[str] = []
for node in adjacency_list:
tgt = self._safe_name(node.target)
tgt = node.target.safe_name()
self.all_pkg_identifiers.append(tgt)
self.all_install_related_targets.append(self._install_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-deps", tgt))
def _safe_name(self, spec: spack.spec.Spec) -> str:
return spec.format("{name}-{version}-{hash}")
def _target(self, name: str) -> str:
# 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

View File

@ -19,10 +19,12 @@
import spack.cmd.env
import spack.config
import spack.environment as ev
import spack.environment.depfile as depfile
import spack.environment.environment
import spack.environment.shell
import spack.error
import spack.modules
import spack.package_base
import spack.paths
import spack.repo
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))
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(
"picked_package,expected_installs",
[