Proposal for debugging mode for CMake scripts
This is follow-up from a post on the forums about the status of debugging CMake scripts. Microsoft is interested in contributing this support for a future CMake release. We're interested in getting community feedback and building consensus to ensure this proposal covers most common scenarios.
While earlier discussion didn't cover using the Debug Adapter Protocol (DAP), this proposal is based on DAP for reasons that are explained below. This is based on feedback from the debugger team at Microsoft, but if there are compelling reasons why DAP is not a good fit for CMake we can change direction.
CMake debugging protocol
Goals and non-goals
This document is a proposal for adding functionality to CMake to allow external debugging tools like Visual Studio or Visual Studio Code to drive CMake script execution and inspect execution state. Currently available tools for debugging CMake scripts like --trace
and variable_watch()
are not interactive and can be difficult to use to introspect CMake targets and properties. Based on initial discussion this proposal defines a protocol to support these features.
- Set breakpoints based on filenames and line numbers.
- Set breakpoints when CMake errors or warnings are triggered.
- Set breakpoints when variable values are read or written.
- Step into, step over, or step out of the currently executing scope.
- View the call stack.
- View the state of currently defined variables.
- View the state of currently defined targets, directories, and tests with their properties.
- View the state of global properties.
- Execute arbitrary CMake while normal script execution is suspended.
These features, while potentially useful, are being considered out of scope for the current proposal.
- Editing the values of variables and properties. Based on feedback CMake has too many assumptions about these states not changing behind the scenes to implement this functionality correctly in version 1.
- Debugging generator expressions. Implementing this effectively requires more consideration of which scenarios are important and how to mark generator expressions for debugging.
- Providing a human-friendly command line interface. A new application could wrap the protocol described here to create a command line tool.
- Debugging child processes when CMake launches another instance of CMake.
- Designing how debugging functionality should be integrated into IDEs.
Why use the Debug Adapter Protocol?
This proposal uses the Debug Adapter Protocol (DAP), a standard debugging protocol already widely supported by tools like Visual Studio, Visual Studio Code, Eclipse, Emacs, and Vim. By using DAP, CMake will simplify the work required by vendors to integrate debugging in their current or future tools and create a more consistent debugging experience between tools.
One downside to using DAP is that the protocol as specified may not have first-class support for features unique to certain debuggers. For example DAP has no notion of a CMake target or target properties. This proposal solves these issues by leveraging how DAP clients ignore unknown fields to allow vendors to declare their support for extensions during protocol initialization and falling back to represent CMake-specific concepts using more general DAP messages if extension support is not declared. Vendors have the option of using these extensions for deeper CMake integration, but extension use is not required.
Because of its generality DAP is also a more complex protocol than a bespoke protocol, especially for a language like CMake with no threading and simple string types in almost all cases. However, DAP is designed to allow implementers to safely ignore large portions of the protocol if they are irrelevant, and the remaining complexity seems to be an appropriate trade-off for broader tool compatibility.
Starting a debugging session
Debugging is enabled by adding the --debugger
flag to a CMake configuration or script command line. By default, communication will happen over stdin and stdout but the --debugger-pipe=<pipe name>
flag can be added to use a named pipe instead.
cmake ... --debugger --debugger-pipe=cmake_debug_pipe
stdin and stdout are supported to simplify integration with external tools. Note that like CMake server a "named pipe" refers to a local domain socket on Unix or a named pipe on Windows and is assumed to be bidirectional. If the --debugger
flag is present but --debugger-pipe
is not, all normal script output (status, messages, errors etc.) will not reach stdout or stderr and will instead be captured in the output
DAP message. If both flags are specified, then normal script output will reach stdout or stderr and will also be captured in the output
DAP message.
Communication and message format
As described in the DAP specification, communication happens over text channel with ASCII encoding for message headers and UTF-8 encoding for message content. Message content is always a JSON object. For reference, a sample message could look like this (\r
and \n
are only shown for clarity and represent literal carriage return and newline ASCII characters).
Content-Length: 119\r\n
\r\n
{
"seq": 153,
"type": "request",
"command": "next",
"arguments": {
"threadId": 3
}
}
As specified, Content-Length
counts the length of the message content in bytes (not including the newlines separating the header from the content). CMake should be able to parse this format with no extra message bracketing required.
DAP supports three types of messages. Additional details can be found in the DAP specification.
- Events are sent from CMake to the client unprompted when important actions happen in script execution like hitting a breakpoint.
- Requests are sent from the client to CMake to configure the debugger and request additional information about execution state.
- Responses are sent from CMake to the client in response to a request.
CMake will listen for messages on a new background thread and add them to a queue for later processing. At the beginning of every CMake command invocation, the thread executing the script will first check the queue for any unprocessed messages, respond as needed, and then resume script execution. For messages like pause
or events like hitting a breakpoint that suspend script execution, the thread executing the script will continue to process messages from the queue, potentially blocking on an empty queue, until receiving a message that resumes execution. Requests that modify execution state (currently only evaluate
from a REPL) will fail if the script is not paused.
Supported messages
This section describes all the DAP messages supported by CMake along with clarification about how CMake will interpret certain messages. It may be helpful to read this section side-by-side with the DAP specification.
initialize request
This is the first message sent. Notably it includes the client's locale, whether line numbers should be zero or one based, and the path format (path
or uri
). Locale is ignored because CMake is not localized. If the path
path format is requested, CMake accept paths following the same rules it would use to normalize paths specified on the command line (like for CMAKE_C_COMPILER
), and emit paths that follow native platform conventions (forward slashes on Linux and backslashes on Windows).
The request is extended to support these optional fields.
-
supportsCMakeTargetsRequest
: If true in both the request and response, data about CMake targets will not be included in thevariables
response and will instead be provided through thecmakeTargets
request. -
supportsCMakeDirectoriesRequest
: If true in both the request and response, data about CMake build system directories will not be included in thevariables
response and will instead be provided through thecmakeDirectories
request. -
supportsCMakeTestsRequest
: If true in both the request and response, data about CMake tests will not be included in thevariables
response and will instead be provided through thecmakeTests
request.
The capabilities in the response are also extended to support these optional fields.
supportsCMakeTargetsRequest
supportsCMakeDirectoriesRequest
supportsCMakeTestsRequest
The response body is extended to add these fields.
-
cmakeVersion
: a required object representing the version of CMake hosting the debugger containing these fields.-
major
: a required integer representing the major version number. -
minor
: a required integer representing the minor version number. -
patch
: a required integer representing the patch number.
-
The response will include these exception filter names.
AUTHOR_WARNING
AUTHOR_ERROR
FATAL_ERROR
INTERNAL_ERROR
MESSAGE
WARNING
LOG
DEPRECATION_ERROR
DEPRECATION_WARNING
Implementation notes
The following capabilities are set to true
in the response. No other Boolean capability values are sent.
supportsFunctionBreakpoints
supportsEvaluateForHovers
supportsExceptionInfoRequest
supportsDataBreakpoints
supportsCMakeTargetsRequest
supportsCMakeDirectoriesRequest
supportsCMakeTestsRequest
supportedChecksumAlgorithms
is set to an empty array. exceptionBreakpointFilters
is set to the following, which correspond to the values of CMake's MessageType
enumeration.
[
{
"filter": "AUTHOR_WARNING",
"label": "Warning (dev)"
}, {
"filter": "AUTHOR_ERROR",
"label": "Error (dev)"
}, {
"filter": "FATAL_ERROR",
"label": "Fatal error"
}, {
"filter": "INTERNAL_ERROR",
"label": "Internal error"
}, {
"filter": "MESSAGE",
"label": "Other messages"
}, {
"filter": "WARNING",
"label": "Warning"
}, {
"filter": "LOG",
"label": "Debug log"
}, {
"filter": "DEPRECATION_ERROR",
"label": "Deprecation error"
}, {
"filter": "DEPRECATION_WARNING",
"label": "Deprecation warning"
}
]
initialized event
This event is sent after the initialize
response.
stopped event
This event indicates that script execution has been suspended for some reason.
Implementation notes
reason
is the only field included in the response body.
terminated event
This event is sent just before the CMake process terminates. Because the CMake process is both the debugee and the debug adapter from the perspective of DAP, this message indicates that both the debug adapter and debugee have terminated.
Implementation notes
No restart
information is included.
output event
This event indicates that the script attempted to write output. If --debugger-pipe
was not specified, normal script output is suppressed from stderr/stdout so that it does not pollute the debugger protocol. If the output would have triggered a exception breakpoint, the output event is fired before triggering the exception breakpoint.
The category
field will always be either stdout
or stderr
depending on the scripts's output. The output
field will contain the output produced by the script, including any preamble (like CMake Warning (dev)
). No other fields are included in the event body.
launch request
This request starts script execution. No additional information is included to configure the launch because this information was already passed to CMake on the command line. The noDebug
field is ignored. This contradicts the DAP specification, but simplifies the implementation in CMake. Clients that do not want debugging should not specify the --debugger
flag on the command line.
attach request
This request is required by DAP, but does not make sense for CMake because the debug adapter and debugee are the same process. Therefore, the response will always indicate failure.
Implementation notes
message
is set to not supported
.
disconnect request
Normally this request terminates the debug adapter and disconnects from the debugee, but because both are the same process for CMake this requests termination of both the debug adapter and debugee. The terminateDebugee
field is ignored. This contradicts the DAP specification, but is required given the CMake process model. After sending the response, CMake will send a terminated
event and then the process will exit.
setBreakpoints request
This request sets all breakpoints for a given source file, overwriting any existing breakpoints for the file. column
is ignored on the requested breakpoints.
Implementation notes
The breakpoints in the response always have verified
set to true
, and include no other fields (notably no id
field is returned).
setFunctionBreakpoints request
This request replaces all existing function breakpoints with new function breakpoints. Function is broadly construed to refer to any CMake command, whether function, macro, or built-in. The name
field on each requested FunctionBreakpoint
is assumed to be case-insensitive, matching CMake's behavior for command invocations. The FunctionBreakpoint
type is extended with these fields.
-
cmakeType
: an optional array of strings from the setfunction
,macro
, orbuilt-in
. The function breakpoint will only fire if the type of the CMake command is one of the specified types. If unspecified, the function breakpoint will fire for any CMake command type.
Implementation notes
The breakpoints in the response always have verified
set to true
, and include no other fields.
setExceptionBreakpoints request
This request sets which types of CMake output (errors, warnings, deprecation warnings, etc.) trigger exception breakpoints.
dataBreakpointInfo request
This request obtains information on possible data breakpoints that could be set on a CMake variable. As specified by DAP, in the request variablesReference
must either refer to a variable container from the scopes
or variables
response, or be unspecified. If unspecified, the response from CMake will have dataId
set to null
.
If the variablesReference
and name
in the request identify a defined CMake variable, the response will have dataId
and description
set to the name of the variable. If not, dataId
is set to null
.
For defined variables, accessTypes
is set to the following value, which includes CMake extensions. See setDataBreakpoints
for the meaning of these extensions.
[ "read", "write", "readWrite", "cmakeModified", "cmakeRemoved" ]
Implementation notes
description
when variablesReference
is unspecified is set to cannot set a data breakpoint on an expression
. description
when variablesReference
and name
do not refer to a CMake variable is set to not a CMake variable
.
setDataBreakpoints request
This request replaces all existing data breakpoints with new data breakpoints. The supported values for accessType
in the requested breakpoints are extended to support these values.
cmakeRead
cmakeUnknownRead
cmakeModified
cmakeUnknownModified
cmakeRemoved
The meanings of these values are the same as the variable_watch()
command. The standard DAP access types correspond as follows.
-
read
:cmakeRead
orcmakeUnknownRead
-
write
:cmakeModified
,cmakeUnknownModified
, orcmakeRemoved
-
readWrite
: all extension values
If accessType
is unspecified, it is assumed to be write
.
Implementation notes
The breakpoints in the response always have verified
set to true
, and include no other fields.
continue request
This request resumes script execution. threadId
is ignored in the request, and allThreadsContinued
is always true
in the response.
next request
This request instructs the debugger to "step over", which is defined as running until the next command invocation in the current CMake scope or a parent scope. threadId
is ignored in the request.
stepIn request
This request instructs the debugger to "step in", which is defined as running until the next command invocation. threadId
is ignored in the request.
stepOut request
This request instructs the debugger to "step out", which is defined as running until the next command invocation outside the current function, macro, include, inline file, or directory scope (depending on what type of scope execution is currently in). threadId
is ignored in the request.
pause request
This request suspends script execution at the next available opportunity. Due to the CMake's internal processing (see Communication and message format), pausing will happen at the beginning of the next command invocation. threadId
is ignored in the request.
stackTrace request
This request returns a stack trace from the current execution state. threadId
is ignored in the request. Stack frames are derived by walking the existing tree of state snapshots created internally by CMake from the current leaf node to the root. In the response, id
for each frame is set to the number of steps from the leaf node to the state snapshot in question. name
is set to the function, macro, directory, or filename depending on the type of state snapshot. source
and line
are set to correspond to the source position, and column
is always set to zero. Each frame is extended with the following fields.
-
cmakeSnapshotType
: a required string from the setBaseType
,BuildsystemDirectoryType
,DeferCallType
,FunctionCallType
,MacroCallType
,IncludeFileType
,InlineListFileType
,PolicyScopeType
, orVariableScopeType
. These correspond to the values defined incmStateEnums::SnapshotType
.
scopes request
This request returns the available variable scopes for a given stack frame. Every frame includes a scope with name
set to Locals
, presentationHint
set to locals
. This scope stores the CMake variables defined in this state snapshot, not inherited from other snapshots. namedVariables
is set to the number of CMake variables.
If the client and CMake did not specify supportsCMakeDirectoriesRequest
as true
, then the top frame also includes a scope with name
set to Directory Properties
and namedVariables
set to the number of properties set on the current CMake directory scope.
The base frame includes a scope with name
set to Global Properties
and namedVariables
set to the number of global CMake properties (see below for implementation concerns).
If the client and CMake did not specify supportsCMakeTargetsRequest
as true
, then the base frame also includes a scope with name
set to Targets
and namedVariables
set to the number of defined CMake targets.
If the client and CMake did not specify supportsCMakeTestsRequest
as true
, then the base frame also includes a scope with name
set to Tests
and namedVariables
set to the number of defined CMake tests.
Implementation notes
variablesReference
is set to the frame ID + 5 (to account for other potential scopes) for the locals scope. variablesReference
set to one for the global properties scope, two for the targets scope, three for the tests scope, and four for the directory properties scope.
indexedVariables
is always set to zero and expensive
is always set to false
for all scopes.
variables request
This request returns all the variable values for a given variable reference, which could be CMake variables, properties, targets, directories, or tests. For CMake variables, global properties, and directory properties, the response includes name
and value
set to appropriate values. variablesReference
is always set to zero to indicate that this is not a structured value.
If the client and CMake did not specify supportsCMakeTargetsRequest
, then a request for variables from the targets scope will have name
set to the target name and value
set to an empty string.
A variables
request with variablesReference
set to an identifier returned by an individual target will return that target's properties. name
will be set to the property name, value
will be set to the property value as returned by get_property(... TARGET ...)
. variablesReference
is always set to zero to indicate that the property value is not structured.
If the client and CMake did not specify supportsCMakeTestsRequest
, then a request for variables from the tests scope will have name
set to the test name and value
set to an empty string.
A variables
request with variablesReference
set to an identifier returned by an individual test will return that test's properties. name
will be set to the property name, value
will be set to the property value as returned by get_property(... TEST ...)
. variablesReference
is always set to zero to indicate that the property value is not structured.
Implementation notes
variablesReference
for a target or test will be set to the total number of stack frames + 5 (to reserve zero, a potential base targets scope, a potential base global properties scope, a potential base test properties scope, and a potential top frame directories scope) + an implementation defined index for the target or test that is always positive. Target and test indices must not conflict.
All variables that represent CMake variables will have a presentationHint
with kind
set to either data
or dataBreakpoint
. data
is used if a data breakpoint has not been set for this variable name, and dataBreakpoint
is used if a data breakpoint has been set for this variable name. This is required to enable data breakpoint support in some debuggers.
threads request
CMake script execution is single-threaded, so this request always returns a single thread with id
equal to one.
Implementation notes
The response has this body.
{
"threads": [{
"id": 1,
"name": "CMake script"
}]
}
evaluate request
This request evaluates a CMake script string or CMake variable name. If context
in the request is set to the standard watch
or hover
values, then expression
is expected to be then name of a CMake variable and result
in the response will store the value of the variable, if it exists. If context
in the request is anything else (like the standard repl
value), then expression
is expected to be a CMake script string and result
in the response will be an empty string because CMake has no return values. Executing a CMake script string is only supported when script execution is paused and will fail if the script is currently running.
Script execution is equivalent to the cmake_language(EVAL CODE ...)
except that errors do not halt overall script execution, only evaluation of the snippet. Evaluation failure is reported by setting result
to evaluation error
.
exceptionInfo request
This request retrieves the details of a CMake error, warning, or message. threadId
is ignored in the request. In the response, exceptionId
will be set to the corresponding label from exceptionBreakpointFilters
(from the initialize
response), description
will be set to the message that was logged, excluding any preamble, and breakMode
is always set to always
. Descriptions are not localized.
Extension messages
CMake defines three extension messages. These messages provide a way to get more detailed information about CMake targets, directories, and tests. The CMakeProperty interface may appear in some responses. While the implementation will not be in TypeScript, the message format is defined here using TypeScript syntax to match the rest of the DAP specification.
interface CMakeProperty {
/**
* The name of the property.
*/
name: string;
/**
* The value of the property.
*/
value: string;
/**
* True if the property was inherited from another CMake scope.
*/
inherited: boolean;
}
cmakeTargets request
This request retrieves currently defined CMake targets and their properties. Clients should only send this request if they specify supportsCMakeTargetsRequest
as true
during the initialize
request and CMake specifies supportsCMakeTargetsRequest
as true
during the initialize
response.
interface CMakeTargetsRequest extends Request {
command: 'cmakeTargets';
arguments?: CMakeTargetsArguments;
}
interface CMakeTargetsArguments {
/**
* If true, target properties are included in the response.
*/
includeDetails?: boolean;
/**
* The set of target names to include in the response. If unspecified, all
* defined targets are included.
*/
targetNames?: string[];
}
interface CMakeTargetsResponse extends Response {
body: {
/**
* The currently defined CMake targets.
*/
targets: CMakeTarget[];
}
}
interface CMakeTarget {
/**
* The name of this target.
*/
name: string;
/**
* The properties defined on this target, including inherited properties.
*/
properties?: CMakeProperty[];
}
cmakeDirectories request
This request retrieves currently defined CMake build system directories and their properties. Clients should only send this request if they specify supportsCMakeDirectoriesRequest
as true
during the initialize
request and CMake specifies supportsCMakeDirectoriesRequest
as true
during the initialize
response.
interface CMakeDirectoriesRequest extends Request {
command: 'cmakeDirectories';
arguments?: CMakeDirectoriesArguments;
}
interface CMakeDirectoriesArguments {
/**
* If true, directory properties are included in the response.
*/
includeDetails?: boolean;
/**
* The set of directories to include in the response. If unspecified, all
* defined directories are included. Directory names should be in a form
* accepted by get_property(... DIRECTORY ...).
*/
directories?: string[];
}
interface CMakeDirectoriesResponse extends Response {
body: {
/**
* The currently defined CMake build system directories.
*/
directories: CMakeDirectory[];
}
}
interface CMakeDirectory {
/**
* The full path to this directory in the system native formt.
*/
path: string;
/**
* The properties defined on this directory, including inherited properties.
*/
properties?: CMakeProperty[];
}
cmakeTests request
This request retrieves currently defined CMake tests and their properties. Clients should only send this request if they specify supportsCMakeTestsRequest
as true
during the initialize
request and CMake specifies supportsCMakeTestsRequest
as true
during the initialize
response.
interface CMakeTestsRequest extends Request {
command: 'cmakeTests';
arguments?: CMakeTestsArguments;
}
interface CMakeTestsArguments {
/**
* If true, test properties are included in the response.
*/
includeDetails?: boolean;
/**
* The set of test names to include in the response. If unspecified, all
* defined tests are included.
*/
testNames?: string[];
}
interface CMakeTestsResponse extends Response {
body: {
/**
* The currently defined CMake tests.
*/
tests: CMakeTest[];
}
}
interface CMakeTest {
/**
* The name of this test.
*/
name: string;
/**
* The properties defined on this test, including inherited properties.
*/
properties?: CMakeProperty[];
}
Unsupported messages
The following messages are specified by DAP as optional and are not supported by CMake.
- cancel request
- continued event
- exited event
- thread event
- breakpoint event: This event could be useful to add later so that breakpoints can be verified once sources are loaded but is considered out of scope for an MVP.
- module event
- loadedSource event
- process event
- capabilities event
- progressStart event
- progressUpdate event
- progressEnd event
- invalidated event
- runInTerminal reverse request
- configurationDone request
- restart request
- terminate request
- breakpointLocations request
- setInstructionBreakpoints request
- stepBack request
- reverseContinue request
- restartFrame request
- goto request
- setVariable request
- source request: This request could be added to support dynamic non-file-based sources, but these are rare in CMake.
- terminateThreads request
- modules request
- loadedSources request
- setExpression request
- stepInTargets request
- gotoTargets request
- completions request
- readMemory request
- disassemble request
Implementation concerns
Location-based breakpoints are internally stored based on filename and line number, and at the beginning of each command execution the current source position is checked against the list of breakpoints. This means that the debugger provides no guarantees that breakpoints are set in valid locations that could be hit during script execution (for example, on a blank line or comment). Adding this support is possible using DAP's breakpoint
event, but is considered out of scope for MVP.
Some properties are not stored internally by CMake but are instead computed on-the-fly when requested, like ALIASED_TARGET
or ALIASED_GLOBAL
. Therefore, the set of property names returned by the variables
or cmakeTargets
requests will be the union of all documented property names for the given scope with property names explicitly defined by the script.
Stack frames will be derived from CMake's internal state snapshots, which do not currently include source positions. Debugger support will require either capturing the source position whenever snapshots are created under debugging, or somehow deriving the source position from already available information. Existing backtraces do not provide enough fidelity for common debugging scenarios, like viewing a stack trace with multiple function calls in the same file.
Variable access by the debugger itself, such as in response to a variables
request, should not trigger variable_watch()
commands or breakpoints. Variable access by user code during an evaluate
request should trigger variable_watch()
commands and breakpoints like cmake_language()
would.
Open questions
-
In earlier discussion there was interest in exposing the currently defined export sets. I am not familiar enough with CMake usage to know what exactly would be useful to return here but I am open to adding it.Resolved as out of scope for MVP.