Search code examples
scalamill

How to toggle mill to build with debug options or not -- from the command line


Executing mill run with the following build file will start the program, pause for a debugger to connect, and then continue under the debugger's control.

import mill._, scalalib._

object foo extends RootModule with ScalaModule {
  def scalaVersion = "3.3.3"

  override def forkArgs = Seq("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005")

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

The problem is that I'd like to easily toggle between running with the debugger and running without. That is, I'd like to replace override def forkArgs... with

  override def forkArgs = if (someMagicIncantation) 
     Seq("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005")
   else
     Seq()

Ideally, someMagicIncantation would be connected to the command line so that mill withDebugger run or mill withDebugger test would connect with the debugger while just mill run would not.

Adding the following allows me to do mill debug on and mill debug off, writing the option string into out/debug.json.

  def debug(arg: String) = T.command {
    if (arg.trim.toLowerCase() == "on")
      "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005"
    else ""
  }

However, I don't know how to make progress from this because commands are not json readable. That is, I don't know how to suck that string back in on a subsequent run of mill.

Any suggestions, including completely different approaches (short of abandoning mill!) would be welcome.


Solution

  • Here's what I finally did for my project. This works for test and testOnly as well as run with minimal extra code (note the 1 line in the test object).

    The disadvantage is that it feels a little hackish because I have to read one of the cached files directly. I'd prefer to view those cache files as private data.

    Usage:

    • mill debug on: Subsequent runs of the program stop for a debugger to attach.
    • mill debug off: Subsequent runs of the program do not stop for the debugger.
    import mill._
    import scalalib._
    
    object foo extends RootModule with ScalaModule with DebugSupport {
      def scalaVersion = "3.3.3"
    
      object test extends ScalaTests {
        def ivyDeps           = Agg(ivy"com.lihaoyi::utest:0.7.11")
        def testFramework     = "utest.runner.Framework"
        override def forkArgs = foo.forkArgs 
      }
    
    }
    
    
    /** Support for attaching a debugger.  Toggle between running with and without a debugger with `mill debug on`
     * and `mill debug off`.  Nested objects (e.g. test) will need to have `override def forkArgs = foo.forkArgs`
     * for whatever value of `foo` you have as the top-level object. */
    trait DebugSupport extends JavaModule {
    
      override def forkArgs = T.input {
        val debugFile = os.pwd / "out" / "debug.json"
        val args      = if (os.isFile(debugFile)) ujson.read(os.read(debugFile))("value").str else ""
        if (args == "") super.forkArgs() else super.forkArgs() :+ args
      }
    
      /** Toggle between debugging on and debugging off. */
      def debug(arg: String) = T.command {
        if (arg.trim.toLowerCase() == "on")
          "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005"
        else ""
      }
    }