Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 54 additions & 23 deletions conan/tools/cmake/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,32 +297,62 @@ def generate(conanfile, preset_path, user_presets_path, preset_prefix, preset_da
if not os.path.exists(user_presets_path):
data = {"version": 4,
"vendor": {"conan": dict()}}
for preset, inherits in inherited_user.items():
for i in inherits:
data.setdefault(preset, []).append({"name": i})
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

if inherited_user:
_IncludingPresets._clean_user_inherits(data, preset_data)
data = _IncludingPresets._append_user_preset_path(data, preset_path, output_dir, absolute_paths)

if not absolute_paths:
try: # Make it relative to the CMakeUserPresets.json if possible
preset_path = os.path.relpath(preset_path, output_dir)
# If we don't normalize, path will be removed in Linux shared folders
# https://github.com/conan-io/conan/issues/18434
preset_path = preset_path.replace("\\", "/")
except ValueError:
pass
data = _IncludingPresets._append_user_preset_path(data, preset_path, output_dir)
if inherited_user:
data = _IncludingPresets._update_stubs(data, inherited_user, output_dir, absolute_paths)

data = json.dumps(data, indent=4)
ConanOutput(str(conanfile)).info(f"CMakeToolchain generated: {user_presets_path}")
save(user_presets_path, data)

@staticmethod
def _update_stubs(data, inherited_user, output_dir, absolute_paths):

included_files = []
for inc in data.get("include", []):
inc_path = os.path.join(output_dir, inc) if not absolute_paths else inc
if os.path.exists(inc_path):
included_files.append(inc_path)

# Read include field from data and get all absolute paths to included presets if they exist
real_preset_names = set()
build_preset_to_configure_preset = {}

for inc_path in included_files:
try:
inc_json = json.loads(load(inc_path))
except Exception:
continue
for preset_type in ("configurePresets", "buildPresets", "testPresets"):
for p in inc_json.get(preset_type, []):
name = p.get("name")
if name:
real_preset_names.add(name)
if preset_type in ("buildPresets", "testPresets"):
configure_preset = p.get("configurePreset")
if configure_preset:
build_preset_to_configure_preset[name] = configure_preset

for preset_type, inherited_names in inherited_user.items():
stubs = []
for p in inherited_names:
if p not in real_preset_names:
stub = {"name": p}
# For buildPresets and testPresets, add configurePreset if mapping exists
# or use the same name if no mapping (assuming same base name)
if preset_type in ("buildPresets", "testPresets"):
stub["configurePreset"] = build_preset_to_configure_preset.get(p, p)
stubs.append(stub)
data[preset_type] = stubs
return data

@staticmethod
def _collect_user_inherits(output_dir, preset_prefix):
# Collect all the existing targets in the user files, to create empty conan- presets
Expand All @@ -348,19 +378,20 @@ def _collect_user_inherits(output_dir, preset_prefix):
return collected_targets

@staticmethod
def _clean_user_inherits(data, preset_data):
for preset_type in "configurePresets", "buildPresets", "testPresets":
presets = preset_data.get(preset_type, [])
presets_names = [p["name"] for p in presets]
other = data.get(preset_type, [])
other[:] = [p for p in other if p["name"] not in presets_names]

@staticmethod
def _append_user_preset_path(data, preset_path, output_dir):
def _append_user_preset_path(data, preset_path, output_dir, absolute_paths):
""" - Appends a 'include' to preset_path if the schema supports it.
- Otherwise it merges to "data" all the configurePresets, buildPresets etc from the
read preset_path.
"""
if not absolute_paths:
try: # Make it relative to the CMakeUserPresets.json if possible
preset_path = os.path.relpath(preset_path, output_dir)
# If we don't normalize, path will be removed in Linux shared folders
# https://github.com/conan-io/conan/issues/18434
preset_path = preset_path.replace("\\", "/")
except ValueError:
pass

if "include" not in data:
data["include"] = []
# Clear the folders that have been deleted
Expand Down
148 changes: 148 additions & 0 deletions test/functional/toolchains/cmake/test_cmake_toolchain_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import platform
import textwrap
from shutil import rmtree

import pytest

Expand Down Expand Up @@ -675,3 +676,150 @@ def generate(self):

c.run_command(f"ctest --preset conan-release")
assert "tests passed" in c.out


@pytest.mark.tool("cmake", "3.23")
@pytest.mark.parametrize("presets", ["CMakePresets.json", "CMakeUserPresets.json"])
def test_cmake_presets_shared_preset(presets):
"""valid user preset file is created when multiple project presets inherit
from the same conan presets.
"""
client = TestClient()
project_presets = textwrap.dedent("""
{
"version": 4,
"cmakeMinimumRequired": {
"major": 3,
"minor": 23,
"patch": 0
},
"include":["ConanPresets.json"],
"configurePresets": [
{
"name": "debug1",
"inherits": ["conan-debug"]
},
{
"name": "debug2",
"inherits": ["conan-debug"]
},
{
"name": "release1",
"inherits": ["conan-release"]
},
{
"name": "release2",
"inherits": ["conan-release"]
}
]
}""")
conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout, CMakeToolchain

class TestPresets(ConanFile):
generators = ["CMakeDeps"]
settings = "build_type"

def layout(self):
cmake_layout(self)

def generate(self):
tc = CMakeToolchain(self)
tc.user_presets_path = 'ConanPresets.json'
tc.generate()
""")

client.save({presets: project_presets,
"conanfile.py": conanfile,
"CMakeLists.txt": ""}) # File must exist for Conan to do Preset things.

client.run("install . -s build_type=Debug")
conan_presets = json.loads(client.load("ConanPresets.json"))
assert len(conan_presets["configurePresets"]) == 1
assert conan_presets["configurePresets"][0]["name"] == "conan-release"

# check that if you remove the build folder and regenerate the presets
# the stubs are regenerated correctly
client.run("install . -s build_type=Release")
client.run_command("cmake --list-presets")
assert "'conan-debug' config" in client.out
assert "'conan-release' config" in client.out

rmtree(os.path.join(client.current_folder, "build"))
client.run("install . -s build_type=Release")
client.run_command("cmake --list-presets")
assert "'conan-release' config" in client.out


@pytest.mark.tool("cmake", "3.23")
@pytest.mark.parametrize("presets", ["CMakePresets.json", "CMakeUserPresets.json"])
def test_cmake_presets_shared_build_preset(presets):
"""valid user preset file is created when multiple project buildPresets inherit
from the same conan buildPresets, and stubs have configurePreset field.
"""
client = TestClient()
project_presets = textwrap.dedent("""
{
"version": 4,
"cmakeMinimumRequired": {
"major": 3,
"minor": 23,
"patch": 0
},
"include":["ConanPresets.json"],
"configurePresets": [
{
"name": "debug1",
"inherits": ["conan-debug"]
},
{
"name": "release1",
"inherits": ["conan-release"]
}
],
"buildPresets": [
{
"name": "build-debug1",
"configurePreset": "debug1",
"inherits": ["conan-debug"]
},
{
"name": "build-release1",
"configurePreset": "release1",
"inherits": ["conan-release"]
}
]
}""")
conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout, CMakeToolchain

class TestPresets(ConanFile):
generators = ["CMakeDeps"]
settings = "build_type"

def layout(self):
cmake_layout(self)

def generate(self):
tc = CMakeToolchain(self)
tc.user_presets_path = 'ConanPresets.json'
tc.generate()
""")

client.save({presets: project_presets,
"conanfile.py": conanfile,
"CMakeLists.txt": ""}) # File must exist for Conan to do Preset things.

client.run("install . -s build_type=Debug")
conan_presets = json.loads(client.load("ConanPresets.json"))
assert "buildPresets" in conan_presets
build_presets = conan_presets["buildPresets"]
assert len(build_presets) == 1, f"Should have 1 buildPreset stub."
release_stub = build_presets[0]
assert release_stub["name"] == "conan-release"
assert "configurePreset" in release_stub, "buildPreset stub should have configurePreset field"
assert release_stub["configurePreset"] == "conan-release"
client.run_command("cmake --list-presets")
assert "'conan-debug' config" in client.out
62 changes: 0 additions & 62 deletions test/integration/toolchains/cmake/test_cmaketoolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,68 +573,6 @@ def layout(self):
assert presets["configurePresets"][0]["binaryDir"] == build_dir


@pytest.mark.parametrize("presets", ["CMakePresets.json", "CMakeUserPresets.json"])
def test_cmake_presets_shared_preset(presets):
"""valid user preset file is created when multiple project presets inherit
from the same conan presets.
"""
client = TestClient()
project_presets = textwrap.dedent("""
{
"version": 4,
"cmakeMinimumRequired": {
"major": 3,
"minor": 23,
"patch": 0
},
"include":["ConanPresets.json"],
"configurePresets": [
{
"name": "debug1",
"inherits": ["conan-debug"]
},
{
"name": "debug2",
"inherits": ["conan-debug"]
},
{
"name": "release1",
"inherits": ["conan-release"]
},
{
"name": "release2",
"inherits": ["conan-release"]
}
]
}""")
conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.cmake import cmake_layout, CMakeToolchain

class TestPresets(ConanFile):
generators = ["CMakeDeps"]
settings = "build_type"

def layout(self):
cmake_layout(self)

def generate(self):
tc = CMakeToolchain(self)
tc.user_presets_path = 'ConanPresets.json'
tc.generate()
""")

client.save({presets: project_presets,
"conanfile.py": conanfile,
"CMakeLists.txt": ""}) # File must exist for Conan to do Preset things.

client.run("install . -s build_type=Debug")

conan_presets = json.loads(client.load("ConanPresets.json"))
assert len(conan_presets["configurePresets"]) == 1
assert conan_presets["configurePresets"][0]["name"] == "conan-release"


def test_cmake_presets_multiconfig():
client = TestClient()
profile = textwrap.dedent("""
Expand Down
Loading