diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py index eece737ae11..71ce2485cce 100644 --- a/conans/client/graph/graph_builder.py +++ b/conans/client/graph/graph_builder.py @@ -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 + 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) diff --git a/conans/model/requires.py b/conans/model/requires.py index 3ecf4508f2d..b05a296db42 100644 --- a/conans/model/requires.py +++ b/conans/model/requires.py @@ -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 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 @@ -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): @@ -324,9 +325,10 @@ 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: @@ -334,9 +336,10 @@ class ToolRequirements: 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: @@ -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. @@ -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 @@ -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 @@ -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". @@ -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 diff --git a/conans/test/assets/genconanfile.py b/conans/test/assets/genconanfile.py index 6722ffbbd68..7b74dd40399 100644 --- a/conans/test/assets/genconanfile.py +++ b/conans/test/assets/genconanfile.py @@ -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]) @@ -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)) @@ -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) diff --git a/conans/test/integration/command_v2/test_info_build_order.py b/conans/test/integration/command_v2/test_info_build_order.py index 4cffa15c99c..8507069a3fa 100644 --- a/conans/test/integration/command_v2/test_info_build_order.py +++ b/conans/test/integration/command_v2/test_info_build_order.py @@ -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") @@ -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" ], @@ -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" ], diff --git a/conans/test/integration/graph/core/graph_manager_test.py b/conans/test/integration/graph/core/graph_manager_test.py index 619155e8a6b..221f3cc3ede 100644 --- a/conans/test/integration/graph/core/graph_manager_test.py +++ b/conans/test/integration/graph/core/graph_manager_test.py @@ -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): @@ -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): diff --git a/conans/test/integration/options/test_options_build_requires.py b/conans/test/integration/options/test_options_build_requires.py new file mode 100644 index 00000000000..6380befc496 --- /dev/null +++ b/conans/test/integration/options/test_options_build_requires.py @@ -0,0 +1,156 @@ +import textwrap + +import pytest + +from conans.test.assets.genconanfile import GenConanfile +from conans.test.utils.tools import TestClient + + +def test_build_requires_options_different(): + # copied from https://github.com/conan-io/conan/pull/9839 + # This is a test that crashed in 1.X, because of conflicting options + client = TestClient() + + conanfile_openssl_1_1_1 = GenConanfile("openssl", "1.1.1") + conanfile_openssl_3_0_0 = GenConanfile("openssl", "3.0.0") \ + .with_option("no_fips", [True, False]) \ + .with_default_option("no_fips", True) + conanfile_cmake = GenConanfile("cmake", "0.1") \ + .with_requires("openssl/1.1.1") + conanfile_consumer = GenConanfile("consumer", "0.1") \ + .with_build_requires("cmake/0.1") \ + .with_requires("openssl/3.0.0") + + client.save({"openssl_1_1_1.py": conanfile_openssl_1_1_1, + "openssl_3_0_0.py": conanfile_openssl_3_0_0, + "conanfile_cmake.py": conanfile_cmake, + "conanfile.py": conanfile_consumer}) + + client.run("create openssl_1_1_1.py") + client.run("create openssl_3_0_0.py") + client.run("create conanfile_cmake.py") + client.run("install conanfile.py") + # This test used to crash, not crashing means ok + assert "openssl/1.1.1" in client.out + assert "openssl/3.0.0: Already installed!" in client.out + assert "cmake/0.1: Already installed!" in client.out + + +def test_different_options_values_profile(): + """ + consumer -> protobuf (library) + \\--(build)-> protobuf (protoc) + protobuf by default is a static library (shared=False) + The profile or CLI args can select for each one (library and protoc) the "shared" value + """ + c = TestClient() + protobuf = textwrap.dedent(""" + from conans import ConanFile + class Proto(ConanFile): + options = {"shared": [True, False]} + default_options = {"shared": False} + + def package_info(self): + self.output.info("MYOPTION: {}-{}".format(self.context, self.options.shared)) + """) + + c.save({"protobuf/conanfile.py": protobuf, + "consumer/conanfile.py": GenConanfile().with_requires("protobuf/1.0") + .with_build_requires("protobuf/1.0")}) + + c.run("create protobuf protobuf/1.0@") + c.run("create protobuf protobuf/1.0@ -o protobuf:shared=True") + c.run("install consumer") + assert "protobuf/1.0: MYOPTION: host-False" in c.out + assert "protobuf/1.0: MYOPTION: build-False" in c.out + # specifying it in the profile works + c.run("install consumer -o protobuf:shared=True") + assert "protobuf/1.0: MYOPTION: host-True" in c.out + assert "protobuf/1.0: MYOPTION: build-False" in c.out + c.run("install consumer -o protobuf:shared=True -o:b protobuf:shared=False") + assert "protobuf/1.0: MYOPTION: host-True" in c.out + assert "protobuf/1.0: MYOPTION: build-False" in c.out + c.run("install consumer -o protobuf:shared=False -o:b protobuf:shared=True") + assert "protobuf/1.0: MYOPTION: host-False" in c.out + assert "protobuf/1.0: MYOPTION: build-True" in c.out + c.run("install consumer -o:b protobuf:shared=True") + assert "protobuf/1.0: MYOPTION: host-False" in c.out + assert "protobuf/1.0: MYOPTION: build-True" in c.out + + +@pytest.mark.parametrize("scope", ["protobuf:", ""]) +def test_different_options_values_recipe(scope): + """ + consumer -> protobuf (library) + \\--(build)-> protobuf (protoc) + protobuf by default is a static library (shared=False) + The "consumer" conanfile.py can use ``self.requires(...,options=)`` to define protobuf:shared + """ + c = TestClient() + protobuf = textwrap.dedent(""" + from conans import ConanFile + class Proto(ConanFile): + options = {"shared": [True, False]} + default_options = {"shared": False} + + def package_info(self): + self.output.info("MYOPTION: {}-{}".format(self.context, self.options.shared)) + """) + consumer_recipe = textwrap.dedent(""" + from conans import ConanFile + class Consumer(ConanFile): + def requirements(self): + self.requires("protobuf/1.0", options={{"{scope}shared": {host}}}) + def build_requirements(self): + self.build_requires("protobuf/1.0", options={{"{scope}shared": {build}}}) + """) + c.save({"conanfile.py": protobuf}) + + c.run("create . protobuf/1.0@") + c.run("create . protobuf/1.0@ -o protobuf:shared=True") + + for host, build in ((True, True), (True, False), (False, True), (False, False)): + c.save({"conanfile.py": consumer_recipe.format(host=host, build=build, scope=scope)}) + c.run("install .") + assert f"protobuf/1.0: MYOPTION: host-{host}" in c.out + assert f"protobuf/1.0: MYOPTION: build-{build}" in c.out + + +def test_different_options_values_recipe_priority(): + """ + consumer ---> mypkg ---> protobuf (library) + \\--(build)-> protobuf (protoc) + protobuf by default is a static library (shared=1) + "consumer" defines a protobuf:shared=3 value, that must be respected for HOST context + But build context, it is assigned by "mypkg", and build-require is private + """ + c = TestClient() + protobuf = textwrap.dedent(""" + from conans import ConanFile + class Proto(ConanFile): + options = {"shared": [1, 2, 3]} + default_options = {"shared": 1} + + def package_id(self): + self.output.info("MYOPTION: {}-{}".format(self.context, self.options.shared)) + """) + my_pkg = textwrap.dedent(""" + from conans import ConanFile + class Consumer(ConanFile): + def requirements(self): + self.requires("protobuf/1.0", options={"shared": 2}) + def build_requirements(self): + self.build_requires("protobuf/1.0", options={"shared": 2}) + """) + c.save({"protobuf/conanfile.py": protobuf, + "mypkg/conanfile.py": my_pkg, + "consumer/conanfile.py": GenConanfile().with_requires("mypkg/1.0") + .with_default_option("protobuf:shared", 3)}) + + c.run("create protobuf protobuf/1.0@ -o protobuf:shared=2") + c.run("create protobuf protobuf/1.0@ -o protobuf:shared=3") + c.run("create mypkg mypkg/1.0@") + + c.run("install consumer") + assert f"protobuf/1.0: MYOPTION: host-3" in c.out + assert f"protobuf/1.0: MYOPTION: build-2" in c.out