What’s in a Warning?
For a while, users have been asking for CMake to do a better job of warning about undesirable uses, such as using functions that should not be used in “modern” CMake. Starting with CMake 4.4, we are finally making inroads on those requests, but the journey there went through some interesting and unexpected territory.
This is a bit of a peek “under the hood” of CMake development, which is a bit of a deviation for a normally user-focused blog, but we wanted to share because it has the potential to impact users, and we wanted to explain why we felt the changes, and the potential pain associated with them, are warranted.
Diagnostics in CMake Are (Were) a Mess
Almost anyone who has built software is familiar with the idea of compiler warnings. The general idea of diagnostics isn’t unknown to build systems, either, and CMake has long had the ability to issue warnings, as well as various user interfaces for manipulating those warnings.
Unfortunately, due to their organic origins, these mechanisms were not very coherent. In a well designed system, ripping out everything related to an individual warning up to (but not including) the underlying logging infrastructure would involve ripping out the warning systems, which would have catastrophic consequences for every other warning. Prior to 4.4, however, CMake didn’t have systems, and performing this exercise for -Wdeprecated would affect -Wdev only minimally, and --warn-uninitialized not only shared exactly zero code with other diagnostics, it didn’t even follow the same naming convention! Rather than having systems to track and manipulate diagnostics, each diagnostic “type” (or category) existed as its own stand-alone implementation.
This resulted in various idiosyncrasies. For example, -Wdeprecated could be manipulated through documented CMake variables. When enabled or disabled via command-line options, that state would persist across subsequent invocations of cmake, and in principle alterations could be confined to the current variable scope. In reality, however, some deprecation messages respected this control via non-cache CMake variables, while others did not. Meanwhile, --warn-uninitialized was strictly global, completely non-persistent, and could not be promoted to an error. -Wdev was somewhere in between.
As bad as the existing user experience was, the outlook for adding new diagnostics was worse. With no ‘central repository’, every diagnostic category relied on its own set of C++ member variables, JSON keys, internal keys and CMake variables to track states at various points during execution, each of which depended on bespoke code for each diagnostic category to shepherd it through each of the many different mechanisms of state tracking. In short, adding a new diagnostic category required dozens of changes to several source files just to allow the user to interact with it. This was all in addition to any code that actually issued the diagnostic.
Something Needed to Be Done
This isn’t just an academic issue or an exercise in making the code cleaner for cleanliness’s sake. CMake recently added SARIF support. This feature is still in the early stages, but one of its goals is to improve CMake’s ability to communicate diagnostics in a more machine-readable manner. CMake’s historic lack of granularity for categorizing diagnostics is one of the implementation challenges for SARIF output. The ability to easily add new categories directly impacts the ability of CMake to produce high-quality SARIF output.
Meanwhile, while we were working on this system internally, long time contributor Robert Maynard submitted a merge request to add diagnostics when an install command is given an absolute path as a DESTINATION. This is exactly the sort of “do a better job of warning about undesirable uses” we want to see implemented. His contribution is much welcomed, but it was also serendipitously timely. The original form introduced new CMake variables (and only CMake variables) to control the diagnostics, whose spelling was not consistent with either of the existing sets of variables, thus serving as a prime example of the problems resulting from a lack of systems and planning. Fortunately, the merge request was submitted shortly after we’d “broken ground” on 4.4’s overhaul of CMake’s diagnostics; instead of perpetuating the old problems, it became a pioneer for the new system.
A New, Unified Vision
One of the questions that arose in the context of adding new diagnostics was how they would interact with existing diagnostics. This led to the decision to refactor the existing systems into something more unified, flexible, and extensible.
CMake’s new diagnostic system closely models policies. A diagnostic state is part of the core CMake state, which means the diagnostic stack can be manipulated in the same manner as the policy stack:
# Temporarily disable a warning
cmake_diagnostic(PUSH)
cmake_diagnostic(SET CMD_DEPRECATED IGNORE)
deprecated_function(…) # Deprecation warning is suppressed
cmake_diagnostic(POP)
This state is also the primary ‘source of truth’, with presets, command-line options and cache variables all interacting with this state rather than via a tangled and sometimes inconsistent web of intermediate representations. While presets do not have direct access, diagnostic state information is now conveyed via maps rather than individual variables. Everywhere else, however, diagnostic state is stored in a single, extensible data structure. Logic to interact with this data structure is based on loops and look-ups and mostly avoids the need to write code for specific diagnostics. (The exceptions are mainly for the sake of backwards compatibility.)
Critically, this means that adding a new diagnostic, ignoring documentation, can be accomplished by adding a single line of code to a single file. By the addition of this line, CMake knows how to handle the relevant -W command-line options, how to persist the state via CMake’s cache, how to set the state via presets, and how the user can manipulate the diagnostic via a new cmake_diagnostic command (which closely mirrors the existing cmake_policy command as well as compiler #pragma diagnostic instructions).
This has already enabled a handful of new diagnostics to be added and ensures that future versions of CMake can add new diagnostic categories with similar ease. More importantly, it means that users can interact with all diagnostics, including those that previously existed, in a unified and consistent manner.
Backwards Compatibility
This brave new world does, unfortunately, come with some caveats for the sake of backward compatibility.
The most notable of these concerns the CMAKE_{WARN,ERROR}_DEPRECATED variables and the CMD_DEPRECATED diagnostic. When CMake policy CMP0218 is not set to NEW (which is the default unless a cmake_minimum_required call has set the policy version to 4.4 or later), CMake will, for compatibility, ignore the diagnostic state for deprecation diagnostics that considered the non-cache state of said variables.
It was also decided that the catch-all ‘for project authors’ category should be CMD_AUTHOR rather than CMD_DEV. Accordingly, -Wdev, has been renamed -Wauthor. In most instances, this will not be noticeable (aside from a warning), since -W[no-][error=]dev is retained as a synonym for the respective flavors of -Wauthor; however, presets wishing to use preset version 12 (also introduced in CMake 4.4 and required to access newly added diagnostics) must use the new "author" spelling. (Older preset versions continue to use "dev".) Similarly, -Wuninitialized and -Wno-unused-cli also continue to accept their historic spellings:
# CMD_UNINITIALIZED=WARN; warning about deprecated spelling
cmake --warn-uninitialized
A final wrinkle is that the order of warning flags matters. Previously, the lack of systematic handling of command-line options meant that options affecting each of dev and deprecated would be independently recorded, and then both would be used to determine the action for deprecation warnings, with dev taking precedence. Most importantly, this meant that order only mattered to the extent that only the last option directly altering each of dev and deprecated would be considered, and an option affecting deprecated would take precedence no matter where in the command line it appeared.
Under the new system, deprecation warnings are a child category of author warnings, and changes are applied immediately. This means that, for example, -Wno-deprecated -Wauthor is equivalent to -Wauthor, because the later alteration ‘overwrites’ the action of the preceding -Wno-deprecated:
# CMD_AUTHOR=WARN CMD_DEPRECATED=WARN
cmake -Wno-deprecated -Wauthor
# CMD_AUTHOR=WARN CMD_DEPRECATED=IGNORE
cmake -Wauthor -Wno-deprecated
Presets avoid this by processing categories in order of ancestry.
Parting Thoughts
The introduction of a proper diagnostic framework in CMake 4.4 is a forward-looking step that we hope will pay many future dividends. Internally, it cleans up a part of the code base that was in pretty rough shape. Externally, while the transition is not completely invisible to end users, it improves consistency and lays the necessary foundation for not just adding more diagnostics, both immediately and in the future, but also providing users with the tools to interact with those diagnostics in a much more fine-grained manner. Our hope is that these changes will make it easier to identify undesirable patterns, ultimately leading to better and more modern CMake in users’ projects.