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