Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a9343b5
investigating consumer flags per compiler
memsharded Aug 1, 2025
d98c7c5
Merge branch 'develop2' into fix/package_info_consumer_flags
memsharded Aug 25, 2025
e505225
new approach of using directly the settings of the consumer node
memsharded Aug 25, 2025
8f6c909
Merge branch 'develop2' into fix/package_info_consumer_flags
memsharded Sep 2, 2025
3442c8c
explore new concept
memsharded Sep 2, 2025
46609d6
Merge branch 'develop2' into fix/package_info_consumer_flags
memsharded Sep 23, 2025
b048ed1
new approach with callables
memsharded Sep 23, 2025
4f6fb7e
Merge branch 'develop2' into fix/package_info_consumer_flags
memsharded Sep 24, 2025
c6a5fa1
new approach with callables and transparent
memsharded Sep 24, 2025
7f73190
adding lambda oneliner test
memsharded Sep 24, 2025
662e0c8
wip
memsharded Oct 19, 2025
1f89034
wip
memsharded Nov 4, 2025
558f81c
Merge branch 'develop2' into fix/package_info_consumer_flags
memsharded Nov 6, 2025
7e26d2d
wip
memsharded Nov 6, 2025
1207017
wip
memsharded Nov 6, 2025
e960419
Fix assert when finding compatible binaries of a package that exists …
AbrilRBS Nov 7, 2025
76f10cb
wip
memsharded Dec 9, 2025
bf103d3
wip
memsharded Dec 9, 2025
61c34c6
wip
memsharded Dec 10, 2025
f46e3cf
Merge branch 'develop2' into fix/package_info_consumer_flags
memsharded Dec 11, 2025
aae6e14
fix
memsharded Dec 11, 2025
d48211f
wip
memsharded Jan 19, 2026
6b3375b
test json generation
memsharded Jan 19, 2026
f1dd720
Merge branch 'develop2' into fix/package_info_consumer_flags
memsharded Jan 30, 2026
26cb150
wip
memsharded Jan 30, 2026
5319577
wip
memsharded Jan 31, 2026
a06a365
wip
memsharded Jan 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion conan/internal/model/conanfile_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,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):
"""
Expand Down Expand Up @@ -71,6 +72,8 @@ def runenv_info(self):

@property
def cpp_info(self):
# At the moment, not doing a full copy, not necessary as access is not concurrent
self._conanfile.cpp_info.set_consumer(self._consumer)
return self._conanfile.cpp_info

@property
Expand Down
71 changes: 67 additions & 4 deletions conan/internal/model/cpp_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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,
Expand Down Expand Up @@ -133,6 +138,35 @@ def serialize(self):
"languages": self._languages
}

@staticmethod
def _evaluate_cond(definitions, conanfile):
"""
Evaluate conditional definitions against the consumer conanfile.
definitions: dict with a single key (e.g. "settings.os", "options.shared") mapping to
either a dict of value->nested_definitions or value->list (final result).
Returns the list of items that match the consumer's settings/options.
"""
assert isinstance(definitions, dict)
assert len(definitions) == 1, definitions
condition, values = next(iter(definitions.items()))
# One condition key per level (e.g. "settings.os")

assert isinstance(values, dict)
if condition.startswith("settings."):
setting_key = condition[len("settings."):]
value = conanfile.settings.get_safe(setting_key)
else:
assert condition.startswith("options.")
option_key = condition[len("options."):]
value = conanfile.options.get_safe(option_key)

result = values.get(value) or values.get("*")
if isinstance(result, list):
return result
if isinstance(result, dict):
return _Component._evaluate_cond(result, conanfile)
return []

@staticmethod
def deserialize(contents):
result = _Component()
Expand Down Expand Up @@ -335,6 +369,8 @@ def defines(self, value):

@property
def cflags(self):
if isinstance(self._cflags, dict):
return self._evaluate_cond(self._cflags, self._consumer_conanfile)
if self._cflags is None:
self._cflags = []
return self._cflags
Expand All @@ -345,6 +381,8 @@ def cflags(self, value):

@property
def cxxflags(self):
if isinstance(self._cxxflags, dict):
return self._evaluate_cond(self._cxxflags, self._consumer_conanfile)
if self._cxxflags is None:
self._cxxflags = []
return self._cxxflags
Expand All @@ -355,6 +393,8 @@ def cxxflags(self, value):

@property
def sharedlinkflags(self):
if isinstance(self._sharedlinkflags, dict):
return self._evaluate_cond(self._sharedlinkflags, self._consumer_conanfile)
if self._sharedlinkflags is None:
self._sharedlinkflags = []
return self._sharedlinkflags
Expand All @@ -365,6 +405,8 @@ def sharedlinkflags(self, value):

@property
def exelinkflags(self):
if isinstance(self._exelinkflags, dict):
return self._evaluate_cond(self._exelinkflags, self._consumer_conanfile)
if self._exelinkflags is None:
self._exelinkflags = []
return self._exelinkflags
Expand Down Expand Up @@ -447,8 +489,8 @@ def get_init(self, attribute, default):

def merge(self, other, overwrite=False):
"""
@param overwrite:
@type other: _Component
:param overwrite:
:type other: _Component
"""
def merge_list(o, d):
d.extend(e for e in o if e not in d)
Expand All @@ -457,8 +499,24 @@ def merge_list(o, d):
other_values = getattr(other, varname)
if other_values is not None:
if not overwrite:
current_values = self.get_init(varname, [])
merge_list(other_values, current_values)
if other._consumer_conanfile is None: # package_info() editable merge
assert self._consumer_conanfile is None
if isinstance(other_values, dict) or isinstance(getattr(self, varname), dict):
# overwite, dicts cannot be merged
setattr(self, varname, other_values)
else:
current_values = self.get_init(varname, [])
merge_list(other_values, current_values)
else: # component aggregation merge, lists can be evaluated
if isinstance(other_values, dict):
other_values = self._evaluate_cond(other_values,
other._consumer_conanfile)
current_values = getattr(self, varname) or []
if isinstance(current_values, dict):
current_values = self._evaluate_cond(current_values,
self._consumer_conanfile)
merge_list(other_values, current_values)
setattr(self, varname, current_values)
else:
setattr(self, varname, other_values)

Expand Down Expand Up @@ -673,6 +731,11 @@ def __init__(self, set_defaults=False):
self.default_components = None
self._package = _Component(set_defaults)

def set_consumer(self, conanfile):
self._package.set_consumer(conanfile)
for comp in self.components.values():
comp.set_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)
Expand Down
2 changes: 1 addition & 1 deletion conan/internal/model/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
195 changes: 195 additions & 0 deletions test/integration/package_id/compatible_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -963,3 +963,198 @@ def no_cppstd_compat(conanfile):
# Now we try again, this time app will find the compatible dep without cppstd
tc.run("install --requires=dep/1.0 -pr=profile -s=compiler.cppstd=17")
assert f"dep/1.0: Found compatible package '{dep_package_id}'" in tc.out


class TestCompatibleFlags:

@pytest.mark.parametrize("components", [True, False])
def test_compatible_flags(self, components):
""" 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()
conanfile = textwrap.dedent("""
from conan import ConanFile

class Pkg(ConanFile):
settings = "os"

def compatibility(self):
if self.settings.os == "Windows" or self.settings.os == "Macos":
return [{"settings": [("os", "Linux")]}]

def package_info(self):
myflags = {
"settings.os": {
"Windows": ["-mywinflag"],
"Linux": ["-mylinuxflag"],
"*": ["-other-os-flag"]
}
}
self.cpp_info.cxxflags = myflags
self.cpp_info.cflags = myflags
self.cpp_info.sharedlinkflags = myflags
self.cpp_info.exelinkflags = myflags
""")
if components:
conanfile = conanfile.replace(".cpp_info.", ".cpp_info.components['mycomp'].")

c.save({"pkg/conanfile.py": conanfile,
"consumer/conanfile.txt": "[requires]\npkg/0.1\n[generators]\nCMakeConfigDeps"})

c.run("create pkg --name=pkg --version=0.1 -s os=Linux --format=json")
pkg_json = json.loads(c.stdout)
expected_serial = {
"settings.os": {
"Windows": ["-mywinflag"],
"Linux": ["-mylinuxflag"],
"*": ["-other-os-flag"]
}
}
if not components:
assert pkg_json["graph"]["nodes"]["1"]["cpp_info"]["root"]["cflags"] == expected_serial

def _check(flag, cmake_file):
assert f"$<$<COMPILE_LANGUAGE:CXX>:$<$<CONFIG:RELEASE>:{flag}>>" in cmake_file
assert f"$<$<COMPILE_LANGUAGE:C>:$<$<CONFIG:RELEASE>:{flag}>>" in cmake_file
assert (f"$<$<STREQUAL:$<TARGET_PROPERTY:TYPE>,SHARED_LIBRARY>:"
f"$<$<CONFIG:RELEASE>:{flag}>>") in cmake_file
assert (f"$<$<STREQUAL:$<TARGET_PROPERTY:TYPE>,EXECUTABLE>:"
f"$<$<CONFIG:RELEASE>:{flag}>>") in cmake_file

c.run("install consumer -s os=Linux")
cmake = c.load("consumer/pkg-Targets-release.cmake")
_check("-mylinuxflag", cmake)

c.run("install consumer -s os=Windows --format=json")
pkg_json = json.loads(c.stdout)
if not components:
assert pkg_json["graph"]["nodes"]["1"]["cpp_info"]["root"]["cflags"] == expected_serial
cmake = c.load("consumer/pkg-Targets-release.cmake")
_check("-mywinflag", cmake)

c.run("install consumer -s os=Macos")
cmake = c.load("consumer/pkg-Targets-release.cmake")
_check("-other-os-flag", cmake)

# Check old CMakeDeps generator
c.run("install consumer -s os=Linux -s arch=x86_64 -g CMakeDeps")
cmake = c.load("consumer/pkg-release-x86_64-data.cmake")
assert "-mylinuxflag" in cmake

def test_editable(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 layout(self):
self.cpp.source.cxxflags = {"settings.compiler": {"msvc": ["-mywineditflag"]}}

def package_info(self):
self.cpp_info.cxxflags = {"settings.compiler": {"msvc": ["-mywinflag"]}}
""")
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"editable add pkg --name=pkg --version=0.1")

c.run(f"install consumer {settings}")
assert f"conanfile.py: CXXFLAGS: ['-mywineditflag']!!!" 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_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 = {"settings.compiler": {"msvc": ["-mywinflag"]}}
""")
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
"""
c = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile

class Pkg(ConanFile):
def package_info(self):
myflags = {
"settings.os": {
"Windows": ["-mywinflag"],
"Linux": ["-mylinuxflag"],
"*": ["-other-os-flag"]
}
}
self.cpp_info.cxxflags = myflags
""")
consumer = textwrap.dedent("""
from conan import ConanFile
class Pkg(ConanFile):
settings = "os"
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("export consumer --name=dep1 --version=0.1")
c.run("export consumer --name=dep2 --version=0.1")

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

c.run("install --requires=dep1/0.1 --requires=dep2/0.1 "
"-s os=Macos --build=missing")
assert "dep1/0.1: FLAGS: ['-other-os-flag']!!!" in c.out
assert "dep2/0.1: FLAGS: ['-other-os-flag']!!!" in c.out
Loading