ctest_build: add PRESET option

Allow users to specify a build preset when using CTest dashboard scripts.

Issue: #27630
This commit is contained in:
Zack Galbreath
2026-04-27 14:30:46 -04:00
committed by Brad King
parent de52c08ba2
commit 348ff1938b
7 changed files with 137 additions and 20 deletions

View File

@@ -15,6 +15,7 @@ Perform the :ref:`CTest Build Step` as a :ref:`Dashboard Client`.
[NUMBER_WARNINGS <num-warn-var>]
[RETURN_VALUE <result-var>]
[CAPTURE_CMAKE_ERROR <result-var>]
[PRESET <preset>]
)
Build the project and store results in ``Build.xml``
@@ -75,6 +76,15 @@ The options are:
``NUMBER_WARNINGS <num-warn-var>``
Store the number of build warnings detected in the given variable.
``PRESET <preset>``
.. versionadded:: 4.4
Specify a :manual:`preset <cmake-presets(7)>` to use when building the
project. Any value set in the CTest script will take priority over a
corresponding setting from the preset. For example, the ``TARGET``
argument will override the :preset:`buildPresets.targets` setting from
the chosen preset.
``RETURN_VALUE <result-var>``
Store the return value of the native build tool in the given variable.

View File

@@ -0,0 +1,5 @@
ctest_build_preset_option
-------------------------
* The :command:`ctest_build` command gained a ``PRESET`` option
drive the build through a build preset.

View File

@@ -6,14 +6,17 @@
#include <utility>
#include <cm/memory>
#include <cm/optional>
#include <cmext/string_view>
#include "cmArgumentParser.h"
#include "cmCMakePresetsGraph.h"
#include "cmCTest.h"
#include "cmCTestBuildHandler.h"
#include "cmCTestGenericHandler.h"
#include "cmExecutionStatus.h"
#include "cmGlobalGenerator.h"
#include "cmJSONState.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmStringAlgorithms.h"
@@ -21,6 +24,8 @@
#include "cmValue.h"
#include "cmake.h"
using BuildPreset = cmCMakePresetsGraph::BuildPreset;
bool cmCTestBuildCommand::InitialPass(std::vector<std::string> const& args,
cmExecutionStatus& status) const
{
@@ -32,7 +37,8 @@ bool cmCTestBuildCommand::InitialPass(std::vector<std::string> const& args,
.Bind("CONFIGURATION"_s, &BuildArguments::Configuration)
.Bind("FLAGS"_s, &BuildArguments::Flags)
.Bind("PROJECT_NAME"_s, &BuildArguments::ProjectName)
.Bind("PARALLEL_LEVEL"_s, &BuildArguments::ParallelLevel);
.Bind("PARALLEL_LEVEL"_s, &BuildArguments::ParallelLevel)
.Bind("PRESET"_s, &BuildArguments::Preset);
return this->Invoke(parser, args, status, [&](BuildArguments& a) {
return this->ExecuteHandlerCommand(a, status);
@@ -46,32 +52,87 @@ std::unique_ptr<cmCTestGenericHandler> cmCTestBuildCommand::InitializeHandler(
auto const& args = static_cast<BuildArguments&>(arguments);
auto handler = cm::make_unique<cmCTestBuildHandler>(this->CTest);
// Build configuration is set according to the following priority order:
// 1) The CONFIGURATION option to ctest_build()
// 2) CTEST_BUILD_CONFIGURATION script variable
// 3) CTEST_CONFIGURATION_TYPE script variable
// 4) The ctest -C command line argument
// 5) The configuration entry from the build preset
cmValue ctestBuildConfiguration =
mf.GetDefinition("CTEST_BUILD_CONFIGURATION");
std::string cmakeBuildConfiguration = cmNonempty(args.Configuration)
? args.Configuration
: cmNonempty(ctestBuildConfiguration) ? *ctestBuildConfiguration
: this->CTest->GetConfigType();
std::string const& cmakeBuildAdditionalFlags = cmNonempty(args.Flags)
? args.Flags
: mf.GetSafeDefinition("CTEST_BUILD_FLAGS");
std::string const& cmakeBuildTarget = cmNonempty(args.Target)
? args.Target
: mf.GetSafeDefinition("CTEST_BUILD_TARGET");
cmValue ctestBuildCommand = mf.GetDefinition("CTEST_BUILD_COMMAND");
if (cmNonempty(ctestBuildCommand)) {
this->CTest->SetCTestConfiguration("MakeCommand", *ctestBuildCommand,
args.Quiet);
} else if (!args.Preset.empty()) {
std::string const sourceDirectory =
mf.GetSafeDefinition("CTEST_SOURCE_DIRECTORY");
cmCMakePresetsGraph presetsGraph;
if (!presetsGraph.ReadProjectPresets(sourceDirectory)) {
status.SetError(
cmStrCat("Could not read presets from \"", sourceDirectory,
"\": ", presetsGraph.parseState.GetErrorMessage()));
return nullptr;
}
auto resolveResult =
presetsGraph.ResolvePreset(args.Preset, presetsGraph.BuildPresets);
auto resolveError = cmCMakePresetsGraph::FormatPresetError<BuildPreset>(
resolveResult.StatusCode, resolveResult.ErrorPresetName,
sourceDirectory);
if (resolveError) {
status.SetError(*resolveError);
return nullptr;
}
std::string buildCommand =
cmStrCat('"', cmSystemTools::GetCMakeCommand(), '"');
buildCommand += " --build . --preset \"";
buildCommand += args.Preset;
buildCommand += "\"";
if (!cmakeBuildConfiguration.empty()) {
buildCommand += " --config \"";
buildCommand += cmakeBuildConfiguration;
buildCommand += "\"";
}
if (!cmakeBuildTarget.empty()) {
buildCommand += " --target \"";
buildCommand += cmakeBuildTarget;
buildCommand += "\"";
}
if (!args.ParallelLevel.empty()) {
buildCommand += " --parallel ";
buildCommand += args.ParallelLevel;
}
if (!cmakeBuildAdditionalFlags.empty()) {
buildCommand += " -- ";
buildCommand += cmakeBuildAdditionalFlags;
}
cmCTestOptionalLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
"SetMakeCommand:" << buildCommand << "\n", args.Quiet);
this->CTest->SetCTestConfiguration("MakeCommand", buildCommand,
args.Quiet);
} else {
cmValue cmakeGeneratorName = mf.GetDefinition("CTEST_CMAKE_GENERATOR");
// Build configuration is determined by: CONFIGURATION argument,
// or CTEST_BUILD_CONFIGURATION script variable, or
// CTEST_CONFIGURATION_TYPE script variable, or ctest -C command
// line argument... in that order.
//
cmValue ctestBuildConfiguration =
mf.GetDefinition("CTEST_BUILD_CONFIGURATION");
std::string cmakeBuildConfiguration = cmNonempty(args.Configuration)
? args.Configuration
: cmNonempty(ctestBuildConfiguration) ? *ctestBuildConfiguration
: this->CTest->GetConfigType();
std::string const& cmakeBuildAdditionalFlags = cmNonempty(args.Flags)
? args.Flags
: mf.GetSafeDefinition("CTEST_BUILD_FLAGS");
std::string const& cmakeBuildTarget = cmNonempty(args.Target)
? args.Target
: mf.GetSafeDefinition("CTEST_BUILD_TARGET");
if (cmNonempty(cmakeGeneratorName)) {
if (cmakeBuildConfiguration.empty()) {
cmakeBuildConfiguration = "Release";

View File

@@ -23,6 +23,7 @@ protected:
std::string Flags;
std::string ProjectName;
std::string ParallelLevel;
std::string Preset;
};
private:

View File

@@ -0,0 +1,10 @@
file(GLOB build_xml_file "${RunCMake_TEST_BINARY_DIR}/Testing/*/Build.xml")
if(build_xml_file)
file(READ "${build_xml_file}" build_xml)
if(NOT build_xml MATCHES "--preset.*my-build-preset")
set(RunCMake_TEST_FAILED
"Build.xml does not contain the expected --preset argument")
endif()
else()
set(RunCMake_TEST_FAILED "Build.xml not found")
endif()

View File

@@ -0,0 +1,21 @@
{
"version": 2,
"cmakeMinimumRequired": {
"major": 3,
"minor": 20,
"patch": 0
},
"configurePresets": [
{
"name": "my-configure-preset",
"generator": "@RunCMake_GENERATOR@",
"binaryDir": "${sourceDir}/../BuildPreset-build"
}
],
"buildPresets": [
{
"name": "my-build-preset",
"configurePreset": "my-configure-preset"
}
]
}

View File

@@ -71,6 +71,15 @@ set(RunCMake_BUILD_COMMAND "${COLOR_WARNING}")
run_ctest(IgnoreColor)
unset(RunCMake_BUILD_COMMAND)
set(RunCMake_USE_CUSTOM_BUILD_COMMAND FALSE)
set(RunCMake_TEST_SOURCE_DIR "${RunCMake_BINARY_DIR}/BuildPreset")
configure_file(
"${RunCMake_SOURCE_DIR}/CMakePresets.json.in"
"${RunCMake_TEST_SOURCE_DIR}/CMakePresets.json"
@ONLY)
run_ctest_build(BuildPreset PRESET my-build-preset)
unset(RunCMake_TEST_SOURCE_DIR)
set(RunCMake_USE_CUSTOM_BUILD_COMMAND FALSE)
if(RunCMake_GENERATOR MATCHES "Ninja")
function(run_NinjaLauncherSingleBuildFailure)