如何覆盖 SBT 中的任务 ``运行`` 和 ``运行Main`` 以使用我自己的 ``ForkOptions``?

How can I override tasks ``run`` and ``runMain`` in SBT to use my own ``ForkOptions``?

问题

在多模块构建中,每个模块都有自己的 baseDirectory 但我想启动模块中定义的应用程序,使用根项目的 baseDirectory 而不是 baseDirectory 相对涉及的模块。

这样,应用程序总是会从根文件夹中获取相对文件名,这是一种非常常见的模式。

问题是 ForkOptions 从模块强制执行 baseDirectory 并且显然没有简单的方法来更改它,因为 forkOptions 是私有的。我想传递一个 forkOptions 并填充根项目中的 baseDirectory

此外,还有包含两个或更多应用程序的模块。因此,我想为包含两个或更多应用程序的给定模块中的每个应用程序进行单独配置。

一个例子说明了1000多个字:

build.sbt

import sbt._
import Keys._

lazy val buildSettings: Seq[Setting[_]] = Defaults.defaultSettings
lazy val forkRunOptions: Seq[Setting[_]] = Seq(fork := true)

addCommandAlias("r1",       "ModuleA/RunnerR1:run")
addCommandAlias("r2",       "ModuleA/RunnerR2:run")

lazy val RunnerR1 = sbt.config("RunnerR1").extend(Compile)
lazy val RunnerR2 = sbt.config("RunnerR2").extend(Compile)

lazy val root =
  project
    .in(file("."))
    .settings(buildSettings:_*)
    .aggregate(ModuleA)

lazy val ModuleA =
  project
    .in(file("ModuleA"))
    .settings(buildSettings:_*)
    .configs(RunnerR1,RunnerR2)
    .settings(inConfig(RunnerR1)(
      forkRunOptions ++
        Seq(
          mainClass in Compile :=  Option("sbt.tests.issueX.Application1"))):_*)
    .settings(inConfig(RunnerR2)(
      forkRunOptions ++
        Seq(
          mainClass in Compile :=  Option("sbt.tests.issueX.Application2"))):_*)

在 SBT 控制台中,我希望这样:

> r1
This is Application1
> r2
This is Application2

但我看到了这个:

> r1
This is Application2
> r2
This is Application2

有什么收获?

不仅如此...SBT 正在 运行 申请中。这不是分叉他们。为什么 fork := true 没有生效?

说明

参见:https://github.com/frgomes/sbt-issue-2247

事实证明,配置并不像人们认为的那样工作。

问题是,在下面的代码片段中,配置 RunnerR1 没有像您预期的那样从模块 ModuleA 继承任务。因此,当您键入 r1r2(即:ModuleA/RunnerR1:runModuleA/RunnerR2:run)时,SBT 将使用委托算法来查找任务和设置,具体取决于如何这些任务和设置已定义,它最终将 运行 任务来自您不期望的范围,或者从您不期望的范围查找设置。

lazy val ModuleA =
  project
    .in(file("ModuleA"))
    .settings(buildSettings:_*)
    .configs(RunnerR1,RunnerR2)
    .settings(inConfig(RunnerR1)(
      forkRunOptions ++
        Seq(
          mainClass in Compile :=  Option("sbt.tests.issueX.Application1"))):_*)

此问题与可用性有关,因为 SBT 提供的 API 具有误导性。最终可以改进或更好地记录此模式,但它更像是一个可用性问题。

绕过难点

请在下方找到如何规避此问题的方法。

由于ForkOptions是私有的,我们必须尽可能提供我们自己的运行应用程序,它基于SBT代码。

简而言之,我们必须保证在所有配置中重新定义runrunMainrunner

import sbt._
import Keys._


//-------------------------------------------------------------
// This file contains a solution for the problem presented by
// https://github.com/sbt/sbt/issues/2247
//-------------------------------------------------------------


lazy val buildSettings: Seq[Setting[_]] = Defaults.defaultSettings ++ runSettings

lazy val runSettings: Seq[Setting[_]] =
  Seq(
    fork in (Compile, run) := true)


def forkRunOptions(s: Scope): Seq[Setting[_]] =
  Seq(
    // see: https://github.com/sbt/sbt/issues/2247
    // see: https://github.com/sbt/sbt/issues/2244
    runner in run in s := {
      val forkOptions: ForkOptions =
        ForkOptions(
          workingDirectory = Some((baseDirectory in ThisBuild).value),
          bootJars         = Nil,
          javaHome         = (javaHome       in s).value,
          connectInput     = (connectInput   in s).value,
          outputStrategy   = (outputStrategy in s).value,
          runJVMOptions    = (javaOptions    in s).value,
          envVars          = (envVars        in s).value)
      new {
        val fork_ = (fork in run).value
        val config: ForkOptions = forkOptions
      } with ScalaRun {
        override def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Option[String] =
          javaRunner(
            Option(mainClass), Option(classpath), options,
            Some("java"), Option(log), fork_,
            config.runJVMOptions, config.javaHome, config.workingDirectory, config.envVars, config.connectInput, config.outputStrategy)
      }
    },
    runner  in runMain in (s) := (runner in run in (s)).value,
    run     in (s) <<= Defaults.runTask    (fullClasspath in s, mainClass in run in s, runner in run in s),
    runMain in (s) <<= Defaults.runMainTask(fullClasspath in s,                        runner in runMain in s)
  )


def javaRunner(mainClass: Option[String] = None,
               classpath: Option[Seq[File]] = None,
               options: Seq[String],
               javaTool: Option[String] = None,
               log: Option[Logger] = None,
               fork: Boolean = false,
               jvmOptions: Seq[String] = Nil,
               javaHome: Option[File] = None,
               cwd: Option[File] = None,
               envVars: Map[String, String] = Map.empty,
               connectInput: Boolean = false,
               outputStrategy: Option[OutputStrategy] = Some(StdoutOutput)): Option[String] = {

  def runner(app: String,
             args: Seq[String],
             cwd: Option[File] = None,
             env: Map[String, String] = Map.empty): Int = {
    import scala.collection.JavaConverters._

    val cmd: Seq[String] = app +: args
    val pb = new java.lang.ProcessBuilder(cmd.asJava)
    if (cwd.isDefined) pb.directory(cwd.get)
    pb.inheritIO
    //FIXME: set environment
    val process = pb.start()
    if (fork) 0
    else {
      def cancel() = {
        if(log.isDefined) log.get.warn("Background process cancelled.")
        process.destroy()
        15
      }
      try process.waitFor catch {
        case e: InterruptedException => cancel()
      }
    }
  }

  val app: String = javaHome.fold("") { p => p.absolutePath + "/bin/" } + javaTool.getOrElse("java")
  val jvm: Seq[String] = jvmOptions.map(p => p.toString)
  val cp: Seq[String] =
    classpath
      .fold(Seq.empty[String]) { paths =>
        Seq(
          "-cp",
          paths
            .map(p => p.absolutePath)
            .mkString(java.io.File.pathSeparator))
      }
  val klass = mainClass.fold(Seq.empty[String]) { name => Seq(name) }
  val xargs: Seq[String] = jvm ++ cp ++ klass ++ options

  if(log.isDefined)
    if(fork) {
      log.get.info(s"Forking: ${app} " + xargs.mkString(" "))
    } else {
      log.get.info(s"Running: ${app} " + xargs.mkString(" "))
    }

  if (cwd.isDefined) IO.createDirectory(cwd.get)
  val exitCode = runner(app, xargs, cwd, envVars)
  if (exitCode == 0)
    None
  else
    Some("Nonzero exit code returned from " + app + ": " + exitCode)
}


addCommandAlias("r1",       "ModuleA/RunnerR1:run")
addCommandAlias("r2",       "ModuleA/RunnerR2:run")


lazy val RunnerR1 = sbt.config("RunnerR1").extend(Compile)
lazy val RunnerR2 = sbt.config("RunnerR2").extend(Compile)


lazy val root =
  project
    .in(file("."))
    .settings(buildSettings:_*)
    .aggregate(ModuleA)

lazy val ModuleA =
  project
    .in(file("ModuleA"))
    .settings(buildSettings:_*)
    .configs(RunnerR1,RunnerR2)
    .settings(inConfig(RunnerR1)(
      forkRunOptions(ThisScope) ++
        Seq(
          mainClass :=  Option("sbt.tests.issueX.Application1"))):_*)
    .settings(inConfig(RunnerR2)(
      forkRunOptions(ThisScope) ++
        Seq(
          mainClass :=  Option("sbt.tests.issueX.Application2"))):_*)