Repository: conan-io/conan
Pull request: explore if this fix header-only package-id
Changelog: Feature: Implement a new package-ID computation that includes transitive dependencies even when the direct dependencies have remove them, for example when depending on a header-only library that depends on a static library.

Docs: https://github.com/conan-io/docs/pull/1575

Close https://github.com/conan-io/conan/issues/6450

#tags: slow
#revisions: 1


Problem statement:
[bug] conan is too lenient in its specification of header_only libraries.
~~Note: title seems to indicate that this affects only header_only but thats not the case.~~

To make matters clear I am using:
`default_package_id_mode = full_version_mode` for this example.

Consider four projects:
```
#liba:
class Conan(ConanFile):
    name = "liba"
    pass
#libb:
class Conan(ConanFile):
    name = "libb"
    requires="liba/1.0.0"

    def package_id(self):
        self.info.header_only()

#libc:
class Conan(ConanFile):
    name="libc"
    requires="libb/1.0.0"
#libd:
class Conan(ConanFile):
    name="libd"
    requires="libc/1.0.0", "liba/1.0.0"

# build initial versions of libs:
cd liba
conan create . 1.0.0@_/_
conan create . 2.0.0@_/_
cd ../libb
conan create . 1.0.0@_/_
conan create . 2.0.0@_/_
cd ../libc
conan create . 1.0.0@_/_
conan create . 2.0.0@_/_
cd ../libd
conan create . 1.0.0@_/_
```
Okay Now i have following tree built:  
`libd` build doesnt matter here because it will be our testcase.  
`libc` built with version 1.0.0 and 2.0.0 and in both depending on `libb` 1.0.0  
`libb` built with version 1.0.0 and 2.0.0 and in both depending on `liba` 1.0.0 - `libb` is header_only!  
`liba` built with version 1.0.0 and 2.0.0  

Alls good so far, now what would you expect if we overrode `liba` in `libd`?  
Well, the only things that have listed dependency on `liba` in conanfiles are `libb` and `libd`.  
Okay - but `libb` is header only - by extension - if `libb` uses any library - then that usage is baked into its `libc`.

Lets now tell `libd` to override the dependency and use `liba 2.0.0` 
```
# edit  libd' conanfile to point to liba 2.0.0
conan create . 2.0.0@_/_
```

`libd` builds without any issues. 

Conan does not require rebuild of the libc - even though it is definite that it depends on liba by proxy - author of `libc` may not even be aware that `liba` exists - but calls will be baked into its code.

What are the users expected to do in this case?
Are they to investigate all dependencies of header only libraries and copy them as is?

This is actually an extreme example of a choice I see as a problem that conan made - that dependencies are transivitely inherited, but their versioning_schema is not.  
This may make sense in some cases - for example when you dont expose in header files any types from your dependency, but its not something thats achievable and done in most of the libraries.
Conan relies on users ability to detect these, rather than providing solution. 

It is an extreme example because I am using 'header_only' here - but because of the way C++ works ( include files (... )) things that you depend on, usually automatically pollute things that depend on you.

Hints from issue discussion:

Commit message: explore if this fix header-only package-id

Patch to review:
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
index f87d9115a17..06a1dbea58c 100644
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -274,15 +274,15 @@ def _compute_package_id(node, default_package_id_mode, default_python_requires_i
         # A bit risky to be done now
         conanfile = node.conanfile
         neighbors = node.neighbors()
-        direct_reqs = []  # of PackageReference
-        indirect_reqs = set()  # of PackageReference, avoid duplicates
+        node.id_direct_prefs = set()  # of PackageReference
+        node.id_indirect_prefs = set()  # of PackageReference, avoid duplicates
         for neighbor in neighbors:
-            ref, nconan = neighbor.ref, neighbor.conanfile
-            direct_reqs.append(neighbor.pref)
-            indirect_reqs.update(nconan.info.requires.refs())
+            node.id_direct_prefs.add(neighbor.pref)
+            node.id_indirect_prefs.update(neighbor.id_direct_prefs)
+            node.id_indirect_prefs.update(neighbor.id_indirect_prefs)
 
         # Make sure not duplicated
-        indirect_reqs.difference_update(direct_reqs)
+        node.id_indirect_prefs.difference_update(node.id_direct_prefs)
         python_requires = getattr(conanfile, "python_requires", None)
         if python_requires:
             if isinstance(python_requires, dict):
@@ -291,8 +291,8 @@ def _compute_package_id(node, default_package_id_mode, default_python_requires_i
                 python_requires = python_requires.all_refs()
         conanfile.info = ConanInfo.create(conanfile.settings.values,
                                           conanfile.options.values,
-                                          direct_reqs,
-                                          indirect_reqs,
+                                          node.id_direct_prefs,
+                                          node.id_indirect_prefs,
                                           default_package_id_mode=default_package_id_mode,
                                           python_requires=python_requires,
                                           default_python_requires_id_mode=
diff --git a/conans/test/functional/package_id/transitive_header_only_test.py b/conans/test/functional/package_id/transitive_header_only_test.py
new file mode 100644
index 00000000000..344234d056b
--- /dev/null
+++ b/conans/test/functional/package_id/transitive_header_only_test.py
@@ -0,0 +1,30 @@
+import textwrap
+import time
+import unittest
+
+from conans.model.ref import ConanFileReference
+from conans.test.utils.tools import TestClient, GenConanfile
+
+
+class TransitiveHeaderOnlyTest(unittest.TestCase):
+
+    def transitive_header_only_test(self):
+        # https://github.com/conan-io/conan/issues/6450
+        client = TestClient()
+        client.run("config set general.default_package_id_mode=full_version_mode")
+        client.save({"conanfile.py": GenConanfile()})
+        client.run("create . liba/1.0@")
+        client.run("create . liba/2.0@")
+        client.save({"conanfile.py": GenConanfile().with_require_plain("liba/1.0")
+                                                   .with_package_id("self.info.header_only()")})
+        client.run("create . libb/1.0@")
+        client.save({"conanfile.py": GenConanfile().with_require_plain("libb/1.0")})
+        client.run("create . libc/1.0@")
+        client.save({"conanfile.py": GenConanfile().with_require_plain("libc/1.0")
+                                                   .with_require_plain("liba/1.0")})
+        client.run("create . libd/1.0@")
+        client.save({"conanfile.py": GenConanfile().with_require_plain("libc/1.0")
+                                                   .with_require_plain("liba/2.0")})
+        client.run("create . libd/1.0@", assert_error=True)
+        self.assertIn("libc/1.0:bfa6c8f046896806f65c8fe554bd57f235b101e8 - Missing", client.out)
+


Code review comments:
diff_hunk: @@ -274,15 +274,15 @@ def _compute_package_id(node, default_package_id_mode, default_python_requires_i
         # A bit risky to be done now
         conanfile = node.conanfile
         neighbors = node.neighbors()
-        direct_reqs = []  # of PackageReference
-        indirect_reqs = set()  # of PackageReference, avoid duplicates
+        node.id_direct_prefs = set()  # of PackageReference
+        node.id_indirect_prefs = set()  # of PackageReference, avoid duplicates
         for neighbor in neighbors:
-            ref, nconan = neighbor.ref, neighbor.conanfile
-            direct_reqs.append(neighbor.pref)
-            indirect_reqs.update(nconan.info.requires.refs())
+            node.id_direct_prefs.add(neighbor.pref)
+            node.id_indirect_prefs.update(neighbor.id_direct_prefs)
+            node.id_indirect_prefs.update(neighbor.id_indirect_prefs)
 
         # Make sure not duplicated
-        indirect_reqs.difference_update(direct_reqs)
+        node.id_indirect_prefs.difference_update(node.id_direct_prefs)
path: conans/client/graph/graph_binaries.py
text: @user1:
I think this line is not needed. The previous loop adds to the `node.indirect` both, the _direct_ and _indirect_ from its neighbors, so no need to have them all together in `indirect` (and the variable will contain only the indirect ones honoring its name).

@author:
Totally necessary. Added a check, it is being used. Some direct requirements can slip into the indirect as well.