Repository: conan-io/conan

Issue:
Prevent overriding dependency versions
My situation is the following:
- `PROJECT/0.2.0` -> `B/0.1.0`, `C/0.1.0`, `D/0.1.0`
- `B/0.1.0` -> `D/0.1.0`
- `C/0.1.0` -> `D/0.1.0`

Until then, everything's fine. Now there's a new version of B that leads to the following:
- `PROJECT/0.3.0` -> `B/0.2.0`, `C/0.1.0`, `D/0.1.0`
- `B/0.2.0` -> `D/0.2.0`
- `C/0.1.0` -> `D/0.1.0`

I am defining `full_version_mode()` everywhere. If PROJECT did not depend on D, Conan would fail saying that the dependencies are incompatible. But if D happens to be a dependency of the project too, then it will override any dependencies' usage of D to its version.
Is there a way to prevent this from happening and have it fail if there are any 2 versions of the same library that are different/incompatible?

If I do not set `build=missing` it's ok for me because it will basically not find the `package_id` for the library with the overridden dependency. But I'd rather have it fail at the dependency graph analysis and re-enable the `build=missing` option.

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [ ] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




Discussion:
Hi @salessandri!

> Is there a way to prevent this from happening and have it fail if there are any 2 versions of the same library that are different/incompatible?

No, if the project declares the override/require to D it will use it in the whole graph, in general, any intermediate require resolves conflicts upstream. We don't have a mechanism to block the overrides.

I don't know if it helps but if you want to automate the check task you could run a `conan info . --json output.json`, in the output you will see if some binary is missing.

```json
[  
   {  
      "revision":"95fa8a06e701a3827a999668979b5e2e",
      "reference":"LIB_B/0.2.0@user/channel",
      "is_ref":true,
      "display_name":"LIB_B/0.2.0@user/channel",
      "id":"75448e85a06e45c0071d9853d912f348a78c06c9",
      "build_id":null,
      "recipe":"Cache",
      "binary":"Missing",
      "creation_date":"2019-02-06 08:30:19",
      "required_by":[  
         "conanfile.py (PROJECT/1.0@None/None)"
      ],
      "requires":[  
         "LIB_D/0.1.0@user/channel"
      ]
   },
   {  
      "revision":"8fcfcefd1d676b8a78e3d1d5fd4cb11e",
      "reference":"LIB_C/0.1.0@user/channel",
      "is_ref":true,
      "display_name":"LIB_C/0.1.0@user/channel",
      "id":"75448e85a06e45c0071d9853d912f348a78c06c9",
      "build_id":null,
      "recipe":"Cache",
      "binary":"Cache",
      "creation_date":"2019-02-06 08:30:17",
      "required_by":[  
         "conanfile.py (PROJECT/1.0@None/None)"
      ],
      "requires":[  
         "LIB_D/0.1.0@user/channel"
      ]
   },
   {  
      "revision":"ff526ddc6ece04b18e4637ef41a1ca88",
      "reference":"LIB_D/0.1.0@user/channel",
      "is_ref":true,
      "display_name":"LIB_D/0.1.0@user/channel",
      "id":"5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
      "build_id":null,
      "recipe":"Cache",
      "binary":"Cache",
      "creation_date":"2019-02-06 08:30:16",
      "required_by":[  
         "LIB_C/0.1.0@user/channel",
         "LIB_B/0.2.0@user/channel",
         "conanfile.py (PROJECT/1.0@None/None)"
      ]
   },
   {  
      "reference":"conanfile.py (PROJECT/1.0@None/None)",
      "is_ref":false,
      "display_name":"conanfile.py (PROJECT/1.0@None/None)",
      "id":"912d0e9c6afb67d0386f383a550ba83fe0fa3950",
      "build_id":null,
      "requires":[  
         "LIB_B/0.2.0@user/channel",
         "LIB_C/0.1.0@user/channel",
         "LIB_D/0.1.0@user/channel"
      ]
   }
]
```


I think I am struggling with this issue too.

Can I ask what you mean by:

> I am defining `full_version_mode()` everywhere.

Do you mean that in the package_id() of PROJECT you are implementing:

```
def package_id(self):
    self.info.requires["B"].full_version_mode()
    self.info.requires["C"].full_version_mode()
    self.info.requires["D"].full_version_mode()
```

as mentioned in:

https://docs.conan.io/en/latest/creating_packages/define_abi_compatibility.html#versioning-schema


@michaelmaguire Yes, that's what I'm doing but in an easier way:

```
def package_id(self):
    self.info.requires.full_version_mode()
```

That makes it apply to all the requires. Also I need to run `conan` without any `--build=missing` arguments. Otherwise, it will try to build the dependency with the updated dependency and I actually want `conan install` to fail.

I really hope they add a way to explicitly override the arguments from the top level but not do it silently.
Related to #2800
We are introducing an opt-in `conan.conf` config to fail when an override happen and it is no explicit (not specified with the "override").  For the next 1.14 release.

Resolving patch:
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -94,6 +94,7 @@
 # sysrequires_mode = enabled          # environment CONAN_SYSREQUIRES_MODE (allowed modes enabled/verify/disabled)
 # vs_installation_preference = Enterprise, Professional, Community, BuildTools # environment CONAN_VS_INSTALLATION_PREFERENCE
 # verbose_traceback = False           # environment CONAN_VERBOSE_TRACEBACK
+# error_on_override = False           # environment CONAN_ERROR_ON_OVERRIDE
 # bash_path = ""                      # environment CONAN_BASH_PATH (only windows)
 # recipe_linter = False               # environment CONAN_RECIPE_LINTER
 # read_only_cache = True              # environment CONAN_READ_ONLY_CACHE
@@ -184,6 +185,7 @@ def env_vars(self):
                "CONAN_USER_HOME_SHORT": self._env_c("general.user_home_short", "CONAN_USER_HOME_SHORT", None),
                "CONAN_USE_ALWAYS_SHORT_PATHS": self._env_c("general.use_always_short_paths", "CONAN_USE_ALWAYS_SHORT_PATHS", None),
                "CONAN_VERBOSE_TRACEBACK": self._env_c("general.verbose_traceback", "CONAN_VERBOSE_TRACEBACK", None),
+               "CONAN_ERROR_ON_OVERRIDE": self._env_c("general.error_on_override", "CONAN_ERROR_ON_OVERRIDE", "False"),
                # http://www.vtk.org/Wiki/CMake_Cross_Compiling
                "CONAN_CMAKE_GENERATOR": self._env_c("general.cmake_generator", "CONAN_CMAKE_GENERATOR", None),
                "CONAN_CMAKE_GENERATOR_PLATFORM": self._env_c("general.cmake_generator_platform", "CONAN_CMAKE_GENERATOR_PLATFORM", None),
diff --git a/conans/model/requires.py b/conans/model/requires.py
--- a/conans/model/requires.py
+++ b/conans/model/requires.py
@@ -4,6 +4,7 @@
 
 from conans.errors import ConanException
 from conans.model.ref import ConanFileReference
+from conans.util.env_reader import get_env
 
 
 class Requirement(object):
@@ -112,6 +113,8 @@ def update(self, down_reqs, output, own_ref, down_ref):
         assert isinstance(own_ref, ConanFileReference) if own_ref else True
         assert isinstance(down_ref, ConanFileReference) if down_ref else True
 
+        error_on_override = get_env("CONAN_ERROR_ON_OVERRIDE", False)
+
         new_reqs = down_reqs.copy()
         if own_ref:
             new_reqs.pop(own_ref.name, None)
@@ -123,9 +126,14 @@ def update(self, down_reqs, output, own_ref, down_ref):
                 # update dependency
                 other_ref = other_req.ref
                 if other_ref and other_ref != req.ref:
-                    output.info("%s requirement %s overridden by %s to %s "
-                                % (own_ref, req.ref, down_ref or "your conanfile",
-                                   other_ref))
+                    msg = "requirement %s overridden by %s to %s " \
+                          % (req.ref, down_ref or "your conanfile", other_ref)
+
+                    if error_on_override and not other_req.override:
+                        raise ConanException(msg)
+
+                    msg = "%s %s" % (own_ref, msg)
+                    output.warn(msg)
                     req.ref = other_ref
 
             new_reqs[name] = req


Test patch:
diff --git a/conans/test/functional/graph/conflict_diamond_test.py b/conans/test/functional/graph/conflict_diamond_test.py
--- a/conans/test/functional/graph/conflict_diamond_test.py
+++ b/conans/test/functional/graph/conflict_diamond_test.py
@@ -1,38 +1,98 @@
+import os
+import textwrap
 import unittest
 
+from conans.client.tools import environment_append
 from conans.paths import CONANFILE
-from conans.test.utils.tools import TestClient
+from conans.test.utils.tools import TestClient, load
+import json
 
 
 class ConflictDiamondTest(unittest.TestCase):
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
 
-    def setUp(self):
-        self.client = TestClient()
+        class HelloReuseConan(ConanFile):
+            name = "%s"
+            version = "%s"
+            requires = %s
+        """)
 
     def _export(self, name, version, deps=None, export=True):
         deps = ", ".join(['"%s"' % d for d in deps or []]) or '""'
-        conanfile = """
-from conans import ConanFile, CMake
-import os
-
-class HelloReuseConan(ConanFile):
-    name = "%s"
-    version = "%s"
-    requires = %s
-""" % (name, version, deps)
+        conanfile = self.conanfile % (name, version, deps)
         files = {CONANFILE: conanfile}
         self.client.save(files, clean_first=True)
         if export:
             self.client.run("export . lasote/stable")
 
-    def reuse_test(self):
+    def setUp(self):
+        self.client = TestClient()
         self._export("Hello0", "0.1")
         self._export("Hello0", "0.2")
         self._export("Hello1", "0.1", ["Hello0/0.1@lasote/stable"])
         self._export("Hello2", "0.1", ["Hello0/0.2@lasote/stable"])
+
+    def test_conflict(self):
+        """ There is a conflict in the graph: branches with requirement in different
+            version, Conan will raise
+        """
         self._export("Hello3", "0.1", ["Hello1/0.1@lasote/stable", "Hello2/0.1@lasote/stable"],
                      export=False)
-
         self.client.run("install . --build missing", assert_error=True)
         self.assertIn("Conflict in Hello2/0.1@lasote/stable", self.client.user_io.out)
         self.assertNotIn("Generated conaninfo.txt", self.client.user_io.out)
+
+    def test_override_silent(self):
+        """ There is a conflict in the graph, but the consumer project depends on the conflicting
+            library, so all the graph will use the version from the consumer project
+        """
+        self._export("Hello3", "0.1",
+                     ["Hello1/0.1@lasote/stable", "Hello2/0.1@lasote/stable",
+                      "Hello0/0.1@lasote/stable"], export=False)
+        self.client.run("install . --build missing", assert_error=False)
+        self.assertIn("Hello2/0.1@lasote/stable requirement Hello0/0.2@lasote/stable overridden"
+                      " by Hello3/0.1@None/None to Hello0/0.1@lasote/stable",
+                      self.client.user_io.out)
+
+    def test_error_on_override(self):
+        """ Given a conflict in dependencies that is overridden by the consumer project, instead
+            of silently output a message, the user can force an error using
+            the env variable 'CONAN_ERROR_ON_OVERRIDE'
+        """
+        with environment_append({'CONAN_ERROR_ON_OVERRIDE': "True"}):
+            self._export("Hello3", "0.1",
+                         ["Hello1/0.1@lasote/stable", "Hello2/0.1@lasote/stable",
+                          "Hello0/0.1@lasote/stable"], export=False)
+            self.client.run("install . --build missing", assert_error=True)
+            self.assertIn("ERROR: Hello2/0.1@lasote/stable: requirement Hello0/0.2@lasote/stable"
+                          " overridden by Hello3/0.1@None/None to Hello0/0.1@lasote/stable",
+                          self.client.user_io.out)
+
+    def test_override_explicit(self):
+        """ Given a conflict in dependencies that is overridden by the consumer project (with
+            the explicit keyword 'override'), it won't raise because it is explicit, even if the
+            user has set env variable 'CONAN_ERROR_ON_OVERRIDE' to True
+        """
+        with environment_append({'CONAN_ERROR_ON_OVERRIDE': "True"}):
+            conanfile = self.conanfile % ("Hello3", "0.1",
+                                          '(("Hello1/0.1@lasote/stable"), '
+                                          '("Hello2/0.1@lasote/stable"), '
+                                          '("Hello0/0.1@lasote/stable", "override"),)')
+            self.client.save({CONANFILE: conanfile})
+            self.client.run("install . --build missing")
+            self.assertIn("Hello2/0.1@lasote/stable requirement Hello0/0.2@lasote/stable overridden"
+                          " by Hello3/0.1@None/None to Hello0/0.1@lasote/stable",
+                          self.client.user_io.out)
+
+            # ...but there is no way to tell Conan that 'Hello3' wants to depend also on 'Hello0'.
+            json_file = os.path.join(self.client.current_folder, 'tmp.json')
+            self.client.run('info . --only=requires --json="{}"'.format(json_file))
+            data = json.loads(load(json_file))
+            hello0 = data[0]
+            self.assertEqual(hello0["reference"], "Hello0/0.1@lasote/stable")
+            self.assertListEqual(sorted(hello0["required_by"]),
+                                 sorted(["Hello2/0.1@lasote/stable", "Hello1/0.1@lasote/stable"]))
+
+
+