ctest_configure: add PRESET option

Allow users to specify a configure preset when using CTest dashboard scripts.
Related to Issue #27630
This commit is contained in:
Zack Galbreath
2026-04-01 19:43:19 -04:00
parent dc84ea229f
commit 383802d93a
6 changed files with 197 additions and 47 deletions

View File

@@ -7,7 +7,9 @@ Perform the :ref:`CTest Configure Step` as a :ref:`Dashboard Client`.
ctest_configure([BUILD <build-dir>] [SOURCE <source-dir>] [APPEND]
[OPTIONS <options>] [RETURN_VALUE <result-var>] [QUIET]
[CAPTURE_CMAKE_ERROR <result-var>])
[CAPTURE_CMAKE_ERROR <result-var>]
[PRESET <preset>]
)
Configure the project build tree and record results in ``Configure.xml``
for submission with the :command:`ctest_submit` command.
@@ -32,6 +34,19 @@ The options are:
``OPTIONS <options>``
Specify a :ref:`semicolon-separated list <CMake Language Lists>` of
command-line arguments to pass to the configuration tool.
This option is ignored when :variable:`CTEST_CONFIGURE_COMMAND` is used.
``PRESET <preset>``
.. versionadded:: 4.4
Specify a :manual:`preset <cmake-presets(7)>` to use when configuring the
project. Any value set in the CTest script will take priority over a
corresponding setting from the preset. For example, the
:variable:`CTEST_BINARY_DIRECTORY` variable will override the
:preset:`configurePresets.binaryDir` setting from the chosen preset.
This option is ignored when :variable:`CTEST_CONFIGURE_COMMAND`
is used.
``RETURN_VALUE <result-var>``
Store in the ``<result-var>`` variable the return value of the native

View File

@@ -4,14 +4,18 @@
#include <chrono>
#include <cstdlib>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include <cm/memory>
#include <cm/optional>
#include <cmext/string_view>
#include "cmArgumentParser.h"
#include "cmCMakePresetsGraph.h"
#include "cmCTest.h"
#include "cmDuration.h"
#include "cmExecutionStatus.h"
@@ -19,6 +23,7 @@
#include "cmGlobalGenerator.h"
#include "cmInstrumentation.h"
#include "cmInstrumentationQuery.h"
#include "cmJSONState.h"
#include "cmList.h"
#include "cmMakefile.h"
#include "cmStringAlgorithms.h"
@@ -27,16 +32,108 @@
#include "cmXMLWriter.h"
#include "cmake.h"
using ConfigurePreset = cmCMakePresetsGraph::ConfigurePreset;
namespace {
cm::optional<ConfigurePreset> LoadPreset(cmExecutionStatus& status,
std::string sourceDirectory,
std::string presetName)
{
// Load a configure preset after verifying its existence and validity.
cmCMakePresetsGraph presetsGraph;
if (!presetsGraph.ReadProjectPresets(sourceDirectory)) {
status.SetError(
cmStrCat("Could not read presets from \"", sourceDirectory,
"\": ", presetsGraph.parseState.GetErrorMessage()));
return cm::nullopt;
}
auto preset = presetsGraph.ConfigurePresets.find(presetName);
if (preset == presetsGraph.ConfigurePresets.end()) {
status.SetError(cmStrCat("No such preset in ", sourceDirectory, ": \"",
presetName, '"'));
return cm::nullopt;
}
if (preset->second.Unexpanded.Hidden) {
status.SetError(cmStrCat("Cannot use hidden preset in ", sourceDirectory,
": \"", presetName, '"'));
return cm::nullopt;
}
auto const& expandedPreset = preset->second.Expanded;
if (!expandedPreset) {
status.SetError(cmStrCat("Could not evaluate preset \"", presetName,
"\": Invalid macro expansion."));
return cm::nullopt;
}
if (!expandedPreset->ConditionResult) {
status.SetError(
cmStrCat("Cannot use disabled preset \"", presetName, "\"."));
return cm::nullopt;
}
return expandedPreset;
}
bool ConstructConfigureCommand(cmExecutionStatus& status, cmMakefile& mf,
std::string const sourceDirectory,
std::string const buildDirectory,
std::string const options,
std::string const presetName,
std::string& configureCommand)
{
configureCommand = cmStrCat('"', cmSystemTools::GetCMakeCommand(), '"');
configureCommand += " \"-S";
configureCommand += cmSystemTools::CollapseFullPath(sourceDirectory);
configureCommand += "\"";
if (!buildDirectory.empty()) {
configureCommand += " \"-B";
configureCommand += cmSystemTools::CollapseFullPath(buildDirectory);
configureCommand += "\"";
}
cmValue cmakeGenerator = mf.GetDefinition("CTEST_CMAKE_GENERATOR");
if (!cmNonempty(cmakeGenerator)) {
if (cmNonempty(cmakeGenerator)) {
configureCommand += " \"-G";
configureCommand += cmakeGenerator;
configureCommand += "\"";
}
bool presetProvidesBuildDir = false;
bool presetProvidesGenerator = false;
if (!presetName.empty()) {
auto expandedPreset = LoadPreset(status, sourceDirectory, presetName);
if (!expandedPreset) {
return false;
}
configureCommand += " \"--preset\"";
configureCommand += " \"";
configureCommand += presetName;
configureCommand += "\"";
if (!expandedPreset->BinaryDir.empty()) {
presetProvidesBuildDir = true;
}
if (!expandedPreset->Generator.empty()) {
presetProvidesGenerator = true;
}
}
if (buildDirectory.empty() && !presetProvidesBuildDir) {
status.SetError("called with no build directory specified. "
"Either use the BUILD argument or set the "
"CTEST_BINARY_DIRECTORY variable.");
return false;
}
if (!cmNonempty(cmakeGenerator) && !presetProvidesGenerator) {
status.SetError(
"called with no configure command specified. "
"If this is a \"built with CMake\" project, set "
@@ -44,40 +141,10 @@ bool ConstructConfigureCommand(cmExecutionStatus& status, cmMakefile& mf,
return false;
}
bool const multiConfig = [&]() -> bool {
cmake* cm = mf.GetCMakeInstance();
auto gg = cm->CreateGlobalGenerator(cmakeGenerator);
return gg && gg->IsMultiConfig();
}();
bool const buildTypeInOptions =
options.find("CMAKE_BUILD_TYPE=") != std::string::npos ||
options.find("CMAKE_BUILD_TYPE:STRING=") != std::string::npos;
configureCommand = cmStrCat('"', cmSystemTools::GetCMakeCommand(), '"');
auto const optionsList = cmList(options);
for (std::string const& option : optionsList) {
configureCommand += " \"";
configureCommand += option;
configureCommand += "\"";
}
cmValue cmakeBuildType = mf.GetDefinition("CTEST_CONFIGURATION_TYPE");
if (!multiConfig && !buildTypeInOptions && cmNonempty(cmakeBuildType)) {
configureCommand += " \"-DCMAKE_BUILD_TYPE:STRING=";
configureCommand += cmakeBuildType;
configureCommand += "\"";
}
if (mf.IsOn("CTEST_USE_LAUNCHERS")) {
configureCommand += " \"-DCTEST_USE_LAUNCHERS:BOOL=TRUE\"";
}
configureCommand += " \"-G";
configureCommand += cmakeGenerator;
configureCommand += "\"";
cmValue cmakeGeneratorPlatform =
mf.GetDefinition("CTEST_CMAKE_GENERATOR_PLATFORM");
if (cmNonempty(cmakeGeneratorPlatform)) {
@@ -94,13 +161,31 @@ bool ConstructConfigureCommand(cmExecutionStatus& status, cmMakefile& mf,
configureCommand += "\"";
}
configureCommand += " \"-S";
configureCommand += cmSystemTools::CollapseFullPath(sourceDirectory);
configureCommand += "\"";
// Append OPTIONS to the configure command.
bool const multiConfig = [&]() -> bool {
cmake* cm = mf.GetCMakeInstance();
auto gg = cm->CreateGlobalGenerator(cmakeGenerator);
return gg && gg->IsMultiConfig();
}();
bool const buildTypeInOptions =
options.find("CMAKE_BUILD_TYPE=") != std::string::npos ||
options.find("CMAKE_BUILD_TYPE:STRING=") != std::string::npos;
auto const optionsList = cmList(options);
for (std::string const& option : optionsList) {
configureCommand += " \"";
configureCommand += option;
configureCommand += "\"";
}
cmValue cmakeBuildType = mf.GetDefinition("CTEST_CONFIGURATION_TYPE");
if (!multiConfig && !buildTypeInOptions && cmNonempty(cmakeBuildType)) {
configureCommand += " \"-DCMAKE_BUILD_TYPE:STRING=";
configureCommand += cmakeBuildType;
configureCommand += "\"";
}
configureCommand += " \"-B";
configureCommand += cmSystemTools::CollapseFullPath(buildDirectory);
configureCommand += "\"";
return true;
}
@@ -111,15 +196,9 @@ bool cmCTestConfigureCommand::ExecuteConfigure(ConfigureArguments const& args,
{
cmMakefile& mf = status.GetMakefile();
std::string const buildDirectory = !args.Build.empty()
std::string buildDirectory = !args.Build.empty()
? args.Build
: mf.GetDefinition("CTEST_BINARY_DIRECTORY");
if (buildDirectory.empty()) {
status.SetError("called with no build directory specified. "
"Either use the BUILD argument or set the "
"CTEST_BINARY_DIRECTORY variable.");
return false;
}
std::string const sourceDirectory = !args.Source.empty()
? args.Source
@@ -135,7 +214,8 @@ bool cmCTestConfigureCommand::ExecuteConfigure(ConfigureArguments const& args,
std::string configureCommand = mf.GetDefinition("CTEST_CONFIGURE_COMMAND");
if (configureCommand.empty() &&
!ConstructConfigureCommand(status, mf, sourceDirectory, buildDirectory,
args.Options, configureCommand)) {
args.Options, args.Preset,
configureCommand)) {
return false;
}
@@ -238,7 +318,8 @@ bool cmCTestConfigureCommand::InitialPass(std::vector<std::string> const& args,
using Args = ConfigureArguments;
static auto const parser =
cmArgumentParser<Args>{ MakeHandlerParser<Args>() } //
.Bind("OPTIONS"_s, &ConfigureArguments::Options);
.Bind("OPTIONS"_s, &ConfigureArguments::Options)
.Bind("PRESET"_s, &ConfigureArguments::Preset);
return this->Invoke(parser, args, status, [&](ConfigureArguments& a) {
return this->ExecuteConfigure(a, status);

View File

@@ -19,6 +19,7 @@ protected:
struct ConfigureArguments : HandlerArguments
{
std::string Options;
std::string Preset;
};
private:

View File

@@ -0,0 +1,21 @@
{
"version": 1,
"cmakeMinimumRequired": {
"major": 3,
"minor": 18,
"patch": 0
},
"configurePresets": [
{
"name": "my-preset",
"generator": "@RunCMake_GENERATOR@",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"MY_CUSTOM_VAR": {
"type": "STRING",
"value": "this-gets-set"
}
}
}
]
}

View File

@@ -0,0 +1,25 @@
if(IS_DIRECTORY "${RunCMake_TEST_SOURCE_DIR}/build")
set(RunCMake_TEST_FAILED
"CTEST_BINARY_DIRECTORY did not override buildDir from preset")
endif()
file(GLOB configure_xml_file "${RunCMake_TEST_BINARY_DIR}/Testing/*/Configure.xml")
if(configure_xml_file)
file(READ "${configure_xml_file}" configure_xml)
if(NOT configure_xml MATCHES "\"--preset\" \"my-preset\"")
set(RunCMake_TEST_FAILED
"Configure.xml does not contain the expected --preset argument")
endif()
else()
set(RunCMake_TEST_FAILED "Configure.xml not found")
endif()
set(cmakecache_file "${RunCMake_TEST_BINARY_DIR}/CMakeCache.txt")
if(EXISTS "${cmakecache_file}")
file(READ "${cmakecache_file}" cmakecache_txt)
if(NOT cmakecache_txt MATCHES "MY_CUSTOM_VAR:STRING=this-gets-set")
set(RunCMake_TEST_FAILED "CMakeCache.txt does not contain MY_CUSTOM_VAR")
endif()
else()
set(RunCMake_TEST_FAILED "CMakeCache.txt not found")
endif()

View File

@@ -8,3 +8,10 @@ function(run_ctest_configure CASE_NAME)
endfunction()
run_ctest_configure(ConfigureQuiet QUIET)
set(RunCMake_TEST_SOURCE_DIR "${RunCMake_BINARY_DIR}/ConfigurePreset")
configure_file(
"${RunCMake_SOURCE_DIR}/CMakePresets.json.in"
"${RunCMake_TEST_SOURCE_DIR}/CMakePresets.json"
@ONLY)
run_ctest_configure(ConfigurePreset PRESET my-preset)