# Copyright 2013-2023 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 os
import os.path
import sys
import pytest
from llnl.util.filesystem import getuid, touch
import spack
import spack.detection
import spack.detection.path
from spack.main import SpackCommand
from spack.spec import Spec
@pytest.fixture
def executables_found(monkeypatch):
def _factory(result):
def _mock_search(path_hints=None):
return result
monkeypatch.setattr(spack.detection.path, "executables_in_path", _mock_search)
return _factory
def define_plat_exe(exe):
if sys.platform == "win32":
exe += ".bat"
return exe
def test_find_external_single_package(mock_executable):
cmake_path = mock_executable("cmake", output="echo cmake version 1.foo")
search_dir = cmake_path.parent.parent
specs_by_package = spack.detection.by_path(["cmake"], path_hints=[str(search_dir)])
assert len(specs_by_package) == 1 and "cmake" in specs_by_package
detected_spec = specs_by_package["cmake"]
assert len(detected_spec) == 1 and detected_spec[0].spec == Spec("cmake@1.foo")
def test_find_external_two_instances_same_package(mock_executable):
# Each of these cmake instances is created in a different prefix
# In Windows, quoted strings are echo'd with quotes includes
# we need to avoid that for proper regex.
cmake1 = mock_executable("cmake", output="echo cmake version 1.foo", subdir=("base1", "bin"))
cmake2 = mock_executable("cmake", output="echo cmake version 3.17.2", subdir=("base2", "bin"))
search_paths = [str(cmake1.parent.parent), str(cmake2.parent.parent)]
finder = spack.detection.path.ExecutablesFinder()
detected_specs = finder.find(pkg_name="cmake", initial_guess=search_paths)
assert len(detected_specs) == 2
spec_to_path = {e.spec: e.prefix for e in detected_specs}
assert spec_to_path[Spec("cmake@1.foo")] == (
spack.detection.executable_prefix(str(cmake1.parent))
)
assert spec_to_path[Spec("cmake@3.17.2")] == (
spack.detection.executable_prefix(str(cmake2.parent))
)
def test_find_external_update_config(mutable_config):
entries = [
spack.detection.DetectedPackage(Spec.from_detection("cmake@1.foo"), "/x/y1/"),
spack.detection.DetectedPackage(Spec.from_detection("cmake@3.17.2"), "/x/y2/"),
]
pkg_to_entries = {"cmake": entries}
scope = spack.config.default_modify_scope("packages")
spack.detection.update_configuration(pkg_to_entries, scope=scope, buildable=True)
pkgs_cfg = spack.config.get("packages")
cmake_cfg = pkgs_cfg["cmake"]
cmake_externals = cmake_cfg["externals"]
assert {"spec": "cmake@1.foo", "prefix": "/x/y1/"} in cmake_externals
assert {"spec": "cmake@3.17.2", "prefix": "/x/y2/"} in cmake_externals
def test_get_executables(working_env, mock_executable):
cmake_path1 = mock_executable("cmake", output="echo cmake version 1.foo")
path_to_exe = spack.detection.executables_in_path([os.path.dirname(cmake_path1)])
cmake_exe = define_plat_exe("cmake")
assert path_to_exe[str(cmake_path1)] == cmake_exe
external = SpackCommand("external")
def test_find_external_cmd_not_buildable(mutable_config, working_env, mock_executable):
"""When the user invokes 'spack external find --not-buildable', the config
for any package where Spack finds an external version should be marked as
not buildable.
"""
cmake_path1 = mock_executable("cmake", output="echo cmake version 1.foo")
os.environ["PATH"] = os.pathsep.join([os.path.dirname(cmake_path1)])
external("find", "--not-buildable", "cmake")
pkgs_cfg = spack.config.get("packages")
assert "cmake" in pkgs_cfg
assert not pkgs_cfg["cmake"]["buildable"]
@pytest.mark.parametrize(
"names,tags,exclude,expected",
[
# find --all
(None, ["detectable"], [], ["builtin.mock.find-externals1"]),
# find --all --exclude find-externals1
(None, ["detectable"], ["builtin.mock.find-externals1"], []),
(None, ["detectable"], ["find-externals1"], []),
# find cmake (and cmake is not detectable)
(["cmake"], ["detectable"], [], []),
],
)
def test_package_selection(names, tags, exclude, expected, mutable_mock_repo):
"""Tests various cases of selecting packages"""
# In the mock repo we only have 'find-externals1' that is detectable
result = spack.cmd.external.packages_to_search_for(names=names, tags=tags, exclude=exclude)
assert set(result) == set(expected)
def test_find_external_no_manifest(mutable_config, working_env, mutable_mock_repo, monkeypatch):
"""The user runs 'spack external find'; the default path for storing
manifest files does not exist. Ensure that the command does not
fail.
"""
monkeypatch.setenv("PATH", "")
monkeypatch.setattr(
spack.cray_manifest, "default_path", os.path.join("a", "path", "that", "doesnt", "exist")
)
external("find")
def test_find_external_empty_default_manifest_dir(
mutable_config, working_env, mutable_mock_repo, tmpdir, monkeypatch
):
"""The user runs 'spack external find'; the default path for storing
manifest files exists but is empty. Ensure that the command does not
fail.
"""
empty_manifest_dir = str(tmpdir.mkdir("manifest_dir"))
monkeypatch.setenv("PATH", "")
monkeypatch.setattr(spack.cray_manifest, "default_path", empty_manifest_dir)
external("find")
@pytest.mark.not_on_windows("Can't chmod on Windows")
@pytest.mark.skipif(getuid() == 0, reason="user is root")
def test_find_external_manifest_with_bad_permissions(
mutable_config, working_env, mutable_mock_repo, tmpdir, monkeypatch
):
"""The user runs 'spack external find'; the default path for storing
manifest files exists but with insufficient permissions. Check that
the command does not fail.
"""
test_manifest_dir = str(tmpdir.mkdir("manifest_dir"))
test_manifest_file_path = os.path.join(test_manifest_dir, "badperms.json")
touch(test_manifest_file_path)
monkeypatch.setenv("PATH", "")
monkeypatch.setattr(spack.cray_manifest, "default_path", test_manifest_dir)
try:
os.chmod(test_manifest_file_path, 0)
output = external("find")
assert "insufficient permissions" in output
assert "Skipping manifest and continuing" in output
finally:
os.chmod(test_manifest_file_path, 0o700)
def test_find_external_manifest_failure(mutable_config, mutable_mock_repo, tmpdir, monkeypatch):
"""The user runs 'spack external find'; the manifest parsing fails with
some exception. Ensure that the command still succeeds (i.e. moves on
to other external detection mechanisms).
"""
# First, create an empty manifest file (without a file to read, the
# manifest parsing is skipped)
test_manifest_dir = str(tmpdir.mkdir("manifest_dir"))
test_manifest_file_path = os.path.join(test_manifest_dir, "test.json")
touch(test_manifest_file_path)
def fail():
raise Exception()
monkeypatch.setattr(spack.cmd.external, "_collect_and_consume_cray_manifest_files", fail)
monkeypatch.setenv("PATH", "")
output = external("find")
assert "Skipping manifest and continuing" in output
def test_find_external_merge(mutable_config, mutable_mock_repo):
"""Check that 'spack find external' doesn't overwrite an existing spec
entry in packages.yaml.
"""
pkgs_cfg_init = {
"find-externals1": {
"externals": [{"spec": "find-externals1@1.1", "prefix": "/preexisting-prefix/"}],
"buildable": False,
}
}
mutable_config.update_config("packages", pkgs_cfg_init)
entries = [
spack.detection.DetectedPackage(Spec.from_detection("find-externals1@1.1"), "/x/y1/"),
spack.detection.DetectedPackage(Spec.from_detection("find-externals1@1.2"), "/x/y2/"),
]
pkg_to_entries = {"find-externals1": entries}
scope = spack.config.default_modify_scope("packages")
spack.detection.update_configuration(pkg_to_entries, scope=scope, buildable=True)
pkgs_cfg = spack.config.get("packages")
pkg_cfg = pkgs_cfg["find-externals1"]
pkg_externals = pkg_cfg["externals"]
assert {"spec": "find-externals1@1.1", "prefix": "/preexisting-prefix/"} in pkg_externals
assert {"spec": "find-externals1@1.2", "prefix": "/x/y2/"} in pkg_externals
def test_list_detectable_packages(mutable_config, mutable_mock_repo):
external("list")
assert external.returncode == 0
def test_overriding_prefix(mock_executable, mutable_config, monkeypatch):
gcc_exe = mock_executable("gcc", output="echo 4.2.1")
search_dir = gcc_exe.parent
@classmethod
def _determine_variants(cls, exes, version_str):
return "languages=c", {"prefix": "/opt/gcc/bin", "compilers": {"c": exes[0]}}
gcc_cls = spack.repo.PATH.get_pkg_class("gcc")
monkeypatch.setattr(gcc_cls, "determine_variants", _determine_variants)
finder = spack.detection.path.ExecutablesFinder()
detected_specs = finder.find(pkg_name="gcc", initial_guess=[str(search_dir)])
assert len(detected_specs) == 1
gcc = detected_specs[0].spec
assert gcc.name == "gcc"
assert gcc.external_path == os.path.sep + os.path.join("opt", "gcc", "bin")
def test_new_entries_are_reported_correctly(mock_executable, mutable_config, monkeypatch):
# Prepare an environment to detect a fake gcc
gcc_exe = mock_executable("gcc", output="echo 4.2.1")
prefix = os.path.dirname(gcc_exe)
monkeypatch.setenv("PATH", prefix)
# The first run will find and add the external gcc
output = external("find", "gcc")
assert "The following specs have been" in output
# The second run should report that no new external
# has been found
output = external("find", "gcc")
assert "No new external packages detected" in output
@pytest.mark.parametrize("command_args", [("-t", "build-tools"), ("-t", "build-tools", "cmake")])
def test_use_tags_for_detection(command_args, mock_executable, mutable_config, monkeypatch):
# Prepare an environment to detect a fake cmake
cmake_exe = mock_executable("cmake", output="echo cmake version 3.19.1")
prefix = os.path.dirname(cmake_exe)
monkeypatch.setenv("PATH", prefix)
openssl_exe = mock_executable("openssl", output="OpenSSL 2.8.3")
prefix = os.path.dirname(openssl_exe)
monkeypatch.setenv("PATH", prefix)
# Test that we detect specs
output = external("find", *command_args)
assert "The following specs have been" in output
assert "cmake" in output
assert "openssl" not in output
@pytest.mark.regression("38733")
@pytest.mark.not_on_windows("the test uses bash scripts")
def test_failures_in_scanning_do_not_result_in_an_error(
mock_executable, monkeypatch, mutable_config
):
"""Tests that scanning paths with wrong permissions, won't cause `external find` to error."""
cmake_exe1 = mock_executable(
"cmake", output="echo cmake version 3.19.1", subdir=("first", "bin")
)
cmake_exe2 = mock_executable(
"cmake", output="echo cmake version 3.23.3", subdir=("second", "bin")
)
# Remove access from the first directory executable
cmake_exe1.parent.chmod(0o600)
value = os.pathsep.join([str(cmake_exe1.parent), str(cmake_exe2.parent)])
monkeypatch.setenv("PATH", value)
output = external("find", "cmake")
assert external.returncode == 0
assert "The following specs have been" in output
assert "cmake" in output
assert "3.23.3" in output
assert "3.19.1" not in output