Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add environment information to CMakePresets #15192

Merged
merged 20 commits into from
Dec 15, 2023
Merged
49 changes: 33 additions & 16 deletions conan/tools/cmake/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@


def write_cmake_presets(conanfile, toolchain_file, generator, cache_variables,
user_presets_path=None, preset_prefix=None):
user_presets_path=None, preset_prefix=None, buildenv=None, runenv=None):
preset_path, preset_data = _CMakePresets.generate(conanfile, toolchain_file, generator,
cache_variables, preset_prefix)
cache_variables, preset_prefix, buildenv, runenv)
_IncludingPresets.generate(conanfile, preset_path, user_presets_path, preset_prefix, preset_data)


class _CMakePresets:
""" Conan generated main CMakePresets.json inside the generators_folder
"""
@staticmethod
def generate(conanfile, toolchain_file, generator, cache_variables, preset_prefix):
def generate(conanfile, toolchain_file, generator, cache_variables, preset_prefix, buildenv, runenv):
cache_variables = cache_variables or {}
if platform.system() == "Windows" and generator == "MinGW Makefiles":
if "CMAKE_SH" not in cache_variables:
Expand Down Expand Up @@ -52,18 +52,19 @@ def generate(conanfile, toolchain_file, generator, cache_variables, preset_prefi
"avoid collision with your CMakePresets.json")
if os.path.exists(preset_path) and multiconfig:
data = json.loads(load(preset_path))
build_preset = _CMakePresets._build_and_test_preset_fields(conanfile, multiconfig,
preset_prefix)
build_preset = _CMakePresets._build_preset_fields(conanfile, multiconfig, preset_prefix)
test_preset = _CMakePresets._test_preset_fields(conanfile, multiconfig, preset_prefix,
runenv)
_CMakePresets._insert_preset(data, "buildPresets", build_preset)
_CMakePresets._insert_preset(data, "testPresets", build_preset)
_CMakePresets._insert_preset(data, "testPresets", test_preset)
configure_preset = _CMakePresets._configure_preset(conanfile, generator, cache_variables,
toolchain_file, multiconfig,
preset_prefix)
preset_prefix, buildenv)
# Conan generated presets should have only 1 configurePreset, no more, overwrite it
data["configurePresets"] = [configure_preset]
else:
data = _CMakePresets._contents(conanfile, toolchain_file, cache_variables, generator,
preset_prefix)
preset_prefix, buildenv, runenv)

preset_content = json.dumps(data, indent=4)
save(preset_path, preset_content)
Expand All @@ -81,27 +82,29 @@ def _insert_preset(data, preset_type, preset):
data[preset_type].append(preset)

@staticmethod
def _contents(conanfile, toolchain_file, cache_variables, generator, preset_prefix):
def _contents(conanfile, toolchain_file, cache_variables, generator, preset_prefix, buildenv,
runenv):
"""
Contents for the CMakePresets.json
It uses schema version 3 unless it is forced to 2
"""
multiconfig = is_multi_configuration(generator)
conf = _CMakePresets._configure_preset(conanfile, generator, cache_variables, toolchain_file,
multiconfig, preset_prefix)
build = _CMakePresets._build_and_test_preset_fields(conanfile, multiconfig, preset_prefix)
multiconfig, preset_prefix, buildenv)
build = _CMakePresets._build_preset_fields(conanfile, multiconfig, preset_prefix)
test = _CMakePresets._test_preset_fields(conanfile, multiconfig, preset_prefix, runenv)
ret = {"version": 3,
"vendor": {"conan": {}},
"cmakeMinimumRequired": {"major": 3, "minor": 15, "patch": 0},
"configurePresets": [conf],
"buildPresets": [build],
"testPresets": [build]
"testPresets": [test]
}
return ret

@staticmethod
def _configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig,
preset_prefix):
preset_prefix, buildenv):
build_type = conanfile.settings.get_safe("build_type")
name = _CMakePresets._configure_preset_name(conanfile, multiconfig)
if preset_prefix:
Expand All @@ -115,6 +118,9 @@ def _configure_preset(conanfile, generator, cache_variables, toolchain_file, mul
"generator": generator,
"cacheVariables": cache_variables,
}
if buildenv:
ret["environment"] = buildenv

if "Ninja" in generator and is_msvc(conanfile):
toolset_arch = conanfile.conf.get("tools.cmake.cmaketoolchain:toolset_arch")
if toolset_arch:
Expand Down Expand Up @@ -166,19 +172,30 @@ def _format_val(val):
return ret

@staticmethod
def _build_and_test_preset_fields(conanfile, multiconfig, preset_prefix):
def _common_preset_fields(conanfile, multiconfig, preset_prefix):
build_type = conanfile.settings.get_safe("build_type")
configure_preset_name = _CMakePresets._configure_preset_name(conanfile, multiconfig)
build_preset_name = _CMakePresets._build_and_test_preset_name(conanfile)
if preset_prefix:
configure_preset_name = f"{preset_prefix}-{configure_preset_name}"
build_preset_name = f"{preset_prefix}-{build_preset_name}"
ret = {"name": build_preset_name,
"configurePreset": configure_preset_name}
ret = {"name": build_preset_name, "configurePreset": configure_preset_name}
if multiconfig:
ret["configuration"] = build_type
return ret

@staticmethod
def _build_preset_fields(conanfile, multiconfig, preset_prefix):
ret = _CMakePresets._common_preset_fields(conanfile, multiconfig, preset_prefix)
return ret

@staticmethod
def _test_preset_fields(conanfile, multiconfig, preset_prefix, runenv):
ret = _CMakePresets._common_preset_fields(conanfile, multiconfig, preset_prefix)
if runenv:
ret["environment"] = runenv
return ret

@staticmethod
def _build_and_test_preset_name(conanfile):
build_type = conanfile.settings.get_safe("build_type")
Expand Down
16 changes: 15 additions & 1 deletion conan/tools/cmake/toolchain/toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
AndroidSystemBlock, AppleSystemBlock, FPicBlock, ArchitectureBlock, GLibCXXBlock, VSRuntimeBlock, \
CppStdBlock, ParallelBlock, CMakeFlagsInitBlock, TryCompileBlock, FindFiles, PkgConfigBlock, \
SkipRPath, SharedLibBock, OutputDirsBlock, ExtraFlagsBlock, CompilersBlock, LinkerScriptsBlock
from conan.tools.env import VirtualBuildEnv, VirtualRunEnv
from conan.tools.intel import IntelCC
from conan.tools.microsoft import VCVars
from conan.tools.microsoft.visual import vs_ide_version
Expand Down Expand Up @@ -214,8 +215,21 @@ def generate(self):
else:
cache_variables[name] = value

buildenv, runenv = None, None

if self._conanfile.conf.get("tools.cmake.cmaketoolchain:presets_environment", default="",
check_type=str, choices=("disabled", "")) != "disabled":
franramirez688 marked this conversation as resolved.
Show resolved Hide resolved

build_env = VirtualBuildEnv(self._conanfile, auto_generate=True).vars()
run_env = VirtualRunEnv(self._conanfile, auto_generate=True).vars()

buildenv = {name: value for name, value in
build_env.items(variable_reference="$penv{{{name}}}")}
runenv = {name: value for name, value in
run_env.items(variable_reference="$penv{{{name}}}")}

write_cmake_presets(self._conanfile, toolchain, self.generator, cache_variables,
self.user_presets_path, self.presets_prefix)
self.user_presets_path, self.presets_prefix, buildenv, runenv)

def _get_generator(self, recipe_generator):
# Returns the name of the generator to be used by CMake
Expand Down
5 changes: 3 additions & 2 deletions conan/tools/env/virtualrunenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ class VirtualRunEnv:
.bat or .sh script
"""

def __init__(self, conanfile):
def __init__(self, conanfile, auto_generate=False):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

adding the same argument that was added here: #15153

"""

:param conanfile: The current recipe object. Always use ``self``.
"""
self._conanfile = conanfile
self._conanfile.virtualrunenv = False
if not auto_generate:
self._conanfile.virtualrunenv = False
self.basename = "conanrunenv"
self.configuration = conanfile.settings.get_safe("build_type")
if self.configuration:
Expand Down
1 change: 1 addition & 0 deletions conans/model/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"tools.cmake.cmaketoolchain:system_version": "Define CMAKE_SYSTEM_VERSION in CMakeToolchain",
"tools.cmake.cmaketoolchain:system_processor": "Define CMAKE_SYSTEM_PROCESSOR in CMakeToolchain",
"tools.cmake.cmaketoolchain:toolset_arch": "Toolset architecture to be used as part of CMAKE_GENERATOR_TOOLSET in CMakeToolchain",
"tools.cmake.cmaketoolchain:presets_environment": "String to define wether to add or not the environment section to the CMake presets. Empty by default, will generate the environment section in CMakePresets. Can take values: 'disabled'.",
"tools.cmake.cmake_layout:build_folder_vars": "Settings and Options that will produce a different build folder and different CMake presets names",
"tools.cmake:cmake_program": "Path to CMake executable",
"tools.cmake:install_strip": "Add --strip to cmake.install()",
Expand Down
114 changes: 114 additions & 0 deletions conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1424,3 +1424,117 @@ def package(self):
else:
assert re.search("Install stdout: '[^']", client.out)
assert re.search("Install stderr: ''", client.out)


@pytest.mark.tool("cmake", "3.23")
def test_add_env_to_presets():
c = TestClient()

tool = textwrap.dedent(r"""
import os
from conan import ConanFile
from conan.tools.files import chdir, save
class Tool(ConanFile):
version = "0.1"
settings = "os", "compiler", "arch", "build_type"
def package(self):
with chdir(self, self.package_folder):
save(self, f"bin/{{self.name}}.bat", f"@echo off\necho running: {{self.name}}/{{self.version}}")
save(self, f"bin/{{self.name}}.sh", f"echo running: {{self.name}}/{{self.version}}")
os.chmod(f"bin/{{self.name}}.sh", 0o777)
def package_info(self):
self.buildenv_info.define("MY_BUILD_VAR", "MY_BUILDVAR_VALUE")
{}
""")

consumer = textwrap.dedent("""
[tool_requires]
mytool/0.1
[test_requires]
mytesttool/0.1
[layout]
cmake_layout
""")

test_env = textwrap.dedent("""
#include <cstdlib>
int main() {
return std::getenv("MY_RUNVAR") ? 0 : 1;
}
""")

cmakelists = textwrap.dedent("""
cmake_minimum_required(VERSION 3.15)
project(MyProject)
if(WIN32)
set(MYTOOL_SCRIPT "mytool.bat")
else()
set(MYTOOL_SCRIPT "mytool.sh")
endif()
add_custom_target(run_mytool COMMAND ${MYTOOL_SCRIPT})
# build var should be available at configure
set(MY_BUILD_VAR $ENV{MY_BUILD_VAR})
if (MY_BUILD_VAR)
message("MY_BUILD_VAR:${MY_BUILD_VAR}")
else()
message("MY_BUILD_VAR NOT FOUND")
endif()
# run var should not be available at configure, just when testing
set(MY_RUNVAR $ENV{MY_RUNVAR})
if (MY_RUNVAR)
message("MY_RUNVAR:${MY_RUNVAR}")
else()
message("MY_RUNVAR NOT FOUND")
endif()
enable_testing()
add_executable(test_env test_env.cpp)
add_test(NAME TestRunEnv COMMAND test_env)
""")

c.save({"tool.py": tool.format(""),
"test_tool.py": tool.format('self.runenv_info.define("MY_RUNVAR", "MY_RUNVAR_VALUE")'),
"conanfile.txt": consumer,
"CMakeLists.txt": cmakelists,
"test_env.cpp": test_env})

c.run("create tool.py --name=mytool")

c.run("create test_tool.py --name=mytesttool")
c.run("create test_tool.py --name=mytesttool -s build_type=Debug")

# do a first conan install with env disabled just to test that the conf works
c.run("install . -g CMakeToolchain -g CMakeDeps -c tools.cmake.cmaketoolchain:presets_environment=disabled")

presets_path = os.path.join("build", "Release", "generators", "CMakePresets.json") \
if platform.system() != "Windows" else os.path.join("build", "generators", "CMakePresets.json")
presets = json.loads(c.load(presets_path))

assert presets["configurePresets"][0].get("env") is None

c.run("install . -g CMakeToolchain -g CMakeDeps")
c.run("install . -g CMakeToolchain -g CMakeDeps -s:h build_type=Debug")

# test that the buildenv is correctly injected to configure and build steps
# that the runenv is not injected to configure, but it is when running tests

preset = "conan-default" if platform.system() == "Windows" else "conan-release"

c.run_command(f"cmake --preset {preset}")
assert "MY_BUILD_VAR:MY_BUILDVAR_VALUE" in c.out
assert "MY_RUNVAR NOT FOUND" in c.out
c.run_command("cmake --build --preset conan-release --target run_mytool --target test_env")
assert "running: mytool/0.1" in c.out

c.run_command("ctest --preset conan-release")
assert "tests passed" in c.out

if platform.system() != "Windows":
c.run_command("cmake --preset conan-debug")
assert "MY_BUILD_VAR:MY_BUILDVAR_VALUE" in c.out
assert "MY_RUNVAR NOT FOUND" in c.out

c.run_command("cmake --build --preset conan-debug --target run_mytool --target test_env")
assert "running: mytool/0.1" in c.out

c.run_command("ctest --preset conan-debug")
assert "tests passed" in c.out
franramirez688 marked this conversation as resolved.
Show resolved Hide resolved
21 changes: 21 additions & 0 deletions conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,27 @@ def _format_val(val):
assert cache_variables["CMAKE_MAKE_PROGRAM"] == "MyMake"


def test_variables_types():
Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved from unit to integration, but testing the same

# https://github.com/conan-io/conan/pull/10941
client = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import CMakeToolchain

class Conan(ConanFile):
settings = "os", "arch", "compiler", "build_type"
def generate(self):
toolchain = CMakeToolchain(self)
toolchain.variables["FOO"] = True
toolchain.generate()
""")
client.save({"conanfile.py": conanfile})
client.run("install . --name=mylib --version=1.0")

toolchain = client.load("conan_toolchain.cmake")
assert 'set(FOO ON CACHE BOOL "Variable FOO conan-toolchain defined")' in toolchain


def test_android_c_library():
client = TestClient()
conanfile = textwrap.dedent("""
Expand Down
14 changes: 0 additions & 14 deletions conans/test/unittests/tools/cmake/test_cmaketoolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,20 +527,6 @@ def test_apple_cmake_osx_sysroot_sdk_mandatory(os, arch, expected_sdk):
assert "Please, specify a suitable value for os.sdk." % expected_sdk in str(excinfo.value)


def test_variables_types(conanfile):
generator_folder = temp_folder()
conanfile.folders.set_base_generators(generator_folder)
# This is a trick for 1.X to use base_generator and not install folder
conanfile.folders.generators = "here"

toolchain = CMakeToolchain(conanfile)
toolchain.variables["FOO"] = True
toolchain.generate()

contents = load(os.path.join(conanfile.generators_folder, "conan_toolchain.cmake"))
assert 'set(FOO ON CACHE BOOL "Variable FOO conan-toolchain defined")' in contents


def test_compilers_block(conanfile):
cmake_mapping = {"c": "C", "cuda": "CUDA", "cpp": "CXX", "objc": "OBJC",
"objcpp": "OBJCXX", "rc": "RC", 'fortran': "Fortran", 'asm': "ASM",
Expand Down