mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
`cmake --install` did not propagate per-script failures into its process exit code. In parallel mode, cmInstallScriptHandler::Install spawned each install script as a child process but never inspected the child exit status or termination signal, and always returned 0. It now reads each child's status after the event loop, prints the failing script's exit code or signal, and returns non-zero if any script failed. A failed parallel install also no longer writes the combined install_manifest.txt, so a partial manifest is not mistaken for a complete install. In serial mode, GetScripts() returns the top-level cmake_install.cmake once per component and per configuration, so multiple scripts run only when installing several components or configurations at once. The loop overwrote its result on every iteration, so an earlier script that failed via cmake_language(EXIT) was masked by a later one that succeeded. The loop now stops at the first failure. This is a no-op for a single component and configuration; it changes only installs of multiple components or configurations, which now stop at the first failed script and report it instead of attempting the rest. Fixes: #27906
294 lines
8.8 KiB
C++
294 lines
8.8 KiB
C++
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
|
file LICENSE.rst or https://cmake.org/licensing for details. */
|
|
|
|
#include "cmInstallScriptHandler.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <cm/memory>
|
|
|
|
#include <cm3p/json/reader.h>
|
|
#include <cm3p/json/value.h>
|
|
#include <cm3p/uv.h>
|
|
|
|
#include "cmsys/FStream.hxx"
|
|
#include "cmsys/RegularExpression.hxx"
|
|
|
|
#include "cmCryptoHash.h"
|
|
#include "cmGeneratedFileStream.h"
|
|
#include "cmInstrumentation.h"
|
|
#include "cmJSONState.h"
|
|
#include "cmProcessOutput.h"
|
|
#include "cmStringAlgorithms.h"
|
|
#include "cmSystemTools.h"
|
|
#include "cmUVHandlePtr.h"
|
|
#include "cmUVProcessChain.h"
|
|
#include "cmUVStream.h"
|
|
|
|
using InstallScript = cmInstallScriptHandler::InstallScript;
|
|
using InstallScriptRunner = cmInstallScriptHandler::InstallScriptRunner;
|
|
|
|
cmInstallScriptHandler::cmInstallScriptHandler(
|
|
std::string binaryDir, std::vector<std::string> components,
|
|
std::string installConfig, std::vector<std::string>& args)
|
|
: Components(std::move(components))
|
|
, BinaryDir(std::move(binaryDir))
|
|
{
|
|
if (this->Components.empty()) {
|
|
this->Components.emplace_back(std::string{});
|
|
}
|
|
|
|
std::string const& file =
|
|
cmStrCat(this->BinaryDir, "/CMakeFiles/InstallScripts.json");
|
|
this->Parallel = false;
|
|
|
|
auto addScript = [this, &args](std::string script, std::string component,
|
|
std::string config) -> void {
|
|
this->Scripts.push_back({ script, config, args });
|
|
if (!component.empty()) {
|
|
this->Scripts.back().command.insert(
|
|
this->Scripts.back().command.end() - 1,
|
|
cmStrCat("-DCMAKE_INSTALL_COMPONENT=", component));
|
|
}
|
|
if (!config.empty()) {
|
|
this->Scripts.back().command.insert(
|
|
this->Scripts.back().command.end() - 1,
|
|
cmStrCat("-DCMAKE_INSTALL_CONFIG_NAME=", config));
|
|
}
|
|
this->Scripts.back().command.emplace_back(script);
|
|
this->Directories.push_back(cmSystemTools::GetFilenamePath(script));
|
|
};
|
|
|
|
int compare = 1;
|
|
if (cmSystemTools::FileExists(file)) {
|
|
cmSystemTools::FileTimeCompare(
|
|
cmStrCat(this->BinaryDir, "/CMakeFiles/cmake.check_cache"), file,
|
|
&compare);
|
|
}
|
|
if (compare < 1) {
|
|
Json::CharReaderBuilder rbuilder;
|
|
auto jsonReader =
|
|
std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
|
|
std::vector<char> content;
|
|
Json::Value value;
|
|
cmJSONState state(file, &value);
|
|
this->Parallel = value["Parallel"].asBool();
|
|
if (this->Parallel) {
|
|
args.insert(args.end() - 1, "-DCMAKE_INSTALL_LOCAL_ONLY=1");
|
|
}
|
|
if (installConfig.empty() && value.isMember("Configs")) {
|
|
for (auto const& config : value["Configs"]) {
|
|
this->Configs.push_back(config.asCString());
|
|
}
|
|
} else {
|
|
this->Configs.push_back(installConfig);
|
|
}
|
|
for (auto const& script : value["InstallScripts"]) {
|
|
for (auto const& component : Components) {
|
|
for (auto const& config : Configs) {
|
|
addScript(script.asCString(), component, config);
|
|
}
|
|
}
|
|
if (!this->Parallel) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
for (auto const& component : Components) {
|
|
addScript(cmStrCat(this->BinaryDir, "/cmake_install.cmake"), component,
|
|
installConfig);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool cmInstallScriptHandler::IsParallel()
|
|
{
|
|
return this->Parallel;
|
|
}
|
|
|
|
std::vector<InstallScript> cmInstallScriptHandler::GetScripts() const
|
|
{
|
|
return this->Scripts;
|
|
}
|
|
|
|
int cmInstallScriptHandler::Install(unsigned int j,
|
|
cmInstrumentation& instrumentation)
|
|
{
|
|
cm::uv_loop_ptr loop;
|
|
loop.init();
|
|
std::vector<InstallScriptRunner> runners;
|
|
runners.reserve(this->Scripts.size());
|
|
|
|
std::vector<std::string> instrumentArg;
|
|
if (instrumentation.HasQuery()) {
|
|
instrumentArg = { cmSystemTools::GetCTestCommand(),
|
|
"--instrument",
|
|
"--command-type",
|
|
"install",
|
|
"--build-dir",
|
|
this->BinaryDir,
|
|
"--config",
|
|
"",
|
|
"--" };
|
|
}
|
|
|
|
for (auto& script : this->Scripts) {
|
|
if (!instrumentArg.empty()) {
|
|
instrumentArg[7] = script.config; // --config <script.config>
|
|
}
|
|
script.command.insert(script.command.begin(), instrumentArg.begin(),
|
|
instrumentArg.end());
|
|
runners.emplace_back(script);
|
|
}
|
|
std::size_t working = 0;
|
|
std::size_t installed = 0;
|
|
std::size_t i = 0;
|
|
|
|
std::function<void()> queueScripts;
|
|
queueScripts = [&runners, &working, &installed, &i, &loop, j,
|
|
&queueScripts]() {
|
|
for (auto queue = std::min(j - working, runners.size() - i); queue > 0;
|
|
--queue) {
|
|
++working;
|
|
bool started = runners[i].start(
|
|
loop, [&runners, &working, &installed, i, &queueScripts]() {
|
|
runners[i].printResult(++installed, runners.size());
|
|
--working;
|
|
queueScripts();
|
|
});
|
|
if (!started) {
|
|
// The child could not be spawned. Release its scheduling slot so the
|
|
// remaining scripts still run; it is counted as a failure after the
|
|
// event loop completes.
|
|
--working;
|
|
}
|
|
++i;
|
|
}
|
|
};
|
|
queueScripts();
|
|
uv_run(loop, UV_RUN_DEFAULT);
|
|
|
|
int result = 0;
|
|
for (auto& runner : runners) {
|
|
if (runner.Failed()) {
|
|
runner.printFailure();
|
|
result = 1;
|
|
}
|
|
}
|
|
|
|
// Write the install manifest only when every script succeeded. A failed
|
|
// install must not leave behind a manifest that looks complete.
|
|
if (result == 0) {
|
|
std::string installManifest;
|
|
for (auto const& component : this->Components) {
|
|
if (component.empty()) {
|
|
installManifest = "install_manifest.txt";
|
|
} else {
|
|
cmsys::RegularExpression regEntry;
|
|
if (regEntry.compile("^[a-zA-Z0-9_.+-]+$") &&
|
|
regEntry.find(component)) {
|
|
installManifest = cmStrCat("install_manifest_", component, ".txt");
|
|
} else {
|
|
cmCryptoHash md5(cmCryptoHash::AlgoMD5);
|
|
md5.Initialize();
|
|
installManifest =
|
|
cmStrCat("install_manifest_", md5.HashString(component), ".txt");
|
|
}
|
|
}
|
|
cmGeneratedFileStream fout(
|
|
cmStrCat(this->BinaryDir, '/', installManifest));
|
|
fout.SetCopyIfDifferent(true);
|
|
for (auto const& dir : this->Directories) {
|
|
auto localManifest = cmStrCat(dir, "/install_local_manifest.txt");
|
|
if (cmSystemTools::FileExists(localManifest)) {
|
|
cmsys::ifstream fin(localManifest.c_str());
|
|
std::string line;
|
|
while (std::getline(fin, line)) {
|
|
fout << line << "\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
InstallScriptRunner::InstallScriptRunner(InstallScript const& script)
|
|
{
|
|
this->Name = cmSystemTools::RelativePath(
|
|
cmSystemTools::GetLogicalWorkingDirectory(), script.path);
|
|
this->Command = script.command;
|
|
}
|
|
|
|
bool InstallScriptRunner::start(cm::uv_loop_ptr& loop,
|
|
std::function<void()> callback)
|
|
{
|
|
cmUVProcessChainBuilder builder;
|
|
builder.AddCommand(this->Command)
|
|
.SetExternalLoop(*loop)
|
|
.SetMergedBuiltinStreams();
|
|
this->Chain = cm::make_unique<cmUVProcessChain>(builder.Start());
|
|
if (!this->Chain->Valid()) {
|
|
return false;
|
|
}
|
|
this->StreamHandler = cmUVStreamRead(
|
|
this->Chain->OutputStream(),
|
|
[this](std::vector<char> data) {
|
|
std::string strdata;
|
|
cmProcessOutput(cmProcessOutput::Auto)
|
|
.DecodeText(data.data(), data.size(), strdata);
|
|
this->Output.push_back(strdata);
|
|
},
|
|
std::move(callback));
|
|
return true;
|
|
}
|
|
|
|
void InstallScriptRunner::printResult(std::size_t n, std::size_t total)
|
|
{
|
|
cmSystemTools::Stdout(cmStrCat('[', n, '/', total, "] ", this->Name, '\n'));
|
|
for (auto const& line : this->Output) {
|
|
cmSystemTools::Stdout(line);
|
|
}
|
|
}
|
|
|
|
bool InstallScriptRunner::Failed() const
|
|
{
|
|
if (!this->Chain || !this->Chain->Valid()) {
|
|
return true;
|
|
}
|
|
auto const& status = this->Chain->GetStatus(0);
|
|
return status.SpawnResult != 0 || status.TermSignal != 0 ||
|
|
status.ExitStatus != 0;
|
|
}
|
|
|
|
void InstallScriptRunner::printFailure()
|
|
{
|
|
std::string detail;
|
|
if (!this->Chain || !this->Chain->Valid()) {
|
|
// The chain never spawned a process (e.g. pipe/loop setup failed).
|
|
detail = "failed to start";
|
|
} else {
|
|
auto const& status = this->Chain->GetStatus(0);
|
|
auto exception = status.GetException();
|
|
switch (exception.first) {
|
|
case cmUVProcessChain::ExceptionCode::None:
|
|
detail = cmStrCat("exited with code ", status.ExitStatus);
|
|
break;
|
|
case cmUVProcessChain::ExceptionCode::Spawn:
|
|
// Prepared, but the process could not be executed.
|
|
detail = cmStrCat("failed to start: ", exception.second);
|
|
break;
|
|
default:
|
|
detail = exception.second;
|
|
break;
|
|
}
|
|
}
|
|
cmSystemTools::Stderr(
|
|
cmStrCat("CMake Error: install script '", this->Name, "' ", detail, '\n'));
|
|
}
|