Repository: conan-io/conan
Pull request #17084 (2024-09-30 09:59:47)

Fix example usage for scoped options help string
Changelog: Omit
Docs: Omit

Minor change to reflect that the `pkg:option=Value` syntax has been deprecated for a bit now

This would error out with 
> The usage of package names `pkg:with_qt` in options is deprecated, use a pattern like `pkg/*:with_qt` instead


Patch:
diff --git a/conan/cli/args.py b/conan/cli/args.py
index dfa4b9665..d25b2c292 100644
--- a/conan/cli/args.py
+++ b/conan/cli/args.py
@@ -48,9 +48,9 @@ def add_common_install_arguments(parser):
                        help='Do not use remote, resolve exclusively in the cache')
 
     update_help = ("Will install newer versions and/or revisions in the local cache "
-                   "for the given reference, or all in case no argument is supplied. "
+                   "for the given reference name, or all references in the graph if no argument is supplied. "
                    "When using version ranges, it will install the latest version that "
-                   "satisfies the range. Also, if using revisions, it will update to the "
+                   "satisfies the range. It will update to the "
                    "latest revision for the resolved version range.")
 
     parser.add_argument("-u", "--update", action="append", nargs="?", help=update_help, const="*")
@@ -96,9 +96,9 @@ def add_profiles_args(parser):
                             help="")
 
     create_config("pr", "profile")
-    create_config("o", "options", "-o pkg:with_qt=true")
-    create_config("s", "settings", "-s compiler=gcc")
-    create_config("c", "conf", "-c tools.cmake.cmaketoolchain:generator=Xcode")
+    create_config("o", "options", '-o="pkg/*:with_qt=True"')
+    create_config("s", "settings", '-s="compiler=gcc"')
+    create_config("c", "conf", '-c="tools.cmake.cmaketoolchain:generator=Xcode"')
 
 
 def add_reference_args(parser):
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
index a2834789a..d79cf16ba 100644
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -67,6 +67,7 @@ class Node(object):
         self.should_build = False  # If the --build or policy wants to build this binary
         self.build_allowed = False
         self.is_conf = False
+        self.replaced_requires = {}  # To track the replaced requires for self.dependencies[old-ref]
 
     def __lt__(self, other):
         """
diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py
index 41793dee9..a442fa4a1 100644
--- a/conans/client/graph/graph_builder.py
+++ b/conans/client/graph/graph_builder.py
@@ -312,6 +312,7 @@ class DepsGraphBuilder(object):
                 node.conanfile.requires.reindex(require, alternative_ref.name)
             require.ref.name = alternative_ref.name
             graph.replaced_requires[original_require] = repr(require.ref)
+            node.replaced_requires[original_require] = require
             break  # First match executes the alternative and finishes checking others
 
     def _create_new_node(self, node, require, graph, profile_host, profile_build, graph_lock):
diff --git a/conans/model/build_info.py b/conans/model/build_info.py
index 4aa0aee2d..b392c7f55 100644
--- a/conans/model/build_info.py
+++ b/conans/model/build_info.py
@@ -564,8 +564,10 @@ class CppInfo:
         if not external:
             return
         # Only direct host (not test) dependencies can define required components
-        direct_dependencies = [d.ref.name for d in conanfile.requires.values()
-                               if not d.build and not d.is_test and d.visible and not d.override]
+        # We use conanfile.dependencies to use the already replaced ones by "replace_requires"
+        # So consumers can keep their ``self.cpp_info.requires = ["pkg_name::comp"]``
+        direct_dependencies = [r.ref.name for r, d in conanfile.dependencies.items() if r.direct
+                               and not r.build and not r.is_test and r.visible and not r.override]
 
         for e in external:
             if e not in direct_dependencies:
diff --git a/conans/model/dependencies.py b/conans/model/dependencies.py
index 0f8640fc8..a2734b5e8 100644
--- a/conans/model/dependencies.py
+++ b/conans/model/dependencies.py
@@ -91,6 +91,12 @@ class ConanFileDependencies(UserRequirementsDict):
     def from_node(node):
         d = OrderedDict((require, ConanFileInterface(transitive.node.conanfile))
                         for require, transitive in node.transitive_deps.items())
+        for old_req, new_req in node.replaced_requires.items():
+            existing = d.pop(new_req, None)
+            if existing is not None:
+                added_req = new_req.copy_requirement()
+                added_req.ref = RecipeReference.loads(old_req)
+                d[added_req] = existing
         return ConanFileDependencies(d)
 
     def filter(self, require_filter, remove_system=True):


Test patch:
diff --git a/test/integration/graph/test_replace_requires.py b/test/integration/graph/test_replace_requires.py
index 31edd82f2..909854d0b 100644
--- a/test/integration/graph/test_replace_requires.py
+++ b/test/integration/graph/test_replace_requires.py
@@ -1,4 +1,5 @@
 import json
+import textwrap
 
 import pytest
 
@@ -157,3 +158,120 @@ def test_replace_requires_test_requires():
     c.run("create gtest")
     c.run("install app -pr=profile")
     assert "gtest/0.1: gtest/0.2" in c.out  # The replacement happens
+
+
+def test_replace_requires_consumer_references():
+    c = TestClient()
+    # IMPORTANT: The replacement package must be target-compatible
+    zlib_ng = textwrap.dedent("""
+        from conan import ConanFile
+        class ZlibNG(ConanFile):
+            name = "zlib-ng"
+            version = "0.1"
+            def package_info(self):
+                self.cpp_info.set_property("cmake_file_name", "ZLIB")
+                self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB")
+        """)
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class App(ConanFile):
+            name = "app"
+            version = "0.1"
+            settings = "build_type"
+            requires = "zlib/0.1"
+            generators = "CMakeDeps"
+
+            def generate(self):
+                self.output.info(f"DEP ZLIB generate: {self.dependencies['zlib'].ref.name}!")
+            def build(self):
+                self.output.info(f"DEP ZLIB build: {self.dependencies['zlib'].ref.name}!")
+            def package_info(self):
+                self.output.info(f"DEP ZLIB package_info: {self.dependencies['zlib'].ref.name}!")
+                self.cpp_info.requires = ["zlib::zlib"]
+        """)
+    c.save({"zlibng/conanfile.py": zlib_ng,
+            "app/conanfile.py": conanfile,
+            "profile": "[replace_requires]\nzlib/0.1: zlib-ng/0.1"})
+    c.run("create zlibng")
+    c.run("build app -pr=profile")
+    assert "zlib/0.1: zlib-ng/0.1" in c.out
+    assert "conanfile.py (app/0.1): DEP ZLIB generate: zlib-ng!" in c.out
+    assert "conanfile.py (app/0.1): DEP ZLIB build: zlib-ng!" in c.out
+    # Check generated CMake code. If the targets are NOT compatible, then the replacement
+    # Cannot happen
+    assert "find_package(ZLIB)" in c.out
+    assert "target_link_libraries(... ZLIB::ZLIB)" in c.out
+    cmake = c.load("app/ZLIBTargets.cmake")
+    assert "add_library(ZLIB::ZLIB INTERFACE IMPORTED)" in cmake
+    c.run("create app -pr=profile")
+    assert "zlib/0.1: zlib-ng/0.1" in c.out
+    assert "app/0.1: DEP ZLIB generate: zlib-ng!" in c.out
+    assert "app/0.1: DEP ZLIB build: zlib-ng!" in c.out
+
+
+def test_replace_requires_consumer_components_options():
+    c = TestClient()
+    # IMPORTANT: The replacement package must be target-compatible
+    zlib_ng = textwrap.dedent("""
+        from conan import ConanFile
+        class ZlibNG(ConanFile):
+            name = "zlib-ng"
+            version = "0.1"
+            options = {"compat": [False, True]}
+            default_options = {"compat": False}
+            def package_info(self):
+                self.cpp_info.set_property("cmake_file_name", "ZLIB")
+                self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB")
+                if self.options.compat:
+                    self.cpp_info.components["myzlib"].set_property("cmake_target_name",
+                                                                    "ZLIB::zmylib")
+        """)
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class App(ConanFile):
+            name = "app"
+            version = "0.1"
+            settings = "build_type"
+            requires = "zlib/0.1"
+            generators = "CMakeDeps"
+
+            def generate(self):
+                self.output.info(f"DEP ZLIB generate: {self.dependencies['zlib'].ref.name}!")
+            def build(self):
+                self.output.info(f"DEP ZLIB build: {self.dependencies['zlib'].ref.name}!")
+            def package_info(self):
+                self.output.info(f"DEP ZLIB package_info: {self.dependencies['zlib'].ref.name}!")
+                self.cpp_info.requires = ["zlib::myzlib"]
+        """)
+    profile = textwrap.dedent("""
+        [options]
+        zlib-ng/*:compat=True
+
+        [replace_requires]
+        zlib/0.1: zlib-ng/0.1
+        """)
+    c.save({"zlibng/conanfile.py": zlib_ng,
+            "app/conanfile.py": conanfile,
+            "profile": profile})
+
+    c.run("create zlibng -o *:compat=True")
+    c.run("build app -pr=profile")
+    assert "zlib/0.1: zlib-ng/0.1" in c.out
+    assert "conanfile.py (app/0.1): DEP ZLIB generate: zlib-ng!" in c.out
+    assert "conanfile.py (app/0.1): DEP ZLIB build: zlib-ng!" in c.out
+    # Check generated CMake code. If the targets are NOT compatible, then the replacement
+    # Cannot happen
+    assert "find_package(ZLIB)" in c.out
+    assert "target_link_libraries(... ZLIB::ZLIB)" in c.out
+    cmake = c.load("app/ZLIBTargets.cmake")
+    assert "add_library(ZLIB::ZLIB INTERFACE IMPORTED)" in cmake
+    cmake = c.load("app/ZLIB-Target-none.cmake")
+    assert "set_property(TARGET ZLIB::ZLIB APPEND PROPERTY INTERFACE_LINK_LIBRARIES ZLIB::zmylib)" \
+           in cmake
+
+    c.run("create app -pr=profile")
+    assert "zlib/0.1: zlib-ng/0.1" in c.out
+    assert "app/0.1: DEP ZLIB generate: zlib-ng!" in c.out
+    assert "app/0.1: DEP ZLIB build: zlib-ng!" in c.out
+    assert "find_package(ZLIB)" in c.out
+    assert "target_link_libraries(... ZLIB::ZLIB)" in c.out
diff --git a/test/unittests/tools/cmake/test_cmaketoolchain.py b/test/unittests/tools/cmake/test_cmaketoolchain.py
index 25ffb1104..09ba6d0fd 100644
--- a/test/unittests/tools/cmake/test_cmaketoolchain.py
+++ b/test/unittests/tools/cmake/test_cmaketoolchain.py
@@ -32,6 +32,7 @@ def conanfile():
     c.folders.set_base_generators("/some/abs/path")  # non-existing to not relativize
     c._conan_node = Mock()
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -229,6 +230,8 @@ def conanfile_apple():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    # FIXME: Repeated fix pattern
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -258,6 +261,7 @@ def conanfile_msvc():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -332,6 +336,7 @@ def test_older_msvc_toolset():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     toolchain = CMakeToolchain(c)
     content = toolchain.content
     assert 'CMAKE_GENERATOR_TOOLSET "v110"' in content
@@ -360,6 +365,7 @@ def test_older_msvc_toolset_update():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     toolchain = CMakeToolchain(c)
     content = toolchain.content
     assert 'CMAKE_GENERATOR_TOOLSET "v142,version=14.29"' in content
@@ -385,6 +391,7 @@ def test_msvc_xp_toolsets():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     toolchain = CMakeToolchain(c)
     content = toolchain.content
     assert 'CMAKE_GENERATOR_TOOLSET "v110_xp"' in content
@@ -411,6 +418,7 @@ def conanfile_linux():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -442,6 +450,7 @@ def conanfile_linux_shared():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -482,6 +491,7 @@ def conanfile_windows_fpic():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -513,6 +523,7 @@ def conanfile_linux_fpic():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -547,6 +558,7 @@ def test_libcxx_abi_flag():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
 
     toolchain = CMakeToolchain(c)
     content = toolchain.content
@@ -597,6 +609,7 @@ def test_apple_cmake_osx_sysroot(os, os_sdk, arch, expected_sdk):
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
 
     toolchain = CMakeToolchain(c)
     content = toolchain.content
@@ -680,6 +693,7 @@ class TestCrossBuild:
         c._conan_node = Mock()
         c._conan_node.dependencies = []
         c._conan_node.transitive_deps = {}
+        c._conan_node.replaced_requires = {}
         return c
 
     def test_cmake_system_name(self, conanfile_cross):