diff --git a/conan/internal/model/conanfile_interface.py b/conan/internal/model/conanfile_interface.py index 76aed619e1f..6f76bafaebc 100644 --- a/conan/internal/model/conanfile_interface.py +++ b/conan/internal/model/conanfile_interface.py @@ -142,3 +142,7 @@ def url(self): @property def extension_properties(self): return getattr(self._conanfile, "extension_properties", {}) + + @property + def conf(self): + return self._conanfile.conf diff --git a/conan/tools/cmake/cmakedeps/templates/config.py b/conan/tools/cmake/cmakedeps/templates/config.py index f7e9fe1ba60..9f31e993aa0 100644 --- a/conan/tools/cmake/cmakedeps/templates/config.py +++ b/conan/tools/cmake/cmakedeps/templates/config.py @@ -28,6 +28,24 @@ def additional_variables_prefixes(self): self.cmakedeps.get_property("cmake_additional_variables_prefixes", self.conanfile, check_type=list) or []) return list(set([self.file_name] + prefix_list)) + @property + def parsed_extra_variables(self): + # Reading configuration from "cmake_extra_variables" property + from conan.tools.cmake.utils import parse_extra_variable + conf_extra_variables = self.conanfile.conf.get("tools.cmake.cmaketoolchain:extra_variables", + default={}, check_type=dict) + dep_extra_variables = self.cmakedeps.get_property("cmake_extra_variables", self.conanfile, + check_type=dict) or {} + # The configuration variables have precedence over the dependency ones + extra_variables = {dep: value for dep, value in dep_extra_variables.items() + if dep not in conf_extra_variables} + parsed_extra_variables = {} + for key, value in extra_variables.items(): + parsed_extra_variables[key] = parse_extra_variable("cmake_extra_variables", + key, value) + return parsed_extra_variables + + @property def context(self): targets_include = "" if not self.generating_module else "module-" @@ -36,6 +54,7 @@ def context(self): "version": self.conanfile.ref.version, "file_name": self.file_name, "additional_variables_prefixes": self.additional_variables_prefixes, + "extra_variables": self.parsed_extra_variables, "pkg_name": self.pkg_name, "config_suffix": self.config_suffix, "check_components_exist": self.cmakedeps.check_components_exist, @@ -83,6 +102,12 @@ def template(self): {% endfor %} + # Definition of extra CMake variables from cmake_extra_variables + + {% for key, value in extra_variables.items() %} + set({{ key }} {{ value }}) + {% endfor %} + # Only the last installed configuration BUILD_MODULES are included to avoid the collision foreach(_BUILD_MODULE {{ pkg_var(pkg_name, 'BUILD_MODULES_PATHS', config_suffix) }} ) message({% raw %}${{% endraw %}{{ file_name }}_MESSAGE_MODE} "Conan: Including build module from '${_BUILD_MODULE}'") diff --git a/conan/tools/cmake/cmakedeps2/config.py b/conan/tools/cmake/cmakedeps2/config.py index 5271330bcdb..00e81e5b6b3 100644 --- a/conan/tools/cmake/cmakedeps2/config.py +++ b/conan/tools/cmake/cmakedeps2/config.py @@ -2,7 +2,7 @@ import jinja2 from jinja2 import Template - +from conan.tools.cmake.utils import parse_extra_variable from conan.internal.api.install.generators import relativize_path @@ -66,6 +66,19 @@ def _context(self): "targets_include_file": targets_include, "build_modules_paths": build_modules_paths} + conf_extra_variables = self._conanfile.conf.get("tools.cmake.cmaketoolchain:extra_variables", + default={}, check_type=dict) + dep_extra_variables = self._cmakedeps.get_property("cmake_extra_variables", self._conanfile, + check_type=dict) or {} + # The configuration variables have precedence over the dependency ones + extra_variables = {dep: value for dep, value in dep_extra_variables.items() + if dep not in conf_extra_variables} + parsed_extra_variables = {} + for key, value in extra_variables.items(): + parsed_extra_variables[key] = parse_extra_variable("cmake_extra_variables", + key, value) + result["extra_variables"] = parsed_extra_variables + result.update(self._get_legacy_vars()) return result @@ -143,4 +156,10 @@ def _template(self): set({{ prefix }}_DEFINITIONS {{ definitions}} ) {% endif %} {% endfor %} + + # Definition of extra CMake variables from cmake_extra_variables + + {% for key, value in extra_variables.items() %} + set({{ key }} {{ value }}) + {% endfor %} """) diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py index bb0b96d4ff3..4188e40d867 100644 --- a/conan/tools/cmake/toolchain/blocks.py +++ b/conan/tools/cmake/toolchain/blocks.py @@ -13,7 +13,7 @@ from conan.tools.build.flags import architecture_flag, architecture_link_flag, libcxx_flags, threads_flags from conan.tools.build.cross_building import cross_building from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME -from conan.tools.cmake.utils import is_multi_configuration +from conan.tools.cmake.utils import is_multi_configuration, parse_extra_variable from conan.tools.intel import IntelCC from conan.tools.microsoft.visual import msvc_version_to_toolset_version, msvc_platform_from_arch from conan.internal.api.install.generators import relativize_path @@ -1201,44 +1201,15 @@ class ExtraVariablesBlock(Block): {% endif %} """) - CMAKE_CACHE_TYPES = ["BOOL", "FILEPATH", "PATH", "STRING", "INTERNAL"] - - def get_exact_type(self, key, value): - if isinstance(value, str): - return f"\"{value}\"" - elif isinstance(value, (int, float)): - return value - elif isinstance(value, dict): - var_value = self.get_exact_type(key, value.get("value")) - is_force = value.get("force") - if is_force: - if not isinstance(is_force, bool): - raise ConanException(f'tools.cmake.cmaketoolchain:extra_variables "{key}" "force" must be a boolean') - is_cache = value.get("cache") - if is_cache: - if not isinstance(is_cache, bool): - raise ConanException(f'tools.cmake.cmaketoolchain:extra_variables "{key}" "cache" must be a boolean') - var_type = value.get("type") - if not var_type: - raise ConanException(f'tools.cmake.cmaketoolchain:extra_variables "{key}" needs "type" defined for cache variable') - if var_type not in self.CMAKE_CACHE_TYPES: - raise ConanException(f'tools.cmake.cmaketoolchain:extra_variables "{key}" invalid type "{var_type}" for cache variable. Possible types: {", ".join(self.CMAKE_CACHE_TYPES)}') - # Set docstring as variable name if not defined - docstring = value.get("docstring") or key - force_str = " FORCE" if is_force else "" # Support python < 3.11 - return f"{var_value} CACHE {var_type} \"{docstring}\"{force_str}" - else: - if is_force: - raise ConanException(f'tools.cmake.cmaketoolchain:extra_variables "{key}" "force" is only allowed for cache variables') - return var_value - def context(self): + from conan.tools.cmake.utils import parse_extra_variable # Reading configuration from "tools.cmake.cmaketoolchain:extra_variables" extra_variables = self._conanfile.conf.get("tools.cmake.cmaketoolchain:extra_variables", default={}, check_type=dict) parsed_extra_variables = {} for key, value in extra_variables.items(): - parsed_extra_variables[key] = self.get_exact_type(key, value) + parsed_extra_variables[key] = parse_extra_variable("tools.cmake.cmaketoolchain:extra_variables", + key, value) return {"extra_variables": parsed_extra_variables} diff --git a/conan/tools/cmake/utils.py b/conan/tools/cmake/utils.py index ffd9fcbd640..203141e27fa 100644 --- a/conan/tools/cmake/utils.py +++ b/conan/tools/cmake/utils.py @@ -1,5 +1,41 @@ +from conan.errors import ConanException + def is_multi_configuration(generator): if not generator: return False return "Visual" in generator or "Xcode" in generator or "Multi-Config" in generator + + +def parse_extra_variable(source, key, value): + CMAKE_CACHE_TYPES = ["BOOL", "FILEPATH", "PATH", "STRING", "INTERNAL"] + if isinstance(value, str): + return f"\"{value}\"" + elif isinstance(value, (int, float)): + return value + elif isinstance(value, dict): + var_value = parse_extra_variable(source, key, value.get("value")) + is_force = value.get("force") + if is_force: + if not isinstance(is_force, bool): + raise ConanException(f'{source} "{key}" "force" must be a boolean') + is_cache = value.get("cache") + if is_cache: + if not isinstance(is_cache, bool): + raise ConanException(f'{source} "{key}" "cache" must be a boolean') + var_type = value.get("type") + if not var_type: + raise ConanException(f'{source} "{key}" needs "type" defined for cache variable') + if var_type not in CMAKE_CACHE_TYPES: + raise ConanException(f'{source} "{key}" invalid type "{var_type}" for cache variable.' + f' Possible types: {", ".join(CMAKE_CACHE_TYPES)}') + # Set docstring as variable name if not defined + docstring = value.get("docstring") or key + force_str = " FORCE" if is_force else "" # Support python < 3.11 + return f"{var_value} CACHE {var_type} \"{docstring}\"{force_str}" + else: + if is_force: + raise ConanException(f'{source} "{key}" "force" is only allowed for cache variables') + return var_value + raise ConanException(f'{source} "{key}" has invalid type. Allowed types: str, int, float, dict,' + f' got {type(value)}') diff --git a/test/functional/toolchains/cmake/test_cmake_extra_variables.py b/test/functional/toolchains/cmake/test_cmake_extra_variables.py new file mode 100644 index 00000000000..b2919f215e8 --- /dev/null +++ b/test/functional/toolchains/cmake/test_cmake_extra_variables.py @@ -0,0 +1,57 @@ +import textwrap +import pytest + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient +new_value = "will_break_next" + + +@pytest.mark.tool("cmake", "3.27") +@pytest.mark.parametrize("generator", ["CMakeDeps", "CMakeConfigDeps"]) +def test_package_info_extra_variables(generator): + """ The dependencies can define extra variables to be used in CMake, + but if the user is setting the cmake_extra_variables conf, + those should have precedence. + """ + client = TestClient() + dep_conanfile = textwrap.dedent(""" + from conan import ConanFile + + class Pkg(ConanFile): + name = "dep" + version = "0.1" + + def package_info(self): + self.cpp_info.set_property("cmake_extra_variables", {"FOO": 42}) + """) + client.save({"dep/conanfile.py": dep_conanfile}) + client.run("create dep") + + cmakelists = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.27) + project(myproject CXX) + find_package(dep CONFIG REQUIRED) + message(STATUS "FOO=${FOO}") + """) + + + conanfile = textwrap.dedent(f""" + from conan import ConanFile + from conan.tools.cmake import CMake + + class Pkg(ConanFile): + settings = "os", "arch", "compiler", "build_type" + generators = "{generator}", "CMakeToolchain" + requires = "dep/0.1" + def build(self): + cmake = CMake(self) + cmake.configure() + """) + client.save({"CMakeLists.txt": cmakelists, + "conanfile.py": conanfile}) + conf = f"-c tools.cmake.cmakedeps:new={new_value}" if generator == "CMakeConfigDeps" else "" + client.run(f"build . {conf} " + """-c tools.cmake.cmaketoolchain:extra_variables="{'FOO': '9'}" """) + + assert "-- FOO=9" in client.out + diff --git a/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py b/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py index 457300b91bd..539f2876909 100644 --- a/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py +++ b/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py @@ -922,3 +922,34 @@ def generate(self): assert "add_library(component_alias" in targets_data assert "add_library(dep::my_aliased_component" in targets_data + + +def test_package_info_extra_variables(): + """ Test extra_variables property - This just shows that it works, + there are tests for cmaketoolchain that check the actual behavior + of parsing the variables""" + client = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + + class Pkg(ConanFile): + name = "pkg" + version = "0.1" + + def package_info(self): + self.cpp_info.set_property("cmake_extra_variables", {"FOO": 42, + "BAR": 42, + "CMAKE_GENERATOR_INSTANCE": "${GENERATOR_INSTANCE}/buildTools/", + "CACHE_VAR_DEFAULT_DOC": {"value": "hello world", + "cache": True, "type": "PATH"}}) + """) + client.save({"conanfile.py": conanfile}) + client.run("create .") + + client.run("install --requires=pkg/0.1 -g CMakeDeps " + """-c tools.cmake.cmaketoolchain:extra_variables="{'BAR': 9}" """) + target = client.load("pkg-config.cmake") + assert 'set(BAR' not in target + assert 'set(CMAKE_GENERATOR_INSTANCE "${GENERATOR_INSTANCE}/buildTools/")' in target + assert 'set(FOO 42)' in target + assert 'set(CACHE_VAR_DEFAULT_DOC "hello world" CACHE PATH "CACHE_VAR_DEFAULT_DOC")' in target diff --git a/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py index 6a6e06e8fb3..4ae809e6e63 100644 --- a/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py +++ b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py @@ -526,3 +526,34 @@ def generate(self): assert "add_library(component_alias" in targets_data assert "add_library(dep::my_aliased_component" in targets_data + + +def test_package_info_extra_variables(): + """ Test extra_variables property - This just shows that it works, + there are tests for cmaketoolchain that check the actual behavior + of parsing the variables""" + client = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + + class Pkg(ConanFile): + name = "pkg" + version = "0.1" + + def package_info(self): + self.cpp_info.set_property("cmake_extra_variables", {"FOO": 42, + "BAR": 42, + "CMAKE_GENERATOR_INSTANCE": "${GENERATOR_INSTANCE}/buildTools/", + "CACHE_VAR_DEFAULT_DOC": {"value": "hello world", + "cache": True, "type": "PATH"}}) + """) + client.save({"conanfile.py": conanfile}) + client.run("create .") + + client.run(f"install --requires=pkg/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value} " + """-c tools.cmake.cmaketoolchain:extra_variables="{'BAR': 9}" """) + target = client.load("pkg-config.cmake") + assert 'set(BAR' not in target + assert 'set(CMAKE_GENERATOR_INSTANCE "${GENERATOR_INSTANCE}/buildTools/")' in target + assert 'set(FOO 42)' in target + assert 'set(CACHE_VAR_DEFAULT_DOC "hello world" CACHE PATH "CACHE_VAR_DEFAULT_DOC")' in target