instrumentation: Write cmakeBuild snippet when build is interrupted

The overall `cmakeBuild` snippet is written only after the native build
tool returns, so interrupting `cmake --build` with Ctrl+C terminated CMake
before it was recorded and lost the build's delineation.

When instrumentation is active, install a scoped, async-signal-safe handler
(POSIX SIGINT; Windows CTRL_C/CTRL_BREAK) that just flags the interrupt. The
existing write then runs during unwind, records the interrupting signal in a
new `interruptSignal` field, skips the post-build index hook, and re-raises so
the exit status still reflects the signal.

The handler lives in its own translation unit, keeping the platform-divergent
signal code out of the main instrumentation implementation.

Issue: #27859
This commit is contained in:
Daksh Mamodiya
2026-06-18 15:38:43 +02:00
parent b801e7c78d
commit bda67b82e7
18 changed files with 612 additions and 8 deletions
+1
View File
@@ -134,4 +134,5 @@ list(APPEND CTEST_CUSTOM_COVERAGE_EXCLUDE
list(APPEND CTEST_CUSTOM_MEMCHECK_IGNORE
kwsys.testProcess-10 # See Source/kwsys/CTestCustom.cmake.in
RunCMake.InstrumentationInterrupt # interrupts cmake with a real signal
)
+11
View File
@@ -450,6 +450,17 @@ Snippet files have a filename with the syntax
The exit code of the command, an integer. This will be ``null`` when
``role`` is ``build``.
``interruptSignal``
.. versionadded:: 4.5
The integer signal number that interrupted the build before it completed
(for example ``2`` for ``SIGINT`` from Ctrl+C). Only included when ``role``
is ``cmakeBuild`` and the build was interrupted. Consumers can use the
presence of this field to distinguish an interrupted build from one that
ran to completion.
Only available as of data version ``1.2``.
``stdout``
.. versionadded:: 4.4
@@ -235,6 +235,11 @@
}
]
},
"interruptSignal": {
"type": "integer",
"description": "The signal number that interrupted the build before it completed. Only included when role is cmakeBuild and the build was interrupted.",
"minimum": 1
},
"role": {
"type": "string",
"description": "The type of command executed.",
@@ -617,6 +622,9 @@
},
"traceFile": {
"$ref": "#/definitions/fields/traceFile"
},
"interruptSignal": {
"$ref": "#/definitions/fields/interruptSignal"
}
},
"additionalProperties": false,
@@ -0,0 +1,10 @@
instrumentation-interrupt
-------------------------
* The :manual:`cmake-instrumentation(7)` data version has been updated to 1.2.
* :manual:`cmake-instrumentation(7)` API now records an overall ``cmakeBuild``
snippet even when a :option:`cmake --build` invocation is interrupted by the
user (for example with Ctrl+C). The snippet includes a new
``interruptSignal`` field, recording the signal that interrupted the build,
so that consumers can distinguish an interrupted build from one that ran
to completion.
+2
View File
@@ -385,6 +385,8 @@ add_library(
cmInstallScriptHandler.cxx
cmInstrumentation.h
cmInstrumentation.cxx
cmInstrumentationInterrupt.h
cmInstrumentationInterrupt.cxx
cmInstrumentationCommand.h
cmInstrumentationCommand.cxx
cmInstrumentationQuery.h
+41 -3
View File
@@ -29,8 +29,10 @@
#include "cmCryptoHash.h"
#include "cmFileLock.h"
#include "cmFileLockResult.h"
#include "cmGeneratedFileStream.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h"
#include "cmInstrumentationInterrupt.h"
#include "cmInstrumentationQuery.h"
#include "cmJSONState.h"
#include "cmList.h"
@@ -555,7 +557,7 @@ Json::Value cmInstrumentation::ReadJsonSnippet(std::string const& file_name)
void cmInstrumentation::WriteInstrumentationJson(
cmInstrumentationQuery::Version version, Json::Value& root,
std::string const& subdir, std::string const& file_name)
std::string const& subdir, std::string const& file_name, Atomic atomic)
{
root["version"] = Json::objectValue;
root["version"]["major"] = version.Major;
@@ -567,8 +569,28 @@ void cmInstrumentation::WriteInstrumentationJson(
std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter());
std::string const& directory = cmStrCat(this->timingDirv1, '/', subdir);
cmSystemTools::MakeDirectory(directory);
std::string const file_path = cmStrCat(directory, '/', file_name);
cmsys::ofstream ftmp(cmStrCat(directory, '/', file_name).c_str());
if (atomic == Atomic::Yes) {
// Write to a temporary file and atomically rename it into place, so that
// an interrupt during the write cannot leave a truncated snippet.
cmGeneratedFileStream ftmp(file_path);
if (!ftmp) {
throw std::runtime_error(std::string("Unable to open: ") + file_name);
}
try {
JsonWriter->write(root, &ftmp);
ftmp << "\n";
// The atomic rename happens when the stream is closed/destroyed.
} catch (std::ios_base::failure& fail) {
cmSystemTools::Error(cmStrCat("Failed to write JSON: ", fail.what()));
} catch (...) {
cmSystemTools::Error("Error writing JSON output for instrumentation.");
}
return;
}
cmsys::ofstream ftmp(file_path.c_str());
if (!ftmp.good()) {
throw std::runtime_error(std::string("Unable to open: ") + file_name);
}
@@ -747,6 +769,15 @@ int cmInstrumentation::InstrumentCommand(
// See SpawnBuildDaemon(); this data is currently meaningless for build.
root["result"] = command_type == "build" ? Json::nullValue : ret;
// If the build was interrupted (e.g. by Ctrl+C), record the signal number
// that stopped it, so consumers can distinguish an interrupted build from
// one that ran to completion. Omitted when no interrupt occurred; only a
// command wrapped by HandleInterrupt can observe a pending signal here.
int sig = cmInstrumentationInterrupt::PendingInterruptSignal();
if (sig != 0) {
root["interruptSignal"] = sig;
}
// Output Sizes
if (root.isMember("outputs")) {
root["outputSizes"] = Json::arrayValue;
@@ -808,7 +839,14 @@ int cmInstrumentation::InstrumentCommand(
}
this->configureSnippetData.clear();
}
this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name);
// Write the cmakeBuild envelope atomically (temp file + rename). This is
// the snippet flushed while unwinding from a user interrupt, where a
// second Ctrl+C could otherwise truncate it mid-write; the atomic write
// guarantees it is either absent or complete. Per-step snippets are never
// flushed under interrupt and are left non-atomic.
this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name,
command_type == "cmakeBuild" ? Atomic::Yes
: Atomic::No);
}
return ret;
}
+8 -1
View File
@@ -48,6 +48,7 @@ public:
cm::optional<std::string> StdOut;
cm::optional<std::string> StdErr;
};
int InstrumentCommand(
std::string command_type, std::vector<std::string> const& command,
std::function<CommandResult()> const& callback,
@@ -100,10 +101,16 @@ private:
Json::Value ReadJsonSnippet(std::string const& file_name);
bool AcquireLock(std::string const& lock_file, cmFileLock& lock,
unsigned long timeout);
enum class Atomic
{
No,
Yes,
};
void WriteInstrumentationJson(cmInstrumentationQuery::Version version,
Json::Value& index,
std::string const& directory,
std::string const& file_name);
std::string const& file_name,
Atomic atomic = Atomic::No);
void InsertStaticSystemInformation(Json::Value& index);
void GetDynamicSystemInformation(double& memory, double& load);
void InsertDynamicSystemInformation(Json::Value& index,
+155
View File
@@ -0,0 +1,155 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#if !defined(_POSIX_C_SOURCE) && !defined(_WIN32) && !defined(__sun) && \
!defined(__OpenBSD__)
// POSIX APIs are needed (sigaction, sigemptyset, SA_RESETHAND).
// NOLINTNEXTLINE(bugprone-reserved-identifier)
# define _POSIX_C_SOURCE 200809L
#endif
#include "cmInstrumentationInterrupt.h"
#include <csignal>
#include <cstdlib>
#ifdef _WIN32
# include <atomic>
# include <windows.h>
#else
# include <cstring>
#endif
namespace {
// Flag shared between the interrupt handler and the build flow that writes the
// `cmakeBuild` snippet. On Windows the console control handler runs on a
// separate thread, so an atomic is required; on POSIX the handler runs in
// signal context, where only `volatile sig_atomic_t` is guaranteed safe.
#ifdef _WIN32
std::atomic<int> buildInterruptSignal{ 0 };
BOOL WINAPI cmInstrumentationConsoleHandler(DWORD type)
{
if (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT) {
int expected = 0;
buildInterruptSignal.compare_exchange_strong(expected, SIGINT);
// Return TRUE so the main thread can finish writing the snippet before the
// process exits. The native build tool shares the console and receives
// the event directly, so it still terminates and unblocks our build loop.
return TRUE;
}
return FALSE;
}
#else
sig_atomic_t volatile buildInterruptSignal = 0;
struct sigaction savedSigIntAction;
extern "C" void cmInstrumentationSignalHandler(int sig)
{
buildInterruptSignal = sig;
}
#endif
// Set when the pending interrupt was injected by the test seam below rather
// than delivered by the OS. An injected interrupt must NOT be re-raised (the
// process exits normally after flushing the snippet), so the test stays a
// clean-exit, leak-checkable case on every generator.
bool buildInterruptInjected = false;
// Test-only seam. An undocumented, unsupported environment variable lets the
// instrumentation test suite inject a "build was interrupted" condition
// deterministically, with no real OS signal -- so the cmakeBuild interrupt
// path can be exercised on every generator and platform. The double-
// underscore name marks it internal; it is never set in normal use. Mirrors
// CTest's internal fake-hook convention.
void InjectTestInterrupt()
{
char const* value = std::getenv("__CMAKE_INSTRUMENTATION_TEST_INTERRUPT");
if (!value) {
return;
}
int sig = std::atoi(value);
if (sig <= 0) {
return;
}
#ifdef _WIN32
buildInterruptSignal.store(sig);
#else
buildInterruptSignal = static_cast<sig_atomic_t>(sig);
#endif
buildInterruptInjected = true;
}
// Install the interrupt handler and clear any previously recorded signal.
void InstallInterruptHandler()
{
#ifdef _WIN32
buildInterruptSignal.store(0);
SetConsoleCtrlHandler(cmInstrumentationConsoleHandler, TRUE);
#else
buildInterruptSignal = 0;
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = cmInstrumentationSignalHandler;
sigemptyset(&sa.sa_mask);
// One-shot: after the first interrupt the default disposition is restored,
// so a second Ctrl+C terminates immediately even while we are mid-flush.
sa.sa_flags = SA_RESETHAND;
sigaction(SIGINT, &sa, &savedSigIntAction);
#endif
}
// Restore the disposition that was in effect before InstallInterruptHandler().
void RestoreInterruptHandler()
{
#ifdef _WIN32
SetConsoleCtrlHandler(cmInstrumentationConsoleHandler, FALSE);
#else
sigaction(SIGINT, &savedSigIntAction, nullptr);
#endif
}
}
int cmInstrumentationInterrupt::PendingInterruptSignal()
{
#ifdef _WIN32
return buildInterruptSignal.load();
#else
return static_cast<int>(buildInterruptSignal);
#endif
}
cmInstrumentationInterrupt::InterruptOutcome
cmInstrumentationInterrupt::HandleInterrupt(
bool active, std::function<int()> const& callback)
{
// Only trap interrupts when instrumentation is active, so non-instrumented
// flows keep their default signal behavior.
if (!active) {
return { callback(), false, 0, true };
}
InstallInterruptHandler();
buildInterruptInjected = false;
// Test-only: allow the suite to inject an interrupt deterministically.
InjectTestInterrupt();
int ret = callback();
int sig = PendingInterruptSignal();
RestoreInterruptHandler();
// A real OS interrupt should be re-raised so the exit status reflects it; an
// injected (test) interrupt should not, so the process exits cleanly.
return { ret, sig != 0, sig, !buildInterruptInjected };
}
void cmInstrumentationInterrupt::RaiseInterrupt(int sig)
{
#ifdef _WIN32
// On Windows the process exits normally after flushing; the caller
// propagates the (failed) build result.
static_cast<void>(sig);
#else
// Restore the default disposition (SA_RESETHAND already did so for the first
// delivery) and re-raise so the exit status reflects the interrupt.
signal(sig, SIG_DFL);
raise(sig);
#endif
}
+46
View File
@@ -0,0 +1,46 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#pragma once
#include "cmConfigure.h" // IWYU pragma: keep
#include <functional>
// Async-signal-safe handling of a user interrupt (Ctrl+C / SIGINT on POSIX, or
// a console Ctrl event on Windows) around an instrumented command, so that the
// command's snippet can still be written before the process exits.
class cmInstrumentationInterrupt
{
public:
// Outcome of running a callback under an installed interrupt handler.
struct InterruptOutcome
{
int ExitCode;
bool Interrupted;
int Signal;
// Whether the caught interrupt should be re-raised so the process exit
// status reflects it. True for a real OS signal; false for a test-
// injected interrupt, which exits cleanly after flushing the snippet.
// Always set explicitly at every construction site (no default member
// initializer, so this stays a C++11 aggregate).
bool ShouldRaise;
};
// Run `callback` with an interrupt handler installed for its duration, so a
// user interrupt sets a flag instead of terminating the process immediately.
// The handler is deliberately minimal (async-signal-safe): it only records
// the signal. When `active` is false (e.g. instrumentation is not enabled),
// no handler is installed and the callback keeps the default signal
// behavior. Returns the callback's exit code, whether an interrupt was
// caught, and the signal number (0 if none).
static InterruptOutcome HandleInterrupt(
bool active, std::function<int()> const& callback);
// Restore the default disposition and re-raise the given signal so that the
// process exits as if by the interrupt. No-op on platforms where re-raising
// is not the appropriate exit mechanism.
static void RaiseInterrupt(int sig);
// The pending interrupt signal number, or 0 if no interrupt occurred.
static int PendingInterruptSignal();
};
+25 -4
View File
@@ -88,6 +88,7 @@
# include "cmFileAPI.h"
# include "cmGraphVizWriter.h"
# include "cmInstrumentation.h"
# include "cmInstrumentationInterrupt.h"
# include "cmInstrumentationQuery.h"
# include "cmMakefileProfilingData.h"
# include "cmSarifLog.h"
@@ -4197,10 +4198,30 @@ int cmake::Build(cmBuildArgs buildArgs, std::vector<std::string> targets,
// Block the instrumentation build daemon from spawning during this build.
// This lock will be released when the process exits at the end of the build.
instrumentation.LockBuildDaemon();
int buildresult = instrumentation.InstrumentCommand(
"cmakeBuild", args, [doBuild]() -> cmInstrumentation::CommandResult {
return { doBuild(), cm::nullopt, cm::nullopt };
});
// Run the build under an interrupt handler so that a user interrupt (e.g.
// Ctrl+C) still writes the overall `cmakeBuild` snippet before we exit.
cmInstrumentationInterrupt::InterruptOutcome buildOutcome =
cmInstrumentationInterrupt::HandleInterrupt(
instrumentation.HasQuery(),
[&instrumentation, &args, &doBuild]() -> int {
return instrumentation.InstrumentCommand(
"cmakeBuild", args,
[&doBuild]() -> cmInstrumentation::CommandResult {
return { doBuild(), cm::nullopt, cm::nullopt };
});
});
int buildresult = buildOutcome.ExitCode;
if (buildOutcome.Interrupted) {
// The build was interrupted and its snippet has been written. Skip the
// post-build indexing hook (which would run callbacks and delete data).
// For a real OS interrupt, re-raise so the exit status reflects it; for a
// test-injected interrupt, exit cleanly. The next indexing run will
// reclaim the snippet written above.
if (buildOutcome.ShouldRaise) {
cmInstrumentationInterrupt::RaiseInterrupt(buildOutcome.Signal);
}
return buildresult;
}
instrumentation.CollectTimingData(
cmInstrumentationQuery::Hook::PostCMakeBuild);
#else
+9
View File
@@ -443,7 +443,16 @@ if(CMAKE_GENERATOR MATCHES "Make|Ninja|FASTBuild")
-DCMAKE_C_COMPILER_ID=${CMAKE_C_COMPILER_ID}
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_C_COMPILER_VERSION=${CMAKE_C_COMPILER_VERSION}
-DCMAKE_EXECUTABLE_SUFFIX=${CMAKE_EXECUTABLE_SUFFIX}
-DCMake_TEST_JSON_SCHEMA=${CMake_TEST_JSON_SCHEMA})
# The real-signal/console-event interrupt case runs in its own suite so it can
# be excluded from MemCheck (the interrupted cmake is killed by a real signal)
# without dropping leak coverage for the rest of the instrumentation tests.
add_RunCMake_test(InstrumentationInterrupt TEST_DIR Instrumentation
-DINSTRUMENTATION_INTERRUPT_REAL=1
-DCMAKE_C_COMPILER_ID=${CMAKE_C_COMPILER_ID}
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_EXECUTABLE_SUFFIX=${CMAKE_EXECUTABLE_SUFFIX})
endif()
add_RunCMake_test(ConfigDir)
if(CMake_TEST_FindPython2)
@@ -0,0 +1,169 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
/* Test helper to exercise instrumentation handling of an interrupted build.
Usage: InterruptBuild <delay-seconds> <command> [args...]
It runs <command> in its own process group, waits <delay-seconds>, then
delivers a user interrupt to that group only -- mimicking a user pressing
Ctrl+C on the build's foreground process group, while leaving the calling
test driver untouched. It then reports a fixed exit code so the test can
assert deterministically:
42 - the build was interrupted and then stopped (expected)
2 - usage error
3 - failed to start the build
99 - the build did not stop after the interrupt
100 + N - the build exited normally with code N (unexpected, POSIX only)
On POSIX the interrupt is SIGINT sent to the child's process group; the
instrumented `cmake` re-raises it, so the child terminates via a signal.
On Windows the interrupt is a CTRL_BREAK_EVENT sent to the child's process
group (created with CREATE_NEW_PROCESS_GROUP); `cmake`'s console handler
treats CTRL_BREAK identically to CTRL_C. Because Windows `cmake` returns
the build's (failed) exit code rather than a signal status, success is
detected as "the child exited promptly after the interrupt". The real
assertion -- that an interrupted snippet was written -- is done by the
accompanying check script. */
#include <stdio.h>
#include <stdlib.h>
#if defined(_WIN32)
# include <windows.h>
# include <string.h>
int main(int argc, char** argv)
{
int delay;
char cmdline[32768];
size_t len = 0;
int i;
size_t n;
STARTUPINFOA si;
PROCESS_INFORMATION pi;
DWORD waitRc;
if (argc < 3) {
fprintf(stderr,
"Usage: InterruptBuild <delay-seconds> <command> [args...]\n");
return 2;
}
delay = atoi(argv[1]);
if (delay <= 0) {
delay = 1;
}
/* Rebuild a command line from argv[2..]. The inputs are controlled by the
test (a cmake path plus simple flags) and never contain embedded quotes,
so wrapping each argument in quotes is sufficient and correct. The buffer
is sized to the CreateProcess command-line limit of 32768 characters,
including the terminating null. */
for (i = 2; i < argc; ++i) {
n = strlen(argv[i]);
if (len + n + 4 >= sizeof(cmdline)) {
return 2;
}
if (i > 2) {
cmdline[len++] = ' ';
}
cmdline[len++] = '"';
memcpy(cmdline + len, argv[i], n);
len += n;
cmdline[len++] = '"';
}
cmdline[len] = '\0';
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
/* New process group so the control event reaches only the build (and its
children), not this helper or the test driver. */
if (!CreateProcessA(NULL, cmdline, NULL, NULL, FALSE,
CREATE_NEW_PROCESS_GROUP, NULL, NULL, &si, &pi)) {
return 3;
}
Sleep((DWORD)delay * 1000);
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pi.dwProcessId);
waitRc = WaitForSingleObject(pi.hProcess, 30000);
if (waitRc != WAIT_OBJECT_0) {
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 99;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 42;
}
#else
# include <signal.h>
# include <unistd.h>
# include <sys/types.h>
# include <sys/wait.h>
int main(int argc, char** argv)
{
int delay;
pid_t build;
pid_t killer;
int status = 0;
if (argc < 3) {
fprintf(stderr,
"Usage: InterruptBuild <delay-seconds> <command> [args...]\n");
return 2;
}
delay = atoi(argv[1]);
if (delay <= 0) {
delay = 1;
}
build = fork();
if (build < 0) {
return 3;
}
if (build == 0) {
/* Become the leader of a new process group, then run the build. The
native build tool inherits this group, so a later group signal reaches
it too. */
setpgid(0, 0);
execvp(argv[2], &argv[2]);
_exit(127);
}
/* Best-effort from the parent side to avoid a setpgid race; ignore errors.
*/
setpgid(build, build);
killer = fork();
if (killer == 0) {
sleep((unsigned int)delay);
killpg(build, SIGINT);
_exit(0);
}
waitpid(build, &status, 0);
if (killer > 0) {
kill(killer, SIGKILL);
waitpid(killer, NULL, 0);
}
if (WIFSIGNALED(status)) {
return 42;
}
if (WIFEXITED(status)) {
return 100 + WEXITSTATUS(status);
}
return 99;
}
#endif
@@ -8,6 +8,8 @@ function(instrument test)
set(OPTIONS
"BUILD"
"BUILD_MAKE_PROGRAM"
"INTERRUPT"
"INTERRUPT_SEAM"
"INSTALL"
"INSTALL_PARALLEL"
"TEST"
@@ -121,6 +123,10 @@ function(instrument test)
if (ARGS_DISABLE_TEST)
list(APPEND ARGS_CONFIGURE_ARGS "-DDISABLE_TEST=ON")
endif()
if (ARGS_INTERRUPT)
list(APPEND ARGS_CONFIGURE_ARGS
"-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c")
endif()
set(RunCMake_TEST_SOURCE_DIR ${RunCMake_SOURCE_DIR}/project)
if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
set(maybe_CMAKE_BUILD_TYPE -DCMAKE_BUILD_TYPE=Debug)
@@ -176,6 +182,51 @@ function(instrument test)
unset(RunCMake_TEST_OUTPUT_MERGE)
endif()
endif()
if (ARGS_INTERRUPT)
# Build just the interrupt helper so it exists for the interrupted build.
# This uninterrupted build runs the postCMakeBuild hook, so remove the
# postCMakeBuild.hook file it produces; its absence after the interrupted
# build below then proves that build's hook was skipped.
run_cmake_command(${test}-helper
${CMAKE_COMMAND} --build . --config Debug --target InterruptBuild)
file(REMOVE ${v1}/postCMakeBuild.hook)
# Run an instrumented build and interrupt it after a few seconds, while the
# slow target is still running. Multi-config generators place the helper
# under a per-config subdirectory; the build below uses --config Debug.
set(helper_dir ${RunCMake_TEST_BINARY_DIR})
if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
set(helper_dir ${helper_dir}/Debug)
endif()
set(helper ${helper_dir}/InterruptBuild${CMAKE_EXECUTABLE_SUFFIX})
set(RunCMake_QUIET_ERROR 1)
run_cmake_command(${test}-build
${helper} 3
${CMAKE_COMMAND} --build . --config Debug)
unset(RunCMake_QUIET_ERROR)
endif()
if (ARGS_INTERRUPT_SEAM)
# Drive the cmakeBuild interrupt path deterministically via the test-only
# injection seam, with no OS signal, so it runs on every generator. First
# build normally so the postCMakeBuild hook runs and creates its marker
# file; remove it so its absence after the injected build proves that
# build's hook was skipped.
set(RunCMake_QUIET_ERROR 1)
run_cmake_command(${test}-warmup
${CMAKE_COMMAND} --build . --config Debug)
file(REMOVE ${v1}/postCMakeBuild.hook)
# Inject an interrupt (SIGINT == 2) via the undocumented test seam and build
# again; cmake exits cleanly but writes the interrupted cmakeBuild snippet
# and skips the hook.
set(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT} 2)
set(RunCMake_TEST_EXPECT_RESULT 0)
run_cmake_command(${test}-build
${CMAKE_COMMAND} --build . --config Debug)
unset(RunCMake_TEST_EXPECT_RESULT)
unset(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT})
unset(RunCMake_QUIET_ERROR)
endif()
if (ARGS_BUILD_MAKE_PROGRAM)
set(RunCMake_TEST_OUTPUT_MERGE 1)
set(RunCMake_QUIET_ERROR 1)
@@ -206,6 +257,22 @@ function(instrument test)
endif()
endfunction()
if (INSTRUMENTATION_INTERRUPT_REAL)
# RunCMake.InstrumentationInterrupt runs ONLY the real-signal/
# console-event interrupt case, as it must be excluded from MemCheck.
#
# POSIX delivers a real SIGINT to a contained process group. On Windows, only
# the Ninja generator is exercised: its native tool reliably stops on the
# console event and does not re-broadcast it to the runner; the other Windows
# make-family generators are covered by the injection seam instead.
if (NOT WIN32 OR RunCMake_GENERATOR MATCHES "Ninja")
instrument(interrupt INTERRUPT
CHECK_SCRIPT check-interrupted.cmake
)
endif()
return()
endif()
# Bad Queries
instrument(bad-option BAD_QUERY
CHECK_SCRIPT check-query-dir.cmake
@@ -403,6 +470,14 @@ if (NOT Skip_COMPILE_TRACE_QUERY_Case)
endif()
endif()
# Test that interrupting `cmake --build` still writes the cmakeBuild snippet,
# recording the interrupting signal. This case uses the deterministic test
# seam (no OS event). The real OS-event counterpart runs in the separate
# RunCMake.InstrumentationInterrupt suite.
instrument(interrupt INTERRUPT_SEAM
CHECK_SCRIPT check-interrupted.cmake
)
# Test make/ninja hooks
if(RunCMake_GENERATOR STREQUAL "FASTBuild")
# FIXME(#27184): This does not work for FASTBuild.
@@ -0,0 +1,35 @@
include(${CMAKE_CURRENT_LIST_DIR}/json.cmake)
# After an interrupted `cmake --build`, exactly one cmakeBuild snippet should be
# present, recording the interrupting signal. Any cmakeBuild snippet from the
# earlier (uninterrupted) helper build was collated and removed by its
# postCMakeBuild hook.
file(GLOB cmakeBuildSnippets LIST_DIRECTORIES false ${v1}/data/cmakeBuild-*.json)
list(LENGTH cmakeBuildSnippets numCmakeBuild)
if (NOT numCmakeBuild EQUAL 1)
add_error("Expected exactly one cmakeBuild snippet, found ${numCmakeBuild}: ${cmakeBuildSnippets}")
else()
read_json("${cmakeBuildSnippets}" contents)
string(JSON interruptSignal ERROR_VARIABLE noSignal GET "${contents}" interruptSignal)
if (noSignal OR NOT interruptSignal MATCHES "^[1-9][0-9]*$")
add_error("cmakeBuild snippet is not marked interrupted:\n${contents}")
endif()
string(JSON version_minor GET "${contents}" version minor)
if (NOT version_minor EQUAL 2)
add_error("cmakeBuild snippet version minor expected 2, got: ${version_minor}")
endif()
endif()
# The postCMakeBuild hook must be skipped entirely on interrupt, so its callback
# must not run. The callback (hook.cmake) writes a postCMakeBuild.hook file
# whenever it runs; the helper build's copy was removed before the interrupted
# build, so its presence here would mean the hook wrongly ran on interrupt.
if (EXISTS ${v1}/postCMakeBuild.hook)
add_error("postCMakeBuild hook should be skipped on interrupt, but it ran")
endif()
if (DEFINED RunCMake_TEST_FAILED)
set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
endif()
@@ -0,0 +1 @@
42
@@ -70,3 +70,12 @@ if (FAIL)
add_test(NAME dummy COMMAND ${CMAKE_COMMAND} -E false)
install(CODE "message(FATAL_ERROR \"Failed install.\")")
endif()
if (INTERRUPT_BUILD_SRC)
add_executable(InterruptBuild "${INTERRUPT_BUILD_SRC}")
add_custom_target(interruptSlow ALL
COMMAND ${CMAKE_COMMAND} -E echo "interruptSlow: begin"
COMMAND ${CMAKE_COMMAND} -E sleep 30
COMMAND ${CMAKE_COMMAND} -E echo "interruptSlow: end"
)
endif()
@@ -0,0 +1,5 @@
{
"version": 1,
"hooks": ["postCMakeBuild"],
"callbacks": ["@GET_HOOK@"]
}
@@ -9,6 +9,8 @@ function(snippet_has_fields snippet contents)
json_has_key("${snippet}" "${contents}" role)
json_has_key("${snippet}" "${contents}" workingDir)
json_has_key("${snippet}" "${contents}" result)
# Only an interrupted build records this; completed commands must omit it.
json_missing_key("${snippet}" "${contents}" interruptSignal)
if (NOT filename MATCHES "^build-*")
json_has_key("${snippet}" "${contents}" command)
else()