ctest: Honor preset binaryDir for initial configuration

Prior to this commit, ctest would sometimes use the current working directory
as its binary directory rather than honoring `binaryDir` from a configure
preset. This would occur when using ctest in dashboard client mode to perform
an initial configuration with a preset.

  `ctest --source-dir=<dir> -T Configure -D CTEST_PRESET=<preset>`

An explicit `--build-dir` still takes precedence over the preset's `binaryDir`.
This commit is contained in:
Zack Galbreath
2026-06-12 10:24:39 -04:00
parent adcfdb6d3c
commit 3b6247a148
15 changed files with 197 additions and 33 deletions

View File

@@ -896,6 +896,13 @@ The available ``<dashboard-options>`` are the following:
specify a different location. The binary directory is created automatically
if it does not yet exist.
When a :manual:`configure preset <cmake-presets(7)>` is specified that
defines a :preset:`binaryDir <configurePresets.binaryDir>`, CTest uses that
path as the binary directory automatically (without requiring
:ctest-dashboard-option:`--build-dir`). An explicit
:ctest-dashboard-option:`--build-dir` takes precedence over the preset's
:preset:`binaryDir <configurePresets.binaryDir>`.
A CMake generator must also be specified. Use
:option:`-D CTEST_CMAKE_GENERATOR=\<gen\> <ctest-dashboard -D>` to supply one,
or set :variable:`CTEST_CONFIGURE_PRESET` to specify a

View File

@@ -353,6 +353,11 @@ CTest
on the :program:`ctest` command line via the
:ctest-dashboard-option:`-D` option.
* :manual:`ctest(1)` gained a :ctest-dashboard-option:`--source-dir` option
to specify the source directory. When combined with
:ctest-option:`-T` Configure, this allows CTest to perform an
initial configure step for an empty binary directory.
* :manual:`ctest(1)` gained support for a :variable:`CTEST_SUBMIT_PARTS`
variable that restricts which parts are uploaded when operating in
:ref:`Dashboard Client` mode.

View File

@@ -750,6 +750,57 @@ int cmCTest::ProcessSteps()
mf.AddDefinition(def.first, def.second);
}
if (!this->Impl->SourceDir.empty() && this->Impl->TestDir.empty() &&
this->Impl->CTestConfigurationOverwrites.find("BuildDirectory") ==
this->Impl->CTestConfigurationOverwrites.end()) {
std::string const configurePresetName =
cmNonempty(mf.GetDefinition("CTEST_CONFIGURE_PRESET"))
? *mf.GetDefinition("CTEST_CONFIGURE_PRESET")
: mf.GetSafeDefinition("CTEST_PRESET");
if (!configurePresetName.empty()) {
// Check if we should use the binary directory from the specified
// configure preset.
std::string const sourceDir =
this->GetCTestConfiguration("SourceDirectory");
std::string const rawPresetsFile =
mf.GetSafeDefinition("CTEST_PRESETS_FILE");
std::string const presetsFile = rawPresetsFile.empty()
? std::string{}
: cmSystemTools::CollapseFullPath(rawPresetsFile, sourceDir);
cmCMakePresetsGraph presetsGraph;
if (!sourceDir.empty()) {
if (!presetsGraph.ReadProjectPresets(sourceDir, presetsFile)) {
cmCTestLog(this, ERROR_MESSAGE,
"Could not read presets from \""
<< sourceDir << "\":\n "
<< presetsGraph.parseState.GetErrorMessage()
<< std::endl);
return 12;
}
cmCMakePresetsGraph::PresetResolveResult<
cmCMakePresetsGraph::ConfigurePreset>
resolveResult = presetsGraph.ResolvePreset(
configurePresetName, presetsGraph.ConfigurePresets);
cm::optional<std::string> resolveError =
cmCMakePresetsGraph::FormatPresetError<
cmCMakePresetsGraph::ConfigurePreset>(
resolveResult.StatusCode, resolveResult.ErrorPresetName,
sourceDir);
if (resolveError) {
cmCTestLog(this, ERROR_MESSAGE, *resolveError << std::endl);
return 12;
}
if (resolveResult.Preset && !resolveResult.Preset->BinaryDir.empty()) {
std::string const binaryDir = resolveResult.Preset->BinaryDir;
this->SetCTestConfiguration("BuildDirectory", binaryDir);
this->Impl->BinaryDir = binaryDir;
cmSystemTools::SetLogicalWorkingDirectory(binaryDir);
mf.AddDefinition("CTEST_BINARY_DIRECTORY", binaryDir);
}
}
}
}
// CTEST_TIME_LIMIT may come from CTestCustom.cmake (already in the makefile)
// or from the config map (just populated by SetCMakeVariables above).
this->SetTimeLimit(mf.GetDefinition("CTEST_TIME_LIMIT"));

View File

@@ -0,0 +1 @@
{ this is not valid json

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 @@
Could not read presets from ".*"

View File

@@ -0,0 +1,13 @@
# --build-dir should take precedence over the preset's binaryDir.
# Configure.xml must be in the explicit build dir, not in ${sourceDir}/build.
file(GLOB configure_xml_file
"${RunCMake_TEST_BINARY_DIR}/Testing/*/Configure.xml")
if(NOT configure_xml_file)
set(RunCMake_TEST_FAILED
"Configure.xml not found in explicit --build-dir (${RunCMake_TEST_BINARY_DIR})")
endif()
if(IS_DIRECTORY "${RunCMake_TEST_SOURCE_DIR}/build")
set(RunCMake_TEST_FAILED
"Preset binaryDir (${RunCMake_TEST_SOURCE_DIR}/build) was created")
endif()

View File

@@ -0,0 +1,23 @@
# The preset's binaryDir is "${sourceDir}/build", so Testing/ should be
# written there, not in the working directory used to invoke ctest.
file(GLOB configure_xml_file
"${RunCMake_TEST_SOURCE_DIR}/build/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 in preset binaryDir")
endif()
set(cmakecache_file "${RunCMake_TEST_SOURCE_DIR}/build/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 in preset binaryDir")
endif()

View File

@@ -0,0 +1 @@
^Cannot find file: .*/DartConfiguration\.tcl$

View File

@@ -0,0 +1,2 @@
^Cannot find file: .*/DartConfiguration\.tcl
No such configure preset in .*: "nonexistent-preset"$

View File

@@ -714,6 +714,76 @@ function(run_configure_no_cmakelists)
endfunction()
run_configure_no_cmakelists()
# Helper for tests that invoke ctest -M/-T Configure with -D CTEST_PRESET.
# Options:
# SOURCE_DIR -- pass --source-dir to ctest
# BUILD_DIR -- create a separate <CASE_NAME>-build dir with
# DartConfiguration.tcl and pass --build-dir to ctest
function(run_ctest_configure_cli_preset CASE_NAME)
cmake_parse_arguments(PARSE_ARGV 1 ARG "SOURCE_DIR;BUILD_DIR;BAD_PRESETS" "PRESET_NAME" "")
set(src "${RunCMake_BINARY_DIR}/${CASE_NAME}")
set(bin "${src}")
if(ARG_BUILD_DIR)
set(bin "${RunCMake_BINARY_DIR}/${CASE_NAME}-build")
endif()
set(preset_name "my-preset")
if(ARG_PRESET_NAME)
set(preset_name "${ARG_PRESET_NAME}")
endif()
configure_file("${RunCMake_SOURCE_DIR}/CMakeLists.txt.in"
"${src}/CMakeLists.txt" @ONLY)
if(ARG_BAD_PRESETS)
configure_file("${RunCMake_SOURCE_DIR}/BadCMakePresets.json.in"
"${src}/CMakePresets.json" @ONLY)
else()
configure_file("${RunCMake_SOURCE_DIR}/CMakePresets.json.in"
"${src}/CMakePresets.json" @ONLY)
endif()
file(REMOVE_RECURSE "${src}/build")
if(ARG_BUILD_DIR)
file(REMOVE_RECURSE "${bin}")
file(MAKE_DIRECTORY "${bin}")
file(WRITE "${bin}/DartConfiguration.tcl"
"BuildDirectory: ${bin}\n"
"SourceDirectory: ${src}\n"
"ConfigureCommand: \"${CMAKE_COMMAND}\" -S\"${src}\" -B\"${bin}\"\n")
endif()
set(extra_args "")
if(ARG_SOURCE_DIR)
list(APPEND extra_args --source-dir "${src}")
endif()
if(ARG_BUILD_DIR)
list(APPEND extra_args --build-dir "${bin}")
endif()
set(RunCMake_TEST_SOURCE_DIR "${src}")
set(RunCMake_TEST_BINARY_DIR "${bin}")
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(${CASE_NAME}
${CMAKE_CTEST_COMMAND}
${extra_args}
-M Experimental
-D "CTEST_PRESET=${preset_name}"
-T Configure
-V)
endfunction()
# CTEST_PRESET via -D reaches ctest_configure() when run with -M/-T.
run_ctest_configure_cli_preset(ConfigurePresetCLIVar BUILD_DIR)
# --source-dir + -D CTEST_PRESET picks up the preset's binaryDir when no
# DartConfiguration.tcl / explicit --build-dir is provided.
run_ctest_configure_cli_preset(ConfigurePresetCLIVarSourceDir SOURCE_DIR)
# An explicit --build-dir takes precedence over the preset's binaryDir.
run_ctest_configure_cli_preset(ConfigurePresetCLIVarBuildDirOverride SOURCE_DIR BUILD_DIR)
# A malformed presets file is an error.
run_ctest_configure_cli_preset(ConfigurePresetCLIVarBadPresets SOURCE_DIR BAD_PRESETS)
# Referencing a preset that does not exist is an error.
run_ctest_configure_cli_preset(ConfigurePresetCLIVarUnknownPreset SOURCE_DIR
PRESET_NAME nonexistent-preset)
# Test --output-junit
function(run_output_junit)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/output-junit)

View File

@@ -56,36 +56,3 @@ run_ctest(ConfigurePresetFromFileVar)
unset(CASE_TEST_PREFIX_CODE)
unset(custom_presets_file)
unset(RunCMake_TEST_SOURCE_DIR)
# Verify that CTEST_PRESET passed via -D on the command line reaches
# ctest_configure() when ctest is run with -M/-T.
set(case_source_dir "${RunCMake_BINARY_DIR}/ConfigurePresetCLIVar")
set(case_binary_dir "${RunCMake_BINARY_DIR}/ConfigurePresetCLIVar-build")
set(CASE_NAME "ConfigurePresetCLIVar")
file(MAKE_DIRECTORY "${case_source_dir}")
configure_file(
"${RunCMake_SOURCE_DIR}/CMakeLists.txt.in"
"${case_source_dir}/CMakeLists.txt"
@ONLY)
configure_file(
"${RunCMake_SOURCE_DIR}/CMakePresets.json.in"
"${case_source_dir}/CMakePresets.json"
@ONLY)
file(REMOVE_RECURSE "${case_binary_dir}")
file(MAKE_DIRECTORY "${case_binary_dir}")
file(WRITE "${case_binary_dir}/DartConfiguration.tcl"
"BuildDirectory: ${case_binary_dir}\n"
"SourceDirectory: ${case_source_dir}\n"
"ConfigureCommand: \"${CMAKE_COMMAND}\" -S\"${case_source_dir}\" -B\"${case_binary_dir}\"\n")
set(RunCMake_TEST_SOURCE_DIR "${case_source_dir}")
set(RunCMake_TEST_BINARY_DIR "${case_binary_dir}")
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(ConfigurePresetCLIVar
${CMAKE_CTEST_COMMAND}
-M Experimental
-D "CTEST_PRESET=my-preset"
-T Configure
-V)
unset(RunCMake_TEST_SOURCE_DIR)
unset(RunCMake_TEST_BINARY_DIR)
unset(RunCMake_TEST_NO_CLEAN)