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:
Daksh Mamodiya
2026-07-05 10:54:50 +02:00
committed by Daniel Tierney
parent ccfef28ee4
commit 38e0434c9a
19 changed files with 253 additions and 86 deletions

View File

@@ -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``.

View File

@@ -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": {

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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")

View File

@@ -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()

View File

@@ -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)

View File

@@ -0,0 +1 @@
42

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)")

View File

@@ -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)")

View File

@@ -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)")

View File

@@ -0,0 +1,5 @@
{
"version": 1,
"hooks": ["postCMakeInstall"],
"callbacks": ["@GET_HOOK@"]
}