conan-io/conan

View on GitHub
conan/tools/microsoft/visual.py

Summary

Maintainability
A
30 mins
Test Coverage
import os
import textwrap

from conans.client.tools import vs_installation_path
from conans.client.tools.version import Version
from conans.errors import ConanException, ConanInvalidConfiguration
from conan.tools.intel.intel_cc import IntelCC

CONAN_VCVARS_FILE = "conanvcvars.bat"


def check_min_vs(conanfile, version, raise_invalid=True):
    """ this is a helper method to allow the migration of 1.X->2.0 and VisualStudio->msvc settings
    withoug breaking recipes
    The legacy "Visual Studio" with different toolset is not managed, not worth the complexity
    """
    compiler = conanfile.settings.get_safe("compiler")
    compiler_version = None
    if compiler == "Visual Studio":
        compiler_version = conanfile.settings.get_safe("compiler.version")
        compiler_version = {"17": "193",
                            "16": "192",
                            "15": "191",
                            "14": "190",
                            "12": "180",
                            "11": "170"}.get(compiler_version)
    elif compiler == "msvc":
        compiler_version = conanfile.settings.get_safe("compiler.version")
        compiler_update = conanfile.settings.get_safe("compiler.update")
        if compiler_version and compiler_update is not None:
            compiler_version += ".{}".format(compiler_update)

    if compiler_version and Version(compiler_version) < version:
        if raise_invalid:
            msg = f"This package doesn't work with VS compiler version '{compiler_version}'" \
                  f", it requires at least '{version}'"
            raise ConanInvalidConfiguration(msg)
        else:
            return False
    return True


def msvc_version_to_vs_ide_version(version):
    _visuals = {'170': '11',
                '180': '12',
                '190': '14',
                '191': '15',
                '192': '16',
                '193': '17'}
    return _visuals[str(version)]


def msvc_version_to_toolset_version(version):
    toolsets = {'170': 'v110',
                '180': 'v120',
                '190': 'v140',
                '191': 'v141',
                '192': 'v142',
                "193": 'v143'}
    return toolsets.get(str(version))


class VCVars:
    def __init__(self, conanfile):
        self._conanfile = conanfile

    def generate(self, scope="build"):
        """
        write a conanvcvars.bat file with the good args from settings
        """
        conanfile = self._conanfile
        os_ = conanfile.settings.get_safe("os")
        if os_ != "Windows":
            return

        compiler = conanfile.settings.get_safe("compiler")
        if compiler not in ("Visual Studio", "msvc", "clang"):
            return

        vs_install_path = conanfile.conf.get("tools.microsoft.msbuild:installation_path")
        if vs_install_path == "":  # Empty string means "disable"
            return

        if compiler == "clang":
            # The vcvars only needed for LLVM/Clang and VS ClangCL, who define runtime
            if not conanfile.settings.get_safe("compiler.runtime"):
                # NMake Makefiles will need vcvars activated, for VS target, defined with runtime
                return
            toolset_version = conanfile.settings.get_safe("compiler.runtime_version")
            vs_version = {"v140": "14",
                          "v141": "15",
                          "v142": "16",
                          "v143": "17"}.get(toolset_version)
            if vs_version is None:
                raise ConanException("Visual Studio Runtime version (v140-v143) not defined")
            vcvars_ver = {"v140": "14.0",
                          "v141": "14.1",
                          "v142": "14.2",
                          "v143": "14.3"}.get(toolset_version)
        else:
            vs_version = vs_ide_version(conanfile)
            vcvars_ver = _vcvars_vers(conanfile, compiler, vs_version)
        vcvarsarch = vcvars_arch(conanfile)

        # The vs_install_path is like
        # C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
        # C:\Program Files (x86)\Microsoft Visual Studio\2017\Community
        # C:\Program Files (x86)\Microsoft Visual Studio 14.0
        vcvars = vcvars_command(vs_version, architecture=vcvarsarch, platform_type=None,
                                winsdk_version=None, vcvars_ver=vcvars_ver,
                                vs_install_path=vs_install_path)

        content = textwrap.dedent("""\
            @echo off
            {}
            """.format(vcvars))
        from conan.tools.env.environment import create_env_script
        create_env_script(conanfile, content, CONAN_VCVARS_FILE, scope)


def vs_ide_version(conanfile):
    """
    Get the VS IDE version as string
    """
    compiler = conanfile.settings.get_safe("compiler")
    compiler_version = (conanfile.settings.get_safe("compiler.base.version") or
                        conanfile.settings.get_safe("compiler.version"))
    if compiler == "msvc":
        toolset_override = conanfile.conf.get("tools.microsoft.msbuild:vs_version", check_type=str)
        if toolset_override:
            visual_version = toolset_override
        else:
            visual_version = msvc_version_to_vs_ide_version(compiler_version)
    else:
        visual_version = compiler_version
    return visual_version


def msvc_runtime_flag(conanfile):
    settings = conanfile.settings
    compiler = settings.get_safe("compiler")
    runtime = settings.get_safe("compiler.runtime")
    if compiler == "Visual Studio":
        return runtime
    if compiler == "clang" and runtime in ("MD", "MT", "MTd", "MDd"):
        # TODO: Remove in 2.0
        return runtime
    if runtime is not None:
        if runtime == "static":
            runtime = "MT"
        elif runtime == "dynamic":
            runtime = "MD"
        else:
            raise ConanException("compiler.runtime should be 'static' or 'dynamic'")
        runtime_type = settings.get_safe("compiler.runtime_type")
        if runtime_type == "Debug":
            runtime = "{}d".format(runtime)
        return runtime
    return ""


def vcvars_command(version, architecture=None, platform_type=None, winsdk_version=None,
                   vcvars_ver=None, start_dir_cd=True, vs_install_path=None):
    """ conan-agnostic construction of vcvars command
    https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line
    """
    # TODO: This comes from conans/client/tools/win.py vcvars_command()
    cmd = []
    if start_dir_cd:
        cmd.append('set "VSCMD_START_DIR=%CD%" &&')

    # The "call" is useful in case it is called from another .bat script
    cmd.append('call "%s" ' % _vcvars_path(version, vs_install_path))
    if architecture:
        cmd.append(architecture)
    if platform_type:
        cmd.append(platform_type)
    if winsdk_version:
        cmd.append(winsdk_version)
    if vcvars_ver:
        cmd.append("-vcvars_ver=%s" % vcvars_ver)
    return " ".join(cmd)


def _vcvars_path(version, vs_install_path):
    # TODO: This comes from conans/client/tools/win.py vcvars_command()
    vs_path = vs_install_path or vs_installation_path(version)
    if not vs_path or not os.path.isdir(vs_path):
        raise ConanException("VS non-existing installation: Visual Studio %s" % version)

    if int(version) > 14:
        vcpath = os.path.join(vs_path, "VC/Auxiliary/Build/vcvarsall.bat")
    else:
        vcpath = os.path.join(vs_path, "VC/vcvarsall.bat")
    return vcpath


def vcvars_arch(conanfile):
    """
    computes the vcvars command line architecture based on conanfile settings (host) and
    settings_build
    :param conanfile:
    :return:
    """
    # TODO: This comes from conans/client/tools/win.py vcvars_command()
    settings_host = conanfile.settings
    try:
        settings_build = conanfile.settings_build
    except AttributeError:
        settings_build = settings_host

    arch_host = str(settings_host.arch)
    arch_build = str(settings_build.arch)

    arch = None
    if arch_build == 'x86_64':
        arch = {'x86': "amd64_x86",
                'x86_64': 'amd64',
                'armv7': 'amd64_arm',
                'armv8': 'amd64_arm64'}.get(arch_host)
    elif arch_build == 'x86':
        arch = {'x86': 'x86',
                'x86_64': 'x86_amd64',
                'armv7': 'x86_arm',
                'armv8': 'x86_arm64'}.get(arch_host)
    elif arch_build == 'armv8':
        arch = {'x86': 'arm64_x86',
                'x86_64': 'arm64_x64',
                'armv7': 'arm64_arm',
                'armv8': 'arm64'}.get(arch_host)

    if not arch:
        raise ConanException('vcvars unsupported architectures %s-%s' % (arch_build, arch_host))

    return arch


def _vcvars_vers(conanfile, compiler, vs_version):
    if int(vs_version) <= 14:
        return None

    vcvars_ver = None
    if compiler == "Visual Studio":
        toolset = conanfile.settings.get_safe("compiler.toolset")
        if toolset is not None:
            vcvars_ver = {"v140": "14.0",
                          "v141": "14.1",
                          "v142": "14.2",
                          "v143": "14.3"}.get(toolset)
    else:
        assert compiler == "msvc"
        # Code similar to CMakeToolchain toolset one
        compiler_version = str(conanfile.settings.compiler.version)
        # The equivalent of compiler 192 is toolset 14.2
        vcvars_ver = "14.{}".format(compiler_version[-1])
    return vcvars_ver


def is_msvc(conanfile, build_context=False):
    """ Validate if current compiler in setttings is 'Visual Studio' or 'msvc'
    :param conanfile: ConanFile instance
    :param build_context: If True, will use the settings from the build context, not host ones
    :return: True, if the host compiler is related to Visual Studio, otherwise, False.
    """
    # FIXME: 2.0: remove "hasattr()" condition
    if not build_context or not hasattr(conanfile, "settings_build"):
        settings = conanfile.settings
    else:
        settings = conanfile.settings_build
    return settings.get_safe("compiler") in ["Visual Studio", "msvc"]


def is_msvc_static_runtime(conanfile):
    """ Validate when building with Visual Studio or msvc and MT on runtime
    :param conanfile: ConanFile instance
    :return: True, if msvc + runtime MT. Otherwise, False
    """
    return is_msvc(conanfile) and "MT" in msvc_runtime_flag(conanfile)


def msvs_toolset(conanfile):
    """ Returns the corresponding platform toolset based on the compiler of the given conanfile.
        In case no toolset is configured in the profile, it will return a toolset based on the
        compiler version, otherwise, it will return the toolset from the profile.
        When there is no compiler version neither toolset configured, it will return None
        It supports Visual Studio, msvc and Intel.
    :param conanfile: Conanfile instance to access settings.compiler
    :return: A toolset when compiler.version is valid or compiler.toolset is configured. Otherwise, None.
    """
    settings = conanfile.settings
    compiler = settings.get_safe("compiler")
    compiler_version = settings.get_safe("compiler.version")
    if compiler == "msvc":
        subs_toolset = settings.get_safe("compiler.toolset")
        if subs_toolset:
            return subs_toolset
        return msvc_version_to_toolset_version(compiler_version)
    if compiler == "intel" and compiler_version:
        compiler_version = compiler_version if "." in compiler_version else \
            f"{compiler_version}.0"
        return "Intel C++ Compiler " + compiler_version
    if compiler == "intel-cc":
        return IntelCC(conanfile).ms_toolset
    if compiler == "Visual Studio":
        toolset = settings.get_safe("compiler.toolset")
        if not toolset:
            toolsets = {"17": "v143",
                        "16": "v142",
                        "15": "v141",
                        "14": "v140",
                        "12": "v120",
                        "11": "v110",
                        "10": "v100",
                        "9": "v90",
                        "8": "v80"}
            toolset = toolsets.get(str(compiler_version))
        return toolset