# 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)
import collections
import filecmp
import os
import sys
import pytest
from llnl.util.filesystem import mkdirp, touch, working_dir
import spack.patch
import spack.paths
import spack.repo
import spack.util.compression
import spack.util.url as url_util
from spack.spec import Spec
from spack.stage import Stage
from spack.util.executable import Executable
# various sha256 sums (using variables for legibility)
# many file based shas will differ between Windows and other platforms
# due to the use of carriage returns ('\r\n') in Windows line endings
# files with contents 'foo', 'bar', and 'baz'
foo_sha256 = (
"b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"
if sys.platform != "win32"
else "bf874c7dd3a83cf370fdc17e496e341de06cd596b5c66dbf3c9bb7f6c139e3ee"
)
bar_sha256 = (
"7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730"
if sys.platform != "win32"
else "556ddc69a75d0be0ecafc82cd4657666c8063f13d762282059c39ff5dbf18116"
)
baz_sha256 = (
"bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c"
if sys.platform != "win32"
else "d30392e66c636a063769cbb1db08cd3455a424650d4494db6379d73ea799582b"
)
biz_sha256 = (
"a69b288d7393261e613c276c6d38a01461028291f6e381623acc58139d01f54d"
if sys.platform != "win32"
else "2f2b087a8f84834fd03d4d1d5b43584011e869e4657504ef3f8b0a672a5c222e"
)
# url patches
# url shas are the same on Windows
url1_sha256 = "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
url2_sha256 = "1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd"
url2_archive_sha256 = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"
platform_url_sha = (
"252c0af58be3d90e5dc5e0d16658434c9efa5d20a5df6c10bf72c2d77f780866"
if sys.platform != "win32"
else "ecf44a8244a486e9ef5f72c6cb622f99718dcd790707ac91af0b8c9a4ab7a2bb"
)
@pytest.fixture()
def mock_patch_stage(tmpdir_factory, monkeypatch):
# Don't disrupt the spack install directory with tests.
mock_path = str(tmpdir_factory.mktemp("mock-patch-stage"))
monkeypatch.setattr(spack.stage, "_stage_root", mock_path)
return mock_path
data_path = os.path.join(spack.paths.test_path, "data", "patch")
@pytest.mark.not_on_windows("Line ending conflict on Windows")
@pytest.mark.parametrize(
"filename, sha256, archive_sha256",
[
# compressed patch -- needs sha256 and archive_256
(
os.path.join(data_path, "foo.tgz"),
"252c0af58be3d90e5dc5e0d16658434c9efa5d20a5df6c10bf72c2d77f780866",
"4e8092a161ec6c3a1b5253176fcf33ce7ba23ee2ff27c75dbced589dabacd06e",
),
# uncompressed patch -- needs only sha256
(os.path.join(data_path, "foo.patch"), platform_url_sha, None),
],
)
def test_url_patch(mock_patch_stage, filename, sha256, archive_sha256, config):
# Make a patch object
url = url_util.path_to_file_url(filename)
s = Spec("patch").concretized()
patch = spack.patch.UrlPatch(s.package, url, sha256=sha256, archive_sha256=archive_sha256)
# make a stage
with Stage(url) as stage: # TODO: url isn't used; maybe refactor Stage
stage.mirror_path = mock_patch_stage
mkdirp(stage.source_path)
with working_dir(stage.source_path):
# write a file to be patched
with open("foo.txt", "w") as f:
f.write(
"""\
first line
second line
"""
)
# write the expected result of patching.
with open("foo-expected.txt", "w") as f:
f.write(
"""\
zeroth line
first line
third line
"""
)
# apply the patch and compare files
with patch.stage:
patch.stage.create()
patch.stage.fetch()
patch.stage.expand_archive()
patch.apply(stage)
with working_dir(stage.source_path):
assert filecmp.cmp("foo.txt", "foo-expected.txt")
def test_patch_in_spec(mock_packages, config):
"""Test whether patches in a package appear in the spec."""
spec = Spec("patch")
spec.concretize()
assert "patches" in list(spec.variants.keys())
# Here the order is bar, foo, baz. Note that MV variants order
# lexicographically based on the hash, not on the position of the
# patch directive.
assert (bar_sha256, foo_sha256, baz_sha256) == spec.variants["patches"].value
assert (foo_sha256, bar_sha256, baz_sha256) == tuple(
spec.variants["patches"]._patches_in_order_of_appearance
)
def test_patch_mixed_versions_subset_constraint(mock_packages, config):
"""If we have a package with mixed x.y and x.y.z versions, make sure that
a patch applied to a version range of x.y.z versions is not applied to
an x.y version.
"""
spec1 = Spec("patch@1.0.1")
spec1.concretize()
assert biz_sha256 in spec1.variants["patches"].value
spec2 = Spec("patch@=1.0")
spec2.concretize()
assert biz_sha256 not in spec2.variants["patches"].value
def test_patch_order(mock_packages, config):
spec = Spec("dep-diamond-patch-top")
spec.concretize()
mid2_sha256 = (
"mid21234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
if sys.platform != "win32"
else "mid21234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
)
mid1_sha256 = (
"0b62284961dab49887e31319843431ee5b037382ac02c4fe436955abef11f094"
if sys.platform != "win32"
else "aeb16c4dec1087e39f2330542d59d9b456dd26d791338ae6d80b6ffd10c89dfa"
)
top_sha256 = (
"f7de2947c64cb6435e15fb2bef359d1ed5f6356b2aebb7b20535e3772904e6db"
if sys.platform != "win32"
else "ff34cb21271d16dbf928374f610bb5dd593d293d311036ddae86c4846ff79070"
)
dep = spec["patch"]
patch_order = dep.variants["patches"]._patches_in_order_of_appearance
# 'mid2' comes after 'mid1' alphabetically
# 'top' comes after 'mid1'/'mid2' alphabetically
# 'patch' comes last of all specs in the dag, alphabetically, so the
# patches of 'patch' to itself are applied last. The patches applied by
# 'patch' are ordered based on their appearance in the package.py file
expected_order = (mid1_sha256, mid2_sha256, top_sha256, foo_sha256, bar_sha256, baz_sha256)
assert expected_order == tuple(patch_order)
def test_nested_directives(mock_packages):
"""Ensure pkg data structures are set up properly by nested directives."""
# this ensures that the patch() directive results were removed
# properly from the DirectiveMeta._directives_to_be_executed list
patcher = spack.repo.PATH.get_pkg_class("patch-several-dependencies")
assert len(patcher.patches) == 0
# this ensures that results of dependency patches were properly added
# to Dependency objects.
libelf_dep = next(iter(patcher.dependencies["libelf"].values()))
assert len(libelf_dep.patches) == 1
assert len(libelf_dep.patches[Spec()]) == 1
libdwarf_dep = next(iter(patcher.dependencies["libdwarf"].values()))
assert len(libdwarf_dep.patches) == 2
assert len(libdwarf_dep.patches[Spec()]) == 1
assert len(libdwarf_dep.patches[Spec("@20111030")]) == 1
fake_dep = next(iter(patcher.dependencies["fake"].values()))
assert len(fake_dep.patches) == 1
assert len(fake_dep.patches[Spec()]) == 2
@pytest.mark.not_on_windows("Test requires Autotools")
def test_patched_dependency(mock_packages, config, install_mockery, mock_fetch):
"""Test whether patched dependencies work."""
spec = Spec("patch-a-dependency")
spec.concretize()
assert "patches" in list(spec["libelf"].variants.keys())
# make sure the patch makes it into the dependency spec
t_sha = (
"c45c1564f70def3fc1a6e22139f62cb21cd190cc3a7dbe6f4120fa59ce33dcb8"
if sys.platform != "win32"
else "3c5b65abcd6a3b2c714dbf7c31ff65fe3748a1adc371f030c283007ca5534f11"
)
assert (t_sha,) == spec["libelf"].variants["patches"].value
# make sure the patch in the dependent's directory is applied to the
# dependency
libelf = spec["libelf"]
pkg = libelf.package
pkg.do_patch()
with pkg.stage:
with working_dir(pkg.stage.source_path):
# output a Makefile with 'echo Patched!' as the default target
configure = Executable("./configure")
configure()
# Make sure the Makefile contains the patched text
with open("Makefile") as mf:
assert "Patched!" in mf.read()
def trigger_bad_patch(pkg):
if not os.path.isdir(pkg.stage.source_path):
os.makedirs(pkg.stage.source_path)
bad_file = os.path.join(pkg.stage.source_path, ".spack_patch_failed")
touch(bad_file)
return bad_file
def test_patch_failure_develop_spec_exits_gracefully(
mock_packages, config, install_mockery, mock_fetch, tmpdir
):
"""
ensure that a failing patch does not trigger exceptions
for develop specs
"""
spec = Spec("patch-a-dependency " "^libelf dev_path=%s" % str(tmpdir))
spec.concretize()
libelf = spec["libelf"]
assert "patches" in list(libelf.variants.keys())
pkg = libelf.package
with pkg.stage:
bad_patch_indicator = trigger_bad_patch(pkg)
assert os.path.isfile(bad_patch_indicator)
pkg.do_patch()
# success if no exceptions raised
def test_patch_failure_restages(mock_packages, config, install_mockery, mock_fetch):
"""
ensure that a failing patch does not trigger exceptions
for non-develop specs and the source gets restaged
"""
spec = Spec("patch-a-dependency")
spec.concretize()
pkg = spec["libelf"].package
with pkg.stage:
bad_patch_indicator = trigger_bad_patch(pkg)
assert os.path.isfile(bad_patch_indicator)
pkg.do_patch()
assert not os.path.isfile(bad_patch_indicator)
def test_multiple_patched_dependencies(mock_packages, config):
"""Test whether multiple patched dependencies work."""
spec = Spec("patch-several-dependencies")
spec.concretize()
# basic patch on libelf
assert "patches" in list(spec["libelf"].variants.keys())
# foo
assert (foo_sha256,) == spec["libelf"].variants["patches"].value
# URL patches
assert "patches" in list(spec["fake"].variants.keys())
# urlpatch.patch, urlpatch.patch.gz
assert (url2_sha256, url1_sha256) == spec["fake"].variants["patches"].value
def test_conditional_patched_dependencies(mock_packages, config):
"""Test whether conditional patched dependencies work."""
spec = Spec("patch-several-dependencies @1.0")
spec.concretize()
# basic patch on libelf
assert "patches" in list(spec["libelf"].variants.keys())
# foo
assert (foo_sha256,) == spec["libelf"].variants["patches"].value
# conditional patch on libdwarf
assert "patches" in list(spec["libdwarf"].variants.keys())
# bar
assert (bar_sha256,) == spec["libdwarf"].variants["patches"].value
# baz is conditional on libdwarf version
assert baz_sha256 not in spec["libdwarf"].variants["patches"].value
# URL patches
assert "patches" in list(spec["fake"].variants.keys())
# urlpatch.patch, urlpatch.patch.gz
assert (url2_sha256, url1_sha256) == spec["fake"].variants["patches"].value
def check_multi_dependency_patch_specs(
libelf, libdwarf, fake, owner, package_dir # specs
): # parent spec properties
"""Validate patches on dependencies of patch-several-dependencies."""
# basic patch on libelf
assert "patches" in list(libelf.variants.keys())
# foo
assert foo_sha256 in libelf.variants["patches"].value
# conditional patch on libdwarf
assert "patches" in list(libdwarf.variants.keys())
# bar
assert bar_sha256 in libdwarf.variants["patches"].value
# baz is conditional on libdwarf version (no guarantee on order w/conds)
assert baz_sha256 in libdwarf.variants["patches"].value
def get_patch(spec, ending):
return next(p for p in spec.patches if p.path_or_url.endswith(ending))
# make sure file patches are reconstructed properly
foo_patch = get_patch(libelf, "foo.patch")
bar_patch = get_patch(libdwarf, "bar.patch")
baz_patch = get_patch(libdwarf, "baz.patch")
assert foo_patch.owner == owner
assert foo_patch.path == os.path.join(package_dir, "foo.patch")
assert foo_patch.sha256 == foo_sha256
assert bar_patch.owner == "builtin.mock.patch-several-dependencies"
assert bar_patch.path == os.path.join(package_dir, "bar.patch")
assert bar_patch.sha256 == bar_sha256
assert baz_patch.owner == "builtin.mock.patch-several-dependencies"
assert baz_patch.path == os.path.join(package_dir, "baz.patch")
assert baz_patch.sha256 == baz_sha256
# URL patches
assert "patches" in list(fake.variants.keys())
# urlpatch.patch, urlpatch.patch.gz
assert (url2_sha256, url1_sha256) == fake.variants["patches"].value
url1_patch = get_patch(fake, "urlpatch.patch")
url2_patch = get_patch(fake, "urlpatch2.patch.gz")
assert url1_patch.owner == "builtin.mock.patch-several-dependencies"
assert url1_patch.url == "http://example.com/urlpatch.patch"
assert url1_patch.sha256 == url1_sha256
assert url2_patch.owner == "builtin.mock.patch-several-dependencies"
assert url2_patch.url == "http://example.com/urlpatch2.patch.gz"
assert url2_patch.sha256 == url2_sha256
assert url2_patch.archive_sha256 == url2_archive_sha256
def test_conditional_patched_deps_with_conditions(mock_packages, config):
"""Test whether conditional patched dependencies with conditions work."""
spec = Spec("patch-several-dependencies @1.0 ^libdwarf@20111030")
spec.concretize()
libelf = spec["libelf"]
libdwarf = spec["libdwarf"]
fake = spec["fake"]
check_multi_dependency_patch_specs(
libelf, libdwarf, fake, "builtin.mock.patch-several-dependencies", spec.package.package_dir
)
def test_write_and_read_sub_dags_with_patched_deps(mock_packages, config):
"""Test whether patched dependencies are still correct after writing and
reading a sub-DAG of a concretized Spec.
"""
spec = Spec("patch-several-dependencies @1.0 ^libdwarf@20111030")
spec.concretize()
# write to YAML and read back in -- new specs will *only* contain
# their sub-DAGs, and won't contain the dependent that patched them
libelf = spack.spec.Spec.from_yaml(spec["libelf"].to_yaml())
libdwarf = spack.spec.Spec.from_yaml(spec["libdwarf"].to_yaml())
fake = spack.spec.Spec.from_yaml(spec["fake"].to_yaml())
# make sure we can still read patches correctly for these specs
check_multi_dependency_patch_specs(
libelf, libdwarf, fake, "builtin.mock.patch-several-dependencies", spec.package.package_dir
)
def test_patch_no_file():
# Give it the attributes we need to construct the error message
FakePackage = collections.namedtuple("FakePackage", ["name", "namespace", "fullname"])
fp = FakePackage("fake-package", "test", "fake-package")
with pytest.raises(ValueError, match="FilePatch:"):
spack.patch.FilePatch(fp, "nonexistent_file", 0, "")
patch = spack.patch.Patch(fp, "nonexistent_file", 0, "")
patch.path = "test"
with pytest.raises(spack.patch.NoSuchPatchError, match="No such patch:"):
patch.apply("")
@pytest.mark.parametrize("level", [-1, 0.0, "1"])
def test_invalid_level(level):
# Give it the attributes we need to construct the error message
FakePackage = collections.namedtuple("FakePackage", ["name", "namespace"])
fp = FakePackage("fake-package", "test")
with pytest.raises(ValueError, match="Patch level needs to be a non-negative integer."):
spack.patch.Patch(fp, "nonexistent_file", level, "")