Skip to content
18 changes: 16 additions & 2 deletions conans/client/graph/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,22 @@ def _create_new_node(self, node, require, graph, profile_host, profile_build, gr
new_node.recipe = recipe_status
new_node.remote = remote

# FIXME
down_options = node.conanfile.up_options
# The consumer "up_options" are the options that come from downstream to this node
if require.options is not None:
# If the consumer has specified "requires(options=xxx)", we need to use it
# It will have less priority than downstream consumers
down_options = Options(options_values=require.options)
down_options.scope(new_ref.name)
# At the moment, the behavior is the most restrictive one: default_options and
# options["dep"].opt=value only propagate to visible and host dependencies
# we will evaluate if necessary a potential "build_options", but recall that it is
# now possible to do "self.build_requires(..., options={k:v})" to specify it
if require.visible and context == CONTEXT_HOST:
# Only visible requirements in the host context propagate options from downstream
Comment thread
memsharded marked this conversation as resolved.
down_options.update_options(node.conanfile.up_options)
else:
down_options = node.conanfile.up_options if require.visible else Options()

self._prepare_node(new_node, profile_host, profile_build, down_options)
require.process_package_type(new_node)
graph.add_node(new_node)
Expand Down
28 changes: 16 additions & 12 deletions conans/model/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ class Requirement:
"""
def __init__(self, ref, *, headers=None, libs=None, build=False, run=None, visible=None,
transitive_headers=None, transitive_libs=None, test=None, package_id_mode=None,
force=None, override=None, direct=None):
force=None, override=None, direct=None, options=None):
# * prevents the usage of more positional parameters, always ref + **kwargs
# By default this is a generic library requirement
self.ref = ref
self._headers = headers # This dependent node has headers that must be -I<headers-path>
self._libs = libs
self._build = build # This dependent node is a build tool that is executed at build time only
self._build = build # This dependent node is a build tool that runs at build time only
self._run = run # node contains executables, shared libs or data necessary at host run time
self._visible = visible # Even if not libsed or visible, the node is unique, can conflict
self._transitive_headers = transitive_headers
Expand All @@ -27,6 +27,7 @@ def __init__(self, ref, *, headers=None, libs=None, build=False, run=None, visib
self._force = force
self._override = override
self._direct = direct
self.options = options

@staticmethod
def _default_if_none(field, default_value):
Expand Down Expand Up @@ -324,19 +325,21 @@ class BuildRequirements:
def __init__(self, requires):
self._requires = requires

def __call__(self, ref, package_id_mode=None, visible=False, run=None):
def __call__(self, ref, package_id_mode=None, visible=False, run=None, options=None):
# TODO: Check which arguments could be user-defined
self._requires.build_require(ref, package_id_mode=package_id_mode, visible=visible, run=run)
self._requires.build_require(ref, package_id_mode=package_id_mode, visible=visible, run=run,
options=options)


class ToolRequirements:
# Just a wrapper around requires for backwards compatibility with self.build_requires() syntax
def __init__(self, requires):
self._requires = requires

def __call__(self, ref, package_id_mode=None, visible=False, run=True):
def __call__(self, ref, package_id_mode=None, visible=False, run=True, options=None):
# TODO: Check which arguments could be user-defined
self._requires.tool_require(ref, package_id_mode=package_id_mode, visible=visible, run=run)
self._requires.tool_require(ref, package_id_mode=package_id_mode, visible=visible, run=run,
options=options)


class TestRequirements:
Expand Down Expand Up @@ -390,7 +393,7 @@ def __call__(self, str_ref, **kwargs):
self._requires[req] = req

def build_require(self, ref, raise_if_duplicated=True, package_id_mode=None, visible=False,
run=None):
run=None,options=None):
"""
Represent a generic build require, could be a tool, like "cmake" or a bundle of build
scripts.
Expand All @@ -404,7 +407,8 @@ def build_require(self, ref, raise_if_duplicated=True, package_id_mode=None, vis
# FIXME: This raise_if_duplicated is ugly, possibly remove
ref = RecipeReference.loads(ref)
req = Requirement(ref, headers=False, libs=False, build=True, run=run, visible=visible,
package_id_mode=package_id_mode)
package_id_mode=package_id_mode, options=options)

if raise_if_duplicated and self._requires.get(req):
raise ConanException("Duplicated requirement: {}".format(ref))
self._requires[req] = req
Expand All @@ -420,7 +424,7 @@ def override(self, ref):
req.override = True
self._requires[req] = req

def test_require(self, ref, run=None):
def test_require(self, ref, run=None, options=None):
"""
Represent a testing framework like gtest

Expand All @@ -436,13 +440,13 @@ def test_require(self, ref, run=None):
# libs = True => We need to link with it
# headers = True => We need to include it
req = Requirement(ref, headers=True, libs=True, build=False, run=run, visible=False,
test=True, package_id_mode=None)
test=True, package_id_mode=None, options=options)
if self._requires.get(req):
raise ConanException("Duplicated requirement: {}".format(ref))
self._requires[req] = req

def tool_require(self, ref, raise_if_duplicated=True, package_id_mode=None, visible=False,
run=True):
run=True, options=None):
"""
Represent a build tool like "cmake".

Expand All @@ -454,7 +458,7 @@ def tool_require(self, ref, raise_if_duplicated=True, package_id_mode=None, visi
# FIXME: This raise_if_duplicated is ugly, possibly remove
ref = RecipeReference.loads(ref)
req = Requirement(ref, headers=False, libs=False, build=True, run=run, visible=visible,
package_id_mode=package_id_mode)
package_id_mode=package_id_mode, options=options)
if raise_if_duplicated and self._requires.get(req):
raise ConanException("Duplicated requirement: {}".format(ref))
self._requires[req] = req
Expand Down
14 changes: 12 additions & 2 deletions conans/test/assets/genconanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,15 @@ def _default_options_render(self):
tmp = "default_options = {%s}" % line
return tmp

@property
def _build_requirements_render(self):
lines = []
for ref, kwargs in self._build_requirements:
args = ", ".join("{}={}".format(k, f'"{v}"' if not isinstance(v, (bool, dict)) else v)
for k, v in kwargs.items())
lines.append(' self.build_requires("{}", {})'.format(ref, args))
return "def build_requirements(self):\n{}\n".format("\n".join(lines))

@property
def _build_requires_render(self):
line = ", ".join(['"{}"'.format(r) for r in self._build_requires])
Expand Down Expand Up @@ -335,12 +344,12 @@ def _requirements_render(self):
lines.append(' self.requires("{}", {})'.format(ref, args))

for ref, kwargs in self._build_requirements or []:
args = ", ".join("{}={}".format(k, f'"{v}"' if not isinstance(v, bool) else v)
args = ", ".join("{}={}".format(k, f'"{v}"' if not isinstance(v, (bool, dict)) else v)
for k, v in kwargs.items())
lines.append(' self.build_requires("{}", {})'.format(ref, args))

for ref, kwargs in self._tool_requirements or []:
args = ", ".join("{}={}".format(k, f'"{v}"' if not isinstance(v, bool) else v)
args = ", ".join("{}={}".format(k, f'"{v}"' if not isinstance(v, (bool, dict)) else v)
for k, v in kwargs.items())
lines.append(' self.tool_requires("{}", {})'.format(ref, args))

Expand Down Expand Up @@ -455,6 +464,7 @@ def __repr__(self):
"package_method", "package_info", "package_id_lines", "test_lines"
):
if member == "requirements":
# FIXME: This seems exclusive, but we could mix them?
v = self._requirements or self._tool_requirements or self._build_requirements
else:
v = getattr(self, "_{}".format(member), None)
Expand Down
14 changes: 8 additions & 6 deletions conans/test/integration/command_v2/test_info_build_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,13 @@ def test_info_build_order_build_require():

def test_info_build_order_options():
c = TestClient()
# The normal default_options do NOT propagate to build_requires, it is necessary to use
# self.requires(..., options=xxx)
c.save({"tool/conanfile.py": GenConanfile().with_option("myopt", [1, 2, 3]),
"dep1/conanfile.py": GenConanfile().with_tool_requires("tool/0.1").
with_default_option("tool:myopt", 1),
"dep2/conanfile.py": GenConanfile().with_tool_requires("tool/0.1").
with_default_option("tool:myopt", 2),
"dep1/conanfile.py": GenConanfile().with_tool_requirement("tool/0.1",
options={"myopt": 1}),
"dep2/conanfile.py": GenConanfile().with_tool_requirement("tool/0.1",
options={"myopt": 2}),
"consumer/conanfile.txt": "[requires]\ndep1/0.1\ndep2/0.1"})
c.run("export tool --name=tool --version=0.1")
c.run("export dep1 --name=dep1 --version=0.1")
Expand Down Expand Up @@ -151,7 +153,7 @@ def test_info_build_order_options():
],
[
{
"ref": "dep1/0.1#56a8318e80ce85706b95baad0e14853c",
"ref": "dep1/0.1#eeabd1fa65a6f7ccc227816a507bb966",
"depends": [
"tool/0.1#b6299fc637530d547c7eaa047d1da91d"
],
Expand All @@ -168,7 +170,7 @@ def test_info_build_order_options():
]
},
{
"ref": "dep2/0.1#0bf82914395fcd67ac96945ffe9dbe08",
"ref": "dep2/0.1#d878e5a90ac7bd8fbb14ce899456cc74",
"depends": [
"tool/0.1#b6299fc637530d547c7eaa047d1da91d"
],
Expand Down
47 changes: 47 additions & 0 deletions conans/test/integration/graph/core/graph_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,33 @@ def test_diamond_reverse_order_conflict(self):
app = deps_graph.root
dep1 = app.dependencies[0].dst
dep2 = app.dependencies[1].dst
self._check_node(app, "app/0.1", deps=[dep1, dep2])
self._check_node(dep1, "dep1/2.0#123", deps=[], dependents=[app])
# dep2 no dependency, it was not resolved due to conflict
self._check_node(dep2, "dep2/1.0#123", deps=[], dependents=[app])

def test_invisible_not_forced(self):
# app -> libb0.1 -(visible=False)----> liba0.1 (NOT forced to lib0.2)
# \-> -----(force not used)-------> liba0.2
self.recipe_cache("liba/0.1")
self.recipe_cache("liba/0.2")
self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1",
visible=False))
consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libb/0.1")
.with_requirement("liba/0.2", force=True))
deps_graph = self.build_consumer(consumer)

self.assertEqual(4, len(deps_graph.nodes))
app = deps_graph.root
libb = app.dependencies[0].dst
liba2 = app.dependencies[1].dst
liba = libb.dependencies[0].dst

# TODO: No Revision??? Because of consumer?
self._check_node(app, "app/0.1", deps=[libb, liba2])
self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
self._check_node(liba, "liba/0.1#123", dependents=[libb])
self._check_node(liba2, "liba/0.2#123", dependents=[app])


class PureOverrideTest(GraphManagerTest):
Expand Down Expand Up @@ -987,6 +1014,26 @@ def test_discarded_override(self):
# TODO: No Revision??? Because of consumer?
self._check_node(app, "app/0.1", deps=[])

def test_invisible_not_overriden(self):
# app -> libb0.1 -(visible=False)----> liba0.1 (NOT overriden to lib0.2)
# \-> -----(override not used)------->/
self.recipe_cache("liba/0.1")
self.recipe_conanfile("libb/0.1", GenConanfile().with_requirement("liba/0.1",
visible=False))
consumer = self.consumer_conanfile(GenConanfile("app", "0.1").with_require("libb/0.1")
.with_requirement("liba/0.2", override=True))
deps_graph = self.build_consumer(consumer)

self.assertEqual(3, len(deps_graph.nodes))
app = deps_graph.root
libb = app.dependencies[0].dst
liba = libb.dependencies[0].dst

# TODO: No Revision??? Because of consumer?
self._check_node(app, "app/0.1", deps=[libb])
self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
self._check_node(liba, "liba/0.1#123", dependents=[libb])


class PackageIDDeductions(GraphManagerTest):

Expand Down
Loading