Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: improve compiler detection #5740

Merged
merged 2 commits into from
Jul 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions conans/client/build/compiler_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import os
import tempfile
from six import StringIO

from conans.client.runner import ConanRunner
from conans.model.version import Version


GCC = "gcc"
LLVM_GCC = "llvm-gcc" # GCC frontend with LLVM backend
CLANG = "clang"
APPLE_CLANG = "apple-clang"
SUNCC = "suncc"
MSVC = "Visual Studio"
INTEL = "intel"
QCC = "qcc"


class CompilerId(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could easily be a namedtuple, but it is ok, no need to change it now

"""
compiler identification description, holds compiler name, full version and
other important properties
"""
def __init__(self, name, major, minor, patch):
self._name = name
self._major = major
self._minor = minor
self._patch = patch
self._version = Version(self.version)

@property
def name(self):
return self._name

@property
def major(self):
return self._major

@property
def minor(self):
return self._minor

@property
def patch(self):
return self._patch

@property
def version(self):
return "%s.%s.%s" % (self._major, self._minor, self._patch)

@property
def major_minor(self):
return "%s.%s" % (self._major, self._minor)

def __str__(self):
return "%s %s" % (self._name, self.version)

def __repr__(self):
return self.__str__()

def __eq__(self, other):
return self.name == other.name and self._version == other._version

def __ne__(self, other):
return not self.__eq__(other)


UNKNOWN_COMPILER = CompilerId(None, None, None, None)

# convert _MSC_VER to the corresponding Visual Studio version
MSVC_TO_VS_VERSION = {800: (1, 0),
900: (2, 0),
1000: (4, 0),
1010: (4, 1),
1020: (4, 2),
1100: (5, 0),
1200: (6, 0),
1300: (7, 0),
1310: (7, 1),
1400: (8, 0),
1500: (9, 0),
1600: (10, 0),
1700: (11, 0),
1800: (12, 0),
1900: (14, 0),
1910: (15, 0),
1911: (15, 3),
1912: (15, 5),
1913: (15, 6),
1914: (15, 7),
1915: (15, 8),
1916: (15, 9),
1920: (16, 0),
1921: (16, 1),
1922: (16, 2),
1923: (16, 3),
1924: (16, 4),
1925: (16, 5),
1926: (16, 6),
1927: (16, 7)}


def _parse_compiler_version(defines):
try:
if '__INTEL_COMPILER' in defines:
compiler = INTEL
version = int(defines['__INTEL_COMPILER'])
major = int(version / 100)
minor = int(version % 100)
patch = int(defines['__INTEL_COMPILER_UPDATE'])
elif '__clang__' in defines:
compiler = APPLE_CLANG if '__apple_build_version__' in defines else CLANG
major = int(defines['__clang_major__'])
minor = int(defines['__clang_minor__'])
patch = int(defines['__clang_patchlevel__'])
elif '__SUNPRO_C' in defines or '__SUNPRO_CC' in defines:
# In particular, the value of __SUNPRO_CC, which is a three-digit hex number.
# The first digit is the major release. The second digit is the minor release.
# The third digit is the micro release. For example, C++ 5.9 is 0x590.
compiler = SUNCC
define = '__SUNPRO_C' if '__SUNPRO_C' in defines else '__SUNPRO_CC'
version = int(defines[define], 16)
major = (version >> 8) & 0xF
minor = (version >> 4) & 0xF
patch = version & 0xF
# MSVC goes after Clang and Intel, as they may define _MSC_VER
elif '_MSC_VER' in defines:
compiler = MSVC
version = int(defines['_MSC_VER'])
# map _MSC_VER into conan-friendly Visual Studio version
# currently, conan uses major only, but here we store minor for the future as well
# https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros?view=vs-2019
major, minor = MSVC_TO_VS_VERSION.get(version)
patch = 0
# GCC must be the last try, as other compilers may define __GNUC__ for compatibility
elif '__GNUC__' in defines:
if '__llvm__' in defines:
compiler = LLVM_GCC
elif '__QNX__' in defines:
compiler = QCC
else:
compiler = GCC
major = int(defines['__GNUC__'])
minor = int(defines['__GNUC_MINOR__'])
patch = int(defines['__GNUC_PATCHLEVEL__'])
else:
return UNKNOWN_COMPILER
return CompilerId(compiler, major, minor, patch)
except KeyError:
return UNKNOWN_COMPILER
except ValueError:
return UNKNOWN_COMPILER
except TypeError:
return UNKNOWN_COMPILER


def detect_compiler_id(executable, runner=None):
runner = runner or ConanRunner()
# use a temporary file, as /dev/null might not be available on all platforms
tmpname = tempfile.mktemp(suffix=".c")
with open(tmpname, "wb") as f:
f.write(b"\n")

cmd = tempfile.mktemp(suffix=".cmd")
with open(cmd, "wb") as f:
f.write(b"echo off\nset MSC_CMD_FLAGS\n")

detectors = [
# "-dM" generate list of #define directives
# "-E" run only preprocessor
# "-x c" compiler as C code
# the output is of lines in form of "#define name value"
"-dM -E -x c",
"--driver-mode=g++ -dM -E -x c", # clang-cl
"-c -xdumpmacros", # SunCC,
# cl (Visual Studio, MSVC)
# "/nologo" Suppress Startup Banner
# "/E" Preprocess to stdout
# "/B1" C front-end
# "/c" Compile Without Linking
# "/TC" Specify Source File Type
'/nologo /E /B1 "%s" /c /TC' % cmd,
"/QdM /E /TC" # icc (Intel) on Windows,
"-Wp,-dM -E -x c" # QNX QCC
]
try:
for detector in detectors:
command = '%s %s "%s"' % (executable, detector, tmpname)
result = StringIO()
if 0 == runner(command, output=result):
output = result.getvalue()
defines = dict()
for line in output.splitlines():
tokens = line.split(' ', 3)
if len(tokens) == 3 and tokens[0] == '#define':
defines[tokens[1]] = tokens[2]
# MSVC dumps macro definitions in single line:
# "MSC_CMD_FLAGS=-D_MSC_VER=1921 -Ze"
elif line.startswith("MSC_CMD_FLAGS="):
line = line[len("MSC_CMD_FLAGS="):].rstrip()
defines = dict()
tokens = line.split()
for token in tokens:
if token.startswith("-D") or token.startswith("/D"):
token = token[2:]
if '=' in token:
name, value = token.split('=', 2)
else:
name, value = token, '1'
defines[name] = value
break
compiler = _parse_compiler_version(defines)
if compiler == UNKNOWN_COMPILER:
continue
return compiler
return UNKNOWN_COMPILER
finally:
os.unlink(tmpname)
os.unlink(cmd)
82 changes: 62 additions & 20 deletions conans/client/conf/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import platform
import re

from conans.client.build.compiler_id import UNKNOWN_COMPILER, LLVM_GCC, detect_compiler_id
from conans.client.output import Color
from conans.client.tools import detected_os, OSInfo
from conans.client.tools.win import latest_visual_studio_version_installed
Expand All @@ -11,6 +12,18 @@
from conans.util.runners import detect_runner


def _get_compiler_and_version(output, compiler_exe):
compiler_id = detect_compiler_id(compiler_exe)
if compiler_id.name == LLVM_GCC:
output.error("%s detected as a frontend using apple-clang. "
"Compiler not supported" % compiler_exe)
return None
if compiler_id != UNKNOWN_COMPILER:
output.success("Found %s %s" % (compiler_id.name, compiler_id.major_minor))
return compiler_id.name, compiler_id.major_minor
return None


def _gcc_compiler(output, compiler_exe="gcc"):

try:
Expand Down Expand Up @@ -70,52 +83,81 @@ def _sun_cc_compiler(output, compiler_exe="cc"):


def _get_default_compiler(output):
"""
find the default compiler on the build machine
search order and priority:
1. CC and CXX environment variables are always top priority
2. Visual Studio detection (Windows only) via vswhere or registry or environment variables
3. Apple Clang (Mac only)
4. cc executable
5. gcc executable
6. clang executable
"""
v2_mode = get_env(CONAN_V2_MODE_ENVVAR, False)
cc = os.environ.get("CC", "")
cxx = os.environ.get("CXX", "")
if cc or cxx: # Env defined, use them
output.info("CC and CXX: %s, %s " % (cc or "None", cxx or "None"))
command = cc or cxx
if "gcc" in command:
gcc = _gcc_compiler(output, command)
if platform.system() == "Darwin" and gcc is None:
output.error(
"%s detected as a frontend using apple-clang. Compiler not supported" % command
)
return gcc
if "clang" in command.lower():
return _clang_compiler(output, command)
if platform.system() == "SunOS" and command.lower() == "cc":
return _sun_cc_compiler(output, command)
if v2_mode:
compiler = _get_compiler_and_version(output, command)
if compiler:
return compiler
else:
if "gcc" in command:
gcc = _gcc_compiler(output, command)
if platform.system() == "Darwin" and gcc is None:
output.error(
"%s detected as a frontend using apple-clang. Compiler not supported" % command
)
return gcc
if "clang" in command.lower():
return _clang_compiler(output, command)
if platform.system() == "SunOS" and command.lower() == "cc":
return _sun_cc_compiler(output, command)
# I am not able to find its version
output.error("Not able to automatically detect '%s' version" % command)
return None

vs = cc = sun_cc = None
if detected_os() == "Windows":
version = latest_visual_studio_version_installed(output)
vs = ('Visual Studio', version) if version else None
gcc = _gcc_compiler(output)
clang = _clang_compiler(output)
if platform.system() == "SunOS":
sun_cc = _sun_cc_compiler(output)

if v2_mode:
cc = _get_compiler_and_version(output, "cc")
gcc = _get_compiler_and_version(output, "gcc")
clang = _get_compiler_and_version(output, "clang")
else:
gcc = _gcc_compiler(output)
clang = _clang_compiler(output)
if platform.system() == "SunOS":
sun_cc = _sun_cc_compiler(output)

if detected_os() == "Windows":
return vs or gcc or clang
return vs or cc or gcc or clang
elif platform.system() == "Darwin":
return clang or gcc
return clang or cc or gcc
elif platform.system() == "SunOS":
return sun_cc or gcc or clang
return sun_cc or cc or gcc or clang
else:
return gcc or clang
return cc or gcc or clang


def _get_profile_compiler_version(compiler, version, output):
major = version.split(".")[0]
tokens = version.split(".")
major = tokens[0]
minor = tokens[1] if len(tokens) > 1 else 0
if compiler == "clang" and int(major) >= 8:
output.info("clang>=8, using the major as version")
return major
elif compiler == "gcc" and int(major) >= 5:
output.info("gcc>=5, using the major as version")
return major
elif compiler == "Visual Studio":
return major
elif compiler == "intel" and (int(major) < 19 or (int(major) == 19 and int(minor) == 0)):
return major
return version


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def run(self, *args, **kwargs):
class RunnerMockWithHelp(RunnerMock):

def __init__(self, return_ok=True, available_args=None):
self.output = None
self.command_called = None
self.return_ok = return_ok
self.available_args = available_args or []
Expand Down
Loading