Repository: conan-io/conan

Issue:
[question] MsBuild tools.microsoft.msbuild:max_cpu_count
Hello,

we are using conan in CI/CD and we were wondering if we can utilize all of the CPUs in a MsBuild build when calling "conan build .".
We already know that we can mimic /maxcpucount:2 with tools.microsoft.msbuild:max_cpu_count=2 in our default.conf file, but how can we set /maxcpucount without any arguments? Because setting /maxcpucount  would use all available CPUs by default... "If you include the -maxcpucount switch without specifying a value, MSBuild will use up to the number of processors on the computer." [(source)](https://learn.microsoft.com/en-us/visualstudio/msbuild/building-multiple-projects-in-parallel-with-msbuild?view=vs-2022).

Another question: is there a better way to set tools.microsoft.msbuild:max_cpu_count=2 in CI/CD besides manually appending it to the default.conf?

Thanks in advance!

### Have you read the CONTRIBUTING guide?

- [ ] I've read the CONTRIBUTING guide


Discussion:
Hi @fellinga 

Thanks for your question.

> We already know that we can mimic /maxcpucount:2 with tools.microsoft.msbuild:max_cpu_count=2 in our default.conf file, but how can we set /maxcpucount without any arguments? 

It is not clear if you are using MSBuild (VS native project) or via CMake. In any case, note that you can also use the ``tools.build:jobs`` conf, and it will define the ``<ProcessorNumber>{}</ProcessorNumber>`` in ``MSBuildToolchain`` or to ``/MP`` flags in ``CMakeToolchain``. This is equally or even more important for multi-processor compilation parallelism than ``max_cpu_count``.

> Because setting /maxcpucount would use all available CPUs by default.

Not really, if the solution doesn't have multiple projects to build in parallel. For intra-project compilation parallelism, you need the above ``conf``.

```python
 njobs = conanfile.conf.get("tools.build:jobs",
                               default=_cpu_count(),
                               check_type=int)
```
This is how the default number of jobs is computed, by default it is already the ``cpu_count``, and that is used for the compilation parallelism

I see it seems there is a small bug in the ``max_cpu_count`` management for ``CMake``, but need clarification if you are using it or not.

One thing more: the ``max_cpu_count`` is not set by default to ``cpu_count``, because our experience is that having both ``/MP`` and ``/m:x`` flags at the same time to the number of cpus, is counter productive, it can easily become slower because having too many concurrent process. I wouldn't recommend setting ``max_cpu_count`` to the number of cpu-counts, at least without deactivating or reducing first the ``tools.build:jobs``
Hi @memsharded,

thanks for answering so quickly! We use MSBuild with VisualStudio and do not use CMake at all.

I'll try to explain my issue a little better, sorry if it was not clear enough:
Currently we run "`MSBuild.exe mySolution.sln /maxcpucount /p:Configuration=Release` in gitlab-ci and are quite happy with it but want to change this command to "conan install . " so that we can use a common pipeline for all projects. Therefore we added:

`   def build(self):
        msbuild = MSBuild(self)        
        msbuild.build("mySolution.sln")
`

to our conanfiles. Now when i call "conan install . " it generates "MSBuild.exe mySolution.sln /p:Configuration=Release" which is almost exactly what we want besides the missing maxcpucount argument. I do not want to specify a hardcoded number of cores since my CI runners do not all have the same amount of CPUs. How can we solve this issue?

Thanks!
Understood. Yes, I think this specific use case is not covered by the current ``conf`` value, but I think it might be doable to take it into account, i will reopen (it was closed by by #17292 and will give it a try. 

Thanks for the feedback!
Proposing https://github.com/conan-io/conan/pull/17301, and the ``maxcpucount=0`` value to mean "all cores", I think it is better than inventing some other arbitrary string.



Patch:
diff --git a/conan/tools/cmake/cmake.py b/conan/tools/cmake/cmake.py
index a94bae711c0..ae3acd5ca88 100644
--- a/conan/tools/cmake/cmake.py
+++ b/conan/tools/cmake/cmake.py
@@ -19,8 +19,8 @@ def _cmake_cmd_line_args(conanfile, generator):
         args.append("-j{}".format(njobs))
 
     maxcpucount = conanfile.conf.get("tools.microsoft.msbuild:max_cpu_count", check_type=int)
-    if maxcpucount and "Visual Studio" in generator:
-        args.append("/m:{}".format(maxcpucount))
+    if maxcpucount is not None and "Visual Studio" in generator:
+        args.append(f"/m:{maxcpucount}" if maxcpucount > 0 else "/m")
 
     # Arguments for verbosity
     if "Visual Studio" in generator:
diff --git a/conan/tools/microsoft/msbuild.py b/conan/tools/microsoft/msbuild.py
index a2ec898caff..e8efb277209 100644
--- a/conan/tools/microsoft/msbuild.py
+++ b/conan/tools/microsoft/msbuild.py
@@ -64,8 +64,8 @@ def command(self, sln, targets=None):
 
         maxcpucount = self._conanfile.conf.get("tools.microsoft.msbuild:max_cpu_count",
                                                check_type=int)
-        if maxcpucount:
-            cmd += " /m:{}".format(maxcpucount)
+        if maxcpucount is not None:
+            cmd += f" /m:{maxcpucount}" if maxcpucount > 0 else " /m"
 
         if targets:
             if not isinstance(targets, list):


Test patch:
diff --git a/test/unittests/tools/cmake/test_cmake_cmd_line_args.py b/test/unittests/tools/cmake/test_cmake_cmd_line_args.py
index 7aa8a2f2096..b8848cda9c4 100644
--- a/test/unittests/tools/cmake/test_cmake_cmd_line_args.py
+++ b/test/unittests/tools/cmake/test_cmake_cmd_line_args.py
@@ -44,3 +44,13 @@ def test_visual_studio(conanfile):
 
     args = _cmake_cmd_line_args(conanfile, 'Ninja')
     assert args == ['-j10']
+
+
+def test_maxcpucount_zero():
+    c = ConfDefinition()
+    c.loads("tools.microsoft.msbuild:max_cpu_count=0")
+
+    conanfile = ConanFileMock()
+    conanfile.conf = c.get_conanfile_conf(None)
+    args = _cmake_cmd_line_args(conanfile, 'Visual Studio 16 2019')
+    assert ["/m"] == args
diff --git a/test/unittests/tools/microsoft/test_msbuild.py b/test/unittests/tools/microsoft/test_msbuild.py
index 3fc98453183..3039fbefd7b 100644
--- a/test/unittests/tools/microsoft/test_msbuild.py
+++ b/test/unittests/tools/microsoft/test_msbuild.py
@@ -1,5 +1,4 @@
 import os
-import textwrap
 
 import pytest
 
@@ -31,14 +30,9 @@ def test_msbuild_targets():
 
 def test_msbuild_cpu_count():
     c = ConfDefinition()
-    c.loads(textwrap.dedent("""\
-        tools.microsoft.msbuild:max_cpu_count=23
-    """))
+    c.loads("tools.microsoft.msbuild:max_cpu_count=23")
 
     settings = MockSettings({"build_type": "Release",
-                             "compiler": "gcc",
-                             "compiler.version": "7",
-                             "os": "Linux",
                              "arch": "x86_64"})
     conanfile = ConanFileMock()
     conanfile.settings = settings
@@ -46,8 +40,12 @@ def test_msbuild_cpu_count():
 
     msbuild = MSBuild(conanfile)
     cmd = msbuild.command('project.sln')
+    assert 'msbuild "project.sln" /p:Configuration="Release" /p:Platform=x64 /m:23' == cmd
 
-    assert '/m:23' in cmd
+    c.loads("tools.microsoft.msbuild:max_cpu_count=0")
+    conanfile.conf = c.get_conanfile_conf(None)
+    cmd = msbuild.command('project.sln')
+    assert 'msbuild "project.sln" /p:Configuration="Release" /p:Platform=x64 /m' == cmd
 
 
 def test_msbuild_toolset():
@@ -81,10 +79,10 @@ def test_msbuild_toolset_for_intel_cc(mode, expected_toolset):
     conanfile = ConanFile()
     conanfile.settings = "os", "compiler", "build_type", "arch"
     conanfile.settings = Settings({"build_type": ["Release"],
-                         "compiler": {"intel-cc": {"version": ["2021.3"], "mode": [mode]},
-                                      "msvc": {"version": ["193"], "cppstd": ["20"]}},
-                         "os": ["Windows"],
-                         "arch": ["x86_64"]})
+                                   "compiler": {"intel-cc": {"version": ["2021.3"], "mode": [mode]},
+                                                "msvc": {"version": ["193"], "cppstd": ["20"]}},
+                                   "os": ["Windows"],
+                                   "arch": ["x86_64"]})
     conanfile.settings.build_type = "Release"
     conanfile.settings.compiler = "intel-cc"
     conanfile.settings.compiler.version = "2021.3"