如何在SBT中覆盖任务``run``和``runMain``以使用我自己的``ForkOptions``?

时间:2015-10-20 18:20:25

标签: scala sbt multi-module

问题

在多模块构建中,每个模块都拥有它自己的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没有起作用?

1 个答案:

答案 0 :(得分:1)

说明

请参阅: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"))):_*)