From 2d3715c5ff468dff5da138bbef5d120c1ab8592d Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 4 Apr 2026 22:14:41 +0100 Subject: [PATCH] fix(gameengine): resolve duplicate CMake preset error from stale Conan layouts When Conan's build output layout changes (e.g. build/generators/ vs build/Release/generators/), CMakeUserPresets.json accumulates includes that define the same preset names, causing CMake to fail with "Duplicate preset". The fix detects preset name collisions across included files and keeps only the newest, in addition to pruning missing files. Co-Authored-By: Claude Opus 4.6 (1M context) --- gameengine/python/dev_commands.py | 43 ++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/gameengine/python/dev_commands.py b/gameengine/python/dev_commands.py index 7751dc85b..840357614 100755 --- a/gameengine/python/dev_commands.py +++ b/gameengine/python/dev_commands.py @@ -338,7 +338,14 @@ def generate(args: argparse.Namespace) -> None: def _fix_cmake_user_presets() -> None: - """Ensure CMakeUserPresets.json only includes existing preset files.""" + """Ensure CMakeUserPresets.json only includes existing, non-conflicting preset files. + + Conan regenerates preset files when the build layout changes (e.g. + ``build-ninja/build/generators/`` vs ``build-ninja/build/Release/generators/``). + If the old file still exists, CMake fails with "Duplicate preset". This + function removes missing includes *and* detects preset name collisions, + keeping only the newest file when duplicates are found. + """ import json as json_mod presets_path = Path("CMakeUserPresets.json") if not presets_path.exists(): @@ -346,11 +353,39 @@ def _fix_cmake_user_presets() -> None: try: data = json_mod.loads(presets_path.read_text()) includes = data.get("include", []) + + # Drop missing files valid = [p for p in includes if Path(p).exists()] - if len(valid) != len(includes): - data["include"] = valid + + # Detect and resolve duplicate preset names across included files + seen_names: dict[str, str] = {} # preset_name -> include_path + duplicates: set[str] = set() + for inc_path in valid: + try: + inc_data = json_mod.loads(Path(inc_path).read_text()) + except (json_mod.JSONDecodeError, OSError): + continue + for key in ("configurePresets", "buildPresets", "testPresets"): + for preset in inc_data.get(key, []): + name = preset.get("name", "") + if name in seen_names and seen_names[name] != inc_path: + # Keep the newer file, drop the older one + older = seen_names[name] + newer = inc_path + if Path(older).stat().st_mtime > Path(newer).stat().st_mtime: + older, newer = newer, older + duplicates.add(older) + seen_names[name] = newer + else: + seen_names[name] = inc_path + + deduped = [p for p in valid if p not in duplicates] + + if deduped != includes: + data["include"] = deduped presets_path.write_text(json_mod.dumps(data, indent=4) + "\n") - print(f" Fixed CMakeUserPresets.json: kept {len(valid)}/{len(includes)} includes") + removed = len(includes) - len(deduped) + print(f" Fixed CMakeUserPresets.json: removed {removed} stale/duplicate include(s)") except (json_mod.JSONDecodeError, OSError): pass