Repository: conan-io/conan

Issue:
Add .definitions dict to MSBuild Helper like CMake helper has
I came across a situation today where I wanted to pass a preprocessor definition to `MSBuild` helper but it is currently not supported.  It is only supported to set msbuild `properties`. 

I understand why this functionality was not included by the `MSBuild` helper initially, but I believe that it's reasonable to add it now, and have the suggested implementation on how to do it. 

I will probably create a PR for this feature at some point if it's "acceptable" but nobody gets around to it before I do. 

I am fairly certain that the reason it's not implemented in the helper is that it's not natively supported by MSBuild.  MSBuild CLI will not accept arbitrary preprocessor definitions and pass them to the compiler for you.  The only "supported" solution is to use the `ForceImportBeforeCppTargets` feature, combined with a `.props` file, and do something like: 

`<PreprocessorDefinitions>_NEW_DEFINE;%(PreprocessorDefinitions)</PreprocessorDefinitions>`

https://docs.microsoft.com/en-us/cpp/ide/working-with-project-properties?view=vs-2017

Conan is in a unique position to implement the requested because it already makes use of `ForceImportBeforeCppTargets`.  So, this request would be to add the `definitions` member as a dict to the `MSBuild` helper just like CMakes, and implement it by simply injecting new preprocessor definition alongside the other variables that get injected.  

- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




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
@@ -30,14 +30,48 @@ 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, output_binary_log=None,
-              property_file_name=None, verbosity=None):
+              property_file_name=None, verbosity=None, definitions=None):
+        """
+        :param project_file: Path to the .sln file.
+        :param targets: List of targets to build.
+        :param upgrade_project: Will call devenv to upgrade the solution to your current Visual Studio.
+        :param build_type: Use a custom build type name instead of the default settings.build_type one.
+        :param arch: Use a custom architecture name instead of the settings.arch one.
+        It will be used to build the /p:Configuration= parameter of MSBuild.
+        It can be used as the key of the platforms parameter. E.g. arch="x86", platforms={"x86": "i386"}
+        :param parallel: Will use the configured number of cores in the conan.conf file or tools.cpu_count():
+        In the solution: Building the solution with the projects in parallel. (/m: parameter).
+        CL compiler: Building the sources in parallel. (/MP: compiler flag)
+        :param force_vcvars: Will ignore if the environment is already set for a different Visual Studio version.
+        :param toolset: Specify a toolset. Will append a /p:PlatformToolset option.
+        :param platforms: Dictionary with the mapping of archs/platforms from Conan naming to another one.
+        It is useful for Visual Studio solutions that have a different naming in architectures.
+        Example: platforms={"x86":"Win32"} (Visual solution uses "Win32" instead of "x86").
+        This dictionary will update the default one:
+        msvc_arch = {'x86': 'x86', 'x86_64': 'x64', 'armv7': 'ARM', 'armv8': 'ARM64'}
+        :param use_env: Applies the argument /p:UseEnv=true to the MSBuild call.
+        :param vcvars_ver: Specifies the Visual Studio compiler toolset to use.
+        :param winsdk_version: Specifies the version of the Windows SDK to use.
+        :param properties: Dictionary with new properties, for each element in the dictionary {name: value}
+        it will append a /p:name="value" option.
+        :param output_binary_log: If set to True then MSBuild will output a binary log file called msbuild.binlog in
+        the working directory. It can also be used to set the name of log file like this
+        output_binary_log="my_log.binlog".
+        This parameter is only supported starting from MSBuild version 15.3 and onwards.
+        :param property_file_name: When None it will generate a file named conan_build.props.
+        You can specify a different name for the generated properties file.
+        :param verbosity: Specifies verbosity level (/verbosity: parameter)
+        :param definitions: Dictionary with additional compiler definitions to be applied during the build.
+        Use value of None to set compiler definition with no value.
+        :return: status code of the MSBuild command invocation
+        """
 
         property_file_name = property_file_name or "conan_build.props"
         self.build_env.parallel = parallel
 
         with tools.environment_append(self.build_env.vars):
             # Path for custom properties file
-            props_file_contents = self._get_props_file_contents()
+            props_file_contents = self._get_props_file_contents(definitions)
             save(property_file_name, props_file_contents)
             vcvars = vcvars_command(self._conanfile.settings, force=force_vcvars,
                                     vcvars_ver=vcvars_ver, winsdk_version=winsdk_version)
@@ -129,7 +163,11 @@ def get_command(self, project_file, props_file_path=None, targets=None, upgrade_
 
         return " ".join(command)
 
-    def _get_props_file_contents(self):
+    def _get_props_file_contents(self, definitions=None):
+
+        def format_macro(name, value):
+            return "%s=%s" % (name, value) if value else name
+
         # how to specify runtime in command line:
         # https://stackoverflow.com/questions/38840332/msbuild-overrides-properties-while-building-vc-project
         runtime_library = {"MT": "MultiThreaded",
@@ -145,23 +183,31 @@ def _get_props_file_contents(self):
             flags = vs_build_type_flags(self._settings, with_flags=False)
             flags.append(vs_std_cpp(self._settings))
 
-        flags_str = " ".join(list(filter(None, flags))) # Removes empty and None elements
+        if definitions:
+            definitions = ";".join([format_macro(name, definitions[name]) for name in definitions])
+
+        flags_str = " ".join(list(filter(None, flags)))  # Removes empty and None elements
         additional_node = "<AdditionalOptions>" \
                           "{} %(AdditionalOptions)" \
                           "</AdditionalOptions>".format(flags_str) if flags_str else ""
         runtime_node = "<RuntimeLibrary>" \
                        "{}" \
                        "</RuntimeLibrary>".format(runtime_library) if runtime_library else ""
+        definitions_node = "<PreprocessorDefinitions>" \
+                           "{};%(PreprocessorDefinitions)" \
+                           "</PreprocessorDefinitions>".format(definitions) if definitions else ""
         template = """<?xml version="1.0" encoding="utf-8"?>
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemDefinitionGroup>
     <ClCompile>
       {runtime_node}
       {additional_node}
+      {definitions_node}
     </ClCompile>
   </ItemDefinitionGroup>
 </Project>""".format(**{"runtime_node": runtime_node,
-                        "additional_node": additional_node})
+                        "additional_node": additional_node,
+                        "definitions_node": definitions_node})
         return template
 
     @staticmethod
diff --git a/conans/client/tools/win.py b/conans/client/tools/win.py
--- a/conans/client/tools/win.py
+++ b/conans/client/tools/win.py
@@ -120,7 +120,7 @@ def msvc_build_command(settings, sln_path, targets=None, upgrade_project=True, b
 @deprecation.deprecated(deprecated_in="1.2", removed_in="2.0",
                         details="Use the MSBuild() build helper instead")
 def build_sln_command(settings, sln_path, targets=None, upgrade_project=True, build_type=None,
-                      arch=None, parallel=True, toolset=None, platforms=None, output=None, verbosity=None):
+                      arch=None, parallel=True, toolset=None, platforms=None, output=None, verbosity=None, definitions=None):
     """
     Use example:
         build_command = build_sln_command(self.settings, "myfile.sln", targets=["SDL2_image"])
@@ -133,7 +133,7 @@ def build_sln_command(settings, sln_path, targets=None, upgrade_project=True, bu
     tmp._output = output
 
     # Generate the properties file
-    props_file_contents = tmp._get_props_file_contents()
+    props_file_contents = tmp._get_props_file_contents(definitions)
     tmp_path = os.path.join(mkdir_tmp(), ".conan_properties")
     save(tmp_path, props_file_contents)
 


Test patch:
diff --git a/conans/test/unittests/client/build/msbuild_test.py b/conans/test/unittests/client/build/msbuild_test.py
--- a/conans/test/unittests/client/build/msbuild_test.py
+++ b/conans/test/unittests/client/build/msbuild_test.py
@@ -146,6 +146,32 @@ def custom_properties_test(self):
         self.assertIn('/p:MyProp1="MyValue1"', command)
         self.assertIn('/p:MyProp2="MyValue2"', command)
 
+    def definitions_test(self):
+        settings = MockSettings({"build_type": "Debug",
+                                 "compiler": "Visual Studio",
+                                 "arch": "x86_64",
+                                 "compiler.runtime": "MDd"})
+        conanfile = MockConanfile(settings)
+        msbuild = MSBuild(conanfile)
+        template = msbuild._get_props_file_contents(definitions={'_WIN32_WINNT': "0x0501"})
+
+        self.assertIn("<PreprocessorDefinitions>"
+                      "_WIN32_WINNT=0x0501;"
+                      "%(PreprocessorDefinitions)</PreprocessorDefinitions>", template)
+
+    def definitions_no_value_test(self):
+        settings = MockSettings({"build_type": "Debug",
+                                 "compiler": "Visual Studio",
+                                 "arch": "x86_64",
+                                 "compiler.runtime": "MDd"})
+        conanfile = MockConanfile(settings)
+        msbuild = MSBuild(conanfile)
+        template = msbuild._get_props_file_contents(definitions={'_DEBUG': None})
+
+        self.assertIn("<PreprocessorDefinitions>"
+                      "_DEBUG;"
+                      "%(PreprocessorDefinitions)</PreprocessorDefinitions>", template)
+
     def verbosity_default_test(self):
         settings = MockSettings({"build_type": "Debug",
                                  "compiler": "Visual Studio",
@@ -173,3 +199,4 @@ def verbosity_explicit_test(self):
         msbuild = MSBuild(conanfile)
         command = msbuild.get_command("projecshould_flags_testt_file.sln", verbosity="quiet")
         self.assertIn('/verbosity:quiet', command)
+