1import copy 2import os 3import re 4 5from conans.client import tools 6from conans.client.build.visual_environment import (VisualStudioBuildEnvironment, 7 vs_build_type_flags, vs_std_cpp) 8from conans.client.tools.env import environment_append, no_op 9from conans.client.tools.intel import intel_compilervars 10from conans.client.tools.oss import cpu_count 11from conans.client.tools.win import vcvars_command 12from conans.errors import ConanException 13from conans.model.conan_file import ConanFile 14from conans.model.version import Version 15from conans.tools import vcvars_command as tools_vcvars_command 16from conans.util.env_reader import get_env 17from conans.util.files import decode_text, save 18from conans.util.runners import version_runner 19 20 21class MSBuild(object): 22 23 def __init__(self, conanfile): 24 if isinstance(conanfile, ConanFile): 25 self._conanfile = conanfile 26 self._settings = self._conanfile.settings 27 self._output = self._conanfile.output 28 self.build_env = VisualStudioBuildEnvironment(self._conanfile, 29 with_build_type_flags=False) 30 else: # backwards compatible with build_sln_command 31 self._settings = conanfile 32 self.build_env = None 33 34 def build(self, project_file, targets=None, upgrade_project=True, build_type=None, arch=None, 35 parallel=True, force_vcvars=False, toolset=None, platforms=None, use_env=True, 36 vcvars_ver=None, winsdk_version=None, properties=None, output_binary_log=None, 37 property_file_name=None, verbosity=None, definitions=None, 38 user_property_file_name=None): 39 """ 40 :param project_file: Path to the .sln file. 41 :param targets: List of targets to build. 42 :param upgrade_project: Will call devenv to upgrade the solution to your 43 current Visual Studio. 44 :param build_type: Use a custom build type instead of the default settings.build_type one. 45 :param arch: Use a custom architecture name instead of the settings.arch one. 46 It will be used to build the /p:Configuration= parameter of MSBuild. 47 It can be used as the key of the platforms parameter. 48 E.g. arch="x86", platforms={"x86": "i386"} 49 :param parallel: Will use the configured number of cores in the conan.conf file or 50 tools.cpu_count(): 51 In the solution: Building the solution with the projects in parallel. (/m: parameter). 52 CL compiler: Building the sources in parallel. (/MP: compiler flag) 53 :param force_vcvars: Will ignore if the environment is already set for a different 54 Visual Studio version. 55 :param toolset: Specify a toolset. Will append a /p:PlatformToolset option. 56 :param platforms: Dictionary with the mapping of archs/platforms from Conan naming to 57 another one. It is useful for Visual Studio solutions that have a different naming in 58 architectures. 59 Example: platforms={"x86":"Win32"} (Visual solution uses "Win32" instead of "x86"). 60 This dictionary will update the default one: 61 msvc_arch = {'x86': 'x86', 'x86_64': 'x64', 'armv7': 'ARM', 'armv8': 'ARM64'} 62 :param use_env: Applies the argument /p:UseEnv=true to the MSBuild call. 63 :param vcvars_ver: Specifies the Visual Studio compiler toolset to use. 64 :param winsdk_version: Specifies the version of the Windows SDK to use. 65 :param properties: Dictionary with new properties, for each element in the dictionary 66 {name: value} it will append a /p:name="value" option. 67 :param output_binary_log: If set to True then MSBuild will output a binary log file 68 called msbuild.binlog in the working directory. It can also be used to set the name of 69 log file like this output_binary_log="my_log.binlog". 70 This parameter is only supported starting from MSBuild version 15.3 and onwards. 71 :param property_file_name: When None it will generate a file named conan_build.props. 72 You can specify a different name for the generated properties file. 73 :param verbosity: Specifies verbosity level (/verbosity: parameter) 74 :param definitions: Dictionary with additional compiler definitions to be applied during 75 the build. Use value of None to set compiler definition with no value. 76 :param user_property_file_name: Specify a user provided .props file with custom definitions 77 :return: status code of the MSBuild command invocation 78 """ 79 property_file_name = property_file_name or "conan_build.props" 80 self.build_env.parallel = parallel 81 82 with environment_append(self.build_env.vars): 83 # Path for custom properties file 84 props_file_contents = self._get_props_file_contents(definitions) 85 property_file_name = os.path.abspath(property_file_name) 86 save(property_file_name, props_file_contents) 87 vcvars = vcvars_command(self._conanfile.settings, arch=arch, force=force_vcvars, 88 vcvars_ver=vcvars_ver, winsdk_version=winsdk_version, 89 output=self._output) 90 command = self.get_command(project_file, property_file_name, 91 targets=targets, upgrade_project=upgrade_project, 92 build_type=build_type, arch=arch, parallel=parallel, 93 toolset=toolset, platforms=platforms, 94 use_env=use_env, properties=properties, 95 output_binary_log=output_binary_log, 96 verbosity=verbosity, 97 user_property_file_name=user_property_file_name) 98 command = "%s && %s" % (vcvars, command) 99 context = no_op() 100 if self._conanfile.settings.get_safe("compiler") == "Intel" and \ 101 self._conanfile.settings.get_safe("compiler.base") == "Visual Studio": 102 context = intel_compilervars(self._conanfile.settings, arch) 103 with context: 104 return self._conanfile.run(command) 105 106 def get_command(self, project_file, props_file_path=None, targets=None, upgrade_project=True, 107 build_type=None, arch=None, parallel=True, toolset=None, platforms=None, 108 use_env=False, properties=None, output_binary_log=None, verbosity=None, 109 user_property_file_name=None): 110 111 targets = targets or [] 112 if not isinstance(targets, (list, tuple)): 113 raise TypeError("targets argument should be a list") 114 properties = properties or {} 115 command = [] 116 117 if upgrade_project and not get_env("CONAN_SKIP_VS_PROJECTS_UPGRADE", False): 118 command.append('devenv "%s" /upgrade &&' % project_file) 119 else: 120 self._output.info("Skipped sln project upgrade") 121 122 build_type = build_type or self._settings.get_safe("build_type") 123 arch = arch or self._settings.get_safe("arch") 124 if toolset is None: # False value to skip adjusting 125 toolset = tools.msvs_toolset(self._settings) 126 verbosity = os.getenv("CONAN_MSBUILD_VERBOSITY") or verbosity or "minimal" 127 if not build_type: 128 raise ConanException("Cannot build_sln_command, build_type not defined") 129 if not arch: 130 raise ConanException("Cannot build_sln_command, arch not defined") 131 132 command.append('msbuild "%s" /p:Configuration="%s"' % (project_file, build_type)) 133 msvc_arch = {'x86': 'x86', 134 'x86_64': 'x64', 135 'armv7': 'ARM', 136 'armv8': 'ARM64'} 137 if platforms: 138 msvc_arch.update(platforms) 139 msvc_arch = msvc_arch.get(str(arch)) 140 if self._settings.get_safe("os") == "WindowsCE": 141 msvc_arch = self._settings.get_safe("os.platform") 142 try: 143 sln = tools.load(project_file) 144 pattern = re.compile(r"GlobalSection\(SolutionConfigurationPlatforms\)" 145 r"(.*?)EndGlobalSection", re.DOTALL) 146 solution_global = pattern.search(sln).group(1) 147 lines = solution_global.splitlines() 148 lines = [s.split("=")[0].strip() for s in lines] 149 except Exception: 150 pass # TODO: !!! what are we catching here? tools.load? .group(1)? .splitlines? 151 else: 152 config = "%s|%s" % (build_type, msvc_arch) 153 if config not in "".join(lines): 154 self._output.warn("***** The configuration %s does not exist in this solution *****" 155 % config) 156 self._output.warn("Use 'platforms' argument to define your architectures") 157 158 if output_binary_log: 159 msbuild_version = MSBuild.get_version(self._settings) 160 if msbuild_version >= "15.3": # http://msbuildlog.com/ 161 command.append('/bl' if isinstance(output_binary_log, bool) 162 else '/bl:"%s"' % output_binary_log) 163 else: 164 raise ConanException("MSBuild version detected (%s) does not support " 165 "'output_binary_log' ('/bl')" % msbuild_version) 166 167 if use_env: 168 command.append('/p:UseEnv=true') 169 else: 170 command.append('/p:UseEnv=false') 171 172 if msvc_arch: 173 command.append('/p:Platform="%s"' % msvc_arch) 174 175 if parallel: 176 command.append('/m:%s' % cpu_count(output=self._output)) 177 178 if targets: 179 command.append("/target:%s" % ";".join(targets)) 180 181 if toolset: 182 command.append('/p:PlatformToolset="%s"' % toolset) 183 184 if verbosity: 185 command.append('/verbosity:%s' % verbosity) 186 187 if props_file_path or user_property_file_name: 188 paths = [os.path.abspath(props_file_path)] if props_file_path else [] 189 if isinstance(user_property_file_name, list): 190 paths.extend([os.path.abspath(p) for p in user_property_file_name]) 191 elif user_property_file_name: 192 paths.append(os.path.abspath(user_property_file_name)) 193 paths = ";".join(paths) 194 command.append('/p:ForceImportBeforeCppTargets="%s"' % paths) 195 196 for name, value in properties.items(): 197 command.append('/p:%s="%s"' % (name, value)) 198 199 return " ".join(command) 200 201 def _get_props_file_contents(self, definitions=None): 202 def format_macro(name, value): 203 return "%s=%s" % (name, value) if value is not None else name 204 # how to specify runtime in command line: 205 # https://stackoverflow.com/questions/38840332/msbuild-overrides-properties-while-building-vc-project 206 runtime_library = {"MT": "MultiThreaded", 207 "MTd": "MultiThreadedDebug", 208 "MD": "MultiThreadedDLL", 209 "MDd": "MultiThreadedDebugDLL"}.get( 210 self._settings.get_safe("compiler.runtime"), "") 211 212 if self.build_env: 213 # Take the flags from the build env, the user was able to alter them if needed 214 flags = copy.copy(self.build_env.flags) 215 flags.append(self.build_env.std) 216 else: # To be removed when build_sln_command is deprecated 217 flags = vs_build_type_flags(self._settings, with_flags=False) 218 flags.append(vs_std_cpp(self._settings)) 219 220 if definitions: 221 definitions = ";".join([format_macro(name, definitions[name]) for name in definitions]) 222 223 flags_str = " ".join(list(filter(None, flags))) # Removes empty and None elements 224 additional_node = "<AdditionalOptions>" \ 225 "{} %(AdditionalOptions)" \ 226 "</AdditionalOptions>".format(flags_str) if flags_str else "" 227 runtime_node = "<RuntimeLibrary>" \ 228 "{}" \ 229 "</RuntimeLibrary>".format(runtime_library) if runtime_library else "" 230 definitions_node = "<PreprocessorDefinitions>" \ 231 "{};%(PreprocessorDefinitions)" \ 232 "</PreprocessorDefinitions>".format(definitions) if definitions else "" 233 template = """<?xml version="1.0" encoding="utf-8"?> 234<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 235 <ItemDefinitionGroup> 236 <ClCompile> 237 {runtime_node} 238 {additional_node} 239 {definitions_node} 240 </ClCompile> 241 </ItemDefinitionGroup> 242</Project>""".format(**{"runtime_node": runtime_node, 243 "additional_node": additional_node, 244 "definitions_node": definitions_node}) 245 return template 246 247 @staticmethod 248 def get_version(settings): 249 msbuild_cmd = "msbuild -version" 250 vcvars = tools_vcvars_command(settings) 251 command = "%s && %s" % (vcvars, msbuild_cmd) 252 try: 253 out = version_runner(command, shell=True) 254 version_line = decode_text(out).split("\n")[-1] 255 prog = re.compile(r"(\d+\.){2,3}\d+") 256 result = prog.match(version_line).group() 257 return Version(result) 258 except Exception as e: 259 raise ConanException("Error retrieving MSBuild version: '{}'".format(e)) 260