From 0711fba50127e4c0999ba64ba0a50036d6c02b27 Mon Sep 17 00:00:00 2001 From: Aleksandrs Gumenuks Date: Fri, 17 Jul 2026 20:53:00 +0200 Subject: [PATCH] An AI aided attempt to implement CMake preset support. relates to #205 --- CHANGELOG.md | 5 + README.md | 3 + .../mbs/internal/BuildscriptGenerator.java | 163 ++++++-- .../mbs/internal/CMakeBuildRunner.java | 14 +- .../mbs/internal/CMakePresetsLoader.java | 389 ++++++++++++++++++ .../mbs/internal/CMakePresetsLoaderTest.java | 118 ++++++ 6 files changed, 652 insertions(+), 40 deletions(-) create mode 100644 de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/CMakePresetsLoader.java create mode 100644 de.marw.cmake4eclipse.mbs/src/test/java/de/marw/cmake4eclipse/mbs/internal/CMakePresetsLoaderTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index fc2fc690..778f6aa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Cmake4eclipse Change Log +## 5.2.0 (2026-07-17) +### Changes +- Enhancement: load `CMakePresets.json` and apply matching configure/build preset settings (generator, binary + directory, cache variables and configure debug/trace/warning flags) for the active Eclipse build configuration. + ## 5.1.0 (2025-10-22) ### Changes - Enhancement: add button to search for MSYS installations diff --git a/README.md b/README.md index a9c261c9..bbfafdb0 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ make your product builds immune to downtimes on cloudsmith. ## Debug and Build This project uses Apache maven as its build system. To build from a command-line, run `mvn package` in the root directory of the project source files. +To run tests from a command-line, run +`MAVEN_OPTS="-Djdk.xml.maxGeneralEntitySizeLimit=0 -Djdk.xml.totalEntitySizeLimit=0" mvn -e -pl releng/targetplatform,de.marw.cmake4eclipse.mbs -am test` +in the root directory of the project source files. There is also a run configuration for eclipse to invoke the maven build: `build cmake4eclipse`. diff --git a/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/BuildscriptGenerator.java b/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/BuildscriptGenerator.java index 86e7e19f..39594821 100644 --- a/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/BuildscriptGenerator.java +++ b/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/BuildscriptGenerator.java @@ -52,6 +52,7 @@ import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.IResourceStatus; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.IPath; @@ -95,6 +96,7 @@ public class BuildscriptGenerator implements IManagedBuilderMakefileGenerator2 { private IBuilder builder; /** build path - relative to the project. Lazily instantiated */ private IPath buildRelPath; + private Optional resolvedPreset = null; /** */ public BuildscriptGenerator() { @@ -113,6 +115,7 @@ public void initialize(int buildKind, IConfiguration cfg, IBuilder builder, IPro this.config = cfg; this.builder = builder; this.buildRelPath = null; + this.resolvedPreset = null; } /*- @@ -128,6 +131,7 @@ public void initialize(IProject project, IManagedBuildInfo info, IProgressMonito // Cache the build tools config = info.getDefaultConfiguration(); builder = config.getEditableBuilder(); + resolvedPreset = null; } private IPath getRelBuildPath() { @@ -137,7 +141,7 @@ private IPath getRelBuildPath() { final ICConfigurationDescription cfgd = ManagedBuildManager.getDescriptionForConfiguration(config); try { CMakeSettings prefs = ConfigurationManager.getInstance().getOrLoad(cfgd); - buildDirStr = prefs.getBuildDirectory(); + buildDirStr = resolveBuildDirectory(cfgd, prefs); } catch (CoreException e) { // storage base is null; treat as bug in CDT.. log.log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "falling back to hard coded build directory", e)); @@ -164,6 +168,9 @@ public IPath getBuildWorkingDir() { // So return a workspace relative path here final IPath fullPath = project.getFullPath(); IPath buildPath = getRelBuildPath(); + if (buildPath.isAbsolute()) { + return buildPath; + } if (buildPath.segmentCount() == 0) { return fullPath; } @@ -205,29 +212,12 @@ private MultiStatus generateBuildscripts(boolean forceGeneration) throws CoreExc final ICConfigurationDescription cfgDes = ManagedBuildManager.getDescriptionForConfiguration(config); - IPath cmakelistsPath; - ICStorageElement storage = cfgDes.getProjectDescription().getStorage(CMakeSettings.CFG_STORAGE_ID, false); - if (storage != null) { - // Cmake4eclipse nature holds a path to the top-level cmakelists.txt file - String cmakelists = storage.getAttribute(CMakeSettings.ATTR_CMAKELISTS_FLDR); - cmakelistsPath = new Path(cmakelists); - } else { - // classic cmake4eclipse with MBS build system... - // .. assumes the top-level cmakelists.txt file is below the (single) source location - ICSourceEntry[] srcEntries = config.getSourceEntries(); - - // do a sanity check.. - if (srcEntries.length == 0) { - // no source folders specified in project - final String msg = "No source directories configured for project"; - MultiStatus status = new MultiStatus(Activator.PLUGIN_ID, IStatus.ERROR, msg + " " + project.getName(), null); - createErrorMarker(project, msg); - return status; - } else { - srcEntries = CDataUtil.resolveEntries(srcEntries, cfgDes); - // assume the first source directory contains a CMakeLists.txt - cmakelistsPath = srcEntries[0].getFullPath(); - } + IPath cmakelistsPath = getCmakelistsPath(cfgDes); + if (cmakelistsPath == null) { + final String msg = "No source directories configured for project"; + MultiStatus status = new MultiStatus(Activator.PLUGIN_ID, IStatus.ERROR, msg + " " + project.getName(), null); + createErrorMarker(project, msg); + return status; } // See if the user has cancelled the build @@ -235,16 +225,19 @@ private MultiStatus generateBuildscripts(boolean forceGeneration) throws CoreExc boolean mustGenerate= forceGeneration; - final IContainer buildFolder; IPath buildPath = getRelBuildPath(); - if (buildPath.segmentCount() == 0) { - buildFolder= project; + final IPath buildLocation; + if (buildPath.isAbsolute()) { + buildLocation = buildPath; + } else if (buildPath.segmentCount() == 0) { + buildLocation = project.getLocation(); } else { - buildFolder = project.getFolder(buildPath); - createFolder((IFolder) buildFolder); + final IFolder buildFolder = project.getFolder(buildPath); + createFolder(buildFolder); + buildLocation = buildFolder.getLocation(); } // make sure we have a resource to attach session properties to - final java.nio.file.Path buildDir = Paths.get(buildFolder.getLocationURI()); + final java.nio.file.Path buildDir = Paths.get(buildLocation.toOSString()); IEclipsePreferences prefs = PreferenceAccess.getPreferences(); try { @@ -305,10 +298,10 @@ public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes at console.getInfoStream().write(msg.getBytes()); } catch (IOException ignore) { } - IContainer cmakelistsDir = cmakelistsPath.isEmpty() ? project : project.getFolder(cmakelistsPath); + IContainer cmakelistsDir = getCmakelistsDir(cmakelistsPath); checkCancel(); - MultiStatus status = invokeCMake(cmakelistsDir, buildFolder.getLocation(), console, overwritingToolkit); + MultiStatus status = invokeCMake(cmakelistsDir, buildLocation, console, overwritingToolkit); // NOTE: Commonbuilder reads getCode() to detect errors, not getSeverity() if (status.getCode() == IStatus.ERROR) { // failed to generate @@ -485,6 +478,7 @@ private List buildCommandline(IPath srcDir, Optional preset = getResolvedPreset(cfgd); boolean needVerboseBuild = false; { @@ -507,10 +501,12 @@ private List buildCommandline(IPath srcDir, Optional buildCommandline(IPath srcDir, Optional buildCommandline(IPath srcDir, Optional entries = PreferenceAccess.toListFromJson(CmakeDefine.class, json); appendDefines(args, entries, null); } + if (preset.isPresent()) { + CMakePresetsLoader.ResolvedConfigurePreset p = preset.get(); + appendDefines(args, p.getCacheVariables(), cfgd); + args.addAll(p.getConfigureArguments()); + } /* project settings... */ { final CMakeSettings prefs = ConfigurationManager.getInstance().getOrLoad(cfgd); @@ -585,6 +590,90 @@ private List buildCommandline(IPath srcDir, Optional 0 && project.getName().equals(cmakelistsPath.segment(0)))) { + return ResourcesPlugin.getWorkspace().getRoot().getFolder(cmakelistsPath); + } + return project.getFolder(cmakelistsPath); + } + + private String resolveBuildDirectory(ICConfigurationDescription cfgd, CMakeSettings prefs) { + String buildDirectory = prefs.getBuildDirectory(); + Optional preset = getResolvedPreset(cfgd); + if (preset.isPresent()) { + String binaryDir = preset.get().getBinaryDir(); + if (binaryDir != null && !binaryDir.isBlank()) { + java.nio.file.Path path = Paths.get(binaryDir); + if (!path.isAbsolute()) { + try { + IPath cmakelistsPath = getCmakelistsPath(cfgd); + if (cmakelistsPath != null) { + IPath sourceLocation = getCmakelistsDir(cmakelistsPath).getLocation(); + if (sourceLocation != null) { + path = Paths.get(sourceLocation.toOSString()).resolve(path); + } + } + } catch (CoreException ex) { + log.log(new Status(IStatus.WARNING, Activator.PLUGIN_ID, "Failed to resolve preset binaryDir", ex)); + } + } + buildDirectory = path.normalize().toString(); + } + } + return buildDirectory; + } + + private Optional getResolvedPreset(ICConfigurationDescription cfgd) { + if (resolvedPreset != null) { + return resolvedPreset; + } + try { + IPath cmakelistsPath = getCmakelistsPath(cfgd); + if (cmakelistsPath == null) { + resolvedPreset = Optional.empty(); + return resolvedPreset; + } + IContainer cmakelistsDir = getCmakelistsDir(cmakelistsPath); + IPath sourceLocation = cmakelistsDir.getLocation(); + if (sourceLocation == null) { + resolvedPreset = Optional.empty(); + return resolvedPreset; + } + resolvedPreset = CMakePresetsLoader.loadResolvedConfigurePreset(Paths.get(sourceLocation.toOSString()), + cfgd.getName()); + return resolvedPreset; + } catch (IOException | RuntimeException | CoreException ex) { + log.log(new Status(IStatus.WARNING, Activator.PLUGIN_ID, "Failed to load CMakePresets.json", ex)); + resolvedPreset = Optional.empty(); + return resolvedPreset; + } + } + + private static boolean hasVariable(Optional preset, String name) { + if (preset.isEmpty()) { + return false; + } + return preset.get().getCacheVariables().stream().anyMatch(v -> name.equalsIgnoreCase(v.getName())); + } + /** * Appends arguments for the specified cmake undefines. * diff --git a/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/CMakeBuildRunner.java b/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/CMakeBuildRunner.java index 2725728d..eac33ee2 100644 --- a/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/CMakeBuildRunner.java +++ b/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/CMakeBuildRunner.java @@ -168,8 +168,16 @@ private String getCommandFromCMakeCache(ICConfigurationDescription cfgd, IProjec final String cwd = CCorePlugin.getDefault().getCdtVariableManager().resolveValue(builderCWD.toString(), "", null, //$NON-NLS-1$ cfgd); - final IFile file0 = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(cwd).append("CMakeCache.txt")); //$NON-NLS-1$ - final File file = Paths.get(file0.getLocationURI()).toFile(); + final File file; + final IFile file0; + IPath cachePath = new Path(cwd).append("CMakeCache.txt"); //$NON-NLS-1$ + if (cachePath.isAbsolute()) { + file = Paths.get(cachePath.toOSString()).toFile(); + file0 = null; + } else { + file0 = ResourcesPlugin.getWorkspace().getRoot().getFile(cachePath); + file = Paths.get(file0.getLocationURI()).toFile(); + } if (file != null && file.isFile()) { final long lastModified = file.lastModified(); @@ -205,7 +213,7 @@ public boolean accept(String key) { } } catch (IOException ex) { throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, - "Failed to parse file " + file0, ex)); + "Failed to parse file " + (file0 == null ? file : file0), ex)); } } } else { diff --git a/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/CMakePresetsLoader.java b/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/CMakePresetsLoader.java new file mode 100644 index 00000000..5e8aca95 --- /dev/null +++ b/de.marw.cmake4eclipse.mbs/src/main/java/de/marw/cmake4eclipse/mbs/internal/CMakePresetsLoader.java @@ -0,0 +1,389 @@ +/******************************************************************************* + * Copyright (c) 2026 Martin Weber. + * + * Content is provided to you under the terms and conditions of the Eclipse Public License Version 2.0 "EPL". + * A copy of the EPL is available at http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package de.marw.cmake4eclipse.mbs.internal; + +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import de.marw.cmake4eclipse.mbs.settings.CmakeDefine; +import de.marw.cmake4eclipse.mbs.settings.CmakeVariableType; + +/** + * Loads and resolves CMake configure/build presets from {@code CMakePresets.json}. + */ +final class CMakePresetsLoader { + private static final Gson gson = new Gson(); + private static final Pattern ENV_REF = Pattern.compile("\\$(p?env)\\{([^}]+)\\}"); + + private CMakePresetsLoader() { + } + + static Optional loadResolvedConfigurePreset(Path sourceDir, String configurationName) + throws IOException { + final Path file = sourceDir.resolve("CMakePresets.json"); + if (!Files.isRegularFile(file)) { + return Optional.empty(); + } + + final JsonObject root; + try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + root = JsonParser.parseReader(reader).getAsJsonObject(); + } + + final Map configurePresets = toPresetMap(root.getAsJsonArray("configurePresets")); + if (configurePresets.isEmpty()) { + return Optional.empty(); + } + final Map buildPresets = toPresetMap(root.getAsJsonArray("buildPresets")); + + final String configurePresetName = resolveConfigurePresetName(configurationName, buildPresets, configurePresets); + if (configurePresetName == null) { + return Optional.empty(); + } + + final JsonObject resolved = resolvePreset(configurePresetName, configurePresets, new HashMap<>(), new HashSet<>()); + if (resolved == null || isHidden(resolved)) { + return Optional.empty(); + } + final String name = getString(resolved, "name"); + final String generator = getString(resolved, "generator"); + final String binaryDir = expandBinaryDir(getString(resolved, "binaryDir"), sourceDir, name, generator); + final List cacheVariables = toCacheVariables(resolved.getAsJsonObject("cacheVariables")); + final List configureArguments = toConfigureArguments(resolved); + + return Optional.of(new ResolvedConfigurePreset(name, generator, binaryDir, cacheVariables, configureArguments)); + } + + private static String resolveConfigurePresetName(String configurationName, Map buildPresets, + Map configurePresets) { + JsonObject buildPreset = findNamedPreset(buildPresets, configurationName); + if (buildPreset != null) { + JsonObject resolvedBuildPreset = resolvePreset(getString(buildPreset, "name"), buildPresets, new HashMap<>(), + new HashSet<>()); + if (resolvedBuildPreset == null) { + return null; + } + String configurePresetName = getString(resolvedBuildPreset, "configurePreset"); + if (configurePresetName != null && configurePresets.containsKey(configurePresetName)) { + return configurePresetName; + } + } + JsonObject configurePreset = findNamedPreset(configurePresets, configurationName); + if (configurePreset != null) { + return getString(configurePreset, "name"); + } + return null; + } + + private static Map toPresetMap(Iterable array) { + if (array == null) { + return Collections.emptyMap(); + } + final Map result = new HashMap<>(); + for (JsonElement preset : array) { + if (preset == null || !preset.isJsonObject()) { + continue; + } + final JsonObject presetObj = preset.getAsJsonObject(); + final String name = getString(presetObj, "name"); + if (name != null && !name.isBlank()) { + result.put(name, presetObj); + } + } + return result; + } + + private static JsonObject findNamedPreset(Map presets, String name) { + if (name == null) { + return null; + } + JsonObject exact = presets.get(name); + if (exact != null) { + return exact; + } + final String needle = name.toLowerCase(Locale.ROOT); + for (Map.Entry entry : presets.entrySet()) { + if (entry.getKey().toLowerCase(Locale.ROOT).equals(needle)) { + return entry.getValue(); + } + } + return null; + } + + private static JsonObject resolvePreset(String name, Map presets, Map cache, + Set visited) { + if (name == null) { + return null; + } + JsonObject fromCache = cache.get(name); + if (fromCache != null) { + return fromCache; + } + JsonObject self = presets.get(name); + if (self == null) { + return null; + } + if (!visited.add(name)) { + return null; + } + + JsonObject merged = new JsonObject(); + for (String parent : getInherits(self)) { + JsonObject resolvedParent = resolvePreset(parent, presets, cache, visited); + if (resolvedParent != null) { + mergeInto(merged, resolvedParent); + } + } + mergeInto(merged, self); + visited.remove(name); + cache.put(name, merged); + return merged; + } + + private static List getInherits(JsonObject preset) { + JsonElement inherits = preset.get("inherits"); + if (inherits == null || inherits.isJsonNull()) { + return Collections.emptyList(); + } + if (inherits.isJsonPrimitive() && inherits.getAsJsonPrimitive().isString()) { + return List.of(inherits.getAsString()); + } + if (inherits.isJsonArray()) { + List names = new ArrayList<>(); + for (JsonElement e : inherits.getAsJsonArray()) { + if (e != null && e.isJsonPrimitive() && e.getAsJsonPrimitive().isString()) { + names.add(e.getAsString()); + } + } + return names; + } + return Collections.emptyList(); + } + + private static void mergeInto(JsonObject target, JsonObject source) { + for (Map.Entry entry : source.entrySet()) { + String key = entry.getKey(); + JsonElement sourceValue = entry.getValue(); + JsonElement targetValue = target.get(key); + if (targetValue != null && targetValue.isJsonObject() && sourceValue.isJsonObject()) { + mergeInto(targetValue.getAsJsonObject(), sourceValue.getAsJsonObject()); + } else { + target.add(key, sourceValue.deepCopy()); + } + } + } + + private static boolean isHidden(JsonObject preset) { + JsonElement hidden = preset.get("hidden"); + return hidden != null && hidden.isJsonPrimitive() && hidden.getAsJsonPrimitive().isBoolean() + && hidden.getAsBoolean(); + } + + private static String getString(JsonObject object, String member) { + JsonElement value = object.get(member); + if (value == null || value.isJsonNull()) { + return null; + } + if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) { + return value.getAsString(); + } + return null; + } + + private static List toCacheVariables(JsonObject cacheVariables) { + if (cacheVariables == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (Map.Entry entry : cacheVariables.entrySet()) { + final String name = entry.getKey(); + final JsonElement value = entry.getValue(); + if (value == null || value.isJsonNull()) { + continue; + } + if (value.isJsonObject()) { + JsonObject object = value.getAsJsonObject(); + String text = asScalarText(object.get("value")); + if (text == null) { + text = gson.toJson(object.get("value")); + } + result.add(new CmakeDefine(name, toType(getString(object, "type")), text == null ? "" : text)); + } else { + result.add(new CmakeDefine(name, inferType(value), asScalarText(value))); + } + } + return result; + } + + private static List toConfigureArguments(JsonObject preset) { + List args = new ArrayList<>(); + JsonObject warnings = preset.getAsJsonObject("warnings"); + if (warnings != null) { + if (getBoolean(warnings, "dev", false)) { + args.add("-Wdev"); + } + if (getBoolean(warnings, "deprecated", false)) { + args.add("-Wdeprecated"); + } + if (getBoolean(warnings, "uninitialized", false)) { + args.add("--warn-uninitialized"); + } + if (getBoolean(warnings, "unusedCli", false)) { + args.add("--warn-unused-cli"); + } + } + JsonObject debug = preset.getAsJsonObject("debug"); + if (debug != null) { + if (getBoolean(debug, "output", false)) { + args.add("--debug-output"); + } + if (getBoolean(debug, "tryCompile", false)) { + args.add("--debug-trycompile"); + } + if (getBoolean(debug, "find", false)) { + args.add("--debug-find"); + } + } + JsonObject trace = preset.getAsJsonObject("trace"); + if (trace != null) { + final String mode = getString(trace, "mode"); + if ("expand".equals(mode)) { + args.add("--trace-expand"); + } else if (mode != null) { + args.add("--trace"); + } else if (getBoolean(trace, "enable", false)) { + args.add("--trace"); + } + } + return args; + } + + private static boolean getBoolean(JsonObject object, String member, boolean defaultValue) { + JsonElement value = object.get(member); + if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) { + return defaultValue; + } + return value.getAsBoolean(); + } + + private static CmakeVariableType inferType(JsonElement value) { + if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isBoolean()) { + return CmakeVariableType.BOOL; + } + return CmakeVariableType.STRING; + } + + private static CmakeVariableType toType(String text) { + if (text == null) { + return CmakeVariableType.STRING; + } + try { + return CmakeVariableType.valueOf(text.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + return CmakeVariableType.STRING; + } + } + + private static String asScalarText(JsonElement value) { + if (value == null || value.isJsonNull()) { + return ""; + } + if (value.isJsonPrimitive()) { + return value.getAsJsonPrimitive().isString() ? value.getAsString() : value.getAsJsonPrimitive().toString(); + } + return gson.toJson(value); + } + + private static String expandBinaryDir(String binaryDir, Path sourceDir, String presetName, String generator) { + if (binaryDir == null || binaryDir.isBlank()) { + return binaryDir; + } + String expanded = binaryDir; + expanded = expanded.replace("${sourceDir}", sourceDir.toString()); + Path parent = sourceDir.getParent(); + expanded = expanded.replace("${sourceParentDir}", parent == null ? "" : parent.toString()); + Path leaf = sourceDir.getFileName(); + expanded = expanded.replace("${sourceDirName}", leaf == null ? "" : leaf.toString()); + expanded = expanded.replace("${presetName}", presetName == null ? "" : presetName); + expanded = expanded.replace("${generator}", generator == null ? "" : generator); + expanded = expandEnvironmentReferences(expanded); + return expanded; + } + + private static String expandEnvironmentReferences(String value) { + Matcher m = ENV_REF.matcher(value); + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String var = m.group(2); + String replacement = System.getenv(var); + if (replacement == null) { + replacement = ""; + } + m.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } + m.appendTail(sb); + return sb.toString(); + } + + static final class ResolvedConfigurePreset { + private final String name; + private final String generator; + private final String binaryDir; + private final List cacheVariables; + private final List configureArguments; + + ResolvedConfigurePreset(String name, String generator, String binaryDir, List cacheVariables, + List configureArguments) { + this.name = name; + this.generator = generator; + this.binaryDir = binaryDir; + this.cacheVariables = new ArrayList<>(cacheVariables); + this.configureArguments = new ArrayList<>(configureArguments); + } + + String getName() { + return name; + } + + String getGenerator() { + return generator; + } + + String getBinaryDir() { + return binaryDir; + } + + List getCacheVariables() { + return new ArrayList<>(cacheVariables); + } + + List getConfigureArguments() { + return new ArrayList<>(configureArguments); + } + } +} diff --git a/de.marw.cmake4eclipse.mbs/src/test/java/de/marw/cmake4eclipse/mbs/internal/CMakePresetsLoaderTest.java b/de.marw.cmake4eclipse.mbs/src/test/java/de/marw/cmake4eclipse/mbs/internal/CMakePresetsLoaderTest.java new file mode 100644 index 00000000..c90a1a37 --- /dev/null +++ b/de.marw.cmake4eclipse.mbs/src/test/java/de/marw/cmake4eclipse/mbs/internal/CMakePresetsLoaderTest.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2026 Martin Weber. + * + * Content is provided to you under the terms and conditions of the Eclipse Public License Version 2.0 "EPL". + * A copy of the EPL is available at http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package de.marw.cmake4eclipse.mbs.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import org.junit.Test; + +import de.marw.cmake4eclipse.mbs.settings.CmakeVariableType; + +public class CMakePresetsLoaderTest { + + @Test + public void testResolvesBuildPresetToConfigurePreset() throws Exception { + Path sourceDir = Files.createTempDirectory("c4e-preset-test"); + try { + String json = "{\n" + + " \"version\": 4,\n" + + " \"configurePresets\": [\n" + + " {\n" + + " \"name\": \"base\",\n" + + " \"generator\": \"Ninja\",\n" + + " \"cacheVariables\": {\n" + + " \"CMAKE_C_STANDARD\": \"11\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"name\": \"debug\",\n" + + " \"inherits\": \"base\",\n" + + " \"binaryDir\": \"${sourceDir}/out/${presetName}\",\n" + + " \"cacheVariables\": {\n" + + " \"ENABLE_TESTS\": true,\n" + + " \"TOOLCHAIN_FILE\": {\"type\": \"FILEPATH\", \"value\": \"toolchain.cmake\"}\n" + + " },\n" + + " \"debug\": {\"output\": true}\n" + + " }\n" + + " ],\n" + + " \"buildPresets\": [\n" + + " {\"name\": \"Debug\", \"configurePreset\": \"debug\"}\n" + + " ]\n" + + "}\n"; + Files.writeString(sourceDir.resolve("CMakePresets.json"), json); + + Optional preset = CMakePresetsLoader + .loadResolvedConfigurePreset(sourceDir, "Debug"); + assertTrue(preset.isPresent()); + CMakePresetsLoader.ResolvedConfigurePreset value = preset.get(); + assertEquals("debug", value.getName()); + assertEquals("Ninja", value.getGenerator()); + assertEquals(sourceDir.resolve("out/debug").normalize().toString(), value.getBinaryDir()); + assertEquals(3, value.getCacheVariables().size()); + assertTrue(value.getCacheVariables().stream().anyMatch(v -> "CMAKE_C_STANDARD".equals(v.getName()))); + assertTrue(value.getCacheVariables().stream().anyMatch(v -> "ENABLE_TESTS".equals(v.getName()) + && v.getType() == CmakeVariableType.BOOL && "true".equals(v.getValue()))); + assertTrue(value.getCacheVariables().stream().anyMatch(v -> "TOOLCHAIN_FILE".equals(v.getName()) + && v.getType() == CmakeVariableType.FILEPATH)); + assertTrue(value.getConfigureArguments().contains("--debug-output")); + } finally { + Files.deleteIfExists(sourceDir.resolve("CMakePresets.json")); + Files.deleteIfExists(sourceDir); + } + } + + @Test + public void testIgnoresMissingPresetFile() throws Exception { + Path sourceDir = Files.createTempDirectory("c4e-preset-empty"); + try { + Optional preset = CMakePresetsLoader + .loadResolvedConfigurePreset(sourceDir, "Debug"); + assertNotNull(preset); + assertFalse(preset.isPresent()); + } finally { + Files.deleteIfExists(sourceDir); + } + } + + @Test + public void testMatchesConfigurePresetByConfigurationName() throws Exception { + Path sourceDir = Files.createTempDirectory("c4e-preset-config-name"); + try { + String json = "{\n" + + " \"version\": 4,\n" + + " \"configurePresets\": [\n" + + " {\n" + + " \"name\": \"Release\",\n" + + " \"generator\": \"Ninja Multi-Config\",\n" + + " \"cacheVariables\": {\n" + + " \"CMAKE_BUILD_TYPE\": \"Release\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}\n"; + Files.writeString(sourceDir.resolve("CMakePresets.json"), json); + + Optional preset = CMakePresetsLoader + .loadResolvedConfigurePreset(sourceDir, "release"); + assertTrue(preset.isPresent()); + assertEquals("Release", preset.get().getName()); + assertTrue(preset.get().getCacheVariables().stream().anyMatch(v -> "CMAKE_BUILD_TYPE".equals(v.getName()))); + } finally { + Files.deleteIfExists(sourceDir.resolve("CMakePresets.json")); + Files.deleteIfExists(sourceDir); + } + } +}