Repository: conan-io/conan
Pull request #18101 (2025-04-08 20:28:42)

New "no_skip=True" trait for tool-require wrappers for transitive deps with different versions
Changelog: Feature: Introduce a new ``no_skip=True`` requirement trait for exceptional cases like one application depending on another application privately with ``requires`` to avoid it being skipped.
Docs: TODO

Close https://github.com/conan-io/conan/issues/18086



Patch:
diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py
index b5fc44c63..99773f89a 100644
--- a/conan/internal/model/requires.py
+++ b/conan/internal/model/requires.py
@@ -11,7 +11,7 @@ class Requirement:
     """
     def __init__(self, ref, *, headers=None, libs=None, build=False, run=None, visible=None,
                  transitive_headers=None, transitive_libs=None, test=None, package_id_mode=None,
-                 force=None, override=None, direct=None, options=None):
+                 force=None, override=None, direct=None, options=None, no_skip=False):
         # * prevents the usage of more positional parameters, always ref + **kwargs
         # By default this is a generic library requirement
         self.ref = ref
@@ -38,6 +38,7 @@ class Requirement:
         self.is_test = test  # to store that it was a test, even if used as regular requires too
         self.skip = False
         self.required_nodes = set()  # store which intermediate nodes are required, to compute "Skip"
+        self.no_skip = no_skip
 
     @property
     def files(self):  # require needs some files in dependency package
diff --git a/conan/tools/scm/git.py b/conan/tools/scm/git.py
index c7bcb6ffe..a8a7dd0d3 100644
--- a/conan/tools/scm/git.py
+++ b/conan/tools/scm/git.py
@@ -277,8 +277,9 @@ class Git:
     def checkout_from_conandata_coordinates(self):
         """
         Reads the "scm" field from the ``conandata.yml``, that must contain at least "url" and
-        "commit" and then do a ``clone(url, target=".")`` followed by a ``checkout(commit)``.
+        "commit" and then do a ``clone(url, target=".")``, ``fetch <commit>``, followed by a ``checkout(commit)``.
         """
         sources = self._conanfile.conan_data["scm"]
-        self.clone(url=sources["url"], target=".")
+        self.clone(url=sources["url"], target=".", args=["--origin=origin"])
+        self.run(f"fetch origin {sources['commit']}")
         self.checkout(commit=sources["commit"])
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
index cc4334bda..1d05c96df 100644
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -503,7 +503,8 @@ class GraphBinariesAnalyzer:
 
                 # Finally accumulate all needed nodes for marking binaries as SKIP download
                 news_req = [r for r in deps_required
-                            if r.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD, BINARY_EDITABLE)
+                            if (r.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD, BINARY_EDITABLE)
+                                or any(req.no_skip for req in r.transitive_deps))
                             if r not in required_nodes]  # Avoid already expanded before
                 new_root_nodes.update(news_req)  # For expanding the next iteration
                 required_nodes.update(deps_required)


Test patch:
diff --git a/test/functional/tools/scm/test_git.py b/test/functional/tools/scm/test_git.py
index e33194333..d3ebd1404 100644
--- a/test/functional/tools/scm/test_git.py
+++ b/test/functional/tools/scm/test_git.py
@@ -727,6 +727,32 @@ class TestGitBasicSCMFlow:
         assert "pkg/0.1: MYCMAKE: mycmake" in c2.out
         assert "pkg/0.1: MYFILE: myheader!" in c2.out
 
+    def test_fetch_commit(self):
+        """ Testing fetch commit
+        """
+        url = git_create_bare_repo()
+        c = TestClient(default_server_user=True)
+        c.run_command('git clone "file://{}" .'.format(url))
+        c.save({"conanfile.py": self.conanfile_scm,
+                "src/myfile.h": "myheader!",
+                "CMakeLists.txt": "mycmake"})
+        c.run_command("git checkout -b mybranch")
+        git_add_changes_commit(folder=c.current_folder)
+        c.run_command("git push --set-upstream origin mybranch")
+        c.run("create .")
+        assert "pkg/0.1: MYCMAKE: mycmake" in c.out
+        assert "pkg/0.1: MYFILE: myheader!" in c.out
+        c.run("upload * -c -r=default")
+        # Create an orphan commit by removing the branch
+        c.run_command("git push origin --delete mybranch")
+        rmdir(c.current_folder)  # Remove current folder to make sure things are not used from here
+
+        # use another fresh client
+        c2 = TestClient(servers=c.servers)
+        c2.run("install --requires=pkg/0.1@ --build=pkg*")
+        assert "pkg/0.1: MYCMAKE: mycmake" in c2.out
+        assert "pkg/0.1: MYFILE: myheader!" in c2.out
+
 
 @pytest.mark.tool("git")
 class TestGitBasicSCMFlowSubfolder:
diff --git a/test/integration/graph/test_require_same_pkg_versions.py b/test/integration/graph/test_require_same_pkg_versions.py
index 6203f55b7..68d52ab20 100644
--- a/test/integration/graph/test_require_same_pkg_versions.py
+++ b/test/integration/graph/test_require_same_pkg_versions.py
@@ -1,3 +1,4 @@
+import re
 import textwrap
 
 from conan.test.assets.genconanfile import GenConanfile
@@ -186,3 +187,93 @@ def test_require_different_options():
     lock = c.load("wine/conan.lock")
     # Testing it doesn't crash or anything like that
     assert "gcc/1.0#616ce3babcecef39a27806c1a5f4b4ff" in lock
+
+
+def test_require_different_versions_transitive():
+    """
+    https://github.com/conan-io/conan/issues/18086
+    """
+    c = TestClient(default_server_user=True, path_with_spaces=False)
+    qemu = textwrap.dedent(r"""
+        import os
+        from conan import ConanFile
+        from conan.tools.files import save
+        class Pkg(ConanFile):
+            name = "myqemu"
+            package_type = "application"
+
+            def package(self):
+                echo = f"@echo off\necho RUNNING {self.name}/{self.version}!!"
+                save(self, os.path.join(self.package_folder, "bin", f"{self.name}.bat"), echo)
+                save(self, os.path.join(self.package_folder, "bin", f"{self.name}.sh"), echo)
+                os.chmod(os.path.join(self.package_folder, "bin", f"{self.name}.sh"), 0o777)
+            """)
+    mytool = textwrap.dedent(r"""
+        import os, platform
+        from conan import ConanFile
+        from conan.tools.files import save, chdir
+        class Pkg(ConanFile):
+            version = "1.0"
+            package_type = "application"
+
+            def requirements(self):
+                version = "1.0" if self.name == "snippy" else "2.0"
+                self.requires(f"myqemu/{version}", visible=False, no_skip=True)
+
+            def package(self):
+                c = f'call "%1/myqemu.bat"' if platform.system() == "Windows" else f'"$1/myqemu.sh"'
+                echo = f"@echo off\necho RUNNING {self.name}/{self.version}!!\n{c}"
+                save(self, os.path.join(self.package_folder, "bin", f"{self.name}.bat"), echo)
+                save(self, os.path.join(self.package_folder, "bin", f"{self.name}"), echo)
+                os.chmod(os.path.join(self.package_folder, "bin", f"{self.name}"), 0o777)
+
+            def package_info(self):
+                pf = self.dependencies["myqemu"].cpp_info.bindir.replace("\\", "/")
+                self.conf_info.define_path(f"user.myorg:{self.name}_qemu", pf)
+            """)
+    consumer = textwrap.dedent("""
+        from conan import ConanFile
+
+        class Pkg(ConanFile):
+            name = "consumer"
+            version = "1.0"
+            def build_requirements(self):
+                self.tool_requires("snippy/1.0")
+                self.tool_requires("valgrind/1.0")
+            def build(self):
+                qemu_snippy = self.conf.get("user.myorg:snippy_qemu")
+                qemu_valgrind = self.conf.get("user.myorg:valgrind_qemu")
+                self.run(f"valgrind {qemu_valgrind}")
+                self.run(f'snippy {qemu_snippy}')
+        """)
+
+    c.save({"qemu/conanfile.py": qemu,
+            "tool/conanfile.py": mytool,
+            "consumer/conanfile.py": consumer})
+
+    c.run("create qemu --version=1.0")
+    c.run("create qemu --version=2.0")
+    c.run("create tool --name=snippy")
+    c.run("create tool --name=valgrind")
+    c.run("build consumer")
+    assert "RUNNING valgrind/1.0!!" in c.out
+    assert "RUNNING myqemu/2.0!!" in c.out
+    assert "RUNNING snippy/1.0!!" in c.out
+    assert "RUNNING myqemu/1.0!!" in c.out
+
+    c.run("upload * -r=default -c")
+    # The "tools.graph:skip_binaries" shouldn't affect the result, it is never skipped
+    for skip in ("-c tools.graph:skip_binaries=True", "-c tools.graph:skip_binaries=False", ""):
+        c.run("remove * -c")
+        # Re-downloads and it works
+        c.run(f"build consumer {skip}")
+        assert "RUNNING valgrind/1.0!!" in c.out
+        assert "RUNNING myqemu/2.0!!" in c.out
+        assert "RUNNING snippy/1.0!!" in c.out
+        assert "RUNNING myqemu/1.0!!" in c.out
+
+    c.run("create consumer")
+    c.run("upload consumer/1.0 -r=default -c")
+    c.run("remove * -c")
+    c.run("install --requires=consumer/1.0")
+    assert re.search(r"Skipped binaries(\s*)myqemu/1.0, myqemu/2.0, snippy/1.0, valgrind/1.0", c.out)