File indexing completed on 2025-02-16 05:12:09

0001 import copy
0002 import os
0003 import re
0004 
0005 from conans.client import tools
0006 from conans.client.build.visual_environment import (VisualStudioBuildEnvironment,
0007                                                     vs_build_type_flags, vs_std_cpp)
0008 from conans.client.tools.env import environment_append, no_op
0009 from conans.client.tools.intel import intel_compilervars
0010 from conans.client.tools.oss import cpu_count
0011 from conans.client.tools.win import vcvars_command
0012 from conans.errors import ConanException
0013 from conans.model.conan_file import ConanFile
0014 from conans.model.version import Version
0015 from conans.tools import vcvars_command as tools_vcvars_command
0016 from conans.util.env_reader import get_env
0017 from conans.util.files import decode_text, save
0018 from conans.util.runners import version_runner
0019 
0020 
0021 class MSBuild(object):
0022 
0023     def __init__(self, conanfile):
0024         if isinstance(conanfile, ConanFile):
0025             self._conanfile = conanfile
0026             self._settings = self._conanfile.settings
0027             self._output = self._conanfile.output
0028             self.build_env = VisualStudioBuildEnvironment(self._conanfile,
0029                                                           with_build_type_flags=False)
0030         else:  # backwards compatible with build_sln_command
0031             self._settings = conanfile
0032             self.build_env = None
0033 
0034     def build(self, project_file, targets=None, upgrade_project=True, build_type=None, arch=None,
0035               parallel=True, force_vcvars=False, toolset=None, platforms=None, use_env=True,
0036               vcvars_ver=None, winsdk_version=None, properties=None, output_binary_log=None,
0037               property_file_name=None, verbosity=None, definitions=None,
0038               user_property_file_name=None):
0039         """
0040         :param project_file: Path to the .sln file.
0041         :param targets: List of targets to build.
0042         :param upgrade_project: Will call devenv to upgrade the solution to your
0043         current Visual Studio.
0044         :param build_type: Use a custom build type instead of the default settings.build_type one.
0045         :param arch: Use a custom architecture name instead of the settings.arch one.
0046         It will be used to build the /p:Configuration= parameter of MSBuild.
0047         It can be used as the key of the platforms parameter.
0048         E.g. arch="x86", platforms={"x86": "i386"}
0049         :param parallel: Will use the configured number of cores in the conan.conf file or
0050         tools.cpu_count():
0051         In the solution: Building the solution with the projects in parallel. (/m: parameter).
0052         CL compiler: Building the sources in parallel. (/MP: compiler flag)
0053         :param force_vcvars: Will ignore if the environment is already set for a different
0054         Visual Studio version.
0055         :param toolset: Specify a toolset. Will append a /p:PlatformToolset option.
0056         :param platforms: Dictionary with the mapping of archs/platforms from Conan naming to
0057         another one. It is useful for Visual Studio solutions that have a different naming in
0058         architectures.
0059         Example: platforms={"x86":"Win32"} (Visual solution uses "Win32" instead of "x86").
0060         This dictionary will update the default one:
0061         msvc_arch = {'x86': 'x86', 'x86_64': 'x64', 'armv7': 'ARM', 'armv8': 'ARM64'}
0062         :param use_env: Applies the argument /p:UseEnv=true to the MSBuild call.
0063         :param vcvars_ver: Specifies the Visual Studio compiler toolset to use.
0064         :param winsdk_version: Specifies the version of the Windows SDK to use.
0065         :param properties: Dictionary with new properties, for each element in the dictionary
0066         {name: value} it will append a /p:name="value" option.
0067         :param output_binary_log: If set to True then MSBuild will output a binary log file
0068         called msbuild.binlog in the working directory. It can also be used to set the name of
0069         log file like this output_binary_log="my_log.binlog".
0070         This parameter is only supported starting from MSBuild version 15.3 and onwards.
0071         :param property_file_name: When None it will generate a file named conan_build.props.
0072         You can specify a different name for the generated properties file.
0073         :param verbosity: Specifies verbosity level (/verbosity: parameter)
0074         :param definitions: Dictionary with additional compiler definitions to be applied during
0075         the build. Use value of None to set compiler definition with no value.
0076         :param user_property_file_name: Specify a user provided .props file with custom definitions
0077         :return: status code of the MSBuild command invocation
0078         """
0079         property_file_name = property_file_name or "conan_build.props"
0080         self.build_env.parallel = parallel
0081 
0082         with environment_append(self.build_env.vars):
0083             # Path for custom properties file
0084             props_file_contents = self._get_props_file_contents(definitions)
0085             property_file_name = os.path.abspath(property_file_name)
0086             save(property_file_name, props_file_contents)
0087             vcvars = vcvars_command(self._conanfile.settings, arch=arch, force=force_vcvars,
0088                                     vcvars_ver=vcvars_ver, winsdk_version=winsdk_version,
0089                                     output=self._output)
0090             command = self.get_command(project_file, property_file_name,
0091                                        targets=targets, upgrade_project=upgrade_project,
0092                                        build_type=build_type, arch=arch, parallel=parallel,
0093                                        toolset=toolset, platforms=platforms,
0094                                        use_env=use_env, properties=properties,
0095                                        output_binary_log=output_binary_log,
0096                                        verbosity=verbosity,
0097                                        user_property_file_name=user_property_file_name)
0098             command = "%s && %s" % (vcvars, command)
0099             context = no_op()
0100             if self._conanfile.settings.get_safe("compiler") == "Intel" and \
0101                 self._conanfile.settings.get_safe("compiler.base") == "Visual Studio":
0102                 context = intel_compilervars(self._conanfile.settings, arch)
0103             with context:
0104                 return self._conanfile.run(command)
0105 
0106     def get_command(self, project_file, props_file_path=None, targets=None, upgrade_project=True,
0107                     build_type=None, arch=None, parallel=True, toolset=None, platforms=None,
0108                     use_env=False, properties=None, output_binary_log=None, verbosity=None,
0109                     user_property_file_name=None):
0110 
0111         targets = targets or []
0112         if not isinstance(targets, (list, tuple)):
0113             raise TypeError("targets argument should be a list")
0114         properties = properties or {}
0115         command = []
0116 
0117         if upgrade_project and not get_env("CONAN_SKIP_VS_PROJECTS_UPGRADE", False):
0118             command.append('devenv "%s" /upgrade &&' % project_file)
0119         else:
0120             self._output.info("Skipped sln project upgrade")
0121 
0122         build_type = build_type or self._settings.get_safe("build_type")
0123         arch = arch or self._settings.get_safe("arch")
0124         if toolset is None:  # False value to skip adjusting
0125             toolset = tools.msvs_toolset(self._settings)
0126         verbosity = os.getenv("CONAN_MSBUILD_VERBOSITY") or verbosity or "minimal"
0127         if not build_type:
0128             raise ConanException("Cannot build_sln_command, build_type not defined")
0129         if not arch:
0130             raise ConanException("Cannot build_sln_command, arch not defined")
0131 
0132         command.append('msbuild "%s" /p:Configuration="%s"' % (project_file, build_type))
0133         msvc_arch = {'x86': 'x86',
0134                      'x86_64': 'x64',
0135                      'armv7': 'ARM',
0136                      'armv8': 'ARM64'}
0137         if platforms:
0138             msvc_arch.update(platforms)
0139         msvc_arch = msvc_arch.get(str(arch))
0140         if self._settings.get_safe("os") == "WindowsCE":
0141             msvc_arch = self._settings.get_safe("os.platform")
0142         try:
0143             sln = tools.load(project_file)
0144             pattern = re.compile(r"GlobalSection\(SolutionConfigurationPlatforms\)"
0145                                  r"(.*?)EndGlobalSection", re.DOTALL)
0146             solution_global = pattern.search(sln).group(1)
0147             lines = solution_global.splitlines()
0148             lines = [s.split("=")[0].strip() for s in lines]
0149         except Exception:
0150             pass  # TODO: !!! what are we catching here? tools.load? .group(1)? .splitlines?
0151         else:
0152             config = "%s|%s" % (build_type, msvc_arch)
0153             if config not in "".join(lines):
0154                 self._output.warn("***** The configuration %s does not exist in this solution *****"
0155                                   % config)
0156                 self._output.warn("Use 'platforms' argument to define your architectures")
0157 
0158         if output_binary_log:
0159             msbuild_version = MSBuild.get_version(self._settings)
0160             if msbuild_version >= "15.3":  # http://msbuildlog.com/
0161                 command.append('/bl' if isinstance(output_binary_log, bool)
0162                                else '/bl:"%s"' % output_binary_log)
0163             else:
0164                 raise ConanException("MSBuild version detected (%s) does not support "
0165                                      "'output_binary_log' ('/bl')" % msbuild_version)
0166 
0167         if use_env:
0168             command.append('/p:UseEnv=true')
0169         else:
0170             command.append('/p:UseEnv=false')
0171 
0172         if msvc_arch:
0173             command.append('/p:Platform="%s"' % msvc_arch)
0174 
0175         if parallel:
0176             command.append('/m:%s' % cpu_count(output=self._output))
0177 
0178         if targets:
0179             command.append("/target:%s" % ";".join(targets))
0180 
0181         if toolset:
0182             command.append('/p:PlatformToolset="%s"' % toolset)
0183 
0184         if verbosity:
0185             command.append('/verbosity:%s' % verbosity)
0186 
0187         if props_file_path or user_property_file_name:
0188             paths = [os.path.abspath(props_file_path)] if props_file_path else []
0189             if isinstance(user_property_file_name, list):
0190                 paths.extend([os.path.abspath(p) for p in user_property_file_name])
0191             elif user_property_file_name:
0192                 paths.append(os.path.abspath(user_property_file_name))
0193             paths = ";".join(paths)
0194             command.append('/p:ForceImportBeforeCppTargets="%s"' % paths)
0195 
0196         for name, value in properties.items():
0197             command.append('/p:%s="%s"' % (name, value))
0198 
0199         return " ".join(command)
0200 
0201     def _get_props_file_contents(self, definitions=None):
0202         def format_macro(name, value):
0203             return "%s=%s" % (name, value) if value is not None else name
0204         # how to specify runtime in command line:
0205         # https://stackoverflow.com/questions/38840332/msbuild-overrides-properties-while-building-vc-project
0206         runtime_library = {"MT": "MultiThreaded",
0207                            "MTd": "MultiThreadedDebug",
0208                            "MD": "MultiThreadedDLL",
0209                            "MDd": "MultiThreadedDebugDLL"}.get(
0210                                self._settings.get_safe("compiler.runtime"), "")
0211 
0212         if self.build_env:
0213             # Take the flags from the build env, the user was able to alter them if needed
0214             flags = copy.copy(self.build_env.flags)
0215             flags.append(self.build_env.std)
0216         else:  # To be removed when build_sln_command is deprecated
0217             flags = vs_build_type_flags(self._settings, with_flags=False)
0218             flags.append(vs_std_cpp(self._settings))
0219 
0220         if definitions:
0221             definitions = ";".join([format_macro(name, definitions[name]) for name in definitions])
0222 
0223         flags_str = " ".join(list(filter(None, flags)))  # Removes empty and None elements
0224         additional_node = "<AdditionalOptions>" \
0225                           "{} %(AdditionalOptions)" \
0226                           "</AdditionalOptions>".format(flags_str) if flags_str else ""
0227         runtime_node = "<RuntimeLibrary>" \
0228                        "{}" \
0229                        "</RuntimeLibrary>".format(runtime_library) if runtime_library else ""
0230         definitions_node = "<PreprocessorDefinitions>" \
0231                            "{};%(PreprocessorDefinitions)" \
0232                            "</PreprocessorDefinitions>".format(definitions) if definitions else ""
0233         template = """<?xml version="1.0" encoding="utf-8"?>
0234 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
0235   <ItemDefinitionGroup>
0236     <ClCompile>
0237       {runtime_node}
0238       {additional_node}
0239       {definitions_node}
0240     </ClCompile>
0241   </ItemDefinitionGroup>
0242 </Project>""".format(**{"runtime_node": runtime_node,
0243                         "additional_node": additional_node,
0244                         "definitions_node": definitions_node})
0245         return template
0246 
0247     @staticmethod
0248     def get_version(settings):
0249         msbuild_cmd = "msbuild -version"
0250         vcvars = tools_vcvars_command(settings)
0251         command = "%s && %s" % (vcvars, msbuild_cmd)
0252         try:
0253             out = version_runner(command, shell=True)
0254             version_line = decode_text(out).split("\n")[-1]
0255             prog = re.compile(r"(\d+\.){2,3}\d+")
0256             result = prog.match(version_line).group()
0257             return Version(result)
0258         except Exception as e:
0259             raise ConanException("Error retrieving MSBuild version: '{}'".format(e))