diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py index 7341fc009f0..a1ec86f456f 100644 --- a/conan/tools/cmake/presets.py +++ b/conan/tools/cmake/presets.py @@ -12,9 +12,9 @@ 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) @@ -22,7 +22,7 @@ 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: @@ -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) @@ -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: @@ -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: @@ -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") diff --git a/conan/tools/cmake/toolchain/toolchain.py b/conan/tools/cmake/toolchain/toolchain.py index cba5103b13c..f60259c4525 100644 --- a/conan/tools/cmake/toolchain/toolchain.py +++ b/conan/tools/cmake/toolchain/toolchain.py @@ -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 @@ -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 diff --git a/conan/tools/env/virtualrunenv.py b/conan/tools/env/virtualrunenv.py index 6bbb1fd3bff..bd9cda077e5 100644 --- a/conan/tools/env/virtualrunenv.py +++ b/conan/tools/env/virtualrunenv.py @@ -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: diff --git a/conans/model/conf.py b/conans/model/conf.py index ec8711e9e5d..72806a7e2b9 100644 --- a/conans/model/conf.py +++ b/conans/model/conf.py @@ -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()", diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py index be6dfc837cc..d6da0f6b1bc 100644 --- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py +++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py @@ -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 + 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 diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py index 2702bb88aa5..09bb37e8e26 100644 --- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py +++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py @@ -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(""" diff --git a/conans/test/unittests/tools/cmake/test_cmaketoolchain.py b/conans/test/unittests/tools/cmake/test_cmaketoolchain.py index 3c6e42458f6..0dee877f9b1 100644 --- a/conans/test/unittests/tools/cmake/test_cmaketoolchain.py +++ b/conans/test/unittests/tools/cmake/test_cmaketoolchain.py @@ -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",