Skip to content

Commit

Permalink
Add environment information to CMakePresets (#15192)
Browse files Browse the repository at this point in the history
* build to config and run to test preset

* first draft

* revert

* wip

* fix format

* test ctest

* simplify

* get env in right place

* move test

* fix windows

* hopefully fix tests

* fix test

* fix test again

* simplify test

* add opt-out

* wip

* auto_generate for run env

* minor changes
  • Loading branch information
czoido authored Dec 15, 2023
1 parent 5becb2a commit 0c786c5
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 33 deletions.
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":

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):
"""
: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
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():
# 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

0 comments on commit 0c786c5

Please sign in to comment.