Skip to content

Commit 5f75738

Browse files
authored
New CMakeDeps (#16964)
* cpp_info.exes * first prototype of cpp_info.exes * wip * wip CmakeDeps2 * wip * wip * wip * wip * wip * wip * wip * working * wip * wip * wip * exploding multi-libs into components * wip * wip * fix tests * wip * wip * wip * fix py38 tests type annotations * wip * wip * fix tests * fix test * consider using dep configuration instead of consumer one * working with dependency configuration instead of base configuration * wip * handling headers=False trait * fallback to consumer config for interface packages * wip try-compile * added conan_cmakedeps_paths.cmake * wip * fix tests * only in-package if existing * added CONAN_RUNTIME_LIB_DIRS * review, using cmakedeps overriden properties * wip * added link-languages feature * review * experimental warnings
1 parent 48e9c81 commit 5f75738

15 files changed

+2342
-8
lines changed

conan/cps/cps.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def from_cpp_info(cpp_info, pkg_type, libname=None):
8585
cps_comp.type = CPSComponentType.INTERFACE
8686
return cps_comp
8787

88-
cpp_info.deduce_cps(pkg_type)
88+
cpp_info.deduce_locations(pkg_type)
8989
cps_comp.type = CPSComponentType.from_conan(cpp_info.type)
9090
cps_comp.location = cpp_info.location
9191
cps_comp.link_location = cpp_info.link_location

conan/tools/cmake/__init__.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
from conan.tools.cmake.toolchain.toolchain import CMakeToolchain
22
from conan.tools.cmake.cmake import CMake
3-
from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps
43
from conan.tools.cmake.layout import cmake_layout
4+
5+
6+
def CMakeDeps(conanfile): # noqa
7+
if conanfile.conf.get("tools.cmake.cmakedeps:new", choices=["will_break_next"]):
8+
from conan.tools.cmake.cmakedeps2.cmakedeps import CMakeDeps2
9+
conanfile.output.warning("Using the new CMakeDeps generator, behind the "
10+
"'tools.cmake.cmakedeps:new' gate conf. This conf will change"
11+
"next release, breaking, so use it only for testing and dev")
12+
return CMakeDeps2(conanfile)
13+
from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps as _CMakeDeps
14+
return _CMakeDeps(conanfile)

conan/tools/cmake/cmakedeps2/__init__.py

Whitespace-only changes.
+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import os
2+
import re
3+
import textwrap
4+
5+
from jinja2 import Template
6+
7+
from conan.internal import check_duplicated_generator
8+
from conan.tools.cmake.cmakedeps2.config import ConfigTemplate2
9+
from conan.tools.cmake.cmakedeps2.config_version import ConfigVersionTemplate2
10+
from conan.tools.cmake.cmakedeps2.target_configuration import TargetConfigurationTemplate2
11+
from conan.tools.cmake.cmakedeps2.targets import TargetsTemplate2
12+
from conan.tools.files import save
13+
from conan.errors import ConanException
14+
from conans.model.dependencies import get_transitive_requires
15+
from conans.util.files import load
16+
17+
FIND_MODE_MODULE = "module"
18+
FIND_MODE_CONFIG = "config"
19+
FIND_MODE_NONE = "none"
20+
FIND_MODE_BOTH = "both"
21+
22+
23+
class CMakeDeps2:
24+
25+
def __init__(self, conanfile):
26+
self._conanfile = conanfile
27+
self.configuration = str(self._conanfile.settings.build_type)
28+
29+
# These are just for legacy compatibility, but not use at al
30+
self.build_context_activated = []
31+
self.build_context_build_modules = []
32+
self.build_context_suffix = {}
33+
# Enable/Disable checking if a component target exists or not
34+
self.check_components_exist = False
35+
36+
self._properties = {}
37+
38+
def generate(self):
39+
check_duplicated_generator(self, self._conanfile)
40+
# Current directory is the generators_folder
41+
generator_files = self._content()
42+
for generator_file, content in generator_files.items():
43+
save(self._conanfile, generator_file, content)
44+
_PathGenerator(self, self._conanfile).generate()
45+
46+
def _content(self):
47+
host_req = self._conanfile.dependencies.host
48+
build_req = self._conanfile.dependencies.direct_build
49+
test_req = self._conanfile.dependencies.test
50+
51+
# Iterate all the transitive requires
52+
ret = {}
53+
for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()):
54+
cmake_find_mode = self.get_property("cmake_find_mode", dep)
55+
cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG
56+
cmake_find_mode = cmake_find_mode.lower()
57+
if cmake_find_mode == FIND_MODE_NONE:
58+
continue
59+
60+
config = ConfigTemplate2(self, dep)
61+
ret[config.filename] = config.content()
62+
config_version = ConfigVersionTemplate2(self, dep)
63+
ret[config_version.filename] = config_version.content()
64+
65+
targets = TargetsTemplate2(self, dep)
66+
ret[targets.filename] = targets.content()
67+
target_configuration = TargetConfigurationTemplate2(self, dep, require)
68+
ret[target_configuration.filename] = target_configuration.content()
69+
return ret
70+
71+
def set_property(self, dep, prop, value, build_context=False):
72+
"""
73+
Using this method you can overwrite the :ref:`property<CMakeDeps Properties>` values set by
74+
the Conan recipes from the consumer.
75+
76+
:param dep: Name of the dependency to set the :ref:`property<CMakeDeps Properties>`. For
77+
components use the syntax: ``dep_name::component_name``.
78+
:param prop: Name of the :ref:`property<CMakeDeps Properties>`.
79+
:param value: Value of the property. Use ``None`` to invalidate any value set by the
80+
upstream recipe.
81+
:param build_context: Set to ``True`` if you want to set the property for a dependency that
82+
belongs to the build context (``False`` by default).
83+
"""
84+
build_suffix = "&build" if build_context else ""
85+
self._properties.setdefault(f"{dep}{build_suffix}", {}).update({prop: value})
86+
87+
def get_property(self, prop, dep, comp_name=None, check_type=None):
88+
dep_name = dep.ref.name
89+
build_suffix = "&build" if dep.context == "build" else ""
90+
dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}"
91+
try:
92+
value = self._properties[f"{dep_comp}{build_suffix}"][prop]
93+
if check_type is not None and not isinstance(value, check_type):
94+
raise ConanException(f'The expected type for {prop} is "{check_type.__name__}", '
95+
f'but "{type(value).__name__}" was found')
96+
return value
97+
except KeyError:
98+
# Here we are not using the cpp_info = deduce_cpp_info(dep) because it is not
99+
# necessary for the properties
100+
return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \
101+
else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type)
102+
103+
def get_cmake_filename(self, dep, module_mode=None):
104+
"""Get the name of the file for the find_package(XXX)"""
105+
# This is used by CMakeDeps to determine:
106+
# - The filename to generate (XXX-config.cmake or FindXXX.cmake)
107+
# - The name of the defined XXX_DIR variables
108+
# - The name of transitive dependencies for calls to find_dependency
109+
if module_mode and self._get_find_mode(dep) in [FIND_MODE_MODULE, FIND_MODE_BOTH]:
110+
ret = self.get_property("cmake_module_file_name", dep)
111+
if ret:
112+
return ret
113+
ret = self.get_property("cmake_file_name", dep)
114+
return ret or dep.ref.name
115+
116+
def _get_find_mode(self, dep):
117+
"""
118+
:param dep: requirement
119+
:return: "none" or "config" or "module" or "both" or "config" when not set
120+
"""
121+
tmp = self.get_property("cmake_find_mode", dep)
122+
if tmp is None:
123+
return "config"
124+
return tmp.lower()
125+
126+
def get_transitive_requires(self, conanfile):
127+
# Prepared to filter transitive tool-requires with visible=True
128+
return get_transitive_requires(self._conanfile, conanfile)
129+
130+
131+
class _PathGenerator:
132+
_conan_cmakedeps_paths = "conan_cmakedeps_paths.cmake"
133+
134+
def __init__(self, cmakedeps, conanfile):
135+
self._conanfile = conanfile
136+
self._cmakedeps = cmakedeps
137+
138+
def generate(self):
139+
template = textwrap.dedent("""\
140+
{% for pkg_name, folder in pkg_paths.items() %}
141+
set({{pkg_name}}_DIR "{{folder}}")
142+
{% endfor %}
143+
{% if host_runtime_dirs %}
144+
set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} )
145+
{% endif %}
146+
""")
147+
148+
host_req = self._conanfile.dependencies.host
149+
build_req = self._conanfile.dependencies.direct_build
150+
test_req = self._conanfile.dependencies.test
151+
152+
# gen_folder = self._conanfile.generators_folder.replace("\\", "/")
153+
# if not, test_cmake_add_subdirectory test fails
154+
# content.append('set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)')
155+
pkg_paths = {}
156+
for req, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()):
157+
cmake_find_mode = self._cmakedeps.get_property("cmake_find_mode", dep)
158+
cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG
159+
cmake_find_mode = cmake_find_mode.lower()
160+
161+
pkg_name = self._cmakedeps.get_cmake_filename(dep)
162+
# https://cmake.org/cmake/help/v3.22/guide/using-dependencies/index.html
163+
if cmake_find_mode == FIND_MODE_NONE:
164+
try:
165+
# This is irrespective of the components, it should be in the root cpp_info
166+
# To define the location of the pkg-config.cmake file
167+
build_dir = dep.cpp_info.builddirs[0]
168+
except IndexError:
169+
build_dir = dep.package_folder
170+
pkg_folder = build_dir.replace("\\", "/") if build_dir else None
171+
if pkg_folder:
172+
config_file = ConfigTemplate2(self._cmakedeps, dep).filename
173+
if os.path.isfile(os.path.join(pkg_folder, config_file)):
174+
pkg_paths[pkg_name] = pkg_folder
175+
continue
176+
177+
# If CMakeDeps generated, the folder is this one
178+
# content.append(f'set({pkg_name}_ROOT "{gen_folder}")')
179+
pkg_paths[pkg_name] = "${CMAKE_CURRENT_LIST_DIR}"
180+
181+
context = {"host_runtime_dirs": self._get_host_runtime_dirs(),
182+
"pkg_paths": pkg_paths}
183+
content = Template(template, trim_blocks=True, lstrip_blocks=True).render(context)
184+
save(self._conanfile, self._conan_cmakedeps_paths, content)
185+
186+
def _get_host_runtime_dirs(self):
187+
host_runtime_dirs = {}
188+
189+
# Get the previous configuration
190+
if os.path.exists(self._conan_cmakedeps_paths):
191+
existing_toolchain = load(self._conan_cmakedeps_paths)
192+
pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)"
193+
variable_match = re.search(pattern_lib_dirs, existing_toolchain)
194+
if variable_match:
195+
capture = variable_match.group(1)
196+
matches = re.findall(r'"\$<\$<CONFIG:([A-Za-z]*)>:([^>]*)>"', capture)
197+
for config, paths in matches:
198+
host_runtime_dirs.setdefault(config, []).append(paths)
199+
200+
is_win = self._conanfile.settings.get_safe("os") == "Windows"
201+
for req in self._conanfile.dependencies.host.values():
202+
config = req.settings.get_safe("build_type", self._cmakedeps.configuration)
203+
aggregated_cppinfo = req.cpp_info.aggregated_components()
204+
runtime_dirs = aggregated_cppinfo.bindirs if is_win else aggregated_cppinfo.libdirs
205+
for d in runtime_dirs:
206+
d = d.replace("\\", "/")
207+
existing = host_runtime_dirs.setdefault(config, [])
208+
if d not in existing:
209+
existing.append(d)
210+
211+
return ' '.join(f'"$<$<CONFIG:{c}>:{i}>"' for c, v in host_runtime_dirs.items() for i in v)
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import textwrap
2+
3+
import jinja2
4+
from jinja2 import Template
5+
6+
7+
class ConfigTemplate2:
8+
"""
9+
FooConfig.cmake
10+
foo-config.cmake
11+
"""
12+
def __init__(self, cmakedeps, conanfile):
13+
self._cmakedeps = cmakedeps
14+
self._conanfile = conanfile
15+
16+
def content(self):
17+
t = Template(self._template, trim_blocks=True, lstrip_blocks=True,
18+
undefined=jinja2.StrictUndefined)
19+
return t.render(self._context)
20+
21+
@property
22+
def filename(self):
23+
f = self._cmakedeps.get_cmake_filename(self._conanfile)
24+
return f"{f}-config.cmake" if f == f.lower() else f"{f}Config.cmake"
25+
26+
@property
27+
def _context(self):
28+
f = self._cmakedeps.get_cmake_filename(self._conanfile)
29+
targets_include = f"{f}Targets.cmake"
30+
pkg_name = self._conanfile.ref.name
31+
build_modules_paths = self._cmakedeps.get_property("cmake_build_modules", self._conanfile,
32+
check_type=list) or []
33+
# FIXME: Proper escaping of paths for CMake and relativization
34+
# FIXME: build_module_paths coming from last config only
35+
build_modules_paths = [f.replace("\\", "/") for f in build_modules_paths]
36+
return {"pkg_name": pkg_name,
37+
"targets_include_file": targets_include,
38+
"build_modules_paths": build_modules_paths}
39+
40+
@property
41+
def _template(self):
42+
return textwrap.dedent("""\
43+
# Requires CMake > 3.15
44+
if(${CMAKE_VERSION} VERSION_LESS "3.15")
45+
message(FATAL_ERROR "The 'CMakeDeps' generator only works with CMake >= 3.15")
46+
endif()
47+
48+
include(${CMAKE_CURRENT_LIST_DIR}/{{ targets_include_file }})
49+
50+
get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
51+
if(NOT isMultiConfig AND NOT CMAKE_BUILD_TYPE)
52+
message(FATAL_ERROR "Please, set the CMAKE_BUILD_TYPE variable when calling to CMake "
53+
"adding the '-DCMAKE_BUILD_TYPE=<build_type>' argument.")
54+
endif()
55+
56+
# build_modules_paths comes from last configuration only
57+
{% for build_module in build_modules_paths %}
58+
message(STATUS "Conan: Including build module from '{{build_module}}'")
59+
include("{{ build_module }}")
60+
{% endfor %}
61+
""")

0 commit comments

Comments
 (0)