diff options
author | Matthew Woehlke <matthew.woehlke@kitware.com> | 2022-11-23 22:02:22 (GMT) |
---|---|---|
committer | Brad King <brad.king@kitware.com> | 2022-12-16 15:11:37 (GMT) |
commit | 746c776caf1207049922edb3ea63586b94fca4c6 (patch) | |
tree | a846db4f6435b8d4f1c592af0a26209ba13b5a1f | |
parent | e8b8d82cbf60e517ad4b9026ba24de1c59165af4 (diff) | |
download | CMake-746c776caf1207049922edb3ea63586b94fca4c6.zip CMake-746c776caf1207049922edb3ea63586b94fca4c6.tar.gz CMake-746c776caf1207049922edb3ea63586b94fca4c6.tar.bz2 |
ConfigureLog: Add infrastructure for structured configure event logging
Add infrastructure for a "configure log". Use YAML for a balance of
machine- and human-readability to records details of configure-time
events in a structured format.
Teach the RunCMake test framework to support matching the configure log.
Issue: #23200
-rw-r--r-- | Help/index.rst | 1 | ||||
-rw-r--r-- | Help/manual/cmake-configure-log.7.rst | 102 | ||||
-rw-r--r-- | Help/release/dev/configure-log.rst | 5 | ||||
-rw-r--r-- | Source/CMakeLists.txt | 2 | ||||
-rw-r--r-- | Source/cmConfigureLog.cxx | 238 | ||||
-rw-r--r-- | Source/cmConfigureLog.h | 61 | ||||
-rw-r--r-- | Source/cmake.cxx | 12 | ||||
-rw-r--r-- | Source/cmake.h | 8 | ||||
-rw-r--r-- | Tests/RunCMake/RunCMake.cmake | 18 |
9 files changed, 442 insertions, 5 deletions
diff --git a/Help/index.rst b/Help/index.rst index fdbf847..16c8f25 100644 --- a/Help/index.rst +++ b/Help/index.rst @@ -57,6 +57,7 @@ Reference Manuals /manual/cmake-buildsystem.7 /manual/cmake-commands.7 /manual/cmake-compile-features.7 + /manual/cmake-configure-log.7 /manual/cmake-developer.7 /manual/cmake-env-variables.7 /manual/cmake-file-api.7 diff --git a/Help/manual/cmake-configure-log.7.rst b/Help/manual/cmake-configure-log.7.rst new file mode 100644 index 0000000..3ae7dca --- /dev/null +++ b/Help/manual/cmake-configure-log.7.rst @@ -0,0 +1,102 @@ +.. cmake-manual-description: CMake Configure Log + +cmake-configure-log(7) +********************** + +.. versionadded:: 3.26 + +.. only:: html + + .. contents:: + +Introduction +============ + +CMake writes a running log, known as the configure log, +of certain events that occur during the "configure" step. +The log file is located at:: + + ${CMAKE_BINARY_DIR}/CMakeFiles/CMakeConfigureLog.yaml + +The configure log does *not* contain a log of all output, errors, +or messages printed while configuring a project. It is a log of +detailed information about specific events, such as toolchain inspection +by :command:`try_compile`, meant for use in debugging the configuration +of a build tree. + +Log Structure +============= + +The configure log is designed to be both machine- and human-readable. + +The log file is a YAML document stream containing zero or more YAML +documents separated by document markers. Each document begins +with a ``---`` document marker line, contains a single YAML mapping +that logs events from one CMake "configure" step, and, if the configure +step finished normally, ends with a ``...`` document marker line: + +.. code-block:: yaml + + --- + version: + major: 1 + minor: 0 + events: + - + kind: "..." + # (other fields omitted) + - + kind: "..." + # (other fields omitted) + ... + +A new document is appended to the log every time CMake configures +the build tree and logs new events. + +The keys of the each document root mapping are: + +``version`` + A YAML mapping that describes the schema version of the log document. + It has keys ``major`` and ``minor`` holding non-negative integer values. + +``events`` + A YAML block sequence of nodes corresponding to events logged during + one CMake "configure" step. Each event is a YAML node containing one + of the `Event Kinds`_ documented below. + +Text Block Encoding +------------------- + +In order to make the log human-readable, text blocks are always +represented using YAML literal block scalars (``|``). +Since literal block scalars do not support escaping, backslashes +and non-printable characters are encoded at the application layer: + +* ``\\`` encodes a backslash. +* ``\xXX`` encodes a byte using two hexadecimal digits, ``XX``. + +.. _`configure-log event kinds`: + +Event Kinds +=========== + +Every event kind is represented by a YAML mapping of the form: + +.. code-block:: yaml + + kind: "<kind>" + backtrace: + - "<file>:<line> (<function>)" + #...event-specific keys... + +The keys common to all events are: + +``kind`` + A string identifying the event kind. + +``backtrace`` + A YAML block sequence reporting the call stack of CMake source + locations at which the event occurred. Each node is a string + specifying one location formatted as ``<file>:<line> (<function>)``. + +Additional mapping keys are specific to each event kind. diff --git a/Help/release/dev/configure-log.rst b/Help/release/dev/configure-log.rst new file mode 100644 index 0000000..8518b21 --- /dev/null +++ b/Help/release/dev/configure-log.rst @@ -0,0 +1,5 @@ +Configure Log +------------- + +* CMake now writes a YAML log of configure-time checks. + See the :manual:`cmake-configure-log(7)` manual. diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 41a901a..db928fc 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -160,6 +160,8 @@ add_library( cmComputeLinkInformation.h cmComputeTargetDepends.h cmComputeTargetDepends.cxx + cmConfigureLog.h + cmConfigureLog.cxx cmConsoleBuf.h cmConsoleBuf.cxx cmConstStack.h diff --git a/Source/cmConfigureLog.cxx b/Source/cmConfigureLog.cxx new file mode 100644 index 0000000..7489edc --- /dev/null +++ b/Source/cmConfigureLog.cxx @@ -0,0 +1,238 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmConfigureLog.h" + +#include <cassert> +#include <cstdio> +#include <iterator> +#include <sstream> +#include <utility> + +#include <cmext/string_view> + +#include <cm3p/json/writer.h> + +#include "cm_utf8.h" + +#include "cmListFileCache.h" +#include "cmMakefile.h" +#include "cmStringAlgorithms.h" +#include "cmSystemTools.h" +#include "cmake.h" + +cmConfigureLog::cmConfigureLog(std::string logDir) + : LogDir(std::move(logDir)) +{ + Json::StreamWriterBuilder builder; + this->Encoder.reset(builder.newStreamWriter()); +} + +cmConfigureLog::~cmConfigureLog() +{ + if (this->Opened) { + this->EndObject(); + this->Stream << "...\n"; + } +} + +void cmConfigureLog::WriteBacktrace(cmMakefile const& mf) +{ + std::vector<std::string> backtrace; + auto root = mf.GetCMakeInstance()->GetHomeDirectory(); + for (auto bt = mf.GetBacktrace(); !bt.Empty(); bt = bt.Pop()) { + auto t = bt.Top(); + if (!t.Name.empty() || t.Line == cmListFileContext::DeferPlaceholderLine) { + t.FilePath = cmSystemTools::RelativeIfUnder(root, t.FilePath); + std::ostringstream s; + s << t; + backtrace.emplace_back(s.str()); + } + } + this->WriteValue("backtrace"_s, backtrace); +} + +void cmConfigureLog::EnsureInit() +{ + if (this->Opened) { + return; + } + assert(!this->Stream.is_open()); + + std::string name = cmStrCat(this->LogDir, "/CMakeConfigureLog.yaml"); + this->Stream.open(name.c_str(), std::ios::out | std::ios::app); + + this->Opened = true; + + this->Stream << "\n---\n"; + this->BeginObject("version"_s); + this->WriteValue("major"_s, 1); + this->WriteValue("minor"_s, 0); + this->EndObject(); + this->BeginObject("events"_s); +} + +cmsys::ofstream& cmConfigureLog::BeginLine() +{ + for (unsigned i = 0; i < this->Indent; ++i) { + this->Stream << " "; + } + return this->Stream; +} + +void cmConfigureLog::EndLine() +{ + this->Stream << std::endl; +} + +void cmConfigureLog::BeginObject(cm::string_view key) +{ + this->BeginLine() << key << ':'; + this->EndLine(); + ++this->Indent; +} + +void cmConfigureLog::EndObject() +{ + assert(this->Indent); + --this->Indent; +} + +void cmConfigureLog::BeginEvent(std::string const& kind) +{ + this->EnsureInit(); + + this->BeginLine() << '-'; + this->EndLine(); + + ++this->Indent; + + this->WriteValue("kind"_s, kind); +} + +void cmConfigureLog::EndEvent() +{ + assert(this->Indent); + --this->Indent; +} + +void cmConfigureLog::WriteValue(cm::string_view key, std::nullptr_t) +{ + this->BeginLine() << key << ": null"; + this->EndLine(); +} + +void cmConfigureLog::WriteValue(cm::string_view key, bool value) +{ + this->BeginLine() << key << ": " << (value ? "true" : "false"); + this->EndLine(); +} + +void cmConfigureLog::WriteValue(cm::string_view key, int value) +{ + this->BeginLine() << key << ": " << value; + this->EndLine(); +} + +void cmConfigureLog::WriteValue(cm::string_view key, std::string const& value) +{ + this->BeginLine() << key << ": "; + this->Encoder->write(value, &this->Stream); + this->EndLine(); +} + +void cmConfigureLog::WriteValue(cm::string_view key, + std::vector<std::string> const& list) +{ + this->BeginObject(key); + for (auto const& value : list) { + this->BeginLine() << "- "; + this->Encoder->write(value, &this->Stream); + this->EndLine(); + } + this->EndObject(); +} + +void cmConfigureLog::WriteLiteralTextBlock(cm::string_view key, + cm::string_view text) +{ + this->BeginLine() << key << ": |"; + this->EndLine(); + + auto const l = text.length(); + if (l) { + ++this->Indent; + this->BeginLine(); + + auto i = decltype(l){ 0 }; + while (i < l) { + // YAML allows ' ', '\t' and "printable characters", but NOT other + // ASCII whitespace; those must be escaped, as must the upper UNICODE + // control characters (U+0080 - U+009F) + static constexpr unsigned int C1_LAST = 0x9F; + auto const c = static_cast<unsigned char>(text[i]); + switch (c) { + case '\r': + // Print a carriage return only if it is not followed by a line feed. + ++i; + if (i == l || text[i] != '\n') { + this->WriteEscape(c); + } + break; + case '\n': + // Print any line feeds except the very last one + if (i + 1 < l) { + this->EndLine(); + this->BeginLine(); + } + ++i; + break; + case '\t': + // Print horizontal tab verbatim + this->Stream.put('\t'); + ++i; + break; + case '\\': + // Escape backslash for disambiguation + this->Stream << "\\\\"; + ++i; + break; + default: + if (c >= 32 && c < 127) { + // Print ascii byte. + this->Stream.put(text[i]); + ++i; + break; + } else if (c > 127) { + // Decode a UTF-8 sequence. + unsigned int c32; + auto const* const s = text.data() + i; + auto const* const e = text.data() + l; + auto const* const n = cm_utf8_decode_character(s, e, &c32); + if (n > s && c32 > C1_LAST) { + auto const k = std::distance(s, n); + this->Stream.write(s, static_cast<std::streamsize>(k)); + i += static_cast<unsigned>(k); + break; + } + } + + // Escape non-printable byte. + this->WriteEscape(c); + ++i; + break; + } + } + + this->EndLine(); + --this->Indent; + } +} + +void cmConfigureLog::WriteEscape(unsigned char c) +{ + char buffer[6]; + int n = snprintf(buffer, sizeof(buffer), "\\x%02x", c); + if (n > 0) { + this->Stream.write(buffer, n); + } +} diff --git a/Source/cmConfigureLog.h b/Source/cmConfigureLog.h new file mode 100644 index 0000000..a71fd0c --- /dev/null +++ b/Source/cmConfigureLog.h @@ -0,0 +1,61 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include <memory> +#include <string> +#include <vector> + +#include <cm/string_view> + +#include "cmsys/FStream.hxx" + +namespace Json { +class StreamWriter; +} + +class cmMakefile; + +class cmConfigureLog +{ +public: + cmConfigureLog(std::string logDir); + ~cmConfigureLog(); + + void WriteBacktrace(cmMakefile const& mf); + + void EnsureInit(); + + void BeginEvent(std::string const& kind); + void EndEvent(); + + void BeginObject(cm::string_view key); + void EndObject(); + + // TODO other value types + void WriteValue(cm::string_view key, std::nullptr_t); + void WriteValue(cm::string_view key, bool value); + void WriteValue(cm::string_view key, int value); + void WriteValue(cm::string_view key, std::string const& value); + void WriteValue(cm::string_view key, std::vector<std::string> const& list); + + void WriteTextBlock(cm::string_view key, cm::string_view text); + void WriteLiteralTextBlock(cm::string_view key, cm::string_view text); + + void WriteLiteralTextBlock(cm::string_view key, std::string const& text) + { + this->WriteLiteralTextBlock(key, cm::string_view{ text }); + } + +private: + std::string LogDir; + cmsys::ofstream Stream; + unsigned Indent = 0; + bool Opened = false; + + std::unique_ptr<Json::StreamWriter> Encoder; + + cmsys::ofstream& BeginLine(); + void EndLine(); + void WriteEscape(unsigned char c); +}; diff --git a/Source/cmake.cxx b/Source/cmake.cxx index befcb55..17027bc 100644 --- a/Source/cmake.cxx +++ b/Source/cmake.cxx @@ -70,6 +70,7 @@ # include <cm3p/curl/curl.h> # include <cm3p/json/writer.h> +# include "cmConfigureLog.h" # include "cmFileAPI.h" # include "cmGraphVizWriter.h" # include "cmVariableWatch.h" @@ -2423,10 +2424,21 @@ int cmake::ActualConfigure() #if !defined(CMAKE_BOOTSTRAP) this->FileAPI = cm::make_unique<cmFileAPI>(this); this->FileAPI->ReadQueries(); + + if (!this->GetIsInTryCompile()) { + this->TruncateOutputLog("CMakeConfigureLog.yaml"); + this->ConfigureLog = cm::make_unique<cmConfigureLog>( + cmStrCat(this->GetHomeOutputDirectory(), "/CMakeFiles"_s)); + } #endif // actually do the configure this->GlobalGenerator->Configure(); + +#if !defined(CMAKE_BOOTSTRAP) + this->ConfigureLog.reset(); +#endif + // Before saving the cache // if the project did not define one of the entries below, add them now // so users can edit the values in the cache: diff --git a/Source/cmake.h b/Source/cmake.h index 325c0c5..10db87d 100644 --- a/Source/cmake.h +++ b/Source/cmake.h @@ -37,6 +37,7 @@ # include "cmMakefileProfilingData.h" #endif +class cmConfigureLog; class cmExternalMakefileProjectGeneratorFactory; class cmFileAPI; class cmFileTimeCache; @@ -521,6 +522,10 @@ public: void SetTraceFile(std::string const& file); void PrintTraceFormatVersion(); +#ifndef CMAKE_BOOTSTRAP + cmConfigureLog* GetConfigureLog() const { return this->ConfigureLog.get(); } +#endif + //! Use trace from another ::cmake instance. void SetTraceRedirect(cmake* other); @@ -714,6 +719,9 @@ private: TraceFormat TraceFormatVar = TRACE_HUMAN; cmGeneratedFileStream TraceFile; cmake* TraceRedirect = nullptr; +#ifndef CMAKE_BOOTSTRAP + std::unique_ptr<cmConfigureLog> ConfigureLog; +#endif bool WarnUninitialized = false; bool WarnUnusedCli = true; bool CheckSystemVars = false; diff --git a/Tests/RunCMake/RunCMake.cmake b/Tests/RunCMake/RunCMake.cmake index 670beb9..54d7eb5 100644 --- a/Tests/RunCMake/RunCMake.cmake +++ b/Tests/RunCMake/RunCMake.cmake @@ -33,7 +33,7 @@ function(run_cmake test) set(platform_name msys) endif() - foreach(o IN ITEMS stdout stderr) + foreach(o IN ITEMS stdout stderr config) if(RunCMake-${o}-file AND EXISTS ${top_src}/${RunCMake-${o}-file}) file(READ ${top_src}/${RunCMake-${o}-file} expect_${o}) string(REGEX REPLACE "\n+$" "" expect_${o} "${expect_${o}}") @@ -144,6 +144,12 @@ function(run_cmake test) if(NOT "${actual_result}" MATCHES "${expect_result}") string(APPEND msg "Result is [${actual_result}], not [${expect_result}].\n") endif() + set(config_file "${RunCMake_TEST_COMMAND_WORKING_DIRECTORY}/CMakeFiles/CMakeConfigureLog.yaml") + if(EXISTS "${config_file}") + file(READ "${config_file}" actual_config) + else() + set(actual_config "") + endif() # Special case: remove ninja no-op line from stderr, but not stdout. # Test cases that look for it should use RunCMake_TEST_OUTPUT_MERGE. @@ -180,7 +186,7 @@ function(run_cmake test) "|[^\n]*Bullseye Testing Technology" ")[^\n]*\n)+" ) - foreach(o IN ITEMS stdout stderr) + foreach(o IN ITEMS stdout stderr config) string(REGEX REPLACE "\r\n" "\n" actual_${o} "${actual_${o}}") string(REGEX REPLACE "${ignore_line_regex}" "\\1" actual_${o} "${actual_${o}}") string(REGEX REPLACE "\n+$" "" actual_${o} "${actual_${o}}") @@ -211,13 +217,15 @@ function(run_cmake test) string(APPEND msg "Command was:\n command> ${command}\n") endif() if(msg) - foreach(o IN ITEMS stdout stderr) + foreach(o IN ITEMS stdout stderr config) if(DEFINED expect_${o}) string(REGEX REPLACE "\n" "\n expect-${o}> " expect_${o} " expect-${o}> ${expect_${o}}") string(APPEND msg "Expected ${o} to match:\n${expect_${o}}\n") endif() - string(REGEX REPLACE "\n" "\n actual-${o}> " actual_${o} " actual-${o}> ${actual_${o}}") - string(APPEND msg "Actual ${o}:\n${actual_${o}}\n") + if(NOT o STREQUAL "config" OR DEFINED expect_${o}) + string(REGEX REPLACE "\n" "\n actual-${o}> " actual_${o} " actual-${o}> ${actual_${o}}") + string(APPEND msg "Actual ${o}:\n${actual_${o}}\n") + endif() endforeach() message(SEND_ERROR "${test}${RunCMake_TEST_VARIANT_DESCRIPTION} - FAILED:\n${msg}") else() |