/* 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 #include #include #include #include #include #include #include #include #include #include "cmsys/FStream.hxx" #include "cmsys/RegularExpression.hxx" #include "cmCryptoHash.h" #include "cmGeneratedFileStream.h" #include "cmInstrumentation.h" #include "cmInstrumentationInterrupt.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 components, std::string installConfig, std::vector& 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)); }; // Trust the parallel install index unless the cache marker is newer. Both // files are written back-to-back while generating this build tree, so any // ordering between them lives in a sub-second window that some filesystems // (e.g. NFS on AIX) do not resolve by write time. Compare at whole-second // resolution: a genuinely stale index left by an older CMake reconfigure is // always at least a configure run (seconds) older, while the sub-second // jitter of a single generate step is ignored. bool indexIsFresh = false; if (cmSystemTools::FileExists(file)) { long int const cacheTime = cmSystemTools::ModifiedTime( cmStrCat(this->BinaryDir, "/CMakeFiles/cmake.check_cache")); indexIsFresh = cacheTime <= cmSystemTools::ModifiedTime(file); } if (indexIsFresh) { Json::CharReaderBuilder rbuilder; auto jsonReader = std::unique_ptr(rbuilder.newCharReader()); std::vector 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 cmInstallScriptHandler::GetScripts() const { return this->Scripts; } int cmInstallScriptHandler::Install(unsigned int j, cmInstrumentation& instrumentation) { cm::uv_loop_ptr loop; loop.init(); std::vector runners; runners.reserve(this->Scripts.size()); std::vector 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.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 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; 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); // 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 (std::size_t k = 0; k < inspect; ++k) { if (runners[k].Failed()) { runners[k].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 callback) { cmUVProcessChainBuilder builder; builder.AddCommand(this->Command) .SetExternalLoop(*loop) .SetMergedBuiltinStreams(); this->Chain = cm::make_unique(builder.Start()); if (!this->Chain->Valid()) { return false; } this->StreamHandler = cmUVStreamRead( this->Chain->OutputStream(), [this](std::vector 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')); }