Search code examples
scalaintellij-ideamill

scala, the mill build tool, and the IntelliJ debugger


I'm writing Scala code and using the Mill build tool. I'd like to be able to use IntelliJ's debugger to debug my code but have not been able to convince it to stop at breakpoints.

I have set up a remote debugging configuration in IntelliJ. I've copy/pasted the suggested Java command line arguments into a script that I use to run Mill:

#!/bin/zsh

export JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005"
mill $@

When I run the program using Mill, the program stops and waits for the debugger to connect. It then runs to completion in spite of having breakpoints set.

breakpoint

The println at line 17 is executed.

The build.sc file for Mill is simplicity itself:

import mill._, scalalib._

object foo extends RootModule with ScalaModule {
  def scalaVersion = "3.3.3"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::os-lib:0.9.3",
    ivy"com.github.scopt::scopt:4.1.0",
    ivy"ca.uwaterloo::da_solver:0.4.0-SNAPSHOT"
  )

  object test extends ScalaTests {
    def ivyDeps       = Agg(ivy"com.lihaoyi::utest:0.7.11")
    def testFramework = "utest.runner.Framework"
  }
}

One of the things I wonder about is the setting for "Use module classpath:"

classpath dropdown

It defaults to <no module>. I've tried that and the other options, all with the same results (breakpoints skipped).

Any suggestions?

I found this StackOverflow post on using the IntelliJ debugger really helpful, except for the above. User @user3416742 asked a similar question in Oct. 2023 but focused on running a unit test. I have that same question; unfortunately no one has answered it yet.


Solution

  • The key insight, from @CrazyCoder, is that mill starts up a subprocess to run the code. That's the process that needs to be connected.

    So, here are the steps:

    In IntelliJ:

    • Create a debug configuration. Start with "Run -> Edit Configurations"
    • Click the "+" in the upper left corner. From the resulting drop-down menu choose "Remote JVM Debug"
    • Give it a name such as "remote-debugging".
    • Leave everything else alone but copy the read-only field labeled "Command line arguments for remote JVM:". In my case (and probably in yours) -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
    • Close the dialog. debug configuration dialog box

    In your mill build file:

    • Find the object in the build file that's used to run the code. In my case, my command line is mill cli.run so I want the cli object in my build file.
    • Add override def forkArgs = Seq("..."), replacing the ... with the line you copied from IntelliJ. forkArgs is documented here. If necessary, search the documentation for forkArgs.
    • In my case, I want the program to suspend execution until I've had time to connect with the debugger (ie it is not a long-running process like a web server). So I changed suspend=n that IntelliJ suggested to suspend=y.
    • If you also want to debug tests in a test suite, you'll need to add the forkArgs line again in the appropriate object(s) in your build file. Or add it to a trait :)

    To do the debugging:

    • Run your program as usual with mill. It should print the line "Listening for transport dt_socket at address: 5005" and then pause.
    • Switch to IntelliJ and set your breakpoints.
    • Click the debug icon on the right end of IntelliJ's menu bar: debug button
    • Your program should run until it hits a breakpoint. Switch to the debugger and go to town!

    Addendum:

    It would be nice to be able to toggle the debug setting from the mill command line. So far, I have not figured out how to do that. Ideas welcome.