Skip to content

Commit

Permalink
CMakePresets compatible with schema 2 (#11655)
Browse files Browse the repository at this point in the history
* tests pending

* Added tests

* Fix default

* not cpp 20

* Update conan/tools/cmake/presets.py

Co-authored-by: James <james@conan.io>

* fix

* Fix win test

* Tested win

Co-authored-by: James <james@conan.io>
  • Loading branch information
lasote and memsharded committed Jul 19, 2022
1 parent d40f5b5 commit abfe3f6
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 18 deletions.
75 changes: 66 additions & 9 deletions conan/tools/cmake/presets.py
Expand Up @@ -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 <ref>", we don't have build_folder, because
# we don't even have a conanfile with a `layout()` to determine the build folder.
Expand All @@ -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": [],
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions conans/model/conf.py
Expand Up @@ -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",
Expand Down
45 changes: 44 additions & 1 deletion conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
82 changes: 74 additions & 8 deletions conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
Expand Up @@ -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()
Expand All @@ -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("\\",
"/")

0 comments on commit abfe3f6

Please sign in to comment.