Repository: conan-io/conan

Issue:
Better information on mismatch in dependency tree
using conan v1.6.1
Consider following scenario:
Monday:
binary foo depends on potato and carrot  in both versions 1.0.0
carrot depends on potato in version 1.0.0
potato does not depends on anything.

All compiles fine.
Tuesday:
updating dependency in foo to potato 2.0.0

This leads to error like this:
```
Packages
    carrot/1.0.0@user/testing:1edc358bbd62390e309c027c1fcc23c7feea92d7 - Missing
    potato/2.0.0@user/testing:de9c231f84c85def9df09875e1785a1319fa8cb6 - Cache

potato/2.0.0@user/testing: Already installed!
carrot/1.0.0@user/testing: WARN: Can't find a 'carrot/1.0.0@user/testing' package for the specified options and settings:
- Settings: arch=x86_64, build_type=Release, compiler=gcc, compiler.libcxx=libstdc++, compiler.version=4.8, os=Linux
- Options: 
- Package ID: ...

ERROR: Missing prebuilt package for 'carrot/1.0.0@user/testing'
Try to build it from sources with "--build carrot"
Or read "http://docs.conan.io/en/latest/faq/troubleshooting.html#error-missing-prebuilt-package"
```
Nowhere it hints to user that it is simply the issue of the carrot depending on the potato 1.0.0. 
One has to dig deeper by for example conan info to compare all the flags and the dependencies to find the issue.

Maybe it would be possible for conan to compare the settings in the prebuilt ones, and get closest match, and inform why exactly that dependency cannot work?


Discussion:
Hi @fulara and thanks a lot for your report.

Basically what is happening here is that Conan overrides potato/1.0.0 dependency declared in carrot by potato/2.0.0 as indicated in the downstream foo package. That behavior is clearly indicated in the trace:

```
foo/1.0.0@dani/test: Exporting package recipe
foo/1.0.0@dani/test: A new conanfile.py version was exported
foo/1.0.0@dani/test: Folder: C:\Users\danimtb\AppData\Local\Temp\tmpnh9d7itmconans\path with spaces\.conan\data\foo\1.0.0\dani\test\export
carrot/1.0.0@dani/test requirement potato/1.0.0@dani/test overriden by foo/1.0.0@dani/test to potato/2.0.0@dani/test
```

My understanding is that, as there is no carrot package with the new potato/2.0.0 dependency, a new carrot package has to be built. I am not sure if that is completely the right behavior (@memsharded could you take a look?) but I think the message is clear and you can even see the status of carrot package marked as "missing".
My problem regarding the error message, is that nowhere it hints to the user that the mismatch against `carrot` is that the `carrot` depends on different `potato` than `foo` does. it prints Settings which are perfect match,  error message is:
```
WARN: Can't find a 'carrot/1.0.0@user/testing' package for the specified options and settings:
```
Whereas thats not true, options and settings match.
Yes, that's right...

As Conan uses [semver mode for compatibility](https://docs.conan.io/en/latest/creating_packages/define_abi_compatibility.html#versioning-schema), changes on the major version of the requirement (potato changes from **1**.0.0 to **2**.0.0) changes the carrot's package ID. As carrot does not exists with potato/2.0.0 doesn't exist this is the result.

This message could be improved for sure.
Yes, I agree with @fulara, showing the requirements (maybe evaluated to what ``package_id()`` says) in the error message won't do any harm, and might help identifying the origin of the missing binary. I think this can be added.

Resolving patch:
diff --git a/conans/client/installer.py b/conans/client/installer.py
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -220,15 +220,17 @@ def call_system_requirements(conanfile, output):
         raise ConanException("Error in system requirements")
 
 
-def raise_package_not_found_error(conan_file, conan_ref, package_id, out, recorder):
+def raise_package_not_found_error(conan_file, conan_ref, package_id, dependencies, out, recorder):
     settings_text = ", ".join(conan_file.info.full_settings.dumps().splitlines())
     options_text = ", ".join(conan_file.info.full_options.dumps().splitlines())
+    dependencies_text = ', '.join(dependencies)
 
-    msg = '''Can't find a '%s' package for the specified options and settings:
+    msg = '''Can't find a '%s' package for the specified settings, options and dependencies:
 - Settings: %s
 - Options: %s
+- Dependencies: %s
 - Package ID: %s
-''' % (conan_ref, settings_text, options_text, package_id)
+''' % (conan_ref, settings_text, options_text, dependencies_text, package_id)
     out.warn(msg)
     recorder.package_install_error(PackageReference(conan_ref, package_id),
                                    INSTALL_ERROR_MISSING, msg)
@@ -268,7 +270,9 @@ def _build(self, nodes_by_level, deps_graph, keep_build, root_node):
                 output = ScopedOutput(str(conan_ref), self._out)
                 package_id = conan_file.info.package_id()
                 if node.binary == BINARY_MISSING:
-                    raise_package_not_found_error(conan_file, conan_ref, package_id, output, self._recorder)
+                    dependencies = [str(dep.dst) for dep in node.dependencies]
+                    raise_package_not_found_error(conan_file, conan_ref, package_id, dependencies,
+                                                  out=output, recorder=self._recorder)
 
                 self._propagate_info(node, inverse_levels, deps_graph, output)
                 if node.binary == BINARY_SKIP:  # Privates not necessary


Test patch:
diff --git a/conans/test/integration/install_missing_dep_test.py b/conans/test/integration/install_missing_dep_test.py
new file mode 100644
--- /dev/null
+++ b/conans/test/integration/install_missing_dep_test.py
@@ -0,0 +1,46 @@
+import unittest
+from conans.test.utils.tools import TestClient
+
+
+class InstallMissingDependency(unittest.TestCase):
+
+    def missing_dep_test(self):
+        client = TestClient(users={"myremote": [("lasote", "mypass")]})
+
+        # Create deps packages
+        dep1_conanfile = """from conans import ConanFile
+class Dep1Pkg(ConanFile):
+    name = "dep1"
+        """
+
+        dep2_conanfile = """from conans import ConanFile
+class Dep2Pkg(ConanFile):
+    name = "dep2"
+    version = "1.0"
+    requires = "dep1/1.0@lasote/testing"
+        """
+
+        client.save({"conanfile.py": dep1_conanfile}, clean_first=True)
+        client.run("create . dep1/1.0@lasote/testing")
+        client.run("create . dep1/2.0@lasote/testing")
+
+        client.save({"conanfile.py": dep2_conanfile}, clean_first=True)
+        client.run("create . lasote/testing")
+
+        # Create final package
+        foo_conanfile = """from conans import ConanFile
+class FooPkg(ConanFile):
+    name = "foo"
+    version = "1.0"
+    requires = "dep1/{dep1_version}@lasote/testing", "dep2/1.0@lasote/testing"
+        """
+        client.save({"conanfile.py": foo_conanfile.format(dep1_version="1.0")}, clean_first=True)
+        client.run("create . lasote/testing")
+
+        # Bump version of one dependency
+        client.save({"conanfile.py": foo_conanfile.format(dep1_version="2.0")}, clean_first=True)
+        error = client.run("create . lasote/testing", ignore_error=True)
+        self.assertTrue(error)
+        self.assertIn("Can't find a 'dep2/1.0@lasote/testing' package", client.user_io.out)
+        self.assertIn("- Dependencies: dep1/2.0@lasote/testing", client.user_io.out)
+