Diagnostics: Add cmake_diagnostic command

Add command to manipulate the new diagnostic stack/state introduced in
the previous commit. Start adding documentation for the system as a
whole.
This commit is contained in:
Matthew Woehlke
2026-03-19 11:29:44 -04:00
parent e74fbb3d44
commit a0612aeb5d
29 changed files with 602 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
cmake_diagnostic
----------------
.. versionadded:: 4.4
Manage CMake Diagnostic settings. See the :manual:`cmake-diagnostics(7)`
manual for a list of available categories.
Synopsis
^^^^^^^^
.. parsed-literal::
`Setting Diagnostics`_
cmake_diagnostic(`SET`_ <category> <action> [RECURSE])
cmake_diagnostic(`PROMOTE`_ <category> <action> [NO_RECURSE])
cmake_diagnostic(`DEMOTE`_ <category> <action> [NO_RECURSE])
`Checking Diagnostic Actions`_
cmake_diagnostic(`GET`_ <diagnostic> <out-var>)
`CMake Diagnostic Stack`_
cmake_diagnostic(`PUSH`_)
cmake_diagnostic(`POP`_)
Setting Diagnostics
^^^^^^^^^^^^^^^^^^^
.. signature::
cmake_diagnostic(SET CMD_<CATEGORY> <action> [RECURSE])
cmake_diagnostic(PROMOTE CMD_<CATEGORY> <action> [NO_RECURSE])
cmake_diagnostic(DEMOTE CMD_<CATEGORY> <action> [NO_RECURSE])
:target:
SET
PROMOTE
DEMOTE
Set or alter the action taken when a diagnostic belonging to a particular
category is triggered.
The ``SET`` subcommand sets the action for the specified diagnostic category.
The ``PROMOTE`` subcommand increases the severity for the specified diagnostic
category, or does nothing if the action was already set to an equal or higher
severity. The ``DEMOTE`` subcommand decreases the severity for the specified
diagnostic category, or does nothing if the action was already set to an equal
or lower severity.
The possible ``<action>``\ s (in order of severity) are:
``IGNORE``
Do nothing.
``WARN``
Report a warning and continue processing.
``SEND_ERROR``
Report an error, continue processing, but skip generation.
The :manual:`cmake(1)` executable will return a non-zero
:ref:`exit code <CMake Exit Code>`.
``FATAL_ERROR``
Report an error, stop processing and generation.
The :manual:`cmake(1)` executable will return a non-zero
:ref:`exit code <CMake Exit Code>`.
Some diagnostic categories are hierarchical. The ``RECURSE`` and
``NO_RECURSE`` options determine whether changing the action for a diagnostic
category also modifies any child categories. By default, the ``PROMOTE`` and
``DEMOTE`` subcommands are recursive, while the ``SET`` subcommand is not.
Note that the alteration for child categories is independent of the prior
action set on any parents; that is, ``PROMOTE`` and ``DEMOTE``, when operating
recursively, will operate on all child categories even if a parent category's
action was not altered.
Checking Diagnostic Actions
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. signature:: cmake_diagnostic(GET CMD_<CATEGORY> <variable>)
:target: GET
Check what action is currently specified for a diagnostic category.
The output ``<variable>`` value will be one of ``IGNORE``, ``WARN``,
``SEND_ERROR`` or ``FATAL_ERROR``.
CMake Diagnostic Stack
^^^^^^^^^^^^^^^^^^^^^^
CMake keeps diagnostic settings on a stack, so changes made by the
``cmake_diagnostic`` command affect only the top of the stack. A new entry on
the diagnostic stack is managed automatically for each subdirectory to protect
its parents and siblings. CMake also manages a new entry for scripts loaded by
:command:`include` and :command:`find_package` commands except when invoked
with the ``NO_DIAGNOSTIC_SCOPE`` option. The ``cmake_diagnostic`` command
provides an interface to manage custom entries on the diagnostic stack:
.. signature:: cmake_diagnostic(PUSH)
Create a new entry on the diagnostic stack.
.. signature:: cmake_diagnostic(POP)
Remove the last diagnostic stack entry created with
``cmake_diagnostic(PUSH)``.
Each ``PUSH`` must have a matching ``POP`` to erase any changes.
This is useful to make temporary changes to diagnostic settings.
Calls to the :command:`cmake_diagnostic(SET)`,
:command:`cmake_diagnostic(PROMOTE)`, or :command:`cmake_diagnostic(DEMOTE)`
commands influence only the current top of the diagnostic stack.
The :command:`block(SCOPE_FOR DIAGNOSTICS)` command offers a more flexible
and more secure way to manage the diagnostic stack. The pop action is done
automatically when leaving the block scope, so there is no need to
precede each :command:`return` with a call to :command:`cmake_diagnostic(POP)`.
.. code-block:: cmake
# stack management with cmake_diagnostic()
function(my_func)
cmake_diagnostic(PUSH)
cmake_diagnostic(SET ...)
if (<cond1>)
...
cmake_diagnostic(POP)
return()
elseif(<cond2>)
...
cmake_diagnostic(POP)
return()
endif()
...
cmake_diagnostic(POP)
endfunction()
# stack management with block()/endblock()
function(my_func)
block(SCOPE_FOR DIAGNOSTICS)
cmake_diagnostic(SET ...)
if (<cond1>)
...
return()
elseif(<cond2>)
...
return()
endif()
...
endblock()
endfunction()
Commands created by the :command:`function` and :command:`macro` commands
record diagnostic settings when they are created and use the pre-record
diagnostics when they are invoked. If the function or macro implementation
sets diagnostics, the changes automatically propagate up through callers until
they reach the closest nested diagnostic stack entry.
+1
View File
@@ -60,6 +60,7 @@ Reference Manuals
/manual/cmake-configure-log.7
/manual/cmake-cxxmodules.7
/manual/cmake-developer.7
/manual/cmake-diagnostics.7
/manual/cmake-env-variables.7
/manual/cmake-file-api.7
/manual/cmake-generator-expressions.7
+1
View File
@@ -17,6 +17,7 @@ These commands are always available.
/command/block
/command/break
/command/cmake_diagnostic
/command/cmake_host_system_information
/command/cmake_language
/command/cmake_minimum_required
+71
View File
@@ -0,0 +1,71 @@
.. cmake-manual-description: CMake Diagnostics Reference
cmake-diagnostics(7)
********************
.. only:: html
.. contents::
.. _cmake-diagnostics-intro:
Introduction
============
CMake Diagnostics are the mechanism by which CMake categorizes and presents
certain advisory information about a project's configuration and the generation
of its build system. These diagnostics can be seen as the build system
equivalent of compiler warnings. Diagnostics provide feedback on potential
issues in several categories:
* Issues that may impact the success of the build.
* Issues that may impact the correctness of the build.
* Issues that may impact the correctness of the project packaging.
* Issues that may impact the ability of the project
to be built with newer versions of dependencies.
* Issues that may impact the ability of the project
to be built with newer versions of CMake.
Diagnostic Actions
------------------
The action taken when a particular diagnostic is triggered depends on the
diagnostic category. Most categories will warn by default. The
:command:`cmake_diagnostic` command and ``-W`` options can be
used to control what action occurs when a diagnostic of a particular category
is triggered. The possible actions are described in the documentation of the
same.
Diagnostic Categories
=====================
The following categories are defined.
``CMD_AUTHOR`` (``-Wauthor``)
-----------------------------
:Default: Warn
Warn about a build system's incorrect use of CMake, or of a CMake interface
provided by a dependency. This is the category triggered by
:command:`message(AUTHOR_WARNING)`. It is also the ancestor of many other
diagnostic categories.
The most important aspect of this category is that it represents issues with
a project's build system which typically require alteration to the same. This
is to say that users simply trying to build a project obtained elsewhere will
typically not be interested in these warnings, except to perhaps report them
to the project's developer(s).
``CMD_DEPRECATED`` (``-Wdeprecated``)
-------------------------------------
:Default: Warn
:Parent: ``CMD_AUTHOR``
Warn about use of a deprecated function or package. This is the category
triggered by :command:`message(DEPRECATION)`.
+2
View File
@@ -582,6 +582,8 @@ add_library(
cmBreakCommand.h
cmBuildCommand.cxx
cmBuildCommand.h
cmCMakeDiagnosticCommand.cxx
cmCMakeDiagnosticCommand.h
cmCMakeHostSystemInformationCommand.cxx
cmCMakeHostSystemInformationCommand.h
cmCMakeLanguageCommand.cxx
+130
View File
@@ -0,0 +1,130 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmCMakeDiagnosticCommand.h"
#include <cm/optional>
#include <cmext/string_view>
#include "cmDiagnostics.h"
#include "cmExecutionStatus.h"
#include "cmMakefile.h"
#include "cmStringAlgorithms.h"
namespace {
using AlterFunction = bool (cmMakefile::*)(cmDiagnostics::DiagnosticCategory,
cmDiagnostics::DiagnosticAction,
bool);
bool HandleAlterMode(std::vector<std::string> const& args,
cmExecutionStatus& status, AlterFunction function,
bool recursive)
{
if (args.size() < 3 || args.size() > 4) {
status.SetError(cmStrCat(
args[0], " must be given exactly 2 or 3 additional arguments."_s));
return false;
}
if (args.size() == 4) {
if (args[3] == "RECURSE") {
recursive = true;
} else if (args[3] == "NO_RECURSE") {
recursive = false;
} else {
status.SetError(
cmStrCat(args[0], " given unknown option \""_s, args[3], "\"."_s));
return false;
}
}
cm::optional<cmDiagnostics::DiagnosticAction> const action =
cmDiagnostics::GetDiagnosticAction(args[2]);
if (!action) {
status.SetError(cmStrCat(
args[0], " given unrecognized diagnostic action \"", args[2], "\"."_s));
return false;
}
cm::optional<cmDiagnostics::DiagnosticCategory> const category =
cmDiagnostics::GetDiagnosticCategory(args[1]);
if (!category) {
status.SetError(cmStrCat(args[0],
" given unrecognized diagnostic category \"",
args[1], "\"."_s));
return false;
}
if (!(status.GetMakefile().*function)(*category, *action, recursive)) {
status.SetError(cmStrCat(args[0], " failed to set diagnostic action."_s));
return false;
}
return true;
}
bool HandleGetMode(std::vector<std::string> const& args,
cmExecutionStatus& status)
{
if (args.size() != 3) {
status.SetError("GET must be given exactly 2 additional arguments.");
return false;
}
cm::optional<cmDiagnostics::DiagnosticCategory> const category =
cmDiagnostics::GetDiagnosticCategory(args[1]);
if (!category) {
status.SetError(cmStrCat(args[0],
" given unrecognized diagnostic category \"",
args[1], "\"."_s));
return false;
}
cmDiagnostics::DiagnosticAction const action =
status.GetMakefile().GetDiagnosticAction(*category);
status.GetMakefile().AddDefinition(args[2],
cmDiagnostics::GetActionString(action));
return true;
}
}
// cmCMakeDiagnosticCommand
bool cmCMakeDiagnosticCommand(std::vector<std::string> const& args,
cmExecutionStatus& status)
{
if (args.empty()) {
status.SetError("requires at least one argument.");
return false;
}
if (args[0] == "SET") {
return HandleAlterMode(args, status, &cmMakefile::SetDiagnostic, false);
}
if (args[0] == "PROMOTE") {
return HandleAlterMode(args, status, &cmMakefile::PromoteDiagnostic, true);
}
if (args[0] == "DEMOTE") {
return HandleAlterMode(args, status, &cmMakefile::DemoteDiagnostic, true);
}
if (args[0] == "GET") {
return HandleGetMode(args, status);
}
if (args[0] == "PUSH") {
if (args.size() > 1) {
status.SetError("PUSH may not be given additional arguments.");
return false;
}
status.GetMakefile().PushDiagnostic();
return true;
}
if (args[0] == "POP") {
if (args.size() > 1) {
status.SetError("POP may not be given additional arguments.");
return false;
}
status.GetMakefile().PopDiagnostic();
return true;
}
status.SetError(cmStrCat("given unknown first argument \"", args[0], '"'));
return false;
}
+18
View File
@@ -0,0 +1,18 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#pragma once
#include "cmConfigure.h" // IWYU pragma: keep
#include <string>
#include <vector>
class cmExecutionStatus;
/**
* \brief Set how CMake should handle diagnostics
*
* cmCMakeDiagnosticCommand sets how CMake should deal with diagnostics.
*/
bool cmCMakeDiagnosticCommand(std::vector<std::string> const& args,
cmExecutionStatus& status);
+2
View File
@@ -17,6 +17,7 @@
#include "cmBlockCommand.h"
#include "cmBreakCommand.h"
#include "cmBuildCommand.h"
#include "cmCMakeDiagnosticCommand.h"
#include "cmCMakeLanguageCommand.h"
#include "cmCMakeMinimumRequired.h"
#include "cmCMakePathCommand.h"
@@ -124,6 +125,7 @@ void GetScriptingCommands(cmState* state)
state->AddFlowControlCommand("while", cmWhileCommand);
state->AddFlowControlCommand("block", cmBlockCommand);
state->AddBuiltinCommand("cmake_diagnostic", cmCMakeDiagnosticCommand);
state->AddBuiltinCommand("cmake_language", cmCMakeLanguageCommand);
state->AddBuiltinCommand("cmake_minimum_required", cmCMakeMinimumRequired);
state->AddBuiltinCommand("cmake_path", cmCMakePathCommand);
+51
View File
@@ -4,6 +4,7 @@
#include <cassert>
#include <map>
#include <string>
#include <utility>
#include <cmext/string_view>
@@ -41,6 +42,56 @@ constexpr cmDiagnostics::DiagnosticCategoryInformation
cmDiagnostics::CategoryInfo[cmDiagnostics::CategoryCount];
#endif
cm::string_view cmDiagnostics::GetActionString(DiagnosticAction action)
{
switch (action) {
case Ignore:
return "IGNORE"_s;
case Warn:
return "WARN"_s;
case SendError:
return "SEND_ERROR"_s;
case FatalError:
return "FATAL_ERROR"_s;
default:
return {};
}
}
cm::string_view cmDiagnostics::GetCategoryString(DiagnosticCategory category)
{
static cm::string_view const names[CategoryCount] = {
{}, // CMD_NONE
#define CATEGORY_NAME(C) #C ""_s,
CM_FOR_EACH_DIAGNOSTIC_CATEGORY(CATEGORY_NAME)
#undef CATEGORY_MAP
};
if (category < CategoryCount) {
return names[category];
}
return {};
}
cm::optional<cmDiagnostics::DiagnosticAction>
cmDiagnostics::GetDiagnosticAction(cm::string_view name)
{
if (name == "IGNORE"_s) {
return DiagnosticAction::Ignore;
}
if (name == "WARN"_s) {
return DiagnosticAction::Warn;
}
if (name == "SEND_ERROR"_s) {
return DiagnosticAction::SendError;
}
if (name == "FATAL_ERROR"_s) {
return DiagnosticAction::FatalError;
}
return cm::nullopt;
}
cm::optional<cmDiagnostics::DiagnosticCategory>
cmDiagnostics::GetDiagnosticCategory(cm::string_view name)
{
+10
View File
@@ -77,6 +77,16 @@ public:
#undef DIAGNOSTIC_CATEGORY_INFO
};
//! convert an action identifier into a string
static cm::string_view GetActionString(DiagnosticAction);
//! convert a category identifier into a string
static cm::string_view GetCategoryString(DiagnosticCategory);
//! Convert a string action into an identifier
static cm::optional<DiagnosticAction> GetDiagnosticAction(
cm::string_view name);
//! Convert a string category into an identifier
static cm::optional<DiagnosticCategory> GetDiagnosticCategory(
cm::string_view name);
+2
View File
@@ -643,6 +643,8 @@ endif()
add_RunCMake_test(CompileFeatures -DCMake_NO_C_STANDARD=${CMake_NO_C_STANDARD} -DCMake_NO_CXX_STANDARD=${CMake_NO_CXX_STANDARD})
add_RunCMake_test(Policy)
add_RunCMake_test(PolicyScope)
add_RunCMake_test(Diagnostics)
add_RunCMake_test(DiagnosticScope)
add_RunCMake_test(WriteBasicConfigVersionFile)
add_RunCMake_test(WriteCompilerDetectionHeader)
add_RunCMake_test(SourceProperties)
@@ -0,0 +1,22 @@
cmake_diagnostic(SET CMD_AUTHOR IGNORE)
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "IGNORE")
message(SEND_ERROR "failed to set diagnostic state")
endif()
block(SCOPE_FOR DIAGNOSTICS)
cmake_diagnostic(SET CMD_AUTHOR SEND_ERROR)
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "SEND_ERROR")
message(SEND_ERROR "failed to set diagnostic state")
endif()
endblock()
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "IGNORE")
message(SEND_ERROR "failed to restore diagnostic state")
endif()
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 4.3)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake NO_DIAGNOSTIC_SCOPE)
@@ -0,0 +1,6 @@
cmake_diagnostic(SET CMD_AUTHOR SEND_ERROR)
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "SEND_ERROR")
message(SEND_ERROR "failed to set diagnostic state")
endif()
@@ -0,0 +1,7 @@
cmake_diagnostic(SET CMD_AUTHOR WARN)
include(DiagnosticInclude.cmake)
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "WARN")
message(SEND_ERROR "include unexpectedly leaked diagnostic state")
endif()
@@ -0,0 +1,7 @@
cmake_diagnostic(SET CMD_AUTHOR WARN)
include(DiagnosticInclude.cmake NO_DIAGNOSTIC_SCOPE)
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "SEND_ERROR")
message(SEND_ERROR "include unexpectedly encapsulated diagnostic state")
endif()
@@ -0,0 +1 @@
1
@@ -0,0 +1,4 @@
^CMake Error in NotClosed\.cmake:
cmake_diagnostic PUSH without matching POP
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)$
@@ -0,0 +1 @@
cmake_diagnostic(PUSH)
@@ -0,0 +1 @@
1
@@ -0,0 +1,4 @@
^CMake Error at NotOpened\.cmake:[0-9]+ \(cmake_diagnostic\):
cmake_diagnostic POP without matching PUSH
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)$
@@ -0,0 +1 @@
cmake_diagnostic(POP)
@@ -0,0 +1,22 @@
cmake_diagnostic(SET CMD_AUTHOR IGNORE)
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "IGNORE")
message(SEND_ERROR "failed to set diagnostic state")
endif()
cmake_diagnostic(PUSH)
cmake_diagnostic(SET CMD_AUTHOR SEND_ERROR)
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "SEND_ERROR")
message(SEND_ERROR "failed to set diagnostic state")
endif()
cmake_diagnostic(POP)
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "IGNORE")
message(SEND_ERROR "failed to restore diagnostic state")
endif()
@@ -0,0 +1,8 @@
include(RunCMake)
run_cmake(PushPop)
run_cmake(Block)
run_cmake(IncludeWithScope)
run_cmake(IncludeWithoutScope)
run_cmake(NotClosed)
run_cmake(NotOpened)
+62
View File
@@ -0,0 +1,62 @@
function(run_test ACTION INIT TARGET EXPECTED)
cmake_diagnostic(SET CMD_AUTHOR ${INIT})
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "${INIT}")
message(SEND_ERROR "failed to set diagnostic state")
endif()
cmake_diagnostic(${ACTION} CMD_AUTHOR ${TARGET})
cmake_diagnostic(GET CMD_AUTHOR action)
if(NOT "${action}" STREQUAL "${EXPECTED}")
message(SEND_ERROR
"failed to change diagnostic state"
" (expected '${EXPECTED}', actual '${action}')"
)
endif()
endfunction()
run_test(SET IGNORE WARN WARN)
run_test(SET IGNORE SEND_ERROR SEND_ERROR)
run_test(SET IGNORE FATAL_ERROR FATAL_ERROR)
run_test(PROMOTE IGNORE IGNORE IGNORE)
run_test(PROMOTE IGNORE WARN WARN)
run_test(PROMOTE IGNORE SEND_ERROR SEND_ERROR)
run_test(PROMOTE IGNORE FATAL_ERROR FATAL_ERROR)
run_test(PROMOTE WARN IGNORE WARN)
run_test(PROMOTE WARN WARN WARN)
run_test(PROMOTE WARN SEND_ERROR SEND_ERROR)
run_test(PROMOTE WARN FATAL_ERROR FATAL_ERROR)
run_test(PROMOTE SEND_ERROR IGNORE SEND_ERROR)
run_test(PROMOTE SEND_ERROR WARN SEND_ERROR)
run_test(PROMOTE SEND_ERROR SEND_ERROR SEND_ERROR)
run_test(PROMOTE SEND_ERROR FATAL_ERROR FATAL_ERROR)
run_test(PROMOTE FATAL_ERROR IGNORE FATAL_ERROR)
run_test(PROMOTE FATAL_ERROR WARN FATAL_ERROR)
run_test(PROMOTE FATAL_ERROR SEND_ERROR FATAL_ERROR)
run_test(PROMOTE FATAL_ERROR FATAL_ERROR FATAL_ERROR)
run_test(DEMOTE IGNORE IGNORE IGNORE)
run_test(DEMOTE IGNORE WARN IGNORE)
run_test(DEMOTE IGNORE SEND_ERROR IGNORE)
run_test(DEMOTE IGNORE FATAL_ERROR IGNORE)
run_test(DEMOTE WARN IGNORE IGNORE)
run_test(DEMOTE WARN WARN WARN)
run_test(DEMOTE WARN SEND_ERROR WARN)
run_test(DEMOTE WARN FATAL_ERROR WARN)
run_test(DEMOTE SEND_ERROR IGNORE IGNORE)
run_test(DEMOTE SEND_ERROR WARN WARN)
run_test(DEMOTE SEND_ERROR SEND_ERROR SEND_ERROR)
run_test(DEMOTE SEND_ERROR FATAL_ERROR SEND_ERROR)
run_test(DEMOTE FATAL_ERROR IGNORE IGNORE)
run_test(DEMOTE FATAL_ERROR WARN WARN)
run_test(DEMOTE FATAL_ERROR SEND_ERROR SEND_ERROR)
run_test(DEMOTE FATAL_ERROR FATAL_ERROR FATAL_ERROR)
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 4.3)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake NO_DIAGNOSTIC_SCOPE)
@@ -0,0 +1,3 @@
include(RunCMake)
run_cmake(Actions)
+2
View File
@@ -461,6 +461,8 @@ class CMakeSignatureObject(CMakeObject):
sigargs = self.targetnames[sig]
else:
def extract_keywords(params):
if params[-1].endswith(')'):
params[-1] = params[-1][:-1]
for p in params:
if p[0].isalpha():
yield p
+1
View File
@@ -311,6 +311,7 @@ CMAKE_CXX_SOURCES="\
cmCMakeLanguageCommand \
cmCMakeMinimumRequired \
cmList \
cmCMakeDiagnosticCommand \
cmCMakePath \
cmCMakePathCommand \
cmCMakePolicyCommand \