From abfe3f69097d9ca5e3a3d8a7d4832a06f40be14b Mon Sep 17 00:00:00 2001 From: Luis Martinez Date: Tue, 19 Jul 2022 08:47:39 +0200 Subject: [PATCH] CMakePresets compatible with schema 2 (#11655) * tests pending * Added tests * Fix default * not cpp 20 * Update conan/tools/cmake/presets.py Co-authored-by: James * fix * Fix win test * Tested win Co-authored-by: James --- conan/tools/cmake/presets.py | 75 +++++++++++++++-- conans/model/conf.py | 1 + .../toolchains/cmake/test_cmake_toolchain.py | 45 +++++++++- .../toolchains/cmake/test_cmaketoolchain.py | 82 +++++++++++++++++-- 4 files changed, 185 insertions(+), 18 deletions(-) diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py index 918f6a83162..b50aa1724ac 100644 --- a/conan/tools/cmake/presets.py +++ b/conan/tools/cmake/presets.py @@ -53,8 +53,13 @@ def _add_configure_preset(conanfile, generator, cache_variables, toolchain_file, "description": "'{}' configure using '{}' generator".format(name, generator), "generator": generator, "cacheVariables": cache_variables, - "toolchainFile": toolchain_file, + } + if not _forced_schema_2(conanfile): + ret["toolchainFile"] = toolchain_file + else: + ret["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"] = toolchain_file + if conanfile.build_folder: # If we are installing a ref: "conan install ", we don't have build_folder, because # we don't even have a conanfile with a `layout()` to determine the build folder. @@ -63,8 +68,34 @@ def _add_configure_preset(conanfile, generator, cache_variables, toolchain_file, return ret +def _forced_schema_2(conanfile): + version = conanfile.conf.get("tools.cmake.cmaketoolchain.presets:max_schema_version", + check_type=int) + if not version: + return False + + if version < 2: + raise ConanException("The minimum value for 'tools.cmake.cmaketoolchain.presets:" + "schema_version' is 2") + if version < 4: + return True + + return False + + +def _schema_version(conanfile, default): + if _forced_schema_2(conanfile): + return 2 + + return default + + def _contents(conanfile, toolchain_file, cache_variables, generator): - ret = {"version": 3, + """ + Contents for the CMakePresets.json + It uses schema version 3 unless it is forced to 2 + """ + ret = {"version": _schema_version(conanfile, default=3), "cmakeMinimumRequired": {"major": 3, "minor": 15, "patch": 0}, "configurePresets": [], "buildPresets": [], @@ -117,25 +148,51 @@ def write_cmake_presets(conanfile, toolchain_file, generator, cache_variables): data = json.dumps(data, indent=4) save(preset_path, data) + save_cmake_user_presets(conanfile, preset_path) + +def save_cmake_user_presets(conanfile, preset_path): # Try to save the CMakeUserPresets.json if layout declared and CMakeLists.txt found if conanfile.source_folder and conanfile.source_folder != conanfile.generators_folder: if os.path.exists(os.path.join(conanfile.source_folder, "CMakeLists.txt")): + """ + Contents for the CMakeUserPresets.json + It uses schema version 4 unless it is forced to 2 + """ user_presets_path = os.path.join(conanfile.source_folder, "CMakeUserPresets.json") if not os.path.exists(user_presets_path): - data = {"version": 4, "include": [preset_path], "vendor": {"conan": dict()}} + data = {"version": _schema_version(conanfile, default=4), + "vendor": {"conan": dict()}} else: data = json.loads(load(user_presets_path)) - if "conan" in data.get("vendor", {}): - # Clear the folders that have been deleted - data["include"] = [i for i in data.get("include", []) if os.path.exists(i)] - if preset_path not in data["include"]: - data["include"].append(preset_path) - + if "conan" not in data.get("vendor", {}): + # The file is not ours, we cannot overwrite it + return + data = _append_user_preset_path(conanfile, data, preset_path) data = json.dumps(data, indent=4) save(user_presets_path, data) +def _append_user_preset_path(conanfile, data, preset_path): + if not _forced_schema_2(conanfile): + if "include" not in data: + data["include"] = [] + # Clear the folders that have been deleted + data["include"] = [i for i in data.get("include", []) if os.path.exists(i)] + if preset_path not in data["include"]: + data["include"].append(preset_path) + return data + else: + # Merge the presets + cmake_preset = json.loads(load(preset_path)) + for preset_type in ("configurePresets", "buildPresets", "testPresets"): + for preset in cmake_preset.get(preset_type, []): + if preset_type not in data: + data[preset_type] = [] + data[preset_type].append(preset) + return data + + def load_cmake_presets(folder): tmp = load(os.path.join(folder, "CMakePresets.json")) return json.loads(tmp) diff --git a/conans/model/conf.py b/conans/model/conf.py index 1c37c95e39c..968bcd87b3a 100644 --- a/conans/model/conf.py +++ b/conans/model/conf.py @@ -21,6 +21,7 @@ "tools.cmake.cmaketoolchain:system_name": "Define CMAKE_SYSTEM_NAME in CMakeToolchain", "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.presets:max_schema_version": "Generate CMakeUserPreset.json compatible with the supplied schema version", "tools.env.virtualenv:auto_use": "Automatically activate virtualenv file generation", "tools.cmake.cmake_layout:build_folder_vars": "Settings and Options that will produce a different build folder and different CMake presets names", "tools.files.download:retry": "Number of retries in case of failure when downloading", diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py index 095ae5c61aa..c4b25b27266 100644 --- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py +++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py @@ -76,7 +76,8 @@ def test_cmake_toolchain_custom_toolchain(): reason="Single config test, Linux CI still without 3.23") @pytest.mark.tool_cmake(version="3.23") @pytest.mark.parametrize("existing_user_presets", [None, "user_provided", "conan_generated"]) -def test_cmake_user_presets_load(existing_user_presets): +@pytest.mark.parametrize("schema2", [True, False]) +def test_cmake_user_presets_load(existing_user_presets, schema2): """ Test if the CMakeUserPresets.cmake is generated and use CMake to use it to verify the right syntax of generated CMakeUserPresets.cmake and CMakePresets.cmake. If the user already provided @@ -813,6 +814,48 @@ def test_cmake_presets_multiple_settings_multi_config(): assert "MSVC_LANG2017" in client.out +@pytest.mark.tool_cmake(version="3.23") +def test_user_presets_version2(): + client = TestClient(path_with_spaces=False) + client.run("new hello/0.1 --template=cmake_exe") + configs = ["-c tools.cmake.cmaketoolchain.presets:max_schema_version=2 ", + "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'"] + client.run("install . {} -s compiler.cppstd=14".format(" ".join(configs))) + client.run("install . {} -s compiler.cppstd=17".format(" ".join(configs))) + client.run("install . {} -s compiler.cppstd=20".format(" ".join(configs))) + + if platform.system() == "Windows": + client.run_command("cmake . --preset 14") + client.run_command("cmake --build --preset 14-release") + client.run_command(r"build\14\Release\hello.exe") + else: + client.run_command("cmake . --preset 14-release") + client.run_command("cmake --build --preset 14-release") + client.run_command("./build/14/Release/hello") + + assert "Hello World Release!" in client.out + + if platform.system() != "Windows": + assert "__cplusplus2014" in client.out + else: + assert "MSVC_LANG2014" in client.out + + if platform.system() == "Windows": + client.run_command("cmake . --preset 17") + client.run_command("cmake --build --preset 17-release") + client.run_command(r"build\17\Release\hello.exe") + else: + client.run_command("cmake . --preset 17-release") + client.run_command("cmake --build --preset 17-release") + client.run_command("./build/17/Release/hello") + + assert "Hello World Release!" in client.out + if platform.system() != "Windows": + assert "__cplusplus2017" in client.out + else: + assert "MSVC_LANG2017" in client.out + + @pytest.mark.tool_cmake def test_cmaketoolchain_sysroot(): client = TestClient(path_with_spaces=False) diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py index 657e6eff355..cbfbb48a781 100644 --- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py +++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py @@ -492,6 +492,66 @@ def configure(self): client.run("create . foo/1.0@ -s os=Android -s os.api_level=23 -c tools.android:ndk_path=/foo") +def test_user_presets_version2(): + client = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import cmake_layout + + class Conan(ConanFile): + name = "foo" + version = "1.0" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeToolchain" + + def layout(self): + cmake_layout(self) + + """) + client.save({"conanfile.py": conanfile, "CMakeLists.txt": "foo"}) + configs = ["-c tools.cmake.cmaketoolchain.presets:max_schema_version=2 ", + "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'"] + client.run("install . {} -s compiler.cppstd=14".format(" ".join(configs))) + client.run("install . {} -s compiler.cppstd=17".format(" ".join(configs))) + + presets = json.loads(client.load("CMakeUserPresets.json")) + assert len(presets["configurePresets"]) == 2 + assert presets["version"] == 2 + assert "build/14/generators/conan_toolchain.cmake" \ + in presets["configurePresets"][0]["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"].replace("\\", + "/") + assert "build/17/generators/conan_toolchain.cmake" \ + in presets["configurePresets"][1]["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"].replace("\\", + "/") + + +def test_user_presets_version2_no_overwrite_user(): + + client = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import cmake_layout + + class Conan(ConanFile): + name = "foo" + version = "1.0" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeToolchain" + + def layout(self): + cmake_layout(self) + + """) + client.save({"conanfile.py": conanfile, "CMakeLists.txt": "foo", + "CMakeUserPresets.json": '{"from_user": 1}'}) + configs = ["-c tools.cmake.cmaketoolchain.presets:max_schema_version=2 ", + "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'"] + client.run("install . {} -s compiler.cppstd=14".format(" ".join(configs))) + + presets = json.loads(client.load("CMakeUserPresets.json")) + assert presets == {"from_user": 1} + + @pytest.mark.skipif(platform.system() != "Windows", reason="Only Windows") def test_presets_paths_correct(): client = TestClient() @@ -506,12 +566,18 @@ class Conan(ConanFile): def layout(self): cmake_layout(self) """) - client.save({"conanfile.py": conanfile}) - client.run("install . ") - contents = json.loads(client.load("build/generators/CMakePresets.json")) - toolchain_file = contents["configurePresets"][0]["toolchainFile"] - assert "/" not in toolchain_file - - binary_dir = contents["configurePresets"][0]["binaryDir"] - assert "/" not in binary_dir + client.save({"conanfile.py": conanfile, "CMakeLists.txt": "foo"}) + configs = ["-c tools.cmake.cmaketoolchain.presets:max_schema_version=2 ", + "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'"] + client.run("install . {} -s compiler.cppstd=14".format(" ".join(configs))) + client.run("install . {} -s compiler.cppstd=17".format(" ".join(configs))) + presets = json.loads(client.load("CMakeUserPresets.json")) + assert len(presets["configurePresets"]) == 2 + assert presets["version"] == 2 + assert "build/14/generators/conan_toolchain.cmake" \ + in presets["configurePresets"][0]["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"].replace("\\", + "/") + assert "build/17/generators/conan_toolchain.cmake" \ + in presets["configurePresets"][1]["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"].replace("\\", + "/")