Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CMakePresets compatible with schema 2 #11655

Merged
merged 10 commits into from Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
memsharded marked this conversation as resolved.
Show resolved Hide resolved

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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is confusing, the above uses a default of =3, and now a default=4. Not sure what it means.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because the schema version of the CMakePreset is 3 but the version of the CMakeUserPreset is 4.
Let me do it better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit better now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still seems a bit confusing, because if user provides schema=3 in the conf, that will be ignored, and generate a schema=2 in the json, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the user provides a tools.cmake.cmaketoolchain.presets:max_schema_version=3, that won't be ignored and generates a schema=2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can convert the conf to tools.cmake.cmaketoolchain.presets:v2_schema=True if you prefer, but I thought that we might end with more similar configs in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I overlooked it is max_schema_version, not schema_version. Then it makes a bit more sense, yeah.

"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("\\",
"/")