diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py index 269e0596d3c..f4f8ccaf391 100644 --- a/conans/client/graph/graph.py +++ b/conans/client/graph/graph.py @@ -83,7 +83,7 @@ def __init__(self, ref, conanfile, context, recipe=None, path=None): # on this node. It includes regular (not private and not build requires) dependencies self._transitive_closure = OrderedDict() self.inverse_closure = set() # set of nodes that have this one in their public - self.ancestors = None # set{ref.name} + self._ancestors = _NodeOrderedDict() # set{ref.name} self._id = None # Unique ID (uuid at the moment) of a node in the graph self.graph_lock_node = None # the locking information can be None @@ -125,6 +125,10 @@ def public_closure(self): def transitive_closure(self): return self._transitive_closure + @property + def ancestors(self): + return self._ancestors + def partial_copy(self): # Used for collapse_graph result = Node(self.ref, self.conanfile, self.context, self.recipe, self.path) diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py index c06bb63ad69..92149ee4f3b 100644 --- a/conans/client/graph/graph_builder.py +++ b/conans/client/graph/graph_builder.py @@ -1,6 +1,6 @@ import time -from conans.client.graph.graph import DepsGraph, Node, RECIPE_EDITABLE, CONTEXT_HOST +from conans.client.graph.graph import DepsGraph, Node, RECIPE_EDITABLE, CONTEXT_HOST, CONTEXT_BUILD from conans.errors import (ConanException, ConanExceptionInUserConanfileMethod, conanfile_exception_formatter) from conans.model.conan_file import get_env_context_manager @@ -56,14 +56,15 @@ def load_graph(self, root_node, check_updates, update, remotes, profile_host, pr root_node.public_closure.add(root_node) root_node.public_deps.add(root_node) root_node.transitive_closure[root_node.name] = root_node - root_node.ancestors = set() + if profile_build: + root_node.conanfile.settings_build = profile_build.processed_settings.copy() + root_node.conanfile.settings_target = None dep_graph.add_node(root_node) # enter recursive computation t1 = time.time() self._expand_node(root_node, dep_graph, Requirements(), None, None, check_updates, - update, remotes, profile_host, profile_build, graph_lock, - context=CONTEXT_HOST) + update, remotes, profile_host, profile_build, graph_lock) logger.debug("GRAPH: Time to load deps %s" % (time.time() - t1)) return dep_graph @@ -95,10 +96,12 @@ def extend_build_requires(self, graph, node, build_requires_refs, check_updates, self._resolve_ranges(graph, build_requires, scope, update, remotes) for br in build_requires: - context = br.build_require_context if node.context == CONTEXT_HOST else node.context + context_switch = bool(br.build_require_context == CONTEXT_BUILD) + populate_settings_target = context_switch # Avoid 'settings_target' for BR-host self._expand_require(br, node, graph, check_updates, update, remotes, profile_host, profile_build, new_reqs, new_options, - graph_lock, context=context) + graph_lock, context_switch=context_switch, + populate_settings_target=populate_settings_target) new_nodes = set(n for n in graph.nodes if n.package_id is None) # This is to make sure that build_requires have precedence over the normal requires @@ -106,7 +109,7 @@ def extend_build_requires(self, graph, node, build_requires_refs, check_updates, return new_nodes def _expand_node(self, node, graph, down_reqs, down_ref, down_options, check_updates, update, - remotes, profile_host, profile_build, graph_lock, context): + remotes, profile_host, profile_build, graph_lock): """ expands the dependencies of the node, recursively param node: Node object to be expanded in this step @@ -123,7 +126,8 @@ def _expand_node(self, node, graph, down_reqs, down_ref, down_options, check_upd if require.override: continue self._expand_require(require, node, graph, check_updates, update, remotes, profile_host, - profile_build, new_reqs, new_options, graph_lock, context) + profile_build, new_reqs, new_options, graph_lock, + context_switch=False) def _resolve_ranges(self, graph, requires, consumer, update, remotes): for require in requires: @@ -178,17 +182,18 @@ def _get_node_requirements(self, node, graph, down_ref, down_options, down_reqs, return new_options, new_reqs def _expand_require(self, require, node, graph, check_updates, update, remotes, profile_host, - profile_build, new_reqs, new_options, graph_lock, context): + profile_build, new_reqs, new_options, graph_lock, context_switch, + populate_settings_target=True): # Handle a requirement of a node. There are 2 possibilities # node -(require)-> new_node (creates a new node in the graph) # node -(require)-> previous (creates a diamond with a previously existing node) # If the required is found in the node ancestors a loop is being closed - # TODO: allow bootstrapping, use references instead of names - name = require.ref.name - if name in node.ancestors or name == node.name: - raise ConanException("Loop detected: '%s' requires '%s' which is an ancestor too" - % (node.ref, require.ref)) + context = CONTEXT_BUILD if context_switch else node.context + name = require.ref.name # TODO: allow bootstrapping, use references instead of names + if node.ancestors.get(name, context) or (name == node.name and context == node.context): + raise ConanException("Loop detected in context %s: '%s' requires '%s'" + " which is an ancestor too" % (context, node.ref, require.ref)) # If the requirement is found in the node public dependencies, it is a diamond previous = node.public_deps.get(name, context=context) @@ -196,9 +201,10 @@ def _expand_require(self, require, node, graph, check_updates, update, remotes, # build_requires and private will create a new node if it is not in the current closure if not previous or ((require.build_require or require.private) and not previous_closure): # new node, must be added and expanded (node -> new_node) - profile = profile_host if context == CONTEXT_HOST else profile_build new_node = self._create_new_node(node, graph, require, check_updates, update, - remotes, profile, graph_lock, context=context) + remotes, profile_host, profile_build, graph_lock, + context_switch=context_switch, + populate_settings_target=populate_settings_target) # The closure of a new node starts with just itself new_node.public_closure.add(new_node) @@ -225,7 +231,7 @@ def _expand_require(self, require, node, graph, check_updates, update, remotes, # RECURSION, keep expanding (depth-first) the new node self._expand_node(new_node, graph, new_reqs, node.ref, new_options, check_updates, - update, remotes, profile_host, profile_build, graph_lock, context) + update, remotes, profile_host, profile_build, graph_lock) if not require.private and not require.build_require: for name, n in new_node.transitive_closure.items(): node.transitive_closure[name] = n @@ -243,9 +249,10 @@ def _expand_require(self, require, node, graph, check_updates, update, remotes, raise ConanException(conflict) # Add current ancestors to the previous node and upstream deps - union = node.ancestors.union([node.name]) for n in previous.public_closure: - n.ancestors.update(union) + n.ancestors.add(node) + for item in node.ancestors: + n.ancestors.add(item) node.connect_closure(previous) graph.add_edge(node, previous, require) @@ -265,7 +272,7 @@ def _expand_require(self, require, node, graph, check_updates, update, remotes, if not graph_lock and self._recurse(previous.public_closure, new_reqs, new_options, previous.context): self._expand_node(previous, graph, new_reqs, node.ref, new_options, check_updates, - update, remotes, profile_host, profile_build, graph_lock, context) + update, remotes, profile_host, profile_build, graph_lock) @staticmethod def _conflicting_references(previous, new_ref, consumer_ref=None): @@ -395,20 +402,42 @@ def _resolve_recipe(self, current_node, dep_graph, requirement, check_updates, return new_ref, dep_conanfile, recipe_status, remote, locked_id def _create_new_node(self, current_node, dep_graph, requirement, check_updates, - update, remotes, profile, graph_lock, context): + update, remotes, profile_host, profile_build, graph_lock, context_switch, + populate_settings_target): + # If there is a context_switch, it is because it is a BR-build + if context_switch: + profile = profile_build + context = CONTEXT_BUILD + else: + profile = profile_host if current_node.context == CONTEXT_HOST else profile_build + context = current_node.context result = self._resolve_recipe(current_node, dep_graph, requirement, check_updates, update, remotes, profile, graph_lock) new_ref, dep_conanfile, recipe_status, remote, locked_id = result + # Assign the profiles depending on the context + if profile_build: # Keep existing behavior (and conanfile members) if no profile_build + dep_conanfile.settings_build = profile_build.processed_settings.copy() + if not context_switch: + if populate_settings_target: + dep_conanfile.settings_target = current_node.conanfile.settings_target + else: + dep_conanfile.settings_target = None + else: + if current_node.context == CONTEXT_HOST: + dep_conanfile.settings_target = profile_host.processed_settings.copy() + else: + dep_conanfile.settings_target = profile_build.processed_settings.copy() + logger.debug("GRAPH: new_node: %s" % str(new_ref)) new_node = Node(new_ref, dep_conanfile, context=context) new_node.revision_pinned = requirement.ref.revision is not None new_node.recipe = recipe_status new_node.remote = remote # Ancestors are a copy of the parent, plus the parent itself - new_node.ancestors = current_node.ancestors.copy() - new_node.ancestors.add(current_node.name) + new_node.ancestors.assign(current_node.ancestors) + new_node.ancestors.add(current_node) if locked_id is not None: new_node.id = locked_id diff --git a/conans/client/graph/graph_manager.py b/conans/client/graph/graph_manager.py index 7a093775415..f0b1175fdcf 100644 --- a/conans/client/graph/graph_manager.py +++ b/conans/client/graph/graph_manager.py @@ -306,7 +306,8 @@ def _recurse_build_requires(self, graph, builder, check_updates, # (no conflicts) # but the dict key is not used at all package_build_requires[br_key] = build_require - elif build_require.name != node.name: # Profile one + # Profile one or in different context + elif build_require.name != node.name or default_context != node.context: new_profile_build_requires.append((build_require, default_context)) if package_build_requires: diff --git a/conans/test/functional/cross_building/graph/_base_test_case.py b/conans/test/functional/cross_building/graph/_base_test_case.py index 874daa07ded..fe31a129fc9 100644 --- a/conans/test/functional/cross_building/graph/_base_test_case.py +++ b/conans/test/functional/cross_building/graph/_base_test_case.py @@ -52,8 +52,8 @@ class BuildRequires(ConanFile): {% if build_requires %} def build_requirements(self): - {%- for it in build_requires %} - self.build_requires("{{ it }}") + {%- for it, force_host in build_requires %} + self.build_requires("{{ it }}"{% if force_host %}, force_host_context=True{% endif %}) {%- endfor %} {%- endif %} @@ -82,8 +82,8 @@ class Protobuf(ConanFile): {% if build_requires %} def build_requirements(self): - {%- for it in build_requires %} - self.build_requires("{{ it }}") + {%- for it, force_host in build_requires %} + self.build_requires("{{ it }}"{% if force_host %}, force_host_context=True{% endif %}) {%- endfor %} {%- endif %} @@ -113,11 +113,11 @@ class Protoc(ConanFile): {% if build_requires %} def build_requirements(self): - {%- for it in build_requires %} - self.build_requires("{{ it }}") + {%- for it, force_host in build_requires %} + self.build_requires("{{ it }}"{% if force_host %}, force_host_context=True{% endif %}) {%- endfor %} {%- endif %} - + def build(self): self.output.info(">> settings.os:".format(self.settings.os)) @@ -144,22 +144,22 @@ def requirements(self): self.requires("{{ it }}") {%- endfor %} {%- endif %} - + {% if build_requires %} def build_requirements(self): - {%- for it in build_requires %} - self.build_requires("{{ it }}") + {%- for it, force_host in build_requires %} + self.build_requires("{{ it }}"{% if force_host %}, force_host_context=True{% endif %}) {%- endfor %} {%- endif %} - + def package_info(self): lib_str = "{{name}}-host" if self.settings.os == "Host" else "{{name}}-build" lib_str += "-" + self.version - self.cpp_info.libs = [lib_str, ] + self.cpp_info.libs = [lib_str, ] self.cpp_info.includedirs = [lib_str, ] self.cpp_info.libdirs = [lib_str, ] self.cpp_info.bindirs = [lib_str, ] - + self.env_info.PATH.append(lib_str) self.env_info.OTHERVAR = lib_str """)) diff --git a/conans/test/functional/cross_building/graph/breq_of_breq_test.py b/conans/test/functional/cross_building/graph/breq_of_breq_test.py index 5cc0c3df0b1..43885e9dd4d 100644 --- a/conans/test/functional/cross_building/graph/breq_of_breq_test.py +++ b/conans/test/functional/cross_building/graph/breq_of_breq_test.py @@ -30,8 +30,10 @@ def build(self): self.output.info(">> settings.os:".format(self.settings.os)) """) - gtest = CrossBuildingBaseTestCase.gtest_tpl.render(build_requires=["cmake/testing@user/channel", ]) - protoc = CrossBuildingBaseTestCase.protoc_tpl.render(build_requires=["cmake/testing@user/channel", ]) + gtest = CrossBuildingBaseTestCase.gtest_tpl.render( + build_requires=((CrossBuildingBaseTestCase.cmake_ref, False), )) + protoc = CrossBuildingBaseTestCase.protoc_tpl.render( + build_requires=((CrossBuildingBaseTestCase.cmake_ref, False), )) def setUp(self): super(BuildRequireOfBuildRequire, self).setUp() diff --git a/conans/test/functional/cross_building/graph/build_requires_in_recipe_test.py b/conans/test/functional/cross_building/graph/build_requires_in_recipe_test.py index cffaf3d327e..5225dac774f 100644 --- a/conans/test/functional/cross_building/graph/build_requires_in_recipe_test.py +++ b/conans/test/functional/cross_building/graph/build_requires_in_recipe_test.py @@ -29,9 +29,11 @@ def build(self): self.output.info(">> settings.os:".format(self.settings.os)) """) - breq = CrossBuildingBaseTestCase.library_tpl.render(name="breq", requires=["breq_lib/testing@user/channel", ]) + breq = CrossBuildingBaseTestCase.library_tpl.render(name="breq", + requires=["breq_lib/testing@user/channel", ]) breq_lib = CrossBuildingBaseTestCase.library_tpl.render(name="breq_lib") - lib = CrossBuildingBaseTestCase.library_tpl.render(name="lib", build_requires=["breq/testing@user/channel", ]) + lib = CrossBuildingBaseTestCase.library_tpl.render( + name="lib", build_requires=[("breq/testing@user/channel", False), ]) breq_lib_ref = ConanFileReference.loads("breq_lib/testing@user/channel") breq_ref = ConanFileReference.loads("breq/testing@user/channel") diff --git a/conans/test/functional/cross_building/graph/protobuf_test.py b/conans/test/functional/cross_building/graph/protobuf_test.py index 80052cf78b1..ac0129fc6cf 100644 --- a/conans/test/functional/cross_building/graph/protobuf_test.py +++ b/conans/test/functional/cross_building/graph/protobuf_test.py @@ -14,7 +14,7 @@ class ProtobufTest(CrossBuildingBaseTestCase): protobuf = textwrap.dedent(""" from conans import ConanFile - + class Protobuf(ConanFile): settings = "os" def requirements(self): @@ -22,11 +22,11 @@ def requirements(self): self.requires("zlib/1.0@user/channel") else: self.requires("zlib/2.0@user/channel") - + def build(self): self.output.info(">> settings.os:".format(self.settings.os)) self.output.info("ZLIB: %s" % self.deps_cpp_info["zlib"].libs) - + def package_info(self): protobuf_str = "protobuf-host" if self.settings.os == "Host" else "protobuf-build" @@ -34,19 +34,19 @@ def package_info(self): self.cpp_info.libdirs = [protobuf_str, ] self.cpp_info.bindirs = [protobuf_str, ] self.cpp_info.libs = [protobuf_str] - + self.env_info.PATH.append(protobuf_str) self.env_info.OTHERVAR = protobuf_str """) app = textwrap.dedent(""" from conans import ConanFile - + class App(ConanFile): settings = "os" requires = "protobuf/testing@user/channel" build_requires = "protobuf/testing@user/channel" - + def build(self): self.output.info(">> settings.os:".format(self.settings.os)) self.output.info("ZLIB: %s" % self.deps_cpp_info["zlib"].libs) diff --git a/conans/test/functional/cross_building/graph/protoc_basic_test.py b/conans/test/functional/cross_building/graph/protoc_basic_test.py index 2665b1f6340..b3eec9e0afa 100644 --- a/conans/test/functional/cross_building/graph/protoc_basic_test.py +++ b/conans/test/functional/cross_building/graph/protoc_basic_test.py @@ -19,17 +19,17 @@ class ClassicProtocExampleBase(CrossBuildingBaseTestCase): application = textwrap.dedent(""" from conans import ConanFile - + class Protoc(ConanFile): name = "app" version = "testing" - + settings = "os" requires = "protobuf/testing@user/channel" - + def build_requirements(self): self.build_requires("protoc/testing@user/channel") - + def build(self): self.output.info(">> settings.os:".format(self.settings.os)) """) @@ -56,7 +56,8 @@ def test_crossbuilding(self, xbuilding): else: profile_build = None - deps_graph = self._build_graph(profile_host=profile_host, profile_build=profile_build, install=True) + deps_graph = self._build_graph(profile_host=profile_host, profile_build=profile_build, + install=True) # Check HOST packages # - application diff --git a/conans/test/functional/cross_building/profile_access/__init__.py b/conans/test/functional/cross_building/profile_access/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/conans/test/functional/cross_building/profile_access/access_other_context_test.py b/conans/test/functional/cross_building/profile_access/access_other_context_test.py new file mode 100644 index 00000000000..16438b782d5 --- /dev/null +++ b/conans/test/functional/cross_building/profile_access/access_other_context_test.py @@ -0,0 +1,99 @@ +import textwrap + +from conans.client.graph.graph import CONTEXT_BUILD, CONTEXT_HOST +from conans.model.profile import Profile +from conans.test.functional.cross_building.graph._base_test_case import CrossBuildingBaseTestCase + + +class NoWayBackToHost(CrossBuildingBaseTestCase): + + application = textwrap.dedent(""" + from conans import ConanFile + + class Application(ConanFile): + name = "app" + version = "testing" + + settings = "os" + + def build_requirements(self): + self.build_requires("protoc/testing@user/channel") + self.build_requires("gtest/testing@user/channel", force_host_context=True) + + def build(self): + self.output.info(">> settings.os:".format(self.settings.os)) + """) + + protoc = CrossBuildingBaseTestCase.protoc_tpl.render( + build_requires=((CrossBuildingBaseTestCase.cmake_ref, False), + (CrossBuildingBaseTestCase.gtest_ref, True))) + + def setUp(self): + super(NoWayBackToHost, self).setUp() + self._cache_recipe(self.cmake_ref, self.cmake) + self._cache_recipe(self.gtest_ref, self.gtest) + self._cache_recipe(self.protobuf_ref, self.protobuf) + self._cache_recipe(self.protoc_ref, self.protoc) + self._cache_recipe(self.app_ref, self.application) + + def test_profile_access(self): + profile_host = Profile() + profile_host.settings["os"] = "Host" + profile_host.process_settings(self.cache) + + profile_build = Profile() + profile_build.settings["os"] = "Build" + profile_build.process_settings(self.cache) + + deps_graph = self._build_graph(profile_host=profile_host, profile_build=profile_build, + install=True) + + # - Application + application = deps_graph.root.dependencies[0].dst + self.assertEqual(application.conanfile.name, "app") + self.assertEqual(application.context, CONTEXT_HOST) + self.assertEqual(application.conanfile.settings.os, "Host") + self.assertEqual(application.conanfile.settings_build.os, "Build") + self.assertEqual(application.conanfile.settings_target, None) + + # - protoc + protoc = application.dependencies[0].dst + self.assertEqual(protoc.conanfile.name, "protoc") + self.assertEqual(protoc.context, CONTEXT_BUILD) + self.assertEqual(protoc.conanfile.settings.os, "Build") + self.assertEqual(protoc.conanfile.settings_build.os, "Build") + self.assertEqual(protoc.conanfile.settings_target.os, "Host") + + # - protoc/protobuf + protobuf = protoc.dependencies[0].dst + self.assertEqual(protobuf.conanfile.name, "protobuf") + self.assertEqual(protobuf.context, CONTEXT_BUILD) + self.assertEqual(protobuf.conanfile.settings.os, "Build") + self.assertEqual(protobuf.conanfile.settings_build.os, "Build") + self.assertEqual(protobuf.conanfile.settings_target.os, "Host") + + # - protoc/cmake + cmake = protoc.dependencies[1].dst + self.assertEqual(cmake.conanfile.name, "cmake") + self.assertEqual(cmake.context, CONTEXT_BUILD) + self.assertEqual(cmake.conanfile.settings.os, "Build") + self.assertEqual(cmake.conanfile.settings_build.os, "Build") + self.assertEqual(cmake.conanfile.settings_target.os, "Build") + + # - protoc/gtest + protoc_gtest = protoc.dependencies[2].dst + self.assertEqual(protoc_gtest.conanfile.name, "gtest") + self.assertEqual(protoc_gtest.context, CONTEXT_BUILD) + self.assertEqual(protoc_gtest.conanfile.settings.os, "Build") + self.assertEqual(protoc_gtest.conanfile.settings_build.os, "Build") + # We can't think about an scenario where a `build_require-host` should know about + # the target context. We are removing this information on purpose. + self.assertEqual(protoc_gtest.conanfile.settings_target, None) + + # - gtest + gtest = application.dependencies[1].dst + self.assertEqual(gtest.conanfile.name, "gtest") + self.assertEqual(gtest.context, CONTEXT_HOST) + self.assertEqual(gtest.conanfile.settings.os, "Host") + self.assertEqual(gtest.conanfile.settings_build.os, "Build") + self.assertEqual(gtest.conanfile.settings_target, None) diff --git a/conans/test/functional/graph/graph_manager_test.py b/conans/test/functional/graph/graph_manager_test.py index 3e581c58071..d040577e39d 100644 --- a/conans/test/functional/graph/graph_manager_test.py +++ b/conans/test/functional/graph/graph_manager_test.py @@ -37,7 +37,7 @@ def test_transitive(self): self.assertEqual(len(libb.dependencies), 0) self.assertEqual(len(libb.dependants), 1) self.assertEqual(libb.inverse_neighbors(), [app]) - self.assertEqual(libb.ancestors, {app.ref.name}) + self.assertEqual(list(libb.ancestors), [app]) self.assertEqual(libb.recipe, RECIPE_INCACHE) self.assertEqual(list(app.public_closure), [libb]) @@ -219,14 +219,14 @@ def test_loop(self): consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) with six.assertRaisesRegex(self, ConanException, - "Loop detected: 'liba/0.1' requires 'libc/0.1'"): + "Loop detected in context host: 'liba/0.1' requires 'libc/0.1'"): self.build_consumer(consumer) def test_self_loop(self): self.recipe_cache("liba/0.1") consumer = self.recipe_consumer("liba/0.2", ["liba/0.1"]) with six.assertRaisesRegex(self, ConanException, - "Loop detected: 'liba/0.2' requires 'liba/0.1'"): + "Loop detected in context host: 'liba/0.2' requires 'liba/0.1'"): self.build_consumer(consumer) @parameterized.expand([("recipe", ), ("profile", )]) @@ -286,8 +286,9 @@ def test_loop_build_require(self): self._cache_recipe(lib_ref, GenConanfile().with_name("lib").with_version("0.1") .with_build_require(tool_ref)) - with six.assertRaisesRegex(self, ConanException, "Loop detected: 'tool/0.1@user/testing' " - "requires 'lib/0.1@user/testing'"): + with six.assertRaisesRegex(self, ConanException, "Loop detected in context host:" + " 'tool/0.1@user/testing' requires" + " 'lib/0.1@user/testing'"): self.build_graph(GenConanfile().with_name("app").with_version("0.1") .with_require(lib_ref)) @@ -619,8 +620,9 @@ def test_loop_private(self): .with_require(lib_ref, private=True)) self._cache_recipe(lib_ref, GenConanfile().with_name("lib").with_version("0.1") .with_require(tool_ref, private=True)) - with six.assertRaisesRegex(self, ConanException, "Loop detected: 'tool/0.1@user/testing' " - "requires 'lib/0.1@user/testing'"): + with six.assertRaisesRegex(self, ConanException, "Loop detected in context host:" + " 'tool/0.1@user/testing'" + " requires 'lib/0.1@user/testing'"): self.build_graph(GenConanfile().with_name("app").with_version("0.1") .with_require(lib_ref)) diff --git a/conans/test/functional/graph/loop_detection_test.py b/conans/test/functional/graph/loop_detection_test.py index ba30b1fdfe7..5f84a7d7cf7 100644 --- a/conans/test/functional/graph/loop_detection_test.py +++ b/conans/test/functional/graph/loop_detection_test.py @@ -23,6 +23,6 @@ class Package{number}Conan(ConanFile): client.run("export . lasote/stable") client.run("install Package3/0.1@lasote/stable --build", assert_error=True) - self.assertIn("ERROR: Loop detected: 'Package2/0.1@lasote/stable' requires " + self.assertIn("ERROR: Loop detected in context host: 'Package2/0.1@lasote/stable' requires " "'Package3/0.1@lasote/stable' which is an ancestor too", client.out)