How to debug a KSP Gradle plugin

When writing a Kotlin Symbol Processor (KSP) plugin for Gradle, it is invaluable to be able to debug your code. In this short article I will show how to set this up properly using Gradle 7.6 and Kotlin 1.8.0, with remote debugging through IntelliJ 2022.3.1, but these instructions should also work for newer versions.

To debug our KSP code, we need to achieve the following:

  • The Gradle task that invokes our KSP processor should be executed
  • Gradle should start and then suspend, waiting for a debugger to attach
  • The KSP execution should happen in the same JVM process that is being debugged
Info

TL;DR: Run Gradle like this (assuming the project is called example):

./gradlew :example:kspKotlin --rerun -Dorg.gradle.debug=true --no-daemon \
    -Pkotlin.compiler.execution.strategy=in-process

then attach a remote JVM debugger at port 5005.

Always executing our KSP task Link to heading

Gradle tries to execute as few tasks as possible, marking them as UP-TO-DATE and skipping them whenever it can. This can cause Gradle to skip our KSP if it thinks nothing’s changed. For debugging it is better to always execute this task. Therefore, we will specify the KSP task kspKotlin explicitly and use Gradle 7’s --rerun option to ensure that specific task is always run, but the other tasks retain their incremental behavior. At this point we have the following command line:

./gradlew :example:kspKotlin --rerun

Alternatively, you can specify any other task (e.g., build) and use the --rerun-tasks option to just rerun all tasks. For example, ./gradlew build --rerun-tasks. However, this is slower than --rerun on a specific task.

Starting Gradle in debugging mode Link to heading

In general, to debug a Gradle build, we need to start Gradle with the -Dorg.gradle.debug=true flag, meaning that Gradle will suspend and listen for a debugger to attach to port 5005.

Info

This is equivalent to setting the following on the Gradle process:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005

However, after attaching the debugger once, the Gradle daemon will stay alive and will no longer suspend on subsequent builds. Therefore, we also need to specify --no-daemon to ensure a new Gradle process is created every time we start the build.

The command-line now looks like this:

./gradlew :example:kspKotlin --rerun -Dorg.gradle.debug=true --no-daemon

Ensuring KSP is executed in the same process Link to heading

By default Kotlin will execute the KSP in a separate Kotlin daemon process, but then we cannot debug our code or trigger breakpoints. To ensure KSP is run in the same process, we need to specify kotlin.compiler.execution.strategy with the value in-process. In previous versions of Kotlin this was a system property (to be specified with the -D option), but since Kotlin 1.7 this is a Gradle property (to be specified with the -P option or in the gradle.properties file), and since Kotlin 1.8 the system property has been removed and has no effect.

As such, we need to specify -Pkotlin.compiler.execution.strategy=in-process and obtain our final command-line:

./gradlew :example:kspKotlin --rerun -Dorg.gradle.debug=true --no-daemon \
    -Pkotlin.compiler.execution.strategy=in-process

Attaching to the Gradle process from IntelliJ Link to heading

When you execute the above Gradle command, it stops and waits for the debugger, showing only > Starting Daemon. At this point we should attach the debugger. In IntelliJ, go to the Run Configurations and create a new Remote JVM Debug run configuration. The default settings connect it to local port 5005, which is correct for our case.

Settings for Remote JVM debugging in IntelliJ.

Then, when you Debug this run configuration, it attaches to the Gradle process. Gradle continues execution, and your breakpoints should be hit.

Troubleshooting Link to heading

Breakpoints are not hit Link to heading

If Gradle waits for the debugger and continues once you’ve attached it, but your breakpoints are not being hit, ensure that:

  • your have refreshed your Gradle project in IntelliJ
  • the Kotlin compiler is executed in the same process (-Pkotlin.compiler.execution.strategy=in-process)
  • the KSP task is actually being executed and not just UP-TO-DATE (--rerun or --rerun-tasks)

Gradle does not wait for the debugger Link to heading

If Gradle does not wait for the debugger, ensure that:

  • you are starting a new instance every time (--no-daemon)
  • you have enabled debugging (-Dorg.gradle.debug=true)

Transport error 202: bind failed: Address already in use Link to heading

Gradle cannot listen for a debugger if another process is already occupying this port. Most likely it’s a previous Gradle daemon that has not died yet. Stop the Gradle daemons using:

./gradlew --stop

Or find out which process is using this port through:

lsof -i :5005

Unable to open debugger port (localhost:5005) Link to heading

If IntelliJ complains that it cannot open the debugger port, then another process is already occupying this port. Try the same steps as above with IntelliJ.

Start Gradle project from IntelliJ Link to heading

It seems that IntelliJ doesn’t understand --no-daemon and doesn’t honor -Dorg.gradle.daemon=false and always runs the project in a daemon. This means that once you start your Gradle project in debugging mode, the daemon will occupy the port and you’ll have various issues. So far the only consistent way I’ve found is to start the project from the command-line using the above command.