Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3/n] [xbuild] Populate 'settings_build' and 'settings_target' in conanfile #6769

Merged
merged 14 commits into from Apr 29, 2020
6 changes: 5 additions & 1 deletion conans/client/graph/graph.py
Expand Up @@ -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}
jgsogo marked this conversation as resolved.
Show resolved Hide resolved
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

Expand Down Expand Up @@ -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)
Expand Down
75 changes: 52 additions & 23 deletions 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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -95,18 +96,20 @@ 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
node.public_closure.sort(key_fn=lambda x: x not in new_nodes)
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
Expand All @@ -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:
Expand Down Expand Up @@ -178,27 +182,29 @@ 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)
previous_closure = node.public_closure.get(name, context=context)
# 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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion conans/client/graph/graph_manager.py
Expand Up @@ -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:
Expand Down
26 changes: 13 additions & 13 deletions conans/test/functional/cross_building/graph/_base_test_case.py
Expand Up @@ -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 %}

Expand Down Expand Up @@ -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 %}

Expand Down Expand Up @@ -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))

Expand All @@ -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
"""))
Expand Down
Expand Up @@ -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()
Expand Down
Expand Up @@ -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")
Expand Down
12 changes: 6 additions & 6 deletions conans/test/functional/cross_building/graph/protobuf_test.py
Expand Up @@ -14,39 +14,39 @@ class ProtobufTest(CrossBuildingBaseTestCase):

protobuf = textwrap.dedent("""
from conans import ConanFile

class Protobuf(ConanFile):
settings = "os"
def requirements(self):
if self.settings.os == "Host":
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"

self.cpp_info.includedirs = [protobuf_str, ]
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)
Expand Down
11 changes: 6 additions & 5 deletions conans/test/functional/cross_building/graph/protoc_basic_test.py
Expand Up @@ -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))
""")
Expand All @@ -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
Expand Down
Empty file.