cmake_language: Add PRINT_TARGETS operation

Print CMake targets in human-readable form for debugging.

Issue: #27513
This commit is contained in:
Tom Osika
2026-06-26 00:57:33 -04:00
parent 477b2fa32f
commit 45623dd5f6
29 changed files with 358 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ Synopsis
cmake_language(`DEFER`_ <options>... CALL <command> [<arg>...])
cmake_language(`SET_DEPENDENCY_PROVIDER`_ <command> SUPPORTED_METHODS <methods>...)
cmake_language(`GET_MESSAGE_LOG_LEVEL`_ <out-var>)
cmake_language(`PRINT_TARGETS`_ <filter>...)
cmake_language(`EXIT`_ <exit-code>)
cmake_language(`TRACE`_ <boolean> ...)
@@ -511,6 +512,58 @@ Getting current message log level
option takes precedence. If neither are set, the default logging level
is returned.
Printing Targets
^^^^^^^^^^^^^^^^
.. versionadded:: 4.5
.. signature::
cmake_language(PRINT_TARGETS <filter>...)
Prints the names of all targets that currently exist, one per line,
annotated with their types (e.g. ``EXECUTABLE``, ``STATIC_LIBRARY``,
``INTERFACE_LIBRARY``, ``UTILITY``). Imported targets are additionally
annotated with ``IMPORTED``. :ref:`Alias Targets` are not listed; their
aliased target is.
The output is sorted by target name and printed as a single status message.
Each ``<filter>`` may be one of:
``REGEX <regex>``
Only list targets whose name matches the given
:ref:`regular expression <Regex Specification>`. If no target matches,
a warning is issued.
``IGNORE_CASE``
Match the ``REGEX`` case-insensitively. Only valid with ``REGEX``.
``IMPORTED_ONLY``
Only list :ref:`Imported Targets`.
``NO_IMPORTED``
Exclude imported targets; only list targets built by this project.
``IMPORTED_ONLY`` and ``NO_IMPORTED`` are mutually exclusive. When neither
is given, both imported and non-imported targets are listed.
Printing Targets Examples
"""""""""""""""""""""""""
.. code-block:: cmake
add_executable(app main.c)
add_library(util STATIC util.c)
find_package(Threads REQUIRED) # provides imported Threads::Threads
cmake_language(PRINT_TARGETS REGEX "^(app|util)$" NO_IMPORTED)
Gives::
-- Printing targets...
Non-imported targets matching REGEX '^(app|util)$' (case sensitive):
app (EXECUTABLE)
util (STATIC_LIBRARY)
Terminating Scripts
^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1,6 @@
print_targets
-------------
* The :command:`cmake_language(PRINT_TARGETS)` command was added
to print the targets that currently exist in human-readable form
for debugging.

View File

@@ -5,13 +5,19 @@
#include <algorithm>
#include <array>
#include <cstddef>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <cm/optional>
#include <cm/string_view>
#include <cmext/string_view>
#include "cmsys/RegularExpression.hxx"
#include "cmArgumentParser.h"
#include "cmArgumentParserTypes.h"
#include "cmDependencyProvider.h"
@@ -25,9 +31,14 @@
#include "cmState.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmTarget.h"
#include "cmValue.h"
#include "cmake.h"
namespace cm {
enum class TargetType;
}
namespace {
bool FatalError(cmExecutionStatus& status, std::string const& error)
@@ -360,6 +371,157 @@ bool cmCMakeLanguageCommandGET_EXPERIMENTAL_FEATURE_ENABLED(
return true;
}
struct PrintTargetsArgs : public ArgumentParser::ParseResult
{
cm::optional<std::string> Regex;
bool ImportedOnly = false;
bool NoImported = false;
bool IgnoreCase = false;
cm::optional<std::string> MessagePrefix;
};
// Lists every target that currently exists, optionally filtered by a
// name REGEX and by imported state. "Currently exists" means anything
// CMake has defined up to this call: targets in the current directory,
// its ancestors, and any already-processed subdirectories. Walking the
// global generator's makefiles captures exactly that set; a name-keyed
// map sorts the output and dedupes imported targets, which are inherited
// into child makefiles and would otherwise be seen many times.
bool cmCMakeLanguageCommandPRINT_TARGETS(
std::vector<cmListFileArgument> const& args, cmExecutionStatus& status)
{
cmMakefile& makefile = status.GetMakefile();
std::vector<std::string> expandedArgs;
makefile.ExpandArguments(args, expandedArgs);
// Drop the leading "PRINT_TARGETS" subcommand keyword.
std::vector<std::string> body(expandedArgs.begin() + 1, expandedArgs.end());
auto const ArgsParser =
cmArgumentParser<PrintTargetsArgs>()
.Bind("REGEX"_s, &PrintTargetsArgs::Regex)
.Bind("IMPORTED_ONLY"_s, &PrintTargetsArgs::ImportedOnly)
.Bind("NO_IMPORTED"_s, &PrintTargetsArgs::NoImported)
.Bind("IGNORE_CASE"_s, &PrintTargetsArgs::IgnoreCase)
.Bind("__MESSAGE_PREFIX"_s, &PrintTargetsArgs::MessagePrefix);
std::vector<std::string> unparsed;
auto parsedArgs = ArgsParser.Parse(body, &unparsed);
if (!unparsed.empty()) {
return FatalError(
status,
cmStrCat(
"Unknown argument(s) given to cmake_language(PRINT_TARGETS) call: \"",
cmJoin(unparsed, "\" \""), "\"."));
}
if (parsedArgs.MaybeReportError(makefile)) {
cmSystemTools::SetFatalErrorOccurred();
return true;
}
if (parsedArgs.ImportedOnly && parsedArgs.NoImported) {
return FatalError(status,
"IMPORTED_ONLY and NO_IMPORTED keywords are mutually "
"exclusive in cmake_language(PRINT_TARGETS) call.");
}
if (parsedArgs.IgnoreCase && !parsedArgs.Regex) {
return FatalError(status,
"IGNORE_CASE keyword in cmake_language(PRINT_TARGETS) "
"call is only valid with REGEX.");
}
// Compile the optional REGEX up front so a bad pattern fails fast. With
// IGNORE_CASE the pattern and the candidate names are both lower-cased.
cm::optional<cmsys::RegularExpression> regex;
if (parsedArgs.Regex) {
cmsys::RegularExpression re;
std::string const pat = parsedArgs.IgnoreCase
? cmSystemTools::LowerCase(*parsedArgs.Regex)
: *parsedArgs.Regex;
if (!re.compile(pat)) {
return FatalError(status,
cmStrCat("REGEX regular expression \"",
*parsedArgs.Regex, "\" cannot compile."));
}
regex = std::move(re);
}
bool const includeNormal = !parsedArgs.ImportedOnly;
bool const includeImported = !parsedArgs.NoImported;
struct TargetInfo
{
cm::TargetType Type;
bool Imported;
};
std::map<std::string, TargetInfo> targets;
for (auto const& mf : makefile.GetGlobalGenerator()->GetMakefiles()) {
if (includeNormal) {
for (auto const& ti : mf->GetTargets()) {
cmTarget const& t = ti.second;
targets.insert({ t.GetName(), { t.GetType(), false } });
}
}
if (includeImported) {
for (cmTarget const* t : mf->GetImportedTargets()) {
targets.insert({ t->GetName(), { t->GetType(), true } });
}
}
}
// Build the body first so the header is suppressed when a REGEX filters
// everything out (matches cmake_language(PRINT_VARIABLES) behavior).
std::string lines;
bool anyMatched = false;
for (auto const& t : targets) {
if (regex) {
std::string const subj =
parsedArgs.IgnoreCase ? cmSystemTools::LowerCase(t.first) : t.first;
if (!regex->find(subj)) {
continue;
}
}
lines +=
cmStrCat(" ", t.first, " (", cmState::GetTargetTypeName(t.second.Type),
t.second.Imported ? ", IMPORTED" : "", ")\n");
anyMatched = true;
}
if (anyMatched) {
// The message opens with a banner line. The internal __MESSAGE_PREFIX
// keyword overrides it (used by wrappers to reproduce legacy output); it
// is not part of the public interface.
std::string const messagePrefix =
parsedArgs.MessagePrefix.value_or("Printing targets...\n");
// The header reflects the imported-filter mode and any REGEX in effect.
char const* label = "All targets";
if (parsedArgs.ImportedOnly) {
label = "Imported targets";
} else if (parsedArgs.NoImported) {
label = "Non-imported targets";
}
std::string out = cmStrCat(messagePrefix, " ", label);
if (parsedArgs.Regex) {
out += cmStrCat(
" matching REGEX '", *parsedArgs.Regex, "' (",
parsedArgs.IgnoreCase ? "case insensitive" : "case sensitive", ")");
}
out += cmStrCat(":\n", lines);
makefile.DisplayStatus(out, -1);
}
if (!anyMatched && parsedArgs.Regex) {
makefile.IssueMessage(
MessageType::WARNING,
cmStrCat("No targets matching REGEX '", *parsedArgs.Regex, "' (",
parsedArgs.IgnoreCase ? "case insensitive" : "case sensitive",
") in cmake_language(PRINT_TARGETS ...)."));
}
return true;
}
}
bool cmCMakeLanguageCommand(std::vector<cmListFileArgument> const& args,
@@ -539,6 +701,10 @@ bool cmCMakeLanguageCommand(std::vector<cmListFileArgument> const& args,
status);
}
if (expArgs[expArg] == "PRINT_TARGETS") {
return cmCMakeLanguageCommandPRINT_TARGETS(args, status);
}
if (expArgs[expArg] == "TRACE") {
++expArg; // Consume "TRACE".

View File

@@ -0,0 +1,14 @@
.*--.*
+All targets matching REGEX '\^zz_print_targets_' \(case sensitive\):
+zz_print_targets_iface \(INTERFACE_LIBRARY\)
+zz_print_targets_imp \(STATIC_LIBRARY, IMPORTED\)
+zz_print_targets_lib \(STATIC_LIBRARY\)
+zz_print_targets_util \(UTILITY\)
.*--.*
+Non-imported targets matching REGEX '\^zz_print_targets_' \(case sensitive\):
+zz_print_targets_iface \(INTERFACE_LIBRARY\)
+zz_print_targets_lib \(STATIC_LIBRARY\)
+zz_print_targets_util \(UTILITY\)
.*--.*
+Imported targets matching REGEX '\^zz_print_targets_' \(case sensitive\):
+zz_print_targets_imp \(STATIC_LIBRARY, IMPORTED\)

View File

@@ -0,0 +1,16 @@
enable_language(C)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/zz_stub.c" "int zz_stub(void) { return 0; }\n")
add_library(zz_print_targets_iface INTERFACE)
add_library(zz_print_targets_imp STATIC IMPORTED)
add_library(zz_print_targets_lib STATIC "${CMAKE_CURRENT_BINARY_DIR}/zz_stub.c")
add_custom_target(zz_print_targets_util)
# Default: both imported and non-imported targets.
cmake_language(PRINT_TARGETS REGEX "^zz_print_targets_")
# NO_IMPORTED drops the imported target.
cmake_language(PRINT_TARGETS REGEX "^zz_print_targets_" NO_IMPORTED)
# IMPORTED_ONLY keeps only the imported target.
cmake_language(PRINT_TARGETS REGEX "^zz_print_targets_" IMPORTED_ONLY)

View File

@@ -0,0 +1,3 @@
.*--.*
+All targets matching REGEX '\^zz_' \(case sensitive\):
+zz_real \(INTERFACE_LIBRARY\)

View File

@@ -0,0 +1,4 @@
# Aliases are not listed; the aliased (real) target is.
add_library(zz_real INTERFACE)
add_library(zz_alias ALIAS zz_real)
cmake_language(PRINT_TARGETS REGEX "^zz_")

View File

@@ -0,0 +1,3 @@
CMake Warning at PrintTargetsEmptyMatch\.cmake:[0-9]+ \(cmake_language\):
No targets matching REGEX '\^zz_no_match_' \(case sensitive\) in
cmake_language\(PRINT_TARGETS \.\.\.\)\.

View File

@@ -0,0 +1,4 @@
add_library(zz_present INTERFACE)
# A REGEX matching no target prints no header, just a warning.
cmake_language(PRINT_TARGETS REGEX "^zz_no_match_")

View File

@@ -0,0 +1,3 @@
CMake Warning at PrintTargetsIgnoreCase\.cmake:[0-9]+ \(cmake_language\):
No targets matching REGEX 'ZZ_IC_LOWER' \(case sensitive\) in
cmake_language\(PRINT_TARGETS \.\.\.\)\.

View File

@@ -0,0 +1,3 @@
.*--.*
+All targets matching REGEX 'ZZ_IC_LOWER' \(case insensitive\):
+zz_ic_lower \(INTERFACE_LIBRARY\)

View File

@@ -0,0 +1,7 @@
add_library(zz_ic_lower INTERFACE)
# Case-sensitive: an uppercase pattern does not match the lowercase target.
cmake_language(PRINT_TARGETS REGEX "ZZ_IC_LOWER")
# IGNORE_CASE: the same pattern now matches.
cmake_language(PRINT_TARGETS REGEX "ZZ_IC_LOWER" IGNORE_CASE)

View File

@@ -0,0 +1,3 @@
CMake Error at PrintTargetsIgnoreCaseRequiresRegex\.cmake:[0-9]+ \(cmake_language\):
cmake_language IGNORE_CASE keyword in cmake_language\(PRINT_TARGETS\) call is
only valid with REGEX\.

View File

@@ -0,0 +1,2 @@
# IGNORE_CASE is only meaningful with REGEX.
cmake_language(PRINT_TARGETS IGNORE_CASE)

View File

@@ -0,0 +1,7 @@
-- Printing targets\.\.\.
All targets matching REGEX '\^zz_mp_' \(case sensitive\):
zz_mp_lib \(INTERFACE_LIBRARY\)
-- CUSTOM_BANNER
All targets matching REGEX '\^zz_mp_' \(case sensitive\):
zz_mp_lib \(INTERFACE_LIBRARY\)

View File

@@ -0,0 +1,7 @@
add_library(zz_mp_lib INTERFACE)
# Default banner opens the message.
cmake_language(PRINT_TARGETS REGEX "^zz_mp_")
# The internal __MESSAGE_PREFIX keyword overrides the default banner.
cmake_language(PRINT_TARGETS REGEX "^zz_mp_" __MESSAGE_PREFIX "CUSTOM_BANNER\n")

View File

@@ -0,0 +1,3 @@
CMake Error at PrintTargetsMutuallyExclusive\.cmake:[0-9]+ \(cmake_language\):
+cmake_language IMPORTED_ONLY and NO_IMPORTED keywords are mutually
+exclusive in cmake_language\(PRINT_TARGETS\) call\.

View File

@@ -0,0 +1 @@
cmake_language(PRINT_TARGETS IMPORTED_ONLY NO_IMPORTED)

View File

@@ -0,0 +1,3 @@
.*--.*
+All targets:
.* +mytarget \(INTERFACE_LIBRARY\)

View File

@@ -0,0 +1,3 @@
add_library(mytarget INTERFACE)
cmake_language(PRINT_TARGETS)

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,2 @@
CMake Error at PrintTargetsRegexError\.cmake:[0-9]+ \(cmake_language\):
cmake_language REGEX regular expression "\[" cannot compile\.

View File

@@ -0,0 +1 @@
cmake_language(PRINT_TARGETS REGEX "[")

View File

@@ -0,0 +1,13 @@
.*--.*
+All targets matching REGEX '\^zz_' \(case sensitive\):
+zz_parent \(INTERFACE_LIBRARY\)
.*--.*
+All targets matching REGEX '\^zz_' \(case sensitive\):
+zz_child \(INTERFACE_LIBRARY\)
+zz_parent \(INTERFACE_LIBRARY\)
.*--.*
+All targets matching REGEX '\^zz_' \(case sensitive\):
+zz_child \(INTERFACE_LIBRARY\)
+zz_parent \(INTERFACE_LIBRARY\)

View File

@@ -0,0 +1,9 @@
add_library(zz_parent INTERFACE)
# Before the subdirectory: only the parent target exists.
cmake_language(PRINT_TARGETS REGEX "^zz_")
add_subdirectory(PrintTargetsScope)
# After: the child's target is visible too.
cmake_language(PRINT_TARGETS REGEX "^zz_")

View File

@@ -0,0 +1,4 @@
add_library(zz_child INTERFACE)
# The child sees its own target and the ancestor's.
cmake_language(PRINT_TARGETS REGEX "^zz_")

View File

@@ -182,3 +182,18 @@ block()
set(RunCMake_TEST_OPTIONS --trace-expand --trace-source=trace_expand_cli_wins.cmake)
run_cmake(trace_expand_cli_wins)
endblock()
# cmake_language(PRINT_TARGETS)
run_cmake(PrintTargets)
run_cmake(PrintTargetsMessagePrefix)
run_cmake(PrintTargetsMutuallyExclusive)
block()
set(RunCMake_TEST_NOT_EXPECT_stdout "zz_alias")
run_cmake(PrintTargetsAlias)
endblock()
run_cmake(PrintTargetsScope)
run_cmake(PrintTargetsEmptyMatch)
run_cmake(PrintTargetsRegexError)
run_cmake(PrintTargetsNoRegex)
run_cmake(PrintTargetsIgnoreCase)
run_cmake(PrintTargetsIgnoreCaseRequiresRegex)