From a9343b58e070752b25ee2305e7875a87513f3e0f Mon Sep 17 00:00:00 2001 From: memsharded Date: Fri, 1 Aug 2025 13:58:01 +0200 Subject: [PATCH 1/7] investigating consumer flags per compiler --- conan/internal/graph/compatibility.py | 2 +- conan/internal/graph/compute_pid.py | 4 +- .../integration/package_id/compatible_test.py | 76 +++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/conan/internal/graph/compatibility.py b/conan/internal/graph/compatibility.py index e9131b81f0e..2ca319c1e38 100644 --- a/conan/internal/graph/compatibility.py +++ b/conan/internal/graph/compatibility.py @@ -159,7 +159,7 @@ def _compatible_infos(conanfile, compatibles): result = [] if compatibles: for elem in compatibles: - compat_info = conanfile.original_info.clone() + compat_info = conanfile.consumer_info.clone() compat_info.compatibility_delta = elem settings = elem.get("settings") if settings: diff --git a/conan/internal/graph/compute_pid.py b/conan/internal/graph/compute_pid.py index 4c4c6abb47b..bf735175045 100644 --- a/conan/internal/graph/compute_pid.py +++ b/conan/internal/graph/compute_pid.py @@ -48,13 +48,13 @@ def compute_package_id(node, modes, config_version, hook_manager): python_requires=python_requires, conf=conanfile.conf.copy_conaninfo_conf(), config_version=config_version.copy() if config_version else None) - conanfile.original_info = conanfile.info.clone() + conanfile.consumer_info = conanfile.info.clone() run_validate_package_id(conanfile, hook_manager) if conanfile.info.settings_target: # settings_target has beed added to conan package via package_id api - conanfile.original_info.settings_target = conanfile.info.settings_target + conanfile.consumer_info.settings_target = conanfile.info.settings_target info = conanfile.info node.package_id = info.package_id() diff --git a/test/integration/package_id/compatible_test.py b/test/integration/package_id/compatible_test.py index b224f2f8406..302c2bde945 100644 --- a/test/integration/package_id/compatible_test.py +++ b/test/integration/package_id/compatible_test.py @@ -724,3 +724,79 @@ def libc_compat(conanfile): tc.run("install --requires=dep/1.0 -s=libc_version=3 -s=compiler.cppstd=14") assert f"dep/1.0: Found compatible package '{dep_package_id}': compiler.cppstd=17, " \ f"libc_version=2" in tc.out + + +def test_compatible_flags(): + """ The compiler flags depends on the consumer settings, not on the binary compatible + settings used to create that compatible binary. This test shows how the new info + can be used to parameterize on the consumer settings + """ + c = TestClient(light=True) + conanfile = textwrap.dedent(""" + from conan import ConanFile + + class Pkg(ConanFile): + settings = "os" + + def compatibility(self): + if self.settings.os == "Windows": + return [{"settings": [("os", "Linux")]}] + + def package_info(self): + if self.consumer_info.settings.os == "Linux": + self.cpp_info.cxxflags = ["-mylinuxflag"] + elif self.consumer_info.settings.os == "Windows": + self.cpp_info.cxxflags = ["-mywinflag"] + """) + consumer = textwrap.dedent(""" + from conan import ConanFile + class Pkg(ConanFile): + requires = "pkg/0.1" + def generate(self): + flags = self.dependencies["pkg"].cpp_info.cxxflags + self.output.info(f"FLAGS: {flags}!!!") + """) + c.save({"pkg/conanfile.py": conanfile, + "consumer/conanfile.py": consumer}) + + c.run("create pkg --name=pkg --version=0.1 -s os=Linux") + + c.run("install consumer -s os=Linux") + assert "conanfile.py: FLAGS: ['-mylinuxflag']!!!" in c.out + c.run("install consumer -s os=Windows") + assert "conanfile.py: FLAGS: ['-mywinflag']!!!" in c.out + + +def test_compatible_flags_direct(): + """ same as above but without compatibility + """ + c = TestClient(light=True) + conanfile = textwrap.dedent(""" + from conan import ConanFile + + class Pkg(ConanFile): + settings = "os" + + def package_info(self): + if self.consumer_info.settings.os == "Linux": + self.cpp_info.cxxflags = ["-mylinuxflag"] + elif self.consumer_info.settings.os == "Windows": + self.cpp_info.cxxflags = ["-mywinflag"] + """) + consumer = textwrap.dedent(""" + from conan import ConanFile + class Pkg(ConanFile): + requires = "pkg/0.1" + def generate(self): + flags = self.dependencies["pkg"].cpp_info.cxxflags + self.output.info(f"FLAGS: {flags}!!!") + """) + c.save({"pkg/conanfile.py": conanfile, + "consumer/conanfile.py": consumer}) + + c.run("create pkg --name=pkg --version=0.1 -s os=Linux") + + c.run("install consumer -s os=Linux") + assert "conanfile.py: FLAGS: ['-mylinuxflag']!!!" in c.out + c.run("install consumer -s os=Windows -s pkg*:os=Linux") + assert "conanfile.py: FLAGS: ['-mywinflag']!!!" in c.out From e5052258e915d2338394a348fcb8a206c772ef2a Mon Sep 17 00:00:00 2001 From: memsharded Date: Mon, 25 Aug 2025 18:36:28 +0200 Subject: [PATCH 2/7] new approach of using directly the settings of the consumer node --- conan/internal/graph/compatibility.py | 2 +- conan/internal/graph/compute_pid.py | 4 +- conan/internal/graph/profile_node_definer.py | 3 + .../integration/package_id/compatible_test.py | 138 +++++++++--------- 4 files changed, 76 insertions(+), 71 deletions(-) diff --git a/conan/internal/graph/compatibility.py b/conan/internal/graph/compatibility.py index 2ca319c1e38..e9131b81f0e 100644 --- a/conan/internal/graph/compatibility.py +++ b/conan/internal/graph/compatibility.py @@ -159,7 +159,7 @@ def _compatible_infos(conanfile, compatibles): result = [] if compatibles: for elem in compatibles: - compat_info = conanfile.consumer_info.clone() + compat_info = conanfile.original_info.clone() compat_info.compatibility_delta = elem settings = elem.get("settings") if settings: diff --git a/conan/internal/graph/compute_pid.py b/conan/internal/graph/compute_pid.py index bf735175045..4c4c6abb47b 100644 --- a/conan/internal/graph/compute_pid.py +++ b/conan/internal/graph/compute_pid.py @@ -48,13 +48,13 @@ def compute_package_id(node, modes, config_version, hook_manager): python_requires=python_requires, conf=conanfile.conf.copy_conaninfo_conf(), config_version=config_version.copy() if config_version else None) - conanfile.consumer_info = conanfile.info.clone() + conanfile.original_info = conanfile.info.clone() run_validate_package_id(conanfile, hook_manager) if conanfile.info.settings_target: # settings_target has beed added to conan package via package_id api - conanfile.consumer_info.settings_target = conanfile.info.settings_target + conanfile.original_info.settings_target = conanfile.info.settings_target info = conanfile.info node.package_id = info.package_id() diff --git a/conan/internal/graph/profile_node_definer.py b/conan/internal/graph/profile_node_definer.py index b84cdd23199..2d294f50f91 100644 --- a/conan/internal/graph/profile_node_definer.py +++ b/conan/internal/graph/profile_node_definer.py @@ -50,6 +50,9 @@ def initialize_conanfile_profile(conanfile, profile_build, profile_host, base_co else: conanfile.settings_target = parent.settings_target.copy() + if parent is not None: + conanfile.settings_consumer = getattr(parent, "settings_consumer", parent.settings) + def _per_package_settings(conanfile, profile, ref): # Prepare the settings for the loaded conanfile diff --git a/test/integration/package_id/compatible_test.py b/test/integration/package_id/compatible_test.py index 302c2bde945..c966db3e2e8 100644 --- a/test/integration/package_id/compatible_test.py +++ b/test/integration/package_id/compatible_test.py @@ -726,77 +726,79 @@ def libc_compat(conanfile): f"libc_version=2" in tc.out -def test_compatible_flags(): - """ The compiler flags depends on the consumer settings, not on the binary compatible - settings used to create that compatible binary. This test shows how the new info - can be used to parameterize on the consumer settings - """ - c = TestClient(light=True) - conanfile = textwrap.dedent(""" - from conan import ConanFile - - class Pkg(ConanFile): - settings = "os" - - def compatibility(self): - if self.settings.os == "Windows": - return [{"settings": [("os", "Linux")]}] - - def package_info(self): - if self.consumer_info.settings.os == "Linux": - self.cpp_info.cxxflags = ["-mylinuxflag"] - elif self.consumer_info.settings.os == "Windows": - self.cpp_info.cxxflags = ["-mywinflag"] - """) - consumer = textwrap.dedent(""" - from conan import ConanFile - class Pkg(ConanFile): - requires = "pkg/0.1" - def generate(self): - flags = self.dependencies["pkg"].cpp_info.cxxflags - self.output.info(f"FLAGS: {flags}!!!") - """) - c.save({"pkg/conanfile.py": conanfile, - "consumer/conanfile.py": consumer}) +class TestCompatibleFlags: + def test_compatible_flags(self): + """ The compiler flags depends on the consumer settings, not on the binary compatible + settings used to create that compatible binary. This test shows how the new info + can be used to parameterize on the consumer settings + """ + c = TestClient(light=True) + conanfile = textwrap.dedent(""" + from conan import ConanFile - c.run("create pkg --name=pkg --version=0.1 -s os=Linux") + class Pkg(ConanFile): + settings = "os" - c.run("install consumer -s os=Linux") - assert "conanfile.py: FLAGS: ['-mylinuxflag']!!!" in c.out - c.run("install consumer -s os=Windows") - assert "conanfile.py: FLAGS: ['-mywinflag']!!!" in c.out + def compatibility(self): + if self.settings.os == "Windows": + return [{"settings": [("os", "Linux")]}] + def package_info(self): + if self.settings_consumer.os == "Linux": + self.cpp_info.cxxflags = ["-mylinuxflag"] + elif self.settings_consumer.os == "Windows": + self.cpp_info.cxxflags = ["-mywinflag"] + """) + consumer = textwrap.dedent(""" + from conan import ConanFile + class Pkg(ConanFile): + settings = "os" + requires = "pkg/0.1" + def generate(self): + flags = self.dependencies["pkg"].cpp_info.cxxflags + self.output.info(f"FLAGS: {flags}!!!") + """) + c.save({"pkg/conanfile.py": conanfile, + "consumer/conanfile.py": consumer}) -def test_compatible_flags_direct(): - """ same as above but without compatibility - """ - c = TestClient(light=True) - conanfile = textwrap.dedent(""" - from conan import ConanFile - - class Pkg(ConanFile): - settings = "os" - - def package_info(self): - if self.consumer_info.settings.os == "Linux": - self.cpp_info.cxxflags = ["-mylinuxflag"] - elif self.consumer_info.settings.os == "Windows": - self.cpp_info.cxxflags = ["-mywinflag"] - """) - consumer = textwrap.dedent(""" - from conan import ConanFile - class Pkg(ConanFile): - requires = "pkg/0.1" - def generate(self): - flags = self.dependencies["pkg"].cpp_info.cxxflags - self.output.info(f"FLAGS: {flags}!!!") - """) - c.save({"pkg/conanfile.py": conanfile, - "consumer/conanfile.py": consumer}) + c.run("create pkg --name=pkg --version=0.1 -s os=Linux") + + c.run("install consumer -s os=Linux") + assert "conanfile.py: FLAGS: ['-mylinuxflag']!!!" in c.out + c.run("install consumer -s os=Windows") + assert "conanfile.py: FLAGS: ['-mywinflag']!!!" in c.out + + def test_compatible_flags_direct(self): + """ same as above but without compatibility + """ + c = TestClient(light=True) + conanfile = textwrap.dedent(""" + from conan import ConanFile + + class Pkg(ConanFile): + settings = "os" + + def package_info(self): + if self.settings_consumer.os == "Linux": + self.cpp_info.cxxflags = ["-mylinuxflag"] + elif self.settings_consumer.os == "Windows": + self.cpp_info.cxxflags = ["-mywinflag"] + """) + consumer = textwrap.dedent(""" + from conan import ConanFile + class Pkg(ConanFile): + settings = "os" + requires = "pkg/0.1" + def generate(self): + flags = self.dependencies["pkg"].cpp_info.cxxflags + self.output.info(f"FLAGS: {flags}!!!") + """) + c.save({"pkg/conanfile.py": conanfile, + "consumer/conanfile.py": consumer}) - c.run("create pkg --name=pkg --version=0.1 -s os=Linux") + c.run("create pkg --name=pkg --version=0.1 -s os=Linux") - c.run("install consumer -s os=Linux") - assert "conanfile.py: FLAGS: ['-mylinuxflag']!!!" in c.out - c.run("install consumer -s os=Windows -s pkg*:os=Linux") - assert "conanfile.py: FLAGS: ['-mywinflag']!!!" in c.out + c.run("install consumer -s os=Linux") + assert "conanfile.py: FLAGS: ['-mylinuxflag']!!!" in c.out + c.run("install consumer -s os=Windows -s pkg*:os=Linux") + assert "conanfile.py: FLAGS: ['-mywinflag']!!!" in c.out From 3442c8ce9f5f2ae4dc4545a6f7323ff8b74a48d5 Mon Sep 17 00:00:00 2001 From: memsharded Date: Tue, 2 Sep 2025 14:32:27 +0200 Subject: [PATCH 3/7] explore new concept --- conan/internal/graph/profile_node_definer.py | 3 -- conan/internal/model/cpp_info.py | 11 +++++- .../integration/package_id/compatible_test.py | 37 ++++++++++--------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/conan/internal/graph/profile_node_definer.py b/conan/internal/graph/profile_node_definer.py index 2d294f50f91..b84cdd23199 100644 --- a/conan/internal/graph/profile_node_definer.py +++ b/conan/internal/graph/profile_node_definer.py @@ -50,9 +50,6 @@ def initialize_conanfile_profile(conanfile, profile_build, profile_host, base_co else: conanfile.settings_target = parent.settings_target.copy() - if parent is not None: - conanfile.settings_consumer = getattr(parent, "settings_consumer", parent.settings) - def _per_package_settings(conanfile, profile, ref): # Prepare the settings for the loaded conanfile diff --git a/conan/internal/model/cpp_info.py b/conan/internal/model/cpp_info.py index 3d6fab03f9a..566f8ec9da1 100644 --- a/conan/internal/model/cpp_info.py +++ b/conan/internal/model/cpp_info.py @@ -345,10 +345,19 @@ def cflags(self, value): @property def cxxflags(self): - if self._cxxflags is None: + if self._cxxflags is None or isinstance(self._cxxflags, dict): self._cxxflags = [] return self._cxxflags + def cxxflags_consumer(self, conanfile): + if not isinstance(self._cxxflags, dict): + return self._cxxflags + for k, v in self._cxxflags.items(): + # FIXME: Just POC for illustrating the UX and idea + if str(conanfile.settings.os) in k: + return v + return [] + @cxxflags.setter def cxxflags(self, value): self._cxxflags = value diff --git a/test/integration/package_id/compatible_test.py b/test/integration/package_id/compatible_test.py index c966db3e2e8..2530372c3f8 100644 --- a/test/integration/package_id/compatible_test.py +++ b/test/integration/package_id/compatible_test.py @@ -744,10 +744,8 @@ def compatibility(self): return [{"settings": [("os", "Linux")]}] def package_info(self): - if self.settings_consumer.os == "Linux": - self.cpp_info.cxxflags = ["-mylinuxflag"] - elif self.settings_consumer.os == "Windows": - self.cpp_info.cxxflags = ["-mywinflag"] + self.cpp_info.cxxflags = {"os=Linux": ["-mylinuxflag"], + "os=Windows": ["-mywinflag"]} """) consumer = textwrap.dedent(""" from conan import ConanFile @@ -755,7 +753,7 @@ class Pkg(ConanFile): settings = "os" requires = "pkg/0.1" def generate(self): - flags = self.dependencies["pkg"].cpp_info.cxxflags + flags = self.dependencies["pkg"].cpp_info.cxxflags_consumer(self) self.output.info(f"FLAGS: {flags}!!!") """) c.save({"pkg/conanfile.py": conanfile, @@ -771,18 +769,14 @@ def generate(self): def test_compatible_flags_direct(self): """ same as above but without compatibility """ - c = TestClient(light=True) + c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): - settings = "os" - def package_info(self): - if self.settings_consumer.os == "Linux": - self.cpp_info.cxxflags = ["-mylinuxflag"] - elif self.settings_consumer.os == "Windows": - self.cpp_info.cxxflags = ["-mywinflag"] + self.cpp_info.cxxflags = {"os=Linux": ["-mylinuxflag"], + "os=Windows": ["-mywinflag"]} """) consumer = textwrap.dedent(""" from conan import ConanFile @@ -790,15 +784,22 @@ class Pkg(ConanFile): settings = "os" requires = "pkg/0.1" def generate(self): - flags = self.dependencies["pkg"].cpp_info.cxxflags + flags = self.dependencies["pkg"].cpp_info.cxxflags_consumer(self) self.output.info(f"FLAGS: {flags}!!!") """) c.save({"pkg/conanfile.py": conanfile, "consumer/conanfile.py": consumer}) - c.run("create pkg --name=pkg --version=0.1 -s os=Linux") + c.run("create pkg --name=pkg --version=0.1") + c.run("export consumer --name=dep1 --version=0.1") + c.run("export consumer --name=dep2 --version=0.1") - c.run("install consumer -s os=Linux") - assert "conanfile.py: FLAGS: ['-mylinuxflag']!!!" in c.out - c.run("install consumer -s os=Windows -s pkg*:os=Linux") - assert "conanfile.py: FLAGS: ['-mywinflag']!!!" in c.out + c.run("install --requires=dep1/0.1 --requires=dep2/0.1 " + "-s os=Macos -s dep1/*:os=Linux -s dep2/*:os=Windows --build=missing") + assert "dep1/0.1: FLAGS: ['-mylinuxflag']!!!" in c.out + assert "dep2/0.1: FLAGS: ['-mywinflag']!!!" in c.out + + c.run("install --requires=dep1/0.1 --requires=dep2/0.1 " + "-s os=Macos -s dep1/*:os=Windows -s dep2/*:os=Linux --build=missing") + assert "dep1/0.1: FLAGS: ['-mywinflag']!!!" in c.out + assert "dep2/0.1: FLAGS: ['-mylinuxflag']!!!" in c.out From b048ed13c587a1c91a80afab8f3bce55e283fb28 Mon Sep 17 00:00:00 2001 From: memsharded Date: Tue, 23 Sep 2025 22:58:05 +0200 Subject: [PATCH 4/7] new approach with callables --- conan/internal/model/cpp_info.py | 14 ++--- .../integration/package_id/compatible_test.py | 62 ++++++++++++++----- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/conan/internal/model/cpp_info.py b/conan/internal/model/cpp_info.py index 566f8ec9da1..af92e287b52 100644 --- a/conan/internal/model/cpp_info.py +++ b/conan/internal/model/cpp_info.py @@ -345,18 +345,16 @@ def cflags(self, value): @property def cxxflags(self): - if self._cxxflags is None or isinstance(self._cxxflags, dict): + if self._cxxflags is None: self._cxxflags = [] + if callable(self._cxxflags): + return self._cxxflags() return self._cxxflags def cxxflags_consumer(self, conanfile): - if not isinstance(self._cxxflags, dict): - return self._cxxflags - for k, v in self._cxxflags.items(): - # FIXME: Just POC for illustrating the UX and idea - if str(conanfile.settings.os) in k: - return v - return [] + if not callable(self._cxxflags): + return self.cxxflags + return self._cxxflags(conanfile) @cxxflags.setter def cxxflags(self, value): diff --git a/test/integration/package_id/compatible_test.py b/test/integration/package_id/compatible_test.py index bb6e416847d..cee47c61d1a 100644 --- a/test/integration/package_id/compatible_test.py +++ b/test/integration/package_id/compatible_test.py @@ -727,12 +727,13 @@ def libc_compat(conanfile): class TestCompatibleFlags: + def test_compatible_flags(self): """ The compiler flags depends on the consumer settings, not on the binary compatible settings used to create that compatible binary. This test shows how the new info can be used to parameterize on the consumer settings """ - c = TestClient(light=True) + c = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile @@ -740,12 +741,20 @@ class Pkg(ConanFile): settings = "os" def compatibility(self): - if self.settings.os == "Windows": + if self.settings.os == "Windows" or self.settings.os == "Macos": return [{"settings": [("os", "Linux")]}] def package_info(self): - self.cpp_info.cxxflags = {"os=Linux": ["-mylinuxflag"], - "os=Windows": ["-mywinflag"]} + def myflags(conanfile=None): + if conanfile is None: + return ["-no-flags"] + if conanfile.settings.get_safe("os") == "Windows": + return ["-mywinflag"] + elif conanfile.settings.get_safe("os") == "Linux": + return ["-mylinuxflag"] + else: + return ["-other-os-flag"] + self.cpp_info.cxxflags = myflags """) consumer = textwrap.dedent(""" from conan import ConanFile @@ -753,8 +762,10 @@ class Pkg(ConanFile): settings = "os" requires = "pkg/0.1" def generate(self): + flags = self.dependencies["pkg"].cpp_info.cxxflags + self.output.info(f"FLAGS REGULAR: {flags}!!!") flags = self.dependencies["pkg"].cpp_info.cxxflags_consumer(self) - self.output.info(f"FLAGS: {flags}!!!") + self.output.info(f"FLAGS CONDITION: {flags}!!!") """) c.save({"pkg/conanfile.py": conanfile, "consumer/conanfile.py": consumer}) @@ -762,9 +773,14 @@ def generate(self): c.run("create pkg --name=pkg --version=0.1 -s os=Linux") c.run("install consumer -s os=Linux") - assert "conanfile.py: FLAGS: ['-mylinuxflag']!!!" in c.out + assert "conanfile.py: FLAGS REGULAR: ['-no-flags']!!!" in c.out + assert "conanfile.py: FLAGS CONDITION: ['-mylinuxflag']!!!" in c.out c.run("install consumer -s os=Windows") - assert "conanfile.py: FLAGS: ['-mywinflag']!!!" in c.out + assert "conanfile.py: FLAGS REGULAR: ['-no-flags']!!!" in c.out + assert "conanfile.py: FLAGS CONDITION: ['-mywinflag']!!!" in c.out + c.run("install consumer -s os=Macos") + assert "conanfile.py: FLAGS REGULAR: ['-no-flags']!!!" in c.out + assert "conanfile.py: FLAGS CONDITION: ['-other-os-flag']!!!" in c.out def test_compatible_flags_direct(self): """ same as above but without compatibility @@ -775,8 +791,16 @@ def test_compatible_flags_direct(self): class Pkg(ConanFile): def package_info(self): - self.cpp_info.cxxflags = {"os=Linux": ["-mylinuxflag"], - "os=Windows": ["-mywinflag"]} + def myflags(conanfile=None): + if conanfile is None: + return ["-no-flags"] + if conanfile.settings.get_safe("os") == "Windows": + return ["-mywinflag"] + elif conanfile.settings.get_safe("os") == "Linux": + return ["-mylinuxflag"] + else: + return ["-other-os-flag"] + self.cpp_info.cxxflags = myflags """) consumer = textwrap.dedent(""" from conan import ConanFile @@ -784,8 +808,10 @@ class Pkg(ConanFile): settings = "os" requires = "pkg/0.1" def generate(self): + flags = self.dependencies["pkg"].cpp_info.cxxflags + self.output.info(f"FLAGS REGULAR: {flags}!!!") flags = self.dependencies["pkg"].cpp_info.cxxflags_consumer(self) - self.output.info(f"FLAGS: {flags}!!!") + self.output.info(f"FLAGS CONDITION: {flags}!!!") """) c.save({"pkg/conanfile.py": conanfile, "consumer/conanfile.py": consumer}) @@ -796,10 +822,18 @@ def generate(self): c.run("install --requires=dep1/0.1 --requires=dep2/0.1 " "-s os=Macos -s dep1/*:os=Linux -s dep2/*:os=Windows --build=missing") - assert "dep1/0.1: FLAGS: ['-mylinuxflag']!!!" in c.out - assert "dep2/0.1: FLAGS: ['-mywinflag']!!!" in c.out + assert "dep1/0.1: FLAGS REGULAR: ['-no-flags']!!!" in c.out + assert "dep1/0.1: FLAGS CONDITION: ['-mylinuxflag']!!!" in c.out + assert "dep2/0.1: FLAGS CONDITION: ['-mywinflag']!!!" in c.out c.run("install --requires=dep1/0.1 --requires=dep2/0.1 " "-s os=Macos -s dep1/*:os=Windows -s dep2/*:os=Linux --build=missing") - assert "dep1/0.1: FLAGS: ['-mywinflag']!!!" in c.out - assert "dep2/0.1: FLAGS: ['-mylinuxflag']!!!" in c.out + assert "dep1/0.1: FLAGS REGULAR: ['-no-flags']!!!" in c.out + assert "dep1/0.1: FLAGS CONDITION: ['-mywinflag']!!!" in c.out + assert "dep2/0.1: FLAGS CONDITION: ['-mylinuxflag']!!!" in c.out + + c.run("install --requires=dep1/0.1 --requires=dep2/0.1 " + "-s os=Macos --build=missing") + assert "dep1/0.1: FLAGS REGULAR: ['-no-flags']!!!" in c.out + assert "dep1/0.1: FLAGS CONDITION: ['-other-os-flag']!!!" in c.out + assert "dep2/0.1: FLAGS CONDITION: ['-other-os-flag']!!!" in c.out From c6a5fa1cd550d7b4703f1fb062a8fc66ac8ddb3b Mon Sep 17 00:00:00 2001 From: memsharded Date: Wed, 24 Sep 2025 13:11:09 +0200 Subject: [PATCH 5/7] new approach with callables and transparent --- conan/internal/model/conanfile_interface.py | 4 +- conan/internal/model/cpp_info.py | 30 ++++++++--- conan/internal/model/dependencies.py | 2 +- .../integration/package_id/compatible_test.py | 51 +++++++++---------- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/conan/internal/model/conanfile_interface.py b/conan/internal/model/conanfile_interface.py index 76aed619e1f..8208809dd11 100644 --- a/conan/internal/model/conanfile_interface.py +++ b/conan/internal/model/conanfile_interface.py @@ -10,8 +10,9 @@ class ConanFileInterface: def __str__(self): return str(self._conanfile) - def __init__(self, conanfile): + def __init__(self, conanfile, consumer): self._conanfile = conanfile + self._consumer = consumer def __eq__(self, other): """ @@ -70,6 +71,7 @@ def runenv_info(self): @property def cpp_info(self): + self._conanfile.cpp_info.set_consumer(self._consumer) return self._conanfile.cpp_info @property diff --git a/conan/internal/model/cpp_info.py b/conan/internal/model/cpp_info.py index af92e287b52..f9137c16060 100644 --- a/conan/internal/model/cpp_info.py +++ b/conan/internal/model/cpp_info.py @@ -104,6 +104,14 @@ def __init__(self, set_defaults=False): self._location = None self._link_location = None + # consumer conditional evaluation + self._consumer = None + + def set_consumer(self, consumer_conanfile): + # This might have race-conditions if consumers generate() of the same graph was + # concurrent, but it is not + self._consumer = consumer_conanfile + def serialize(self): return { "includedirs": self._includedirs, @@ -335,6 +343,8 @@ def defines(self, value): @property def cflags(self): + if callable(self._cflags): + return self._cflags(self._consumer) if self._cflags is None: self._cflags = [] return self._cflags @@ -345,23 +355,20 @@ def cflags(self, value): @property def cxxflags(self): + if callable(self._cxxflags): + return self._cxxflags(self._consumer) if self._cxxflags is None: self._cxxflags = [] - if callable(self._cxxflags): - return self._cxxflags() return self._cxxflags - def cxxflags_consumer(self, conanfile): - if not callable(self._cxxflags): - return self.cxxflags - return self._cxxflags(conanfile) - @cxxflags.setter def cxxflags(self, value): self._cxxflags = value @property def sharedlinkflags(self): + if callable(self._sharedlinkflags): + return self._sharedlinkflags(self._consumer) if self._sharedlinkflags is None: self._sharedlinkflags = [] return self._sharedlinkflags @@ -372,6 +379,8 @@ def sharedlinkflags(self, value): @property def exelinkflags(self): + if callable(self._exelinkflags): + return self._exelinkflags(self._consumer) if self._exelinkflags is None: self._exelinkflags = [] return self._exelinkflags @@ -680,6 +689,13 @@ def __init__(self, set_defaults=False): self.default_components = None self._package = _Component(set_defaults) + def set_consumer(self, consumer_conanfile): + # This might have race-conditions if consumers generate() of the same graph was + # concurrent, but it is not + self._package.set_consumer(consumer_conanfile) + for c in self.components.values(): + c.set_consumer(consumer_conanfile) + def __getattr__(self, attr): # all cpp_info.xxx of not defined things will go to the global package return getattr(self._package, attr) diff --git a/conan/internal/model/dependencies.py b/conan/internal/model/dependencies.py index b830e04a0bb..eb6bc0325cf 100644 --- a/conan/internal/model/dependencies.py +++ b/conan/internal/model/dependencies.py @@ -89,7 +89,7 @@ class ConanFileDependencies(UserRequirementsDict): @staticmethod def from_node(node): - d = OrderedDict((require, ConanFileInterface(transitive.node.conanfile)) + d = OrderedDict((require, ConanFileInterface(transitive.node.conanfile, node.conanfile)) for require, transitive in node.transitive_deps.items()) if node.replaced_requires: cant_be_removed = set() diff --git a/test/integration/package_id/compatible_test.py b/test/integration/package_id/compatible_test.py index cee47c61d1a..be022d4ca89 100644 --- a/test/integration/package_id/compatible_test.py +++ b/test/integration/package_id/compatible_test.py @@ -745,9 +745,7 @@ def compatibility(self): return [{"settings": [("os", "Linux")]}] def package_info(self): - def myflags(conanfile=None): - if conanfile is None: - return ["-no-flags"] + def myflags(conanfile): if conanfile.settings.get_safe("os") == "Windows": return ["-mywinflag"] elif conanfile.settings.get_safe("os") == "Linux": @@ -755,6 +753,9 @@ def myflags(conanfile=None): else: return ["-other-os-flag"] self.cpp_info.cxxflags = myflags + self.cpp_info.cflags = myflags + self.cpp_info.sharedlinkflags = myflags + self.cpp_info.exelinkflags = myflags """) consumer = textwrap.dedent(""" from conan import ConanFile @@ -762,10 +763,11 @@ class Pkg(ConanFile): settings = "os" requires = "pkg/0.1" def generate(self): - flags = self.dependencies["pkg"].cpp_info.cxxflags - self.output.info(f"FLAGS REGULAR: {flags}!!!") - flags = self.dependencies["pkg"].cpp_info.cxxflags_consumer(self) - self.output.info(f"FLAGS CONDITION: {flags}!!!") + cpp_info = self.dependencies["pkg"].cpp_info + self.output.info(f"CXXFLAGS: {cpp_info.cxxflags}!!!") + self.output.info(f"CFLAGS: {cpp_info.cflags}!!!") + self.output.info(f"EXEFLAGS: {cpp_info.exelinkflags}!!!") + self.output.info(f"SHAREDFLAGS: {cpp_info.sharedlinkflags}!!!") """) c.save({"pkg/conanfile.py": conanfile, "consumer/conanfile.py": consumer}) @@ -773,14 +775,14 @@ def generate(self): c.run("create pkg --name=pkg --version=0.1 -s os=Linux") c.run("install consumer -s os=Linux") - assert "conanfile.py: FLAGS REGULAR: ['-no-flags']!!!" in c.out - assert "conanfile.py: FLAGS CONDITION: ['-mylinuxflag']!!!" in c.out + for f in ("CXXFLAGS", "CFLAGS", "EXEFLAGS", "SHAREDFLAGS"): + assert f"conanfile.py: {f}: ['-mylinuxflag']!!!" in c.out c.run("install consumer -s os=Windows") - assert "conanfile.py: FLAGS REGULAR: ['-no-flags']!!!" in c.out - assert "conanfile.py: FLAGS CONDITION: ['-mywinflag']!!!" in c.out + for f in ("CXXFLAGS", "CFLAGS", "EXEFLAGS", "SHAREDFLAGS"): + assert f"conanfile.py: {f}: ['-mywinflag']!!!" in c.out c.run("install consumer -s os=Macos") - assert "conanfile.py: FLAGS REGULAR: ['-no-flags']!!!" in c.out - assert "conanfile.py: FLAGS CONDITION: ['-other-os-flag']!!!" in c.out + for f in ("CXXFLAGS", "CFLAGS", "EXEFLAGS", "SHAREDFLAGS"): + assert f"conanfile.py: {f}: ['-other-os-flag']!!!" in c.out def test_compatible_flags_direct(self): """ same as above but without compatibility @@ -791,9 +793,7 @@ def test_compatible_flags_direct(self): class Pkg(ConanFile): def package_info(self): - def myflags(conanfile=None): - if conanfile is None: - return ["-no-flags"] + def myflags(conanfile): if conanfile.settings.get_safe("os") == "Windows": return ["-mywinflag"] elif conanfile.settings.get_safe("os") == "Linux": @@ -809,9 +809,7 @@ class Pkg(ConanFile): requires = "pkg/0.1" def generate(self): flags = self.dependencies["pkg"].cpp_info.cxxflags - self.output.info(f"FLAGS REGULAR: {flags}!!!") - flags = self.dependencies["pkg"].cpp_info.cxxflags_consumer(self) - self.output.info(f"FLAGS CONDITION: {flags}!!!") + self.output.info(f"FLAGS: {flags}!!!") """) c.save({"pkg/conanfile.py": conanfile, "consumer/conanfile.py": consumer}) @@ -822,18 +820,15 @@ def generate(self): c.run("install --requires=dep1/0.1 --requires=dep2/0.1 " "-s os=Macos -s dep1/*:os=Linux -s dep2/*:os=Windows --build=missing") - assert "dep1/0.1: FLAGS REGULAR: ['-no-flags']!!!" in c.out - assert "dep1/0.1: FLAGS CONDITION: ['-mylinuxflag']!!!" in c.out - assert "dep2/0.1: FLAGS CONDITION: ['-mywinflag']!!!" in c.out + assert "dep1/0.1: FLAGS: ['-mylinuxflag']!!!" in c.out + assert "dep2/0.1: FLAGS: ['-mywinflag']!!!" in c.out c.run("install --requires=dep1/0.1 --requires=dep2/0.1 " "-s os=Macos -s dep1/*:os=Windows -s dep2/*:os=Linux --build=missing") - assert "dep1/0.1: FLAGS REGULAR: ['-no-flags']!!!" in c.out - assert "dep1/0.1: FLAGS CONDITION: ['-mywinflag']!!!" in c.out - assert "dep2/0.1: FLAGS CONDITION: ['-mylinuxflag']!!!" in c.out + assert "dep1/0.1: FLAGS: ['-mywinflag']!!!" in c.out + assert "dep2/0.1: FLAGS: ['-mylinuxflag']!!!" in c.out c.run("install --requires=dep1/0.1 --requires=dep2/0.1 " "-s os=Macos --build=missing") - assert "dep1/0.1: FLAGS REGULAR: ['-no-flags']!!!" in c.out - assert "dep1/0.1: FLAGS CONDITION: ['-other-os-flag']!!!" in c.out - assert "dep2/0.1: FLAGS CONDITION: ['-other-os-flag']!!!" in c.out + assert "dep1/0.1: FLAGS: ['-other-os-flag']!!!" in c.out + assert "dep2/0.1: FLAGS: ['-other-os-flag']!!!" in c.out From 7f73190b65028409f36ddbb86cc0466f5153a000 Mon Sep 17 00:00:00 2001 From: memsharded Date: Wed, 24 Sep 2025 13:21:57 +0200 Subject: [PATCH 6/7] adding lambda oneliner test --- .../integration/package_id/compatible_test.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/integration/package_id/compatible_test.py b/test/integration/package_id/compatible_test.py index be022d4ca89..44d7142ca74 100644 --- a/test/integration/package_id/compatible_test.py +++ b/test/integration/package_id/compatible_test.py @@ -784,6 +784,39 @@ def generate(self): for f in ("CXXFLAGS", "CFLAGS", "EXEFLAGS", "SHAREDFLAGS"): assert f"conanfile.py: {f}: ['-other-os-flag']!!!" in c.out + def test_simple_lambda(self): + """ same as above, but more compact condition + """ + c = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.microsoft import is_msvc + + class Pkg(ConanFile): + def package_info(self): + self.cpp_info.cxxflags = lambda c: ["-mywinflag"] if is_msvc(c) else [] + """) + consumer = textwrap.dedent(""" + from conan import ConanFile + class Pkg(ConanFile): + settings = "compiler" + requires = "pkg/0.1" + def generate(self): + cpp_info = self.dependencies["pkg"].cpp_info + self.output.info(f"CXXFLAGS: {cpp_info.cxxflags}!!!") + """) + c.save({"pkg/conanfile.py": conanfile, + "consumer/conanfile.py": consumer}) + + settings = "-s compiler=msvc -s compiler.version=193 -s compiler.runtime=dynamic" + c.run(f"create pkg --name=pkg --version=0.1 {settings}") + + c.run(f"install consumer {settings}") + assert f"conanfile.py: CXXFLAGS: ['-mywinflag']!!!" in c.out + c.run(f"install consumer {settings} -s &:compiler=clang -s &:compiler.version=19") + assert f"conanfile.py: CXXFLAGS: []!!!" in c.out + + def test_compatible_flags_direct(self): """ same as above but without compatibility """ From 7e26d2de0267562292bdb12c634c9d7e33b83e05 Mon Sep 17 00:00:00 2001 From: memsharded Date: Thu, 6 Nov 2025 10:30:30 +0100 Subject: [PATCH 7/7] wip --- conan/internal/model/cpp_info.py | 57 +++++++++++-------- .../cmake/cmakedeps2/target_configuration.py | 24 ++++---- .../integration/package_id/compatible_test.py | 35 +++++++++++- 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/conan/internal/model/cpp_info.py b/conan/internal/model/cpp_info.py index ae756e8ca98..119eb29333e 100644 --- a/conan/internal/model/cpp_info.py +++ b/conan/internal/model/cpp_info.py @@ -89,6 +89,8 @@ def __init__(self, set_defaults=False): self._sysroot = None self._requires = None + self._consumer_conanfile = None + # LEGACY 1.X fields, can be removed in 2.X self.names = MockInfoProperty("cpp_info.names") self.filenames = MockInfoProperty("cpp_info.filenames") @@ -104,6 +106,9 @@ def __init__(self, set_defaults=False): self._location = None self._link_location = None + def set_consumer(self, conanfile): + self._consumer_conanfile = conanfile + def serialize(self): return { "includedirs": self._includedirs, @@ -335,60 +340,64 @@ def defines(self, value): @property def cflags(self): - if self._cflags is None or callable(self._cflags): + if self._cflags is None: self._cflags = [] + elif callable(self._cflags): + if self._consumer_conanfile is None: + ConanOutput().warning(f"Callable for cflags: {self._cflags}, but generator " + "didn't call 'set_consumer()' first") + return [] + return self._cflags(self._consumer_conanfile) return self._cflags - def cflags_consumer(self, consumer): - if callable(self._cflags): - return self._cflags(consumer) - return self.cflags - @cflags.setter def cflags(self, value): self._cflags = value @property def cxxflags(self): - if self._cxxflags is None or callable(self._cxxflags): # To not break + if self._cxxflags is None: self._cxxflags = [] + elif callable(self._cxxflags): + if self._consumer_conanfile is None: + ConanOutput().warning(f"Callable for cxxflags: {self._cxxflags}, but generator " + "didn't call 'set_consumer()' first") + return [] + return self._cxxflags(self._consumer_conanfile) return self._cxxflags - def cxxflags_consumer(self, consumer): - if callable(self._cxxflags): - return self._cxxflags(consumer) - return self.cxxflags - @cxxflags.setter def cxxflags(self, value): self._cxxflags = value @property def sharedlinkflags(self): - if self._sharedlinkflags is None or callable(self._sharedlinkflags): + if self._sharedlinkflags is None: self._sharedlinkflags = [] + elif callable(self._sharedlinkflags): + if self._consumer_conanfile is None: + ConanOutput().warning(f"Callable for sharedlinkflags: {self._sharedlinkflags}, but " + f"generator didn't call 'set_consumer()' first") + return [] + return self._sharedlinkflags(self._consumer_conanfile) return self._sharedlinkflags - def sharedlinkflags_consumer(self, consumer): - if callable(self._sharedlinkflags): - return self._sharedlinkflags(consumer) - return self.sharedlinkflags - @sharedlinkflags.setter def sharedlinkflags(self, value): self._sharedlinkflags = value @property def exelinkflags(self): - if self._exelinkflags is None or callable(self._exelinkflags): + if self._exelinkflags is None: self._exelinkflags = [] + elif callable(self._exelinkflags): + if self._consumer_conanfile is None: + ConanOutput().warning(f"Callable for exelinkflags: {self._exelinkflags}, but " + f"generator didn't call 'set_consumer()' first") + return [] + return self._exelinkflags(self._consumer_conanfile) return self._exelinkflags - def exelinkflags_consumer(self, consumer): - if callable(self._exelinkflags): - return self._exelinkflags(consumer) - return self.exelinkflags - @exelinkflags.setter def exelinkflags(self, value): self._exelinkflags = value diff --git a/conan/tools/cmake/cmakedeps2/target_configuration.py b/conan/tools/cmake/cmakedeps2/target_configuration.py index 4d96c3f4f3b..6b57a59d6f5 100644 --- a/conan/tools/cmake/cmakedeps2/target_configuration.py +++ b/conan/tools/cmake/cmakedeps2/target_configuration.py @@ -187,26 +187,22 @@ def _get_cmake_lib(self, info, components, pkg_folder, pkg_folder_var, comp_name for i in info.includedirs) if info.includedirs else "" requires = self._requires(info, components) assert isinstance(requires, dict) - extra_libs = self._cmakedeps.get_property("cmake_extra_interface_libs", self._conanfile, - comp_name=comp_name, check_type=list) or [] - sources = [self._path(source, pkg_folder, pkg_folder_var) for source in info.sources] - consumer = self._cmakedeps._conanfile # noqa - cflags = info.cflags_consumer(consumer) - cxxflags = info.cxxflags_consumer(consumer) - sharedlinkflags = info.sharedlinkflags_consumer(consumer) - exelinkflags = info.exelinkflags_consumer(consumer) - defines = info.defines_consumer(consumer) + defines = " ".join(cmake_escape_value(f) for f in info.defines) # FIXME: Filter by lib traits!!!!! if not self._require.headers: # If not depending on headers, paths and includedirs = defines = None + extra_libs = self._cmakedeps.get_property("cmake_extra_interface_libs", self._conanfile, + comp_name=comp_name, check_type=list) or [] + sources = [self._path(source, pkg_folder, pkg_folder_var) for source in info.sources] + info.set_consumer(self._cmakedeps._conanfile) # noqa target = {"type": "INTERFACE", "includedirs": includedirs, - "defines": " ".join(cmake_escape_value(f) for f in defines or []), + "defines": defines, "requires": requires, - "cxxflags": " ".join(cmake_escape_value(f) for f in cxxflags), - "cflags": " ".join(cmake_escape_value(f) for f in cflags), - "sharedlinkflags": " ".join(cmake_escape_value(v) for v in sharedlinkflags), - "exelinkflags": " ".join(cmake_escape_value(v) for v in exelinkflags), + "cxxflags": " ".join(cmake_escape_value(f) for f in info.cxxflags), + "cflags": " ".join(cmake_escape_value(f) for f in info.cflags), + "sharedlinkflags": " ".join(cmake_escape_value(v) for v in info.sharedlinkflags), + "exelinkflags": " ".join(cmake_escape_value(v) for v in info.exelinkflags), "system_libs": " ".join(info.system_libs + extra_libs), "sources": " ".join(sources) } diff --git a/test/integration/package_id/compatible_test.py b/test/integration/package_id/compatible_test.py index 5f1750c8f15..9d35694ed05 100644 --- a/test/integration/package_id/compatible_test.py +++ b/test/integration/package_id/compatible_test.py @@ -808,7 +808,8 @@ class Pkg(ConanFile): requires = "pkg/0.1" def generate(self): cpp_info = self.dependencies["pkg"].cpp_info - self.output.info(f"CXXFLAGS: {cpp_info.cxxflags_consumer(self)}!!!") + cpp_info.set_consumer(self) + self.output.info(f"CXXFLAGS: {cpp_info.cxxflags}!!!") """) c.save({"pkg/conanfile.py": conanfile, "consumer/conanfile.py": consumer}) @@ -845,8 +846,9 @@ class Pkg(ConanFile): settings = "os" requires = "pkg/0.1" def generate(self): - flags = self.dependencies["pkg"].cpp_info.cxxflags_consumer(self) - self.output.info(f"FLAGS: {flags}!!!") + cpp_info = self.dependencies["pkg"].cpp_info + cpp_info.set_consumer(self) + self.output.info(f"FLAGS: {cpp_info.cxxflags}!!!") """) c.save({"pkg/conanfile.py": conanfile, "consumer/conanfile.py": consumer}) @@ -870,6 +872,33 @@ def generate(self): assert "dep1/0.1: FLAGS: ['-other-os-flag']!!!" in c.out assert "dep2/0.1: FLAGS: ['-other-os-flag']!!!" in c.out + def test_warnings(self): + c = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + class Pkg(ConanFile): + def package_info(self): + def myflags(conanfile): + return ["-myflag"] + self.cpp_info.cxxflags = myflags + """) + consumer = textwrap.dedent(""" + from conan import ConanFile + class Pkg(ConanFile): + requires = "pkg/0.1" + def generate(self): + cpp_info = self.dependencies["pkg"].cpp_info + self.output.info(f"FLAGS: {cpp_info.cxxflags}!!!") + """) + c.save({"pkg/conanfile.py": conanfile, + "consumer/conanfile.py": consumer}) + + c.run("create pkg --name=pkg --version=0.1") + c.run("install consumer") + print(c.out) + assert "WARN: Callable for cxxflags:" in c.out + assert "FLAGS: []!!!" in c.out + def test_compatibility_remove_cppstd(): """ This test tries to reflect the following scenario: