Repository: conan-io/conan
Pull request #16214 (2024-05-07 12:04:47)

allow gnuinstall dirs local flow
Changelog: Feature: Allow GNUInstallDirs definition in ``CMakeToolchain`` for the local ``conan install/build`` flow too.
Docs: Omit

Close https://github.com/conan-io/conan/issues/14733


Patch:
diff --git a/conan/cli/commands/download.py b/conan/cli/commands/download.py
index f8087103b..dbaa4f73e 100644
--- a/conan/cli/commands/download.py
+++ b/conan/cli/commands/download.py
@@ -21,7 +21,7 @@ def download(conan_api: ConanAPI, parser, *args):
 
     parser.add_argument('pattern', nargs="?",
                         help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                             "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                             "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                              "If revision is not specified, it is assumed latest one.")
     parser.add_argument("--only-recipe", action='store_true', default=False,
                         help='Download only the recipe/s, not the binary packages.')
diff --git a/conan/cli/commands/graph.py b/conan/cli/commands/graph.py
index bf591c461..c5fb5f168 100644
--- a/conan/cli/commands/graph.py
+++ b/conan/cli/commands/graph.py
@@ -248,7 +248,7 @@ def graph_explain(conan_api, parser,  subparser, *args):
                            help='Whether the provided reference is a build-require')
     subparser.add_argument('--missing', nargs="?",
                            help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                                "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                                "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                                 "If revision is not specified, it is assumed latest one.")
 
     args = parser.parse_args(*args)
diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py
index 1fd4a8811..a905c2acd 100644
--- a/conan/cli/commands/list.py
+++ b/conan/cli/commands/list.py
@@ -207,7 +207,7 @@ def list(conan_api: ConanAPI, parser, *args):
     """
     parser.add_argument('pattern', nargs="?",
                         help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                             "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                             "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                              "If revision is not specified, it is assumed latest one.")
     parser.add_argument('-p', '--package-query', default=None, action=OnceArgument,
                         help="List only the packages matching a specific query, e.g, os=Windows AND "
diff --git a/conan/cli/commands/remove.py b/conan/cli/commands/remove.py
index 984463b4f..74ac93d99 100644
--- a/conan/cli/commands/remove.py
+++ b/conan/cli/commands/remove.py
@@ -41,7 +41,7 @@ def remove(conan_api: ConanAPI, parser, *args):
     """
     parser.add_argument('pattern', nargs="?",
                         help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                             "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                             "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                              "If revision is not specified, it is assumed latest one.")
     parser.add_argument('-c', '--confirm', default=False, action='store_true',
                         help='Remove without requesting a confirmation')
diff --git a/conan/cli/commands/upload.py b/conan/cli/commands/upload.py
index 3d1171e04..282a70df9 100644
--- a/conan/cli/commands/upload.py
+++ b/conan/cli/commands/upload.py
@@ -50,7 +50,7 @@ def upload(conan_api: ConanAPI, parser, *args):
     """
     parser.add_argument('pattern', nargs="?",
                         help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                             "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                             "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                              "If revision is not specified, it is assumed latest one.")
     parser.add_argument('-p', '--package-query', default=None, action=OnceArgument,
                         help="Only upload packages matching a specific query. e.g: os=Windows AND "
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index adb338e5b..1855f8f60 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -969,11 +969,10 @@ class OutputDirsBlock(Block):
 
     @property
     def template(self):
-        if not self._conanfile.package_folder:
-            return ""
-
         return textwrap.dedent("""
+           {% if package_folder %}
            set(CMAKE_INSTALL_PREFIX "{{package_folder}}")
+           {% endif %}
            {% if default_bin %}
            set(CMAKE_INSTALL_BINDIR "{{default_bin}}")
            set(CMAKE_INSTALL_SBINDIR "{{default_bin}}")
@@ -998,9 +997,8 @@ class OutputDirsBlock(Block):
         return elements[0] if elements else None
 
     def context(self):
-        if not self._conanfile.package_folder:
-            return {}
-        return {"package_folder": self._conanfile.package_folder.replace("\\", "/"),
+        pf = self._conanfile.package_folder
+        return {"package_folder": pf.replace("\\", "/") if pf else None,
                 "default_bin": self._get_cpp_info_value("bindirs"),
                 "default_lib": self._get_cpp_info_value("libdirs"),
                 "default_include": self._get_cpp_info_value("includedirs"),
diff --git a/conans/client/graph/install_graph.py b/conans/client/graph/install_graph.py
index 51cec4fcb..14775ab01 100644
--- a/conans/client/graph/install_graph.py
+++ b/conans/client/graph/install_graph.py
@@ -510,7 +510,7 @@ class InstallGraph:
 
         raise ConanException(textwrap.dedent(f'''\
            Missing prebuilt package for '{missing_pkgs}'. You can try:
-               - List all available packages using 'conan list {ref}:* -r=remote'
+               - List all available packages using 'conan list "{ref}:*" -r=remote'
                - Explain missing binaries: replace 'conan install ...' with 'conan graph explain ...'
                - {build_msg}
 


Test patch:
diff --git a/conans/test/functional/only_source_test.py b/conans/test/functional/only_source_test.py
index c51b146c3..2a1a7cc04 100644
--- a/conans/test/functional/only_source_test.py
+++ b/conans/test/functional/only_source_test.py
@@ -30,7 +30,7 @@ class OnlySourceTest(unittest.TestCase):
                       "--build=hello1/1.1@lasote/stable'",
                       client.out)
         # Only 1 reference!
-        assert "List all available packages using 'conan list hello0/0.0@lasote/stable:* -r=remote'" in client.out
+        assert "List all available packages using 'conan list \"hello0/0.0@lasote/stable:*\" -r=remote'" in client.out
 
         # We generate the package for hello0/0.0
         client.run("install --requires=hello0/0.0@lasote/stable --build hello0*")
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index 032081f95..a9b108d83 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -1438,3 +1438,78 @@ def test_toolchain_keep_absolute_paths():
     assert os.path.isabs(user_presets["include"][0])
     presets = json.loads(c.load(user_presets["include"][0]))
     assert os.path.isabs(presets["configurePresets"][0]["toolchainFile"])
+
+
+def test_output_dirs_gnudirs_local_default():
+    # https://github.com/conan-io/conan/issues/14733
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.cmake import cmake_layout
+        from conan.tools.files import load
+
+        class Conan(ConanFile):
+            name = "pkg"
+            version = "0.1"
+            settings = "os", "arch", "compiler", "build_type"
+            generators = "CMakeToolchain"
+            def build(self):
+                tc = load(self, "conan_toolchain.cmake")
+                self.output.info(tc)
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("create .")
+
+    def _assert_install(out):
+        assert 'set(CMAKE_INSTALL_BINDIR "bin")' in out
+        assert 'set(CMAKE_INSTALL_SBINDIR "bin")' in out
+        assert 'set(CMAKE_INSTALL_LIBEXECDIR "bin")' in out
+        assert 'set(CMAKE_INSTALL_LIBDIR "lib")' in out
+        assert 'set(CMAKE_INSTALL_INCLUDEDIR "include")' in out
+
+    _assert_install(c.out)
+    assert "CMAKE_INSTALL_PREFIX" in c.out
+
+    c.run("build .")
+    _assert_install(c.out)
+    assert "CMAKE_INSTALL_PREFIX" not in c.out
+
+
+def test_output_dirs_gnudirs_local_custom():
+    # https://github.com/conan-io/conan/issues/14733
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.cmake import cmake_layout
+        from conan.tools.files import load
+
+        class Conan(ConanFile):
+            name = "pkg"
+            version = "0.1"
+            settings = "os", "arch", "compiler", "build_type"
+            generators = "CMakeToolchain"
+            def layout(self):
+                self.cpp.package.bindirs = ["mybindir"]
+                self.cpp.package.includedirs = ["myincludedir"]
+                self.cpp.package.libdirs = ["mylibdir"]
+
+            def build(self):
+                tc = load(self, "conan_toolchain.cmake")
+                self.output.info(tc)
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("create .")
+
+    def _assert_install(out):
+        assert 'set(CMAKE_INSTALL_BINDIR "mybindir")' in out
+        assert 'set(CMAKE_INSTALL_SBINDIR "mybindir")' in out
+        assert 'set(CMAKE_INSTALL_LIBEXECDIR "mybindir")' in out
+        assert 'set(CMAKE_INSTALL_LIBDIR "mylibdir")' in out
+        assert 'set(CMAKE_INSTALL_INCLUDEDIR "myincludedir")' in out
+
+    _assert_install(c.out)
+    assert "CMAKE_INSTALL_PREFIX" in c.out
+
+    c.run("build .")
+    _assert_install(c.out)
+    assert "CMAKE_INSTALL_PREFIX" not in c.out