Repository: conan-io/conan
Pull request #11809 (2022-08-08 13:38:19)

Fix cmake layout path
Changelog: Bugfix: Update cmake_layout build and generators folders to honor os path format.
Docs: omit
closes: #11795

Pointing this: https://github.com/conan-io/conan/pull/11796 to 1.51.1 and also adding test

- [ ] Refer to the issue that supports this Pull Request.
- [ ] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [ ] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [ ] I've followed the PEP8 style guides for Python code.
- [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one. 

<sup>**Note:** By default this PR will skip the slower tests and will use a limited set of python versions. Check [here](https://github.com/conan-io/conan/blob/develop/.github/PR_INCREASE_TESTING.md) how to increase the testing level by writing some tags in the current PR body text.</sup>



Patch:
diff --git a/conan/tools/cmake/layout.py b/conan/tools/cmake/layout.py
index 6a464f5c3..6124a04fc 100644
--- a/conan/tools/cmake/layout.py
+++ b/conan/tools/cmake/layout.py
@@ -24,14 +24,14 @@ def cmake_layout(conanfile, generator=None, src_folder="."):
     build_folder = "build" if not subproject else os.path.join(subproject, "build")
     custom_conf = get_build_folder_custom_vars(conanfile)
     if custom_conf:
-        build_folder = "{}/{}".format(build_folder, custom_conf)
+        build_folder = os.path.join(build_folder, custom_conf)
 
     if multi:
         conanfile.folders.build = build_folder
     else:
-        conanfile.folders.build = "{}/{}".format(build_folder, build_type)
+        conanfile.folders.build = os.path.join(build_folder, build_type)
 
-    conanfile.folders.generators = "{}/{}".format(build_folder, "generators")
+    conanfile.folders.generators = os.path.join(build_folder, "generators")
 
     conanfile.cpp.source.includedirs = ["include"]
 
diff --git a/conan/tools/system/package_manager.py b/conan/tools/system/package_manager.py
index 435350a4b..a416d2db5 100644
--- a/conan/tools/system/package_manager.py
+++ b/conan/tools/system/package_manager.py
@@ -221,7 +221,7 @@ class PkgUtil(_SystemPackageManagerTool):
 
 class Chocolatey(_SystemPackageManagerTool):
     tool_name = "choco"
-    install_command = "{tool} --install --yes {packages}"
+    install_command = "{tool} install --yes {packages}"
     update_command = "{tool} outdated"
     check_command = '{tool} search --local-only --exact {package} | ' \
                     'findstr /c:"1 packages installed."'
diff --git a/conans/client/build/cppstd_flags.py b/conans/client/build/cppstd_flags.py
index 9b96ce7cd..650ecc89d 100644
--- a/conans/client/build/cppstd_flags.py
+++ b/conans/client/build/cppstd_flags.py
@@ -50,14 +50,10 @@ def cppstd_flag_new(settings):
 
 
 def cppstd_default(settings):
-    if getattr(settings, "get_safe", None):
-        compiler = settings.get_safe("compiler")
-        compiler_version = settings.get_safe("compiler.version")
-        compiler_base = settings.get_safe("compiler.base")
-    else:
-        compiler = str(settings.compiler)
-        compiler_version = str(settings.compiler.version)
-        compiler_base = str(settings.compiler.base)
+
+    compiler = settings.get_safe("compiler")
+    compiler_version = settings.get_safe("compiler.version")
+    compiler_base = settings.get_safe("compiler.base")
     intel_cppstd_default = _intel_visual_cppstd_default if compiler_base == "Visual Studio" \
         else _intel_gcc_cppstd_default
     default = {"gcc": _gcc_cppstd_default(compiler_version),
diff --git a/conans/client/installer.py b/conans/client/installer.py
index cac13df55..5f9faddde 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -218,10 +218,10 @@ class _PackageBuilder(object):
                 conanfile.folders.set_base_build(base_build)
                 conanfile.folders.set_base_imports(base_build)
                 conanfile.folders.set_base_package(base_package)
+                # In local cache, generators folder always in build_folder
+                conanfile.folders.set_base_generators(base_build)
 
                 if not skip_build:
-                    # In local cache, generators folder always in build_folder
-                    conanfile.folders.set_base_generators(base_build)
                     # In local cache, install folder always is build_folder
                     conanfile.folders.set_base_install(base_build)
                     self._build(conanfile, pref)
diff --git a/conans/model/options.py b/conans/model/options.py
index 1b29d8957..642c4afe2 100644
--- a/conans/model/options.py
+++ b/conans/model/options.py
@@ -226,6 +226,11 @@ class OptionsValues(object):
     def __contains__(self, item):
         return item in self._package_values
 
+    def get_safe(self, attr):
+        if attr not in self._package_values:
+            return None
+        return getattr(self._package_values, attr)
+
     def __getitem__(self, item):
         return self._reqs_options.setdefault(item, PackageOptionValues())
 
diff --git a/conans/model/values.py b/conans/model/values.py
index 8fa335312..20391ca86 100644
--- a/conans/model/values.py
+++ b/conans/model/values.py
@@ -8,6 +8,10 @@ class Values(object):
         self._dict = {}  # {key: Values()}
         self._modified = {}  # {"compiler.version.arch": (old_value, old_reference)}
 
+    def get_safe(self, attr):
+        values = [v[1] for v in self.as_list() if v[0] == attr]
+        return values[0] if values else None
+
     def __getattr__(self, attr):
         if attr not in self._dict:
             return None
diff --git a/pyinstaller.py b/pyinstaller.py
index 022db9af1..61f020fca 100644
--- a/pyinstaller.py
+++ b/pyinstaller.py
@@ -90,7 +90,8 @@ def pyinstall(source_folder):
     hidden = ("--hidden-import=glob "  # core stdlib
               "--hidden-import=pathlib "
               "--hidden-import=distutils.dir_util "
-              # The conan.tools integration
+              # Modules that can be imported in ConanFile conan.tools and errors
+              "--hidden-import=conan.errors "
               "--hidden-import=conan.tools.microsoft "
               "--hidden-import=conan.tools.gnu --hidden-import=conan.tools.cmake "
               "--hidden-import=conan.tools.meson --hidden-import=conan.tools.apple "
@@ -99,7 +100,7 @@ def pyinstall(source_folder):
               "--hidden-import=conan.tools.google --hidden-import=conan.tools.intel "
               "--hidden-import=conan.tools.layout --hidden-import=conan.tools.premake "
               "--hidden-import=conan.tools.qbs --hidden-import=conan.tools.scm "
-              "--hidden-import=conan.tools.system")
+              "--hidden-import=conan.tools.system --hidden-import=conan.tools.system.package_manager")
     if platform.system() != "Windows":
         hidden += " --hidden-import=setuptools.msvc"
         win_ver = ""


Test patch:
diff --git a/conans/test/integration/command/create_test.py b/conans/test/integration/command/create_test.py
index 048a366e7..8fcb36c7c 100644
--- a/conans/test/integration/command/create_test.py
+++ b/conans/test/integration/command/create_test.py
@@ -583,8 +583,6 @@ def test_default_framework_dirs_with_layout():
     assert "FRAMEWORKS: []" in client.out
 
 
-import pytest
-import re
 @pytest.mark.parametrize("with_layout", [True, False])
 def test_defaults_in_components_without_layout(with_layout):
     lib_conan_file = textwrap.dedent("""
@@ -659,3 +657,30 @@ def test_defaults_in_components_without_layout(with_layout):
         assert "FOO LIBDIRS: ['lib']" in client.out
         assert "FOO INCLUDEDIRS: ['include']" in client.out
         assert "FOO RESDIRS: ['res']" in client.out
+
+
+def test_create_keep_build_sets_generators_folder_even_with_cmake_layout_defined():
+    """
+    Issue related: https://github.com/conan-io/conan/issues/11785
+    """
+    conanfile = textwrap.dedent("""
+    from conan import ConanFile
+    from conan.tools.cmake import cmake_layout
+
+    class LibConan(ConanFile):
+        settings = 'os', 'arch', 'compiler', 'build_type'
+        name = "lib"
+        version = "1.0"
+
+        def layout(self):
+            cmake_layout(self)
+
+        def package(self):
+            self.output.info("GENERATORS FOLDER: {}".format(self.generators_folder))
+    """)
+    client = TestClient()
+    client.save({"conanfile.py": conanfile})
+    client.run("create . -s build_type=Release")
+    client.run("create -kb . -s build_type=Release")
+    # self.generators_folder should be not None when -kb is used
+    assert re.search(r"GENERATORS FOLDER: .*generators", str(client.out))
diff --git a/conans/test/integration/package_id/test_validate.py b/conans/test/integration/package_id/test_validate.py
index d273fe335..60b644872 100644
--- a/conans/test/integration/package_id/test_validate.py
+++ b/conans/test/integration/package_id/test_validate.py
@@ -1,4 +1,5 @@
 import json
+import re
 import textwrap
 import unittest
 
@@ -39,6 +40,59 @@ class TestValidate(unittest.TestCase):
         self.assertEqual(myjson[0]["binary"], BINARY_INVALID)
         self.assertEqual(myjson[0]["id"], 'INVALID')
 
+    def test_validate_header_only(self):
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+            from conan import ConanFile
+            from conan.errors import ConanInvalidConfiguration
+            from conan.tools.build import check_min_cppstd
+            class Pkg(ConanFile):
+                settings = "os", "compiler"
+                options = {"shared": [True, False], "header_only": [True, False],}
+                default_options = {"shared": False, "header_only": True}
+
+                def package_id(self):
+                   if self.info.options.header_only:
+                       self.info.clear()
+
+                def validate(self):
+                    if self.info.options.get_safe("header_only") == "False":
+                        if self.info.settings.get_safe("compiler.version") == "12":
+                          raise ConanInvalidConfiguration("This package cannot exist in gcc 12")
+                        check_min_cppstd(self, 11)
+                        # These configurations are impossible
+                        if self.info.settings.os != "Windows" and self.info.options.shared:
+                            raise ConanInvalidConfiguration("shared is only supported under windows")
+
+                    # HOW CAN WE VALIDATE CPPSTD > 11 WHEN HEADER ONLY?
+
+            """)
+
+        client.save({"conanfile.py": conanfile})
+
+        client.run("create . pkg/0.1@ -s os=Linux -s compiler=gcc "
+                   "-s compiler.version=11 -s compiler.libcxx=libstdc++11")
+        assert re.search(r"Package '(.*)' created", str(client.out))
+
+        client.run("create . pkg/0.1@ -o header_only=False -s os=Linux -s compiler=gcc "
+                   "-s compiler.version=12 -s compiler.libcxx=libstdc++11", assert_error=True)
+
+        assert "Invalid ID: This package cannot exist in gcc 12" in client.out
+
+        client.run("create . pkg/0.1@ -o header_only=False -s os=Macos -s compiler=gcc "
+                   "-s compiler.version=11 -s compiler.libcxx=libstdc++11 -s compiler.cppstd=98",
+                   assert_error=True)
+
+        assert "Invalid ID: Current cppstd (98) is lower than the required C++ " \
+               "standard (11)" in client.out
+
+        client.run("create . pkg/0.1@ -o header_only=False -o shared=True "
+                   "-s os=Macos -s compiler=gcc "
+                   "-s compiler.version=11 -s compiler.libcxx=libstdc++11 -s compiler.cppstd=11",
+                   assert_error=True)
+
+        assert "Invalid ID: shared is only supported under windows" in client.out
+
     def test_validate_compatible(self):
         client = TestClient()
         conanfile = textwrap.dedent("""
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index 1bb64565e..08262873f 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -585,6 +585,29 @@ def test_presets_paths_correct():
                                                                                                "/")
 
 
+@pytest.mark.skipif(platform.system() != "Windows", reason="Only Windows")
+def test_presets_paths_normalization():
+    # https://github.com/conan-io/conan/issues/11795
+    client = TestClient()
+    conanfile = textwrap.dedent("""
+            from conan import ConanFile
+            from conan.tools.cmake import cmake_layout
+
+            class Conan(ConanFile):
+                settings = "os", "arch", "compiler", "build_type"
+                generators = "CMakeToolchain"
+
+                def layout(self):
+                    cmake_layout(self)
+            """)
+    client.save({"conanfile.py": conanfile, "CMakeLists.txt": "foo"})
+    client.run("install .")
+
+    presets = json.loads(client.load("CMakeUserPresets.json"))
+
+    assert "/" not in presets["include"]
+
+
 def test_presets_updated():
     """If a preset file is generated more than once, the values are always added and, in case the
     configurePreset or buildPreset already exist, the new preset is updated """
diff --git a/conans/test/unittests/model/options_test.py b/conans/test/unittests/model/options_test.py
index 4d5a1db40..5fa45bf2a 100644
--- a/conans/test/unittests/model/options_test.py
+++ b/conans/test/unittests/model/options_test.py
@@ -1,6 +1,7 @@
 import sys
 import unittest
 
+import pytest
 import six
 
 from conans.errors import ConanException
@@ -280,6 +281,14 @@ class OptionsValuesTest(unittest.TestCase):
         Boost:thread.multi=off
         """)
 
+    def test_get_safe(self):
+        option_values = OptionsValues(self.sut.as_list())
+        assert option_values.get_safe("missing") is None
+        assert option_values.get_safe("optimized") == 3
+        with pytest.raises(ConanException):
+            # This is not supported at the moment
+            option_values["Boost"].get_safe("thread")
+
     def test_from_list(self):
         option_values = OptionsValues(self.sut.as_list())
         self.assertEqual(option_values.dumps(), self.sut.dumps())