mirror of
https://github.com/Kitware/CMake.git
synced 2026-08-08 16:50:48 +00:00
Instrumentation: Write cmakeInstall snippet on interrupted install
With instrumentation enabled, interrupting `cmake --install` (Ctrl+C) terminated the process before its overall `cmakeInstall` envelope snippet was written, orphaning the per-script `install` snippets. Extend the `cmake --build` interrupt handling to the install site: wrap the `cmakeInstall` command in `HandleInterrupt`, skip the post-install hook, and re-raise so the exit status reflects the signal. Add cooperative cancellation so the command unwinds -- serial-loop and parallel `queueScripts` guards stop launching further scripts, and the parallel failure aggregation is scoped to dispatched runners so un-launched scripts are not counted as failed. Force a non-zero result on interrupt (Windows cannot re-raise), and write the envelope atomically so a second Ctrl+C cannot truncate it. Issue: #27859
This commit is contained in:
committed by
Daniel Tierney
parent
ccfef28ee4
commit
38e0434c9a
@@ -454,11 +454,11 @@ Snippet files have a filename with the syntax
|
||||
``interruptSignal``
|
||||
.. versionadded:: 4.5
|
||||
|
||||
The integer signal number that interrupted the build before it completed
|
||||
The integer signal number that interrupted the command 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.
|
||||
is ``cmakeBuild`` or ``cmakeInstall`` and the command was interrupted.
|
||||
Consumers can use the presence of this field to distinguish an interrupted
|
||||
command from one that ran to completion.
|
||||
|
||||
Only available as of data version ``1.2``.
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
},
|
||||
"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.",
|
||||
"description": "The signal number that interrupted the command before it completed. Only included when role is cmakeBuild or cmakeInstall and the command was interrupted.",
|
||||
"minimum": 1
|
||||
},
|
||||
"role": {
|
||||
|
||||
@@ -8,3 +8,6 @@ instrumentation-interrupt
|
||||
``interruptSignal`` field, recording the signal that interrupted the build,
|
||||
so that consumers can distinguish an interrupted build from one that ran
|
||||
to completion.
|
||||
* Similarly, :manual:`cmake-instrumentation(7)` API now records an overall
|
||||
``cmakeInstall`` snippet, marked with the same ``interruptSignal`` field,
|
||||
when a :option:`cmake --install` invocation is interrupted by the user.
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "cmCryptoHash.h"
|
||||
#include "cmGeneratedFileStream.h"
|
||||
#include "cmInstrumentation.h"
|
||||
#include "cmInstrumentationInterrupt.h"
|
||||
#include "cmJSONState.h"
|
||||
#include "cmProcessOutput.h"
|
||||
#include "cmStringAlgorithms.h"
|
||||
@@ -152,6 +153,14 @@ int cmInstallScriptHandler::Install(unsigned int j,
|
||||
std::function<void()> queueScripts;
|
||||
queueScripts = [&runners, &working, &installed, &i, &loop, j,
|
||||
&queueScripts]() {
|
||||
if (cmInstrumentationInterrupt::PendingInterruptSignal() != 0) {
|
||||
// Interrupted (e.g. Ctrl+C): launch no further scripts. In-flight
|
||||
// children share the process group, receive the signal, and exit on
|
||||
// their own, draining the event loop; queueScripts is the single
|
||||
// re-entry point for launching, so guarding it here stops all remaining
|
||||
// work without killing anything.
|
||||
return;
|
||||
}
|
||||
for (auto queue = std::min(j - working, runners.size() - i); queue > 0;
|
||||
--queue) {
|
||||
++working;
|
||||
@@ -173,10 +182,18 @@ int cmInstallScriptHandler::Install(unsigned int j,
|
||||
queueScripts();
|
||||
uv_run(loop, UV_RUN_DEFAULT);
|
||||
|
||||
// Aggregate child results. When an interrupt stopped queueScripts, the
|
||||
// runners beyond the dispatched prefix [0, i) were never started (they have
|
||||
// a null process handle); those are "not run", not "failed", so exclude
|
||||
// them. With no interrupt every runner was dispatched and this inspects
|
||||
// them all, matching the non-interrupt behavior exactly.
|
||||
std::size_t const inspect =
|
||||
cmInstrumentationInterrupt::PendingInterruptSignal() != 0 ? i
|
||||
: runners.size();
|
||||
int result = 0;
|
||||
for (auto& runner : runners) {
|
||||
if (runner.Failed()) {
|
||||
runner.printFailure();
|
||||
for (std::size_t k = 0; k < inspect; ++k) {
|
||||
if (runners[k].Failed()) {
|
||||
runners[k].printFailure();
|
||||
result = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,8 +770,8 @@ 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
|
||||
// If the command was interrupted (e.g. by Ctrl+C), record the signal number
|
||||
// that stopped it, so consumers can distinguish an interrupted command 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();
|
||||
@@ -840,14 +840,15 @@ int cmInstrumentation::InstrumentCommand(
|
||||
}
|
||||
this->configureSnippetData.clear();
|
||||
}
|
||||
// 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.
|
||||
// Write the cmakeBuild/cmakeInstall 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.
|
||||
bool const atomicEnvelope =
|
||||
command_type == "cmakeBuild" || command_type == "cmakeInstall";
|
||||
this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name,
|
||||
command_type == "cmakeBuild" ? Atomic::Yes
|
||||
: Atomic::No);
|
||||
atomicEnvelope ? Atomic::Yes : Atomic::No);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -26,32 +26,34 @@
|
||||
#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.
|
||||
// Flag shared between the interrupt handler and the command flow that writes
|
||||
// the instrumentation envelope 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 };
|
||||
std::atomic<int> interruptSignal{ 0 };
|
||||
|
||||
BOOL WINAPI cmInstrumentationConsoleHandler(DWORD type)
|
||||
{
|
||||
if (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT) {
|
||||
int expected = 0;
|
||||
buildInterruptSignal.compare_exchange_strong(expected, SIGINT);
|
||||
interruptSignal.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.
|
||||
// process exits. Child processes (native build tool, install scripts,
|
||||
// tests) share the console and receive the event directly, so they still
|
||||
// terminate and unblock our loop.
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
#else
|
||||
sig_atomic_t volatile buildInterruptSignal = 0;
|
||||
sig_atomic_t volatile interruptSignal = 0;
|
||||
struct sigaction savedSigIntAction;
|
||||
|
||||
extern "C" void cmInstrumentationSignalHandler(int sig)
|
||||
{
|
||||
buildInterruptSignal = sig;
|
||||
interruptSignal = sig;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -59,14 +61,14 @@ extern "C" void cmInstrumentationSignalHandler(int sig)
|
||||
// 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;
|
||||
bool interruptInjected = 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.
|
||||
// instrumentation test suite inject an "interrupted" condition
|
||||
// deterministically, with no real OS signal -- so the instrumentation
|
||||
// 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");
|
||||
@@ -78,21 +80,21 @@ void InjectTestInterrupt()
|
||||
return;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
buildInterruptSignal.store(sig);
|
||||
interruptSignal.store(sig);
|
||||
#else
|
||||
buildInterruptSignal = static_cast<sig_atomic_t>(sig);
|
||||
interruptSignal = static_cast<sig_atomic_t>(sig);
|
||||
#endif
|
||||
buildInterruptInjected = true;
|
||||
interruptInjected = true;
|
||||
}
|
||||
|
||||
// Install the interrupt handler and clear any previously recorded signal.
|
||||
void InstallInterruptHandler()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
buildInterruptSignal.store(0);
|
||||
interruptSignal.store(0);
|
||||
SetConsoleCtrlHandler(cmInstrumentationConsoleHandler, TRUE);
|
||||
#else
|
||||
buildInterruptSignal = 0;
|
||||
interruptSignal = 0;
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = cmInstrumentationSignalHandler;
|
||||
@@ -118,9 +120,9 @@ void RestoreInterruptHandler()
|
||||
int cmInstrumentationInterrupt::PendingInterruptSignal()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return buildInterruptSignal.load();
|
||||
return interruptSignal.load();
|
||||
#else
|
||||
return static_cast<int>(buildInterruptSignal);
|
||||
return static_cast<int>(interruptSignal);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -134,7 +136,7 @@ cmInstrumentationInterrupt::HandleInterrupt(
|
||||
return { callback(), false, 0, true };
|
||||
}
|
||||
InstallInterruptHandler();
|
||||
buildInterruptInjected = false;
|
||||
interruptInjected = false;
|
||||
// Test-only: allow the suite to inject an interrupt deterministically.
|
||||
InjectTestInterrupt();
|
||||
int ret = callback();
|
||||
@@ -142,7 +144,7 @@ cmInstrumentationInterrupt::HandleInterrupt(
|
||||
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 };
|
||||
return { ret, sig != 0, sig, !interruptInjected };
|
||||
}
|
||||
|
||||
void cmInstrumentationInterrupt::RaiseInterrupt(int sig)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "cmGlobalGenerator.h"
|
||||
#include "cmInstallScriptHandler.h"
|
||||
#include "cmInstrumentation.h"
|
||||
#include "cmInstrumentationInterrupt.h"
|
||||
#include "cmInstrumentationQuery.h"
|
||||
#include "cmList.h"
|
||||
#include "cmMakefile.h"
|
||||
@@ -958,6 +959,12 @@ int do_install(int ac, char const* const* av)
|
||||
ret_ = handler.Install(jobs, instrumentation);
|
||||
} else {
|
||||
for (auto const& script : handler.GetScripts()) {
|
||||
if (cmInstrumentationInterrupt::PendingInterruptSignal() != 0) {
|
||||
// Interrupted (e.g. Ctrl+C): launch no further scripts. The script
|
||||
// currently running executes in-process and finishes on its own; we
|
||||
// simply stop starting new ones.
|
||||
break;
|
||||
}
|
||||
std::vector<std::string> cmd = script.command;
|
||||
cmake cm(cmState::Role::Script);
|
||||
cmSystemTools::SetMessageCallback(
|
||||
@@ -976,15 +983,41 @@ int do_install(int ac, char const* const* av)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cmInstrumentationInterrupt::PendingInterruptSignal() != 0) {
|
||||
// Any caught interrupt makes the install unsuccessful even if the work
|
||||
// that did run happened to succeed. Windows has no signal to re-raise,
|
||||
// so this is what forces a non-zero exit status there; on POSIX it also
|
||||
// keeps the snippet `result` consistent with the re-raised signal.
|
||||
ret_ = 1;
|
||||
}
|
||||
return int(ret_ > 0);
|
||||
};
|
||||
|
||||
std::vector<std::string> cmd;
|
||||
cm::append(cmd, av, av + ac);
|
||||
ret = instrumentation.InstrumentCommand(
|
||||
"cmakeInstall", cmd, [doInstall]() -> cmInstrumentation::CommandResult {
|
||||
return { doInstall(), cm::nullopt, cm::nullopt };
|
||||
});
|
||||
// Run the install under an interrupt handler so that a user interrupt (e.g.
|
||||
// Ctrl+C) still writes the overall `cmakeInstall` snippet before we exit.
|
||||
cmInstrumentationInterrupt::InterruptOutcome installOutcome =
|
||||
cmInstrumentationInterrupt::HandleInterrupt(
|
||||
instrumentation.HasQuery(),
|
||||
[&instrumentation, &cmd, &doInstall]() -> int {
|
||||
return instrumentation.InstrumentCommand(
|
||||
"cmakeInstall", cmd,
|
||||
[&doInstall]() -> cmInstrumentation::CommandResult {
|
||||
return { doInstall(), cm::nullopt, cm::nullopt };
|
||||
});
|
||||
});
|
||||
ret = installOutcome.ExitCode;
|
||||
if (installOutcome.Interrupted) {
|
||||
// The install was interrupted and its snippet has been written. Skip the
|
||||
// post-install 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.
|
||||
if (installOutcome.ShouldRaise) {
|
||||
cmInstrumentationInterrupt::RaiseInterrupt(installOutcome.Signal);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
instrumentation.CollectTimingData(
|
||||
cmInstrumentationQuery::Hook::PostCMakeInstall);
|
||||
return ret;
|
||||
|
||||
@@ -12,6 +12,8 @@ function(instrument test)
|
||||
"INTERRUPT_SEAM"
|
||||
"INSTALL"
|
||||
"INSTALL_PARALLEL"
|
||||
"INSTALL_SEAM"
|
||||
"INSTALL_INTERRUPT"
|
||||
"TEST"
|
||||
"WORKFLOW"
|
||||
"COPY_QUERIES"
|
||||
@@ -127,6 +129,11 @@ function(instrument test)
|
||||
list(APPEND ARGS_CONFIGURE_ARGS
|
||||
"-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c")
|
||||
endif()
|
||||
if (ARGS_INSTALL_INTERRUPT)
|
||||
list(APPEND ARGS_CONFIGURE_ARGS
|
||||
"-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c"
|
||||
"-DINSTALL_INTERRUPT=ON")
|
||||
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)
|
||||
@@ -200,7 +207,7 @@ function(instrument test)
|
||||
endif()
|
||||
set(helper ${helper_dir}/InterruptBuild${CMAKE_EXECUTABLE_SUFFIX})
|
||||
set(RunCMake_QUIET_ERROR 1)
|
||||
run_cmake_command(${test}-build
|
||||
run_cmake_command(${test}-signal
|
||||
${helper} 3
|
||||
${CMAKE_COMMAND} --build . --config Debug)
|
||||
unset(RunCMake_QUIET_ERROR)
|
||||
@@ -221,7 +228,7 @@ function(instrument test)
|
||||
# and skips the hook.
|
||||
set(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT} 2)
|
||||
set(RunCMake_TEST_EXPECT_RESULT 0)
|
||||
run_cmake_command(${test}-build
|
||||
run_cmake_command(${test}-seam
|
||||
${CMAKE_COMMAND} --build . --config Debug)
|
||||
unset(RunCMake_TEST_EXPECT_RESULT)
|
||||
unset(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT})
|
||||
@@ -239,6 +246,63 @@ function(instrument test)
|
||||
if (ARGS_INSTALL)
|
||||
run_cmake_command(${test}-install ${CMAKE_COMMAND} --install . --prefix install --config Debug)
|
||||
endif()
|
||||
if (ARGS_INSTALL_SEAM)
|
||||
# Drive the cmakeInstall interrupt path deterministically via the test-only
|
||||
# injection seam, with no OS signal, so it runs on every generator. First
|
||||
# install normally so the postCMakeInstall hook runs and creates its marker
|
||||
# file; remove it (and the manifest) so their absence after the injected
|
||||
# install proves that install's hook was skipped and left nothing complete.
|
||||
set(RunCMake_QUIET_ERROR 1)
|
||||
run_cmake_command(${test}-warmup
|
||||
${CMAKE_COMMAND} --install . --prefix install --config Debug)
|
||||
file(REMOVE ${v1}/postCMakeInstall.hook)
|
||||
file(REMOVE ${RunCMake_TEST_BINARY_DIR}/install_manifest.txt)
|
||||
|
||||
# Inject an interrupt (SIGINT == 2) via the undocumented test seam and
|
||||
# install again; cmake exits with a non-zero status but writes the
|
||||
# interrupted cmakeInstall snippet and skips the hook.
|
||||
set(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT} 2)
|
||||
set(RunCMake_TEST_EXPECT_RESULT 1)
|
||||
run_cmake_command(${test}-seam
|
||||
${CMAKE_COMMAND} --install . --prefix install --config Debug)
|
||||
unset(RunCMake_TEST_EXPECT_RESULT)
|
||||
unset(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT})
|
||||
unset(RunCMake_QUIET_ERROR)
|
||||
endif()
|
||||
if (ARGS_INSTALL_INTERRUPT)
|
||||
# Build just the interrupt helper and main so the parallel install has
|
||||
# something to do. This build runs no postCMakeInstall hook (the query only
|
||||
# requests that hook), so remove any stale marker; its absence after the
|
||||
# interrupted install then proves that install's hook was skipped.
|
||||
run_cmake_command(${test}-helper
|
||||
${CMAKE_COMMAND} --build . --config Debug --target InterruptBuild main)
|
||||
file(REMOVE ${v1}/postCMakeBuild.hook)
|
||||
file(REMOVE ${v1}/postCMakeInstall.hook)
|
||||
file(REMOVE ${RunCMake_TEST_BINARY_DIR}/install_manifest.txt)
|
||||
|
||||
# Pin InstallScripts.json newest so the parallel install path is chosen
|
||||
# deterministically (the staleness heuristic can otherwise fall back to
|
||||
# serial on coarse-mtime filesystems).
|
||||
file(TOUCH_NOCREATE ${RunCMake_TEST_BINARY_DIR}/CMakeFiles/InstallScripts.json)
|
||||
|
||||
# Run an instrumented parallel install and interrupt it after a few seconds,
|
||||
# while a slow install script is still running and others are pending.
|
||||
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})
|
||||
# Merge stdout/stderr: the parallel aggregation prints one "User interrupt"
|
||||
# diagnostic per in-flight script to stderr, whose presence and count are
|
||||
# cosmetic and race with the re-raise that terminates the process.
|
||||
set(RunCMake_TEST_OUTPUT_MERGE 1)
|
||||
set(RunCMake_QUIET_ERROR 1)
|
||||
run_cmake_command(${test}-signal
|
||||
${helper} 3
|
||||
${CMAKE_COMMAND} --install . --prefix install --config Debug -j 1)
|
||||
unset(RunCMake_QUIET_ERROR)
|
||||
unset(RunCMake_TEST_OUTPUT_MERGE)
|
||||
endif()
|
||||
if (ARGS_TEST)
|
||||
run_cmake_command(${test}-test ${CMAKE_CTEST_COMMAND} . -C Debug)
|
||||
endif()
|
||||
@@ -266,9 +330,14 @@ if (INSTRUMENTATION_INTERRUPT_REAL)
|
||||
# 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
|
||||
instrument(interrupt-build INTERRUPT
|
||||
CHECK_SCRIPT check-interrupted.cmake
|
||||
)
|
||||
# Interrupt a parallel `cmake --install` with a real OS signal, proving the
|
||||
# cooperative cancellation stops pending install scripts and skips the hook.
|
||||
instrument(interrupt-install INSTALL_INTERRUPT
|
||||
CHECK_SCRIPT check-installation-interrupted.cmake
|
||||
)
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
@@ -476,13 +545,17 @@ 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
|
||||
# Test that interrupting `cmake --build` or `cmake --install` still writes the
|
||||
# overall cmakeBuild/cmakeInstall snippet, recording the interrupting signal,
|
||||
# and skips the corresponding post-command hook. These cases use the
|
||||
# deterministic test seam (no OS event); the real OS-event counterparts run in
|
||||
# the separate RunCMake.InstrumentationInterrupt suite.
|
||||
instrument(interrupt-build INTERRUPT_SEAM
|
||||
CHECK_SCRIPT check-interrupted.cmake
|
||||
)
|
||||
instrument(interrupt-install BUILD INSTALL_SEAM
|
||||
CHECK_SCRIPT check-installation-interrupted.cmake
|
||||
)
|
||||
|
||||
# Test make/ninja hooks
|
||||
if(RunCMake_GENERATOR STREQUAL "FASTBuild")
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/json.cmake)
|
||||
|
||||
# After an interrupted `cmake --install`, exactly one cmakeInstall snippet should
|
||||
# be present, marked with the interrupting signal, and the postCMakeInstall hook
|
||||
# must have been skipped.
|
||||
check_interrupted_snippet(cmakeInstall postCMakeInstall)
|
||||
|
||||
# An interrupted install is incomplete, so it must not leave behind an install
|
||||
# manifest that looks complete.
|
||||
if (EXISTS ${RunCMake_TEST_BINARY_DIR}/install_manifest.txt)
|
||||
add_error("install_manifest.txt should be absent after an interrupted install")
|
||||
endif()
|
||||
@@ -1,35 +1,6 @@
|
||||
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()
|
||||
# present, marked with the interrupting signal, and the postCMakeBuild hook must
|
||||
# have been skipped.
|
||||
check_interrupted_snippet(cmakeBuild postCMakeBuild)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
42
|
||||
@@ -71,3 +71,30 @@ function(json_equals expected_file actual_file)
|
||||
endif()
|
||||
return(PROPAGATE RunCMake_TEST_FAILED ERROR_MESSAGE)
|
||||
endfunction()
|
||||
|
||||
# Verify the aftermath of an interrupted top-level command whose overall
|
||||
# instrumentation snippet has role `role` and whose post-command hook is `hook`.
|
||||
# Exactly one such snippet must be present and marked with the interrupting
|
||||
# signal (any snippet from an earlier uninterrupted warm-up run was collated and
|
||||
# removed by its hook). The hook must have been skipped entirely, so its
|
||||
# callback (hook.cmake writes a `${hook}.hook` marker whenever it runs, and any
|
||||
# earlier copy was removed before the interrupted run) must not have run.
|
||||
function(check_interrupted_snippet role hook)
|
||||
file(GLOB snippets LIST_DIRECTORIES false ${v1}/data/${role}-*.json)
|
||||
list(LENGTH snippets num)
|
||||
if (NOT num EQUAL 1)
|
||||
add_error("Expected exactly one ${role} snippet, found ${num}: ${snippets}")
|
||||
else()
|
||||
read_json("${snippets}" contents)
|
||||
string(JSON interruptSignal ERROR_VARIABLE noSignal GET "${contents}" interruptSignal)
|
||||
if (noSignal OR NOT interruptSignal MATCHES "^[1-9][0-9]*$")
|
||||
add_error("${role} snippet is not marked interrupted:\n${contents}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (EXISTS ${v1}/${hook}.hook)
|
||||
add_error("${hook} hook should be skipped on interrupt, but it ran")
|
||||
endif()
|
||||
|
||||
return(PROPAGATE RunCMake_TEST_FAILED ERROR_MESSAGE)
|
||||
endfunction()
|
||||
|
||||
@@ -79,3 +79,13 @@ if (INTERRUPT_BUILD_SRC)
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "interruptSlow: end"
|
||||
)
|
||||
endif()
|
||||
|
||||
if (INSTALL_INTERRUPT)
|
||||
# Enable parallel install and add several slow install subdirectories so that
|
||||
# a `cmake --install -j 1` runs long enough to be interrupted with install
|
||||
# scripts still pending.
|
||||
set_property(GLOBAL PROPERTY INSTALL_PARALLEL ON)
|
||||
add_subdirectory(installSlow1)
|
||||
add_subdirectory(installSlow2)
|
||||
add_subdirectory(installSlow3)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# A deliberately slow install step so that a parallel `cmake --install -j 1` has
|
||||
# scripts still pending when a user interrupt arrives. The interrupt must stop
|
||||
# the remaining install scripts from being launched.
|
||||
install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E sleep 10)")
|
||||
@@ -0,0 +1,4 @@
|
||||
# A deliberately slow install step so that a parallel `cmake --install -j 1` has
|
||||
# scripts still pending when a user interrupt arrives. The interrupt must stop
|
||||
# the remaining install scripts from being launched.
|
||||
install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E sleep 10)")
|
||||
@@ -0,0 +1,4 @@
|
||||
# A deliberately slow install step so that a parallel `cmake --install -j 1` has
|
||||
# scripts still pending when a user interrupt arrives. The interrupt must stop
|
||||
# the remaining install scripts from being launched.
|
||||
install(CODE "execute_process(COMMAND \"${CMAKE_COMMAND}\" -E sleep 10)")
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"hooks": ["postCMakeInstall"],
|
||||
"callbacks": ["@GET_HOOK@"]
|
||||
}
|
||||
Reference in New Issue
Block a user