Skip to content

Running cmake--build command with multiple targets supplied to the --target option only uses the return value of the last target that ran

When cmake is used to build a project via the --build option and is supplied multiple targets via the --targets switch, it creates command to pass to the native build system for each target and runs each of those commands regardless of whether previous commands failed.

Furthermore the return code of the cmake build command is always the return code of the final native build command that ran. It doesn't take into account the return values of previous build targets

This can result in scenarios where the cmake --build command returns true when building multiple targets when one of earlier build targets has failed.

This causes issues for example such as attempting to use cmake on automated build system that runs tests via multiple custom targets.

A minimum repro for this issue is below. It involves just having a single CMakeLists.txt with multiple targets, followed by invoking the cmake --build command with those multiple targets

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)

project(BuildCommandFailRepro)

add_custom_target(ReturnsFalse COMMAND ${CMAKE_COMMAND} -E false)
add_custom_target(ReturnsTrue COMMAND ${CMAKE_COMMAND} -E true)

Running cmake --build against the targets two targets in the order of

  • ReturnsFalse
  • ReturnsTrue

Results in a return code of 0 for the cmake --build command

Invalid zero return code

cmake .
cmake --build . --target ReturnsFalse ReturnsTrue
echo %errorlevel%

Swapping the order of the targets results in a return code 1 for the cmake --build command

Valid zero return code

cmake .
cmake --build . --target ReturnsTrue ReturnsFalse
echo %errorlevel%

I have tracked the issue down to the call of cmSystemTools::RunSingleCommand from the cmGlobalGenerator::Build function.

The issue occurs in cmSystemTools::RunSingleCommand, after the a command completes from the cmsysProcess_WaitForExit(cp, nullptr); call, the return code is then captured in the following code if the process exits without crashing

bool result = true;
if (cmsysProcess_GetState(cp) == cmsysProcess_State_Exited) {
  if (retVal) {
    *retVal = cmsysProcess_GetExitValue(cp);
  } else {
    if (cmsysProcess_GetExitValue(cp) != 0) {
      result = false;
    }
  }
} else if (cmsysProcess_GetState(cp) == cmsysProcess_State_Exception) {

The issue with the above code is that when theint* retVal variable is non-nullptr it is populated with the return value of the process, but the result variable isn't set to result = *retVal != 0

This means that the command can return a non-zero exit code, while the cmSystemTools::RunSingleCommand function returns true.

Now this could also be avoided in the cmGlobalGenerator::Build function by checking the retVal != 0 as a logical-or in the if statement that invokes the cmSystemTools::RunSingleCommand function

if (!cmSystemTools::RunSingleCommand(command->PrimaryCommand, outputPtr,
                                         outputPtr, &retVal, nullptr,
                                         outputflag, timeout)) {

Changing the above to the following would also resolve the issue

if (!cmSystemTools::RunSingleCommand(command->PrimaryCommand, outputPtr,
                                         outputPtr, &retVal, nullptr,
                                         outputflag, timeout) || retVal != 0) {
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information