bugfix: make compiler preferences slightly saner (#17590)
* bugfix: make compiler preferences slightly saner This fixes two issues with the way we currently select compilers. If multiple compilers have the same "id" (os/arch/compiler/version), we currently prefer them by picking this one with the most supported languages. This can have some surprising effects: * If you have no `gfortran` but you have `gfortran-8`, you can detect `clang` that has no configured C compiler -- just `f77` and `f90`. This happens frequently on macOS with homebrew. The bug is due to some kludginess about the way we detect mixed `clang`/`gfortran`. * We can prefer suffixed versions of compilers to non-suffixed versions, which means we may select `clang-gpu` over `clang` at LLNL. But, `clang-gpu` is not actually clang, and it can break builds. We should prefer `clang` if it's available. - [x] prefer compilers that have C compilers and prefer no name variation to variation. * tests: add test for which()
This commit is contained in:
parent
665a47607e
commit
f528022a7d
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
|
|
||||||
@llnl.util.lang.memoized
|
@llnl.util.lang.memoized
|
||||||
def get_compiler_version_output(compiler_path, version_arg, ignore_errors=()):
|
def _get_compiler_version_output(compiler_path, version_arg, ignore_errors=()):
|
||||||
"""Invokes the compiler at a given path passing a single
|
"""Invokes the compiler at a given path passing a single
|
||||||
version argument and returns the output.
|
version argument and returns the output.
|
||||||
|
|
||||||
@ -42,6 +42,18 @@ def get_compiler_version_output(compiler_path, version_arg, ignore_errors=()):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def get_compiler_version_output(compiler_path, *args, **kwargs):
|
||||||
|
"""Wrapper for _get_compiler_version_output()."""
|
||||||
|
# This ensures that we memoize compiler output by *absolute path*,
|
||||||
|
# not just executable name. If we don't do this, and the path changes
|
||||||
|
# (e.g., during testing), we can get incorrect results.
|
||||||
|
if not os.path.isabs(compiler_path):
|
||||||
|
compiler_path = spack.util.executable.which_string(
|
||||||
|
compiler_path, required=True)
|
||||||
|
|
||||||
|
return _get_compiler_version_output(compiler_path, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def tokenize_flags(flags_str):
|
def tokenize_flags(flags_str):
|
||||||
"""Given a compiler flag specification as a string, this returns a list
|
"""Given a compiler flag specification as a string, this returns a list
|
||||||
where the entries are the flags. For compiler options which set values
|
where the entries are the flags. For compiler options which set values
|
||||||
|
@ -650,23 +650,18 @@ def make_compiler_list(detected_versions):
|
|||||||
Returns:
|
Returns:
|
||||||
list of Compiler objects
|
list of Compiler objects
|
||||||
"""
|
"""
|
||||||
# We don't sort on the path of the compiler
|
group_fn = lambda x: (x.id, x.variation, x.language)
|
||||||
sort_fn = lambda x: (x.id, x.variation, x.language)
|
sorted_compilers = sorted(detected_versions, key=group_fn)
|
||||||
compilers_s = sorted(detected_versions, key=sort_fn)
|
|
||||||
|
|
||||||
# Gather items in a dictionary by the id, name variation and language
|
# Gather items in a dictionary by the id, name variation and language
|
||||||
compilers_d = {}
|
compilers_d = {}
|
||||||
for sort_key, group in itertools.groupby(compilers_s, key=sort_fn):
|
for sort_key, group in itertools.groupby(sorted_compilers, key=group_fn):
|
||||||
compiler_id, name_variation, language = sort_key
|
compiler_id, name_variation, language = sort_key
|
||||||
by_compiler_id = compilers_d.setdefault(compiler_id, {})
|
by_compiler_id = compilers_d.setdefault(compiler_id, {})
|
||||||
by_name_variation = by_compiler_id.setdefault(name_variation, {})
|
by_name_variation = by_compiler_id.setdefault(name_variation, {})
|
||||||
by_name_variation[language] = next(x.path for x in group)
|
by_name_variation[language] = next(x.path for x in group)
|
||||||
|
|
||||||
# For each unique compiler id select the name variation with most entries
|
def _default_make_compilers(cmp_id, paths):
|
||||||
# i.e. the one that supports most languages
|
|
||||||
compilers = []
|
|
||||||
|
|
||||||
def _default(cmp_id, paths):
|
|
||||||
operating_system, compiler_name, version = cmp_id
|
operating_system, compiler_name, version = cmp_id
|
||||||
compiler_cls = spack.compilers.class_for_compiler_name(compiler_name)
|
compiler_cls = spack.compilers.class_for_compiler_name(compiler_name)
|
||||||
spec = spack.spec.CompilerSpec(compiler_cls.name, version)
|
spec = spack.spec.CompilerSpec(compiler_cls.name, version)
|
||||||
@ -677,16 +672,38 @@ def _default(cmp_id, paths):
|
|||||||
)
|
)
|
||||||
return [compiler]
|
return [compiler]
|
||||||
|
|
||||||
for compiler_id, by_compiler_id in compilers_d.items():
|
# For compilers with the same compiler id:
|
||||||
_, selected_name_variation = max(
|
#
|
||||||
(len(by_compiler_id[variation]), variation)
|
# - Prefer with C compiler to without
|
||||||
for variation in by_compiler_id
|
# - Prefer with C++ compiler to without
|
||||||
)
|
# - Prefer no variations to variations (e.g., clang to clang-gpu)
|
||||||
|
#
|
||||||
|
sort_fn = lambda variation: (
|
||||||
|
'cc' not in by_compiler_id[variation], # None last
|
||||||
|
'cxx' not in by_compiler_id[variation], # None last
|
||||||
|
variation.prefix,
|
||||||
|
variation.suffix,
|
||||||
|
)
|
||||||
|
|
||||||
|
compilers = []
|
||||||
|
for compiler_id, by_compiler_id in compilers_d.items():
|
||||||
|
ordered = sorted(by_compiler_id, key=sort_fn)
|
||||||
|
selected_variation = ordered[0]
|
||||||
|
selected = by_compiler_id[selected_variation]
|
||||||
|
|
||||||
|
# fill any missing parts from subsequent entries
|
||||||
|
for lang in ['cxx', 'f77', 'fc']:
|
||||||
|
if lang not in selected:
|
||||||
|
next_lang = next((
|
||||||
|
by_compiler_id[v][lang] for v in ordered
|
||||||
|
if lang in by_compiler_id[v]), None)
|
||||||
|
if next_lang:
|
||||||
|
selected[lang] = next_lang
|
||||||
|
|
||||||
# Add it to the list of compilers
|
|
||||||
selected = by_compiler_id[selected_name_variation]
|
|
||||||
operating_system, _, _ = compiler_id
|
operating_system, _, _ = compiler_id
|
||||||
make_compilers = getattr(operating_system, 'make_compilers', _default)
|
make_compilers = getattr(
|
||||||
|
operating_system, 'make_compilers', _default_make_compilers)
|
||||||
|
|
||||||
compilers.extend(make_compilers(compiler_id, selected))
|
compilers.extend(make_compilers(compiler_id, selected))
|
||||||
|
|
||||||
return compilers
|
return compilers
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -14,7 +16,7 @@
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def no_compilers_yaml(mutable_config, monkeypatch):
|
def no_compilers_yaml(mutable_config):
|
||||||
"""Creates a temporary configuration without compilers.yaml"""
|
"""Creates a temporary configuration without compilers.yaml"""
|
||||||
|
|
||||||
for scope, local_config in mutable_config.scopes.items():
|
for scope, local_config in mutable_config.scopes.items():
|
||||||
@ -130,3 +132,121 @@ def test_compiler_add(
|
|||||||
new_compiler = new_compilers - old_compilers
|
new_compiler = new_compilers - old_compilers
|
||||||
assert any(c.version == spack.version.Version(mock_compiler_version)
|
assert any(c.version == spack.version.Version(mock_compiler_version)
|
||||||
for c in new_compiler)
|
for c in new_compiler)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def clangdir(tmpdir):
|
||||||
|
"""Create a directory with some dummy compiler scripts in it.
|
||||||
|
|
||||||
|
Scripts are:
|
||||||
|
- clang
|
||||||
|
- clang++
|
||||||
|
- gcc
|
||||||
|
- g++
|
||||||
|
- gfortran-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
with tmpdir.as_cwd():
|
||||||
|
with open('clang', 'w') as f:
|
||||||
|
f.write("""\
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "$1" = "--version" ]; then
|
||||||
|
echo "clang version 11.0.0 (clang-1100.0.33.16)"
|
||||||
|
echo "Target: x86_64-apple-darwin18.7.0"
|
||||||
|
echo "Thread model: posix"
|
||||||
|
echo "InstalledDir: /dummy"
|
||||||
|
else
|
||||||
|
echo "clang: error: no input files"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
""")
|
||||||
|
shutil.copy('clang', 'clang++')
|
||||||
|
|
||||||
|
gcc_script = """\
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "$1" = "-dumpversion" ]; then
|
||||||
|
echo "8"
|
||||||
|
elif [ "$1" = "-dumpfullversion" ]; then
|
||||||
|
echo "8.4.0"
|
||||||
|
elif [ "$1" = "--version" ]; then
|
||||||
|
echo "{0} (GCC) 8.4.0 20120313 (Red Hat 8.4.0-1)"
|
||||||
|
echo "Copyright (C) 2010 Free Software Foundation, Inc."
|
||||||
|
else
|
||||||
|
echo "{1}: fatal error: no input files"
|
||||||
|
echo "compilation terminated."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"""
|
||||||
|
with open('gcc-8', 'w') as f:
|
||||||
|
f.write(gcc_script.format('gcc', 'gcc-8'))
|
||||||
|
with open('g++-8', 'w') as f:
|
||||||
|
f.write(gcc_script.format('g++', 'g++-8'))
|
||||||
|
with open('gfortran-8', 'w') as f:
|
||||||
|
f.write(gcc_script.format('GNU Fortran', 'gfortran-8'))
|
||||||
|
os.chmod('clang', 0o700)
|
||||||
|
os.chmod('clang++', 0o700)
|
||||||
|
os.chmod('gcc-8', 0o700)
|
||||||
|
os.chmod('g++-8', 0o700)
|
||||||
|
os.chmod('gfortran-8', 0o700)
|
||||||
|
|
||||||
|
yield tmpdir
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.regression('17590')
|
||||||
|
def test_compiler_find_mixed_suffixes(
|
||||||
|
no_compilers_yaml, working_env, clangdir):
|
||||||
|
"""Ensure that we'll mix compilers with different suffixes when necessary.
|
||||||
|
"""
|
||||||
|
os.environ['PATH'] = str(clangdir)
|
||||||
|
output = compiler('find', '--scope=site')
|
||||||
|
|
||||||
|
assert 'clang@11.0.0' in output
|
||||||
|
assert 'gcc@8.4.0' in output
|
||||||
|
|
||||||
|
config = spack.compilers.get_compiler_config('site', False)
|
||||||
|
clang = next(c['compiler'] for c in config
|
||||||
|
if c['compiler']['spec'] == 'clang@11.0.0')
|
||||||
|
gcc = next(c['compiler'] for c in config
|
||||||
|
if c['compiler']['spec'] == 'gcc@8.4.0')
|
||||||
|
|
||||||
|
gfortran_path = str(clangdir.join('gfortran-8'))
|
||||||
|
|
||||||
|
assert clang['paths'] == {
|
||||||
|
'cc': str(clangdir.join('clang')),
|
||||||
|
'cxx': str(clangdir.join('clang++')),
|
||||||
|
# we only auto-detect mixed clang on macos
|
||||||
|
'f77': gfortran_path if sys.platform == 'darwin' else None,
|
||||||
|
'fc': gfortran_path if sys.platform == 'darwin' else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert gcc['paths'] == {
|
||||||
|
'cc': str(clangdir.join('gcc-8')),
|
||||||
|
'cxx': str(clangdir.join('g++-8')),
|
||||||
|
'f77': gfortran_path,
|
||||||
|
'fc': gfortran_path,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.regression('17590')
|
||||||
|
def test_compiler_find_prefer_no_suffix(
|
||||||
|
no_compilers_yaml, working_env, clangdir):
|
||||||
|
"""Ensure that we'll pick 'clang' over 'clang-gpu' when there is a choice.
|
||||||
|
"""
|
||||||
|
with clangdir.as_cwd():
|
||||||
|
shutil.copy('clang', 'clang-gpu')
|
||||||
|
shutil.copy('clang++', 'clang++-gpu')
|
||||||
|
os.chmod('clang-gpu', 0o700)
|
||||||
|
os.chmod('clang++-gpu', 0o700)
|
||||||
|
|
||||||
|
os.environ['PATH'] = str(clangdir)
|
||||||
|
output = compiler('find', '--scope=site')
|
||||||
|
|
||||||
|
assert 'clang@11.0.0' in output
|
||||||
|
assert 'gcc@8.4.0' in output
|
||||||
|
|
||||||
|
config = spack.compilers.get_compiler_config('site', False)
|
||||||
|
clang = next(c['compiler'] for c in config
|
||||||
|
if c['compiler']['spec'] == 'clang@11.0.0')
|
||||||
|
|
||||||
|
assert clang['paths']['cc'] == str(clangdir.join('clang'))
|
||||||
|
assert clang['paths']['cxx'] == str(clangdir.join('clang++'))
|
||||||
|
@ -6,7 +6,10 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import llnl.util.filesystem as fs
|
import llnl.util.filesystem as fs
|
||||||
|
|
||||||
import spack
|
import spack
|
||||||
import spack.util.executable as ex
|
import spack.util.executable as ex
|
||||||
from spack.hooks.sbang import filter_shebangs_in_directory
|
from spack.hooks.sbang import filter_shebangs_in_directory
|
||||||
@ -35,3 +38,18 @@ def test_read_unicode(tmpdir, working_env):
|
|||||||
# read the unicode back in and see whether things work
|
# read the unicode back in and see whether things work
|
||||||
script = ex.Executable('./%s' % script_name)
|
script = ex.Executable('./%s' % script_name)
|
||||||
assert u'\xc3' == script(output=str).strip()
|
assert u'\xc3' == script(output=str).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_which(tmpdir):
|
||||||
|
os.environ["PATH"] = str(tmpdir)
|
||||||
|
assert ex.which("spack-test-exe") is None
|
||||||
|
with pytest.raises(ex.CommandNotFoundError):
|
||||||
|
ex.which("spack-test-exe", required=True)
|
||||||
|
|
||||||
|
with tmpdir.as_cwd():
|
||||||
|
fs.touch("spack-test-exe")
|
||||||
|
fs.set_executable('spack-test-exe')
|
||||||
|
|
||||||
|
exe = ex.which("spack-test-exe")
|
||||||
|
assert exe is not None
|
||||||
|
assert exe.path == str(tmpdir.join("spack-test-exe"))
|
||||||
|
@ -239,7 +239,8 @@ def which_string(*args, **kwargs):
|
|||||||
return exe
|
return exe
|
||||||
|
|
||||||
if required:
|
if required:
|
||||||
tty.die("spack requires '%s'. Make sure it is in your path." % args[0])
|
raise CommandNotFoundError(
|
||||||
|
"spack requires '%s'. Make sure it is in your path." % args[0])
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -266,3 +267,7 @@ def which(*args, **kwargs):
|
|||||||
|
|
||||||
class ProcessError(spack.error.SpackError):
|
class ProcessError(spack.error.SpackError):
|
||||||
"""ProcessErrors are raised when Executables exit with an error code."""
|
"""ProcessErrors are raised when Executables exit with an error code."""
|
||||||
|
|
||||||
|
|
||||||
|
class CommandNotFoundError(spack.error.SpackError):
|
||||||
|
"""Raised when ``which()`` can't find a required executable."""
|
||||||
|
Loading…
Reference in New Issue
Block a user