Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions conan/internal/model/conanfile_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions conan/tools/cmake/cmakedeps/templates/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note quite sure if the config is the best place for this, I write them here for symmetry with CMakeConfigDeps where they are written next to the cmake_additional_variables_prefixes too, maybe we want to move them elsewhere?

# 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-"
Expand All @@ -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,
Expand Down Expand Up @@ -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}'")
Expand Down
21 changes: 20 additions & 1 deletion conan/tools/cmake/cmakedeps2/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 %}
""")
37 changes: 4 additions & 33 deletions conan/tools/cmake/toolchain/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}


Expand Down
36 changes: 36 additions & 0 deletions conan/tools/cmake/utils.py
Original file line number Diff line number Diff line change
@@ -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)}')
57 changes: 57 additions & 0 deletions test/functional/toolchains/cmake/test_cmake_extra_variables.py
Original file line number Diff line number Diff line change
@@ -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

31 changes: 31 additions & 0 deletions test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 31 additions & 0 deletions test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading