summaryrefslogblamecommitdiff
path: root/lib/spack/spack/test/cmd/external.py
blob: e9a387aac03e66aa9da7673b4a2f019028df6fa7 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                                                                         


                                                                         
         
              
          
 

             

                                              
            

                           
                                   
                           

 





                                          

                                                                                      
                   
 
 
                         
                               
                     


              
                                                       
                                                                            
                                         
 
                                                                                       
 


                                                                                   
 
 
                                                                   
                                                                    

                                                                


                                                                                                  
 

                                                                              
 

                                                             
                                                 
                                                             

                                                  
                                                             
     


                                                     
               

                                                                                       
     
                                       
 
                                                         
                                                                                     
 


                                            
 

                                                                          

 

                                                                             
                                                                                     
                                        
                                                     

 
                                   

 
                                                                                       



                                                                              
                                                                             


                                                                        
                              
                                             

 



                                  
                                                                     
                                              
                                                                     












                                                                                                



                                                                        




                                                                                                 


                                                  
                                                                       
  



                                                                        



                                                                                

 
                                                     
                                                         
                                                     
                                                                       
  



                                                                        

                                                                              
                                  

                                                                               

                                            


                                                           



                                                
                                                                                                





                                                                           

                                                                          




                                  



                                                                                             

 




                                                                          


                                                                                             


         
                                                           
               

                                                                                              
     

                                                         
                                                                                     
 


                                           
 

                                                                                             






                                                                     
                                                                         
                                                         
                               


                                                    
                                                                                     
 
                                                  
                                                                           
 

                                                                                 
 




                                                                               

 
                                                                                          
                                                 
                                                         
                                     
                                      

                                                      

                                                    


                                                       



                                                        
                                                                                                 
                                                                                            
                                                   
                                                                            
                                       
                                      
 
                                                                    
                                         
                                      

                               



                                                    


                                
                                                         






















                                                                                                 
# 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