From 0fc655200bc095e547a80ae7146f8ca42268fc02 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 30 Jan 2023 14:59:55 +0100 Subject: [PATCH] Fix cmake presets (#13004) * adding CMakePresets prefix * wip * reveiw * wip * fixes * fix CMakePresets only 1 configure --- conan/tools/cmake/cmake.py | 5 +- conan/tools/cmake/presets.py | 126 +++++++----------- conan/tools/cmake/toolchain/toolchain.py | 2 +- conans/test/conftest.py | 3 +- .../toolchains/cmake/test_cmake_toolchain.py | 31 ++--- .../toolchains/cmake/test_cmaketoolchain.py | 23 ++-- 6 files changed, 83 insertions(+), 107 deletions(-) diff --git a/conan/tools/cmake/cmake.py b/conan/tools/cmake/cmake.py index 9b8e1a0f0eb..d52a8ecb450 100644 --- a/conan/tools/cmake/cmake.py +++ b/conan/tools/cmake/cmake.py @@ -1,7 +1,7 @@ import os from conan.tools.build import build_jobs -from conan.tools.cmake.presets import load_cmake_presets, get_configure_preset +from conan.tools.cmake.presets import load_cmake_presets from conan.tools.cmake.utils import is_multi_configuration from conan.tools.files import chdir, mkdir from conan.tools.microsoft.msbuild import msbuild_verbosity_cmd_line_arg @@ -53,7 +53,8 @@ def __init__(self, conanfile): self._conanfile = conanfile cmake_presets = load_cmake_presets(conanfile.generators_folder) - configure_preset = get_configure_preset(cmake_presets, conanfile) + # Conan generated presets will have exactly 1 configurePresets, no more + configure_preset = cmake_presets["configurePresets"][0] self._generator = configure_preset["generator"] self._toolchain_file = configure_preset.get("toolchainFile") diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py index 11a443a62f6..5dd3822eba5 100644 --- a/conan/tools/cmake/presets.py +++ b/conan/tools/cmake/presets.py @@ -9,18 +9,11 @@ from conans.util.files import save, load -def _build_preset(conanfile, multiconfig): - return _common_build_and_test_preset_fields(conanfile, multiconfig) - - -def _test_preset(conanfile, multiconfig): - return _common_build_and_test_preset_fields(conanfile, multiconfig) - - -def _common_build_and_test_preset_fields(conanfile, multiconfig): +def _build_and_test_preset_fields(conanfile, multiconfig): build_type = conanfile.settings.get_safe("build_type") configure_preset_name = _configure_preset_name(conanfile, multiconfig) - ret = {"name": _build_and_test_preset_name(conanfile), + build_preset_name = _build_and_test_preset_name(conanfile) + ret = {"name": build_preset_name, "configurePreset": configure_preset_name} if multiconfig: ret["configuration"] = build_type @@ -151,17 +144,16 @@ def _contents(conanfile, toolchain_file, cache_variables, generator): Contents for the CMakePresets.json It uses schema version 3 unless it is forced to 2 """ + multiconfig = is_multi_configuration(generator) + conf = _configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig) + build = _build_and_test_preset_fields(conanfile, multiconfig) ret = {"version": _schema_version(conanfile, default=3), + "vendor": {"conan": {}}, "cmakeMinimumRequired": {"major": 3, "minor": 15, "patch": 0}, - "configurePresets": [], - "buildPresets": [], - "testPresets": [] - } - multiconfig = is_multi_configuration(generator) - ret["buildPresets"].append(_build_preset(conanfile, multiconfig)) - ret["testPresets"].append(_test_preset(conanfile, multiconfig)) - _conf = _configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig) - ret["configurePresets"].append(_conf) + "configurePresets": [conf], + "buildPresets": [build], + "testPresets": [build] + } return ret @@ -187,55 +179,55 @@ def write_cmake_presets(conanfile, toolchain_file, generator, cache_variables, preset_path = os.path.join(conanfile.generators_folder, "CMakePresets.json") multiconfig = is_multi_configuration(generator) - - if os.path.exists(preset_path): + if os.path.exists(preset_path) and multiconfig: data = json.loads(load(preset_path)) - build_preset = _build_preset(conanfile, multiconfig) + build_preset = _build_and_test_preset_fields(conanfile, multiconfig) _insert_preset(data, "buildPresets", build_preset) - - test_preset = _test_preset(conanfile, multiconfig) - _insert_preset(data, "testPresets", test_preset) - + _insert_preset(data, "testPresets", build_preset) configure_preset = _configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig) - _insert_preset(data, "configurePresets", configure_preset) + # Conan generated presets should have only 1 configurePreset, no more, overwrite it + data["configurePresets"] = [configure_preset] else: data = _contents(conanfile, toolchain_file, cache_variables, generator) - data = json.dumps(data, indent=4) - save(preset_path, data) - save_cmake_user_presets(conanfile, preset_path, user_presets_path) + preset_content = json.dumps(data, indent=4) + save(preset_path, preset_content) + _save_cmake_user_presets(conanfile, preset_path, user_presets_path) -def save_cmake_user_presets(conanfile, preset_path, user_presets_path=None): - if user_presets_path is False: +def _save_cmake_user_presets(conanfile, preset_path, user_presets_path): + if not user_presets_path: return - # 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 user_presets_path: - output_dir = os.path.join(conanfile.source_folder, user_presets_path) \ - if not os.path.isabs(user_presets_path) else user_presets_path - else: - output_dir = conanfile.source_folder - - if user_presets_path or os.path.exists(os.path.join(output_dir, "CMakeLists.txt")): - """ - Contents for the CMakeUserPresets.json - It uses schema version 4 unless it is forced to 2 - """ - user_presets_path = os.path.join(output_dir, "CMakeUserPresets.json") - if not os.path.exists(user_presets_path): - data = {"version": _schema_version(conanfile, default=4), - "vendor": {"conan": dict()}} - else: - data = json.loads(load(user_presets_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) + # If generators folder is the same as source folder, do not create the user presets + # we already have the CMakePresets.json right there + if not (conanfile.source_folder and conanfile.source_folder != conanfile.generators_folder): + return + + user_presets_path = os.path.join(conanfile.source_folder, user_presets_path) + if os.path.isdir(user_presets_path): # Allows user to specify only the folder + output_dir = user_presets_path + user_presets_path = os.path.join(user_presets_path, "CMakeUserPresets.json") + else: + output_dir = os.path.dirname(user_presets_path) + + if not os.path.exists(os.path.join(output_dir, "CMakeLists.txt")): + return + + # It uses schema version 4 unless it is forced to 2 + if not os.path.exists(user_presets_path): + data = {"version": _schema_version(conanfile, default=4), + "vendor": {"conan": dict()}} + else: + data = json.loads(load(user_presets_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 _get_already_existing_preset_index(name, presets): @@ -281,23 +273,3 @@ def _append_user_preset_path(conanfile, data, preset_path): def load_cmake_presets(folder): tmp = load(os.path.join(folder, "CMakePresets.json")) return json.loads(tmp) - - -def get_configure_preset(cmake_presets, conanfile): - expected_name = _configure_preset_name(conanfile, multiconfig=False) - # Do we find a preset for the current configuration? - for preset in cmake_presets["configurePresets"]: - if preset["name"] == expected_name: - return preset - - expected_name = _configure_preset_name(conanfile, multiconfig=True) - # In case of multi-config generator or None build_type - for preset in cmake_presets["configurePresets"]: - if preset["name"] == expected_name: - return preset - - # FIXME: Might be an issue if someone perform several conan install that involves different - # CMake generators (multi and single config). Would be impossible to determine which - # is the correct configurePreset because the generator IS in the configure preset. - - raise ConanException("Not available configurePreset, expected name is {}".format(expected_name)) diff --git a/conan/tools/cmake/toolchain/toolchain.py b/conan/tools/cmake/toolchain/toolchain.py index 0bfb04bff6a..a18d515fb30 100644 --- a/conan/tools/cmake/toolchain/toolchain.py +++ b/conan/tools/cmake/toolchain/toolchain.py @@ -148,7 +148,7 @@ def __init__(self, conanfile, generator=None): ("output_dirs", OutputDirsBlock)]) check_using_build_profile(self._conanfile) - self.user_presets_path = None + self.user_presets_path = "CMakeUserPresets.json" def _context(self): """ Returns dict, the context for the template diff --git a/conans/test/conftest.py b/conans/test/conftest.py index e7022144854..f41b99dbaa8 100644 --- a/conans/test/conftest.py +++ b/conans/test/conftest.py @@ -87,8 +87,7 @@ }, "3.23": { "path": {'Windows': 'C:/cmake/cmake-3.23.1-win64-x64/bin', - 'Darwin': '/Users/jenkins/cmake/cmake-3.23.1/bin' - if not MacOS_arm else "skip-tests", + 'Darwin': '/Users/jenkins/cmake/cmake-3.23.1/bin', # Not available in Linux 'Linux': "skip-tests"} } diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py index 19d7479bdeb..bfa6239c047 100644 --- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py +++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py @@ -556,20 +556,20 @@ def test_cmake_toolchain_runtime_types_cmake_older_than_3_15(): def test_cmake_presets_missing_option(): client = TestClient(path_with_spaces=False) client.run("new hello/0.1 --template=cmake_exe") - settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=' \ - '\'["options.missing"]\'' + settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=\'["options.missing"]\' ' \ + '-c tools.cmake.cmaketoolchain:generator=Ninja' client.run("install . {}".format(settings_layout)) - assert os.path.exists(os.path.join(client.current_folder, "build", "generators")) + assert os.path.exists(os.path.join(client.current_folder, "build", "Release", "generators")) @pytest.mark.tool_cmake(version="3.23") def test_cmake_presets_missing_setting(): client = TestClient(path_with_spaces=False) client.run("new hello/0.1 --template=cmake_exe") - settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=' \ - '\'["settings.missing"]\'' + settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=\'["settings.missing"]\' ' \ + '-c tools.cmake.cmaketoolchain:generator=Ninja' client.run("install . {}".format(settings_layout)) - assert os.path.exists(os.path.join(client.current_folder, "build", "generators")) + assert os.path.exists(os.path.join(client.current_folder, "build", "Release", "generators")) @pytest.mark.tool_cmake(version="3.23") @@ -733,8 +733,8 @@ def test_remove_missing_presets(): def test_cmake_presets_options_single_config(): client = TestClient(path_with_spaces=False) client.run("new hello/0.1 --template=cmake_lib") - conf_layout = '-c tools.cmake.cmake_layout:build_folder_vars=\'["settings.compiler", ' \ - '"options.shared"]\'' + conf_layout = '-c tools.cmake.cmake_layout:build_folder_vars=\'["settings.compiler",' \ + '"settings.build_type", "options.shared"]\'' default_compiler = {"Darwin": "apple-clang", "Windows": "visual studio", # FIXME: replace it with 'msvc' in develop2 @@ -744,12 +744,12 @@ def test_cmake_presets_options_single_config(): client.run("install . {} -o shared={}".format(conf_layout, shared)) shared_str = "shared_true" if shared else "shared_false" assert os.path.exists(os.path.join(client.current_folder, - "build", "{}-{}".format(default_compiler, shared_str), + "build", "{}-release-{}".format(default_compiler, shared_str), "generators")) client.run("install . {}".format(conf_layout)) assert os.path.exists(os.path.join(client.current_folder, - "build", "{}-shared_false".format(default_compiler), + "build", "{}-release-shared_false".format(default_compiler), "generators")) user_presets_path = os.path.join(client.current_folder, "CMakeUserPresets.json") @@ -759,12 +759,12 @@ def test_cmake_presets_options_single_config(): if platform.system() == "Darwin": for shared in (True, False): shared_str = "shared_true" if shared else "shared_false" - client.run_command("cmake . --preset apple-clang-{}-release".format(shared_str)) - client.run_command("cmake --build --preset apple-clang-{}-release".format(shared_str)) - client.run_command("ctest --preset apple-clang-{}-release".format(shared_str)) + client.run_command("cmake . --preset apple-clang-release-{}".format(shared_str)) + client.run_command("cmake --build --preset apple-clang-release-{}".format(shared_str)) + client.run_command("ctest --preset apple-clang-release-{}".format(shared_str)) the_lib = "libhello.a" if not shared else "libhello.dylib" path = os.path.join(client.current_folder, - "build", "apple-clang-{}".format(shared_str), "release", the_lib) + "build", "apple-clang-release-{}".format(shared_str), the_lib) assert os.path.exists(path) @@ -986,9 +986,6 @@ def test_cmake_presets_with_conanfile_txt(): c.run("install .") c.run("install . -s build_type=Debug") - assert os.path.exists(os.path.join(c.current_folder, "CMakeUserPresets.json")) - presets_path = os.path.join(c.current_folder, "build", "generators", "CMakePresets.json") - assert os.path.exists(presets_path) if platform.system() != "Windows": c.run_command("cmake --preset debug") diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py index c308a627999..50f1eb78986 100644 --- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py +++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py @@ -399,6 +399,7 @@ def test_cmake_presets_multiconfig(): client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Release --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) + assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 1 assert presets["buildPresets"][0]["configuration"] == "Release" assert len(presets["testPresets"]) == 1 @@ -406,6 +407,7 @@ def test_cmake_presets_multiconfig(): client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) + assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 2 assert presets["buildPresets"][0]["configuration"] == "Release" assert presets["buildPresets"][1]["configuration"] == "Debug" @@ -417,6 +419,7 @@ def test_cmake_presets_multiconfig(): "--profile:h=profile") client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=MinSizeRel --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) + assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 4 assert presets["buildPresets"][0]["configuration"] == "Release" assert presets["buildPresets"][1]["configuration"] == "Debug" @@ -432,6 +435,7 @@ def test_cmake_presets_multiconfig(): client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile") client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) + assert len(presets["configurePresets"]) == 1 assert len(presets["buildPresets"]) == 4 assert presets["buildPresets"][0]["configuration"] == "Release" assert presets["buildPresets"][1]["configuration"] == "Debug" @@ -449,6 +453,9 @@ def test_cmake_presets_multiconfig(): def test_cmake_presets_singleconfig(): + """ without defining a layout, single config always overwrites + the existing CMakePresets.json + """ client = TestClient() profile = textwrap.dedent(""" [settings] @@ -472,22 +479,22 @@ def test_cmake_presets_singleconfig(): assert len(presets["testPresets"]) == 1 assert presets["testPresets"][0]["configurePreset"] == "release" - # Now two configurePreset, but named correctly + # This overwrites the existing profile, as there is no layout client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) - assert len(presets["configurePresets"]) == 2 - assert presets["configurePresets"][1]["name"] == "debug" + assert len(presets["configurePresets"]) == 1 + assert presets["configurePresets"][0]["name"] == "debug" - assert len(presets["buildPresets"]) == 2 - assert presets["buildPresets"][1]["configurePreset"] == "debug" + assert len(presets["buildPresets"]) == 1 + assert presets["buildPresets"][0]["configurePreset"] == "debug" - assert len(presets["testPresets"]) == 2 - assert presets["testPresets"][1]["configurePreset"] == "debug" + assert len(presets["testPresets"]) == 1 + assert presets["testPresets"][0]["configurePreset"] == "debug" # Repeat configuration, it shouldn't add a new one client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile") presets = json.loads(client.load("CMakePresets.json")) - assert len(presets["configurePresets"]) == 2 + assert len(presets["configurePresets"]) == 1 def test_toolchain_cache_variables():