diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py index e25878b1d21..556e4a0a8ab 100644 --- 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 index bd7aa3fe09a..8644a544602 100644 --- 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 diff --git a/conans/test/functional/graph/conflict_diamond_test.py b/conans/test/functional/graph/conflict_diamond_test.py index 6c399f253f9..6e7203dafcc 100644 --- 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"])) + + +