Repository: conan-io/conan

Issue:
MSBuild /bl flag for Binary Logging
MSBuild has a command line flag to activate the output of a Binary Log that can replay builds to gather diagnostics on builds.

I would be really useful to add this utility to the MSBuild helper.

[MSBuild Binary Log Docs](https://github.com/Microsoft/msbuild/blob/master/documentation/wiki/Binary-Log.md)




Discussion:

Resolving patch:
diff --git a/conans/client/build/msbuild.py b/conans/client/build/msbuild.py
--- a/conans/client/build/msbuild.py
+++ b/conans/client/build/msbuild.py
@@ -1,5 +1,6 @@
 import copy
 import re
+import subprocess
 
 from conans import tools
 from conans.client.build.visual_environment import (VisualStudioBuildEnvironment,
@@ -7,8 +8,9 @@
 from conans.client.tools.oss import cpu_count
 from conans.client.tools.win import vcvars_command
 from conans.errors import ConanException
+from conans.model.version import Version
 from conans.util.env_reader import get_env
-from conans.util.files import tmp_file
+from conans.util.files import tmp_file, decode_text
 from conans.model.conan_file import ConanFile
 
 
@@ -26,7 +28,7 @@ def __init__(self, conanfile):
 
     def build(self, project_file, targets=None, upgrade_project=True, build_type=None, arch=None,
               parallel=True, force_vcvars=False, toolset=None, platforms=None, use_env=True,
-              vcvars_ver=None, winsdk_version=None, properties=None):
+              vcvars_ver=None, winsdk_version=None, properties=None, output_binary_log=None):
 
         self.build_env.parallel = parallel
 
@@ -40,13 +42,13 @@ def build(self, project_file, targets=None, upgrade_project=True, build_type=Non
                                            targets=targets, upgrade_project=upgrade_project,
                                            build_type=build_type, arch=arch, parallel=parallel,
                                            toolset=toolset, platforms=platforms,
-                                           use_env=use_env, properties=properties)
+                                           use_env=use_env, properties=properties, output_binary_log=output_binary_log)
                 command = "%s && %s" % (vcvars, command)
                 return self._conanfile.run(command)
 
     def get_command(self, project_file, props_file_path=None, targets=None, upgrade_project=True,
                     build_type=None, arch=None, parallel=True, toolset=None, platforms=None,
-                    use_env=False, properties=None):
+                    use_env=False, properties=None, output_binary_log=None):
 
         targets = targets or []
         properties = properties or {}
@@ -87,6 +89,15 @@ def get_command(self, project_file, props_file_path=None, targets=None, upgrade_
                 self._output.warn("***** The configuration %s does not exist in this solution *****" % config)
                 self._output.warn("Use 'platforms' argument to define your architectures")
 
+        if output_binary_log:
+            msbuild_version = MSBuild.get_version(self._settings)
+            if msbuild_version >= "15.3":  # http://msbuildlog.com/
+                command.append('/bl' if isinstance(output_binary_log, bool)
+                               else '/bl:"%s"' % output_binary_log)
+            else:
+                raise ConanException("MSBuild version detected (%s) does not support "
+                                     "'output_binary_log' ('/bl')" % msbuild_version)
+
         if use_env:
             command.append('/p:UseEnv=true')
 
@@ -144,3 +155,17 @@ def _get_props_file_contents(self):
 </Project>""".format(**{"runtime_node": runtime_node,
                         "additional_node": additional_node})
         return template
+
+    @staticmethod
+    def get_version(settings):
+        msbuild_cmd = "msbuild -version"
+        vcvars = vcvars_command(settings)
+        command = "%s && %s" % (vcvars, msbuild_cmd)
+        try:
+            out, err = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).communicate()
+            version_line = decode_text(out).split("\n")[-1]
+            prog = re.compile("(\d+\.){2,3}\d+")
+            result = prog.match(version_line).group()
+            return Version(result)
+        except Exception as e:
+            raise ConanException("Error retrieving MSBuild version: '{}'".format(e))


Test patch:
diff --git a/conans/test/build_helpers/msbuild_test.py b/conans/test/build_helpers/msbuild_test.py
--- a/conans/test/build_helpers/msbuild_test.py
+++ b/conans/test/build_helpers/msbuild_test.py
@@ -1,10 +1,15 @@
+import os
 import platform
 import unittest
 
+import mock
 from nose.plugins.attrib import attr
+from parameterized import parameterized
 
 from conans import tools
 from conans.client.build.msbuild import MSBuild
+from conans.errors import ConanException
+from conans.model.version import Version
 from conans.paths import CONANFILE
 from conans.test.utils.conanfile import MockSettings, MockConanfile
 from conans.test.utils.tools import TestClient
@@ -150,3 +155,114 @@ def build(self):
         client.save(files)
         client.run("create . danimtb/testing")
         self.assertIn("build() completed", client.out)
+
+    @unittest.skipUnless(platform.system() == "Windows", "Requires MSBuild")
+    def binary_logging_on_test(self):
+        settings = MockSettings({"build_type": "Debug",
+                                 "compiler": "Visual Studio",
+                                 "compiler.version": "15",
+                                 "arch": "x86_64",
+                                 "compiler.runtime": "MDd"})
+        conanfile = MockConanfile(settings)
+        msbuild = MSBuild(conanfile)
+        command = msbuild.get_command("dummy.sln", output_binary_log=True)
+        self.assertIn("/bl", command)
+
+    @unittest.skipUnless(platform.system() == "Windows", "Requires MSBuild")
+    def binary_logging_on_with_filename_test(self):
+        bl_filename = "a_special_log.log"
+        settings = MockSettings({"build_type": "Debug",
+                                 "compiler": "Visual Studio",
+                                 "compiler.version": "15",
+                                 "arch": "x86_64",
+                                 "compiler.runtime": "MDd"})
+        conanfile = MockConanfile(settings)
+        msbuild = MSBuild(conanfile)
+        command = msbuild.get_command("dummy.sln", output_binary_log=bl_filename)
+        expected_command = '/bl:"%s"' % bl_filename
+        self.assertIn(expected_command, command)
+
+    def binary_logging_off_explicit_test(self):
+        settings = MockSettings({"build_type": "Debug",
+                                 "compiler": "Visual Studio",
+                                 "compiler.version": "15",
+                                 "arch": "x86_64",
+                                 "compiler.runtime": "MDd"})
+        conanfile = MockConanfile(settings)
+        msbuild = MSBuild(conanfile)
+        command = msbuild.get_command("dummy.sln", output_binary_log=False)
+        self.assertNotIn("/bl", command)
+
+    def binary_logging_off_implicit_test(self):
+        settings = MockSettings({"build_type": "Debug",
+                                 "compiler": "Visual Studio",
+                                 "compiler.version": "15",
+                                 "arch": "x86_64",
+                                 "compiler.runtime": "MDd"})
+        conanfile = MockConanfile(settings)
+        msbuild = MSBuild(conanfile)
+        command = msbuild.get_command("dummy.sln")
+        self.assertNotIn("/bl", command)
+
+    @unittest.skipUnless(platform.system() == "Windows", "Requires MSBuild")
+    @mock.patch("conans.client.build.msbuild.MSBuild.get_version")
+    def binary_logging_not_supported_test(self, mock_get_version):
+        mock_get_version.return_value = Version("14")
+
+        mocked_settings = MockSettings({"build_type": "Debug",
+                                        "compiler": "Visual Studio",
+                                        "compiler.version": "15",
+                                        "arch": "x86_64",
+                                        "compiler.runtime": "MDd"})
+        conanfile = MockConanfile(mocked_settings)
+        except_text = "MSBuild version detected (14) does not support 'output_binary_log' ('/bl')"
+        msbuild = MSBuild(conanfile)
+
+        with self.assertRaises(ConanException) as exc:
+            msbuild.get_command("dummy.sln", output_binary_log=True)
+        self.assertIn(except_text, str(exc.exception))
+
+    @unittest.skipUnless(platform.system() == "Windows", "Requires MSBuild")
+    def get_version_test(self):
+        settings = MockSettings({"build_type": "Debug",
+                                 "compiler": "Visual Studio",
+                                 "compiler.version": "15",
+                                 "arch": "x86_64",
+                                 "compiler.runtime": "MDd"})
+        version = MSBuild.get_version(settings)
+        self.assertRegexpMatches(version, "(\d+\.){2,3}\d+")
+        self.assertGreater(version, "15.1")
+
+    @parameterized.expand([("True",), ("'my_log.binlog'",)])
+    @unittest.skipUnless(platform.system() == "Windows", "Requires MSBuild")
+    def binary_log_build_test(self, value):
+        conan_build_vs = """
+from conans import ConanFile, MSBuild
+
+class HelloConan(ConanFile):
+    name = "Hello"
+    version = "1.2.1"
+    exports = "*"
+    settings = "os", "build_type", "arch", "compiler"
+
+    def build(self):
+        msbuild = MSBuild(self)
+        msbuild.build("MyProject.sln", output_binary_log=%s)
+"""
+        client = TestClient()
+        files = get_vs_project_files()
+        files[CONANFILE] = conan_build_vs % value
+        client.save(files)
+        client.run("install . -s compiler=\"Visual Studio\" -s compiler.version=15")
+        client.run("build .")
+
+        if value == "'my_log.binlog'":
+            log_name = value[1:1]
+            flag = "/bl:%s" % log_name
+        else:
+            log_name = "msbuild.binlog"
+            flag = "/bl"
+
+        self.assertIn(flag, client.out)
+        log_path = os.path.join(client.current_folder, log_name)
+        self.assertTrue(os.path.exists(log_path))