diff --git a/main/src/main/scala/sbt/ScriptedPlugin.scala b/main/src/main/scala/sbt/ScriptedPlugin.scala index f4822c3fb..35deb1104 100644 --- a/main/src/main/scala/sbt/ScriptedPlugin.scala +++ b/main/src/main/scala/sbt/ScriptedPlugin.scala @@ -8,7 +8,6 @@ package sbt import java.io.File -import java.lang.reflect.Method import sbt.Def._ import sbt.Keys._ @@ -47,7 +46,7 @@ object ScriptedPlugin extends AutoPlugin { val scriptedParallelInstances = settingKey[Int]( "Configures the number of scripted instances for parallel testing, only used in batch mode." ) - val scriptedRun = taskKey[Method]("") + val scriptedRun = taskKey[ScriptedRun]("") val scriptedLaunchOpts = settingKey[Seq[String]]("options to pass to jvm launching scripted tasks") val scriptedDependencies = taskKey[Unit]("") @@ -114,21 +113,8 @@ object ScriptedPlugin extends AutoPlugin { } } - private[sbt] def scriptedRunTask: Initialize[Task[Method]] = Def.taskDyn { - val fCls = classOf[File] - val bCls = classOf[Boolean] - val asCls = classOf[Array[String]] - val lfCls = classOf[java.util.List[File]] - val iCls = classOf[Int] - - val clazz = scriptedTests.value.getClass - val method = - if (scriptedBatchExecution.value) - clazz.getMethod("runInParallel", fCls, bCls, asCls, fCls, asCls, lfCls, iCls) - else - clazz.getMethod("run", fCls, bCls, asCls, fCls, asCls, lfCls) - - Def.task(method) + private[sbt] def scriptedRunTask: Initialize[Task[ScriptedRun]] = Def.task { + ScriptedRun.of(scriptedTests.value, scriptedBatchExecution.value) } private[sbt] final case class ScriptedTestPage(page: Int, total: Int) @@ -191,21 +177,16 @@ object ScriptedPlugin extends AutoPlugin { private[sbt] def scriptedTask: Initialize[InputTask[Unit]] = Def.inputTask { val args = scriptedParser(sbtTestDirectory.value).parsed Def.unit(scriptedDependencies.value) - try { - val method = scriptedRun.value - val scriptedInstance = scriptedTests.value - val dir = sbtTestDirectory.value - val log = Boolean box scriptedBufferLog.value - val launcher = sbtLauncher.value - val opts = scriptedLaunchOpts.value.toArray - val empty = new java.util.ArrayList[File]() - val instances = Int box scriptedParallelInstances.value - - if (scriptedBatchExecution.value) - method.invoke(scriptedInstance, dir, log, args.toArray, launcher, opts, empty, instances) - else method.invoke(scriptedInstance, dir, log, args.toArray, launcher, opts, empty) - () - } catch { case e: java.lang.reflect.InvocationTargetException => throw e.getCause } + scriptedRun.value.run( + sbtTestDirectory.value, + scriptedBufferLog.value, + args, + sbtLauncher.value, + Fork.javaCommand((scripted / javaHome).value, "java").getAbsolutePath, + scriptedLaunchOpts.value, + new java.util.ArrayList[File](), + scriptedParallelInstances.value + ) } private[this] def getJars(config: Configuration): Initialize[Task[PathFinder]] = Def.task { diff --git a/main/src/main/scala/sbt/ScriptedRun.scala b/main/src/main/scala/sbt/ScriptedRun.scala new file mode 100644 index 000000000..ea7d7054c --- /dev/null +++ b/main/src/main/scala/sbt/ScriptedRun.scala @@ -0,0 +1,179 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt + +import java.io.File +import java.lang.reflect.Method +import scala.annotation.unused + +sealed trait ScriptedRun { + final def run( + resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Seq[String], + launcherJar: File, + javaCommand: String, + launchOpts: Seq[String], + prescripted: java.util.List[File], + instances: Int, + ): Unit = { + try { + invoke( + resourceBaseDirectory, + bufferLog, + tests.toArray, + launcherJar, + javaCommand, + launchOpts.toArray, + prescripted, + instances, + ) + () + } catch { case e: java.lang.reflect.InvocationTargetException => throw e.getCause } + } + + protected def invoke( + resourceBaseDirectory: File, + bufferLog: java.lang.Boolean, + tests: Array[String], + launcherJar: File, + javaCommand: String, + launchOpts: Array[String], + prescripted: java.util.List[File], + instances: java.lang.Integer, + ): AnyRef + +} + +object ScriptedRun { + + def of(scriptedTests: AnyRef, batchExecution: Boolean): ScriptedRun = { + val fCls = classOf[File] + val bCls = classOf[Boolean] + val asCls = classOf[Array[String]] + val sCls = classOf[String] + val lfCls = classOf[java.util.List[File]] + val iCls = classOf[Int] + + val clazz = scriptedTests.getClass + if (batchExecution) + try new RunInParallelV2( + scriptedTests, + clazz.getMethod("runInParallel", fCls, bCls, asCls, fCls, sCls, asCls, lfCls, iCls) + ) + catch { + case _: NoSuchMethodException => + new RunInParallelV1( + scriptedTests, + clazz.getMethod("runInParallel", fCls, bCls, asCls, fCls, asCls, lfCls, iCls) + ) + } + else + try new RunV2( + scriptedTests, + clazz.getMethod("run", fCls, bCls, asCls, fCls, sCls, asCls, lfCls) + ) + catch { + case _: NoSuchMethodException => + new RunV1(scriptedTests, clazz.getMethod("run", fCls, bCls, asCls, fCls, asCls, lfCls)) + } + } + + private class RunV1(scriptedTests: AnyRef, run: Method) extends ScriptedRun { + override protected def invoke( + resourceBaseDirectory: File, + bufferLog: java.lang.Boolean, + tests: Array[String], + launcherJar: File, + @unused javaCommand: String, + launchOpts: Array[String], + prescripted: java.util.List[File], + @unused instances: java.lang.Integer, + ): AnyRef = + run.invoke( + scriptedTests, + resourceBaseDirectory, + bufferLog, + tests, + launcherJar, + launchOpts, + prescripted, + ) + } + + private class RunInParallelV1(scriptedTests: AnyRef, runInParallel: Method) extends ScriptedRun { + override protected def invoke( + resourceBaseDirectory: File, + bufferLog: java.lang.Boolean, + tests: Array[String], + launcherJar: File, + @unused javaCommand: String, + launchOpts: Array[String], + prescripted: java.util.List[File], + instances: Integer, + ): AnyRef = + runInParallel.invoke( + scriptedTests, + resourceBaseDirectory, + bufferLog, + tests, + launcherJar, + launchOpts, + prescripted, + instances, + ) + } + + private class RunV2(scriptedTests: AnyRef, run: Method) extends ScriptedRun { + override protected def invoke( + resourceBaseDirectory: File, + bufferLog: java.lang.Boolean, + tests: Array[String], + launcherJar: File, + javaCommand: String, + launchOpts: Array[String], + prescripted: java.util.List[File], + @unused instances: java.lang.Integer, + ): AnyRef = + run.invoke( + scriptedTests, + resourceBaseDirectory, + bufferLog, + tests, + launcherJar, + javaCommand, + launchOpts, + prescripted, + ) + } + + private class RunInParallelV2(scriptedTests: AnyRef, runInParallel: Method) extends ScriptedRun { + override protected def invoke( + resourceBaseDirectory: File, + bufferLog: java.lang.Boolean, + tests: Array[String], + launcherJar: File, + javaCommand: String, + launchOpts: Array[String], + prescripted: java.util.List[File], + instances: Integer, + ): AnyRef = + runInParallel.invoke( + scriptedTests, + resourceBaseDirectory, + bufferLog, + tests, + launcherJar, + javaCommand, + launchOpts, + prescripted, + instances, + ) + } + +} diff --git a/notes/1.6.0/configure-scripted-java-home.md b/notes/1.6.0/configure-scripted-java-home.md new file mode 100644 index 000000000..f2ace91d8 --- /dev/null +++ b/notes/1.6.0/configure-scripted-java-home.md @@ -0,0 +1,14 @@ +[@kxbmap]: https://github.com/kxbmap + +[#6673]: https://github.com/sbt/sbt/pull/6673 + +### Improvements + +- Make javaHome that forks scripted tests configurable. [#6673][] by [@kxbmap][] + - Normally scripted tests are forked using the JVM that is running sbt. If set `scripted / javaHome`, forked using it. + - Or use `java++` command before scripted. + +### Fixes with compatibility implications + +- Change type of `scriptedRun` task key from `TaskKey[java.lang.reflect.Method]` to `TaskKey[sbt.ScriptedRun]` + - `sbt.ScriptedRun` is a new interface for hiding substance of scripted invocation. diff --git a/run/src/main/scala/sbt/Fork.scala b/run/src/main/scala/sbt/Fork.scala index 9745c28a6..b2cb449d0 100644 --- a/run/src/main/scala/sbt/Fork.scala +++ b/run/src/main/scala/sbt/Fork.scala @@ -114,7 +114,7 @@ object Fork { (classpathOption, newOptions) } - private def javaCommand(javaHome: Option[File], name: String): File = { + private[sbt] def javaCommand(javaHome: Option[File], name: String): File = { val home = javaHome.getOrElse(new File(System.getProperty("java.home"))) new File(new File(home, "bin"), name) } diff --git a/sbt-app/src/sbt-test/project/scripted-java-home/build.sbt b/sbt-app/src/sbt-test/project/scripted-java-home/build.sbt new file mode 100644 index 000000000..207694da1 --- /dev/null +++ b/sbt-app/src/sbt-test/project/scripted-java-home/build.sbt @@ -0,0 +1,23 @@ +lazy val scriptedJavaVersion = settingKey[Long]("") + +lazy val root = (project in file(".")) + .enablePlugins(SbtPlugin) + .settings( + scriptedJavaVersion := { + val versions = discoveredJavaHomes.value + .map { case (jv, _) => JavaVersion(jv).numbers } + .collect { + case Vector(1L, ver, _*) => ver + case Vector(ver, _*) => ver + } + if (versions.isEmpty) sys.error("No Java versions discovered") + else versions.max + }, + commands += Command.command("setJavaVersion") { state => + val extracted = Project.extract(state) + import extracted._ + val jv = (currentRef / scriptedJavaVersion).get(structure.data).get + s"java++ $jv!" :: state + }, + scriptedLaunchOpts += s"-Dscripted.java.version=${scriptedJavaVersion.value}" + ) diff --git a/sbt-app/src/sbt-test/project/scripted-java-home/changes/build.sbt b/sbt-app/src/sbt-test/project/scripted-java-home/changes/build.sbt new file mode 100644 index 000000000..c72b1c31d --- /dev/null +++ b/sbt-app/src/sbt-test/project/scripted-java-home/changes/build.sbt @@ -0,0 +1,13 @@ +lazy val check = taskKey[Unit]("check") + +lazy val root = (project in file(".")) + .settings( + check := { + val version = sys.props("java.version").stripPrefix("1.").takeWhile(_.isDigit) + val expected = sys.props("scripted.java.version") + assert( + version == expected, + s"Expected Java version is '$expected', but actual is '$version'" + ) + } + ) diff --git a/sbt-app/src/sbt-test/project/scripted-java-home/changes/test b/sbt-app/src/sbt-test/project/scripted-java-home/changes/test new file mode 100644 index 000000000..15675b169 --- /dev/null +++ b/sbt-app/src/sbt-test/project/scripted-java-home/changes/test @@ -0,0 +1 @@ +> check diff --git a/sbt-app/src/sbt-test/project/scripted-java-home/test b/sbt-app/src/sbt-test/project/scripted-java-home/test new file mode 100644 index 000000000..c41dd3978 --- /dev/null +++ b/sbt-app/src/sbt-test/project/scripted-java-home/test @@ -0,0 +1,5 @@ +$ copy-file changes/build.sbt src/sbt-test/group/name/build.sbt +$ copy-file changes/test src/sbt-test/group/name/test + +> setJavaVersion +> scripted diff --git a/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/RemoteSbtCreator.scala b/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/RemoteSbtCreator.scala index 41044871f..b35b6a396 100644 --- a/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/RemoteSbtCreator.scala +++ b/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/RemoteSbtCreator.scala @@ -27,14 +27,22 @@ final class LauncherBasedRemoteSbtCreator( directory: File, launcher: File, log: Logger, - launchOpts: Seq[String] = Nil, + javaCommand: String, + launchOpts: Seq[String], ) extends RemoteSbtCreator { - def newRemote(server: IPC.Server) = { + def this( + directory: File, + launcher: File, + log: Logger, + launchOpts: Seq[String] = Nil, + ) = this(directory, launcher, log, "java", launchOpts) + + def newRemote(server: IPC.Server): Process = { val launcherJar = launcher.getAbsolutePath val globalBase = "-Dsbt.global.base=" + (new File(directory, "global")).getAbsolutePath val scripted = "-Dsbt.scripted=true" val args = List("<" + server.port) - val cmd = "java" :: launchOpts.toList ::: globalBase :: scripted :: "-jar" :: launcherJar :: args ::: Nil + val cmd = javaCommand :: launchOpts.toList ::: globalBase :: scripted :: "-jar" :: launcherJar :: args ::: Nil val io = BasicIO(false, log).withInput(_.close()) val p = Process(cmd, directory) run (io) val thread = new Thread() { override def run() = { p.exitValue(); server.close() } } @@ -46,11 +54,21 @@ final class LauncherBasedRemoteSbtCreator( final class RunFromSourceBasedRemoteSbtCreator( directory: File, log: Logger, - launchOpts: Seq[String] = Nil, + javaCommand: String, + launchOpts: Seq[String], scalaVersion: String, sbtVersion: String, classpath: Seq[File], ) extends RemoteSbtCreator { + def this( + directory: File, + log: Logger, + launchOpts: Seq[String] = Nil, + scalaVersion: String, + sbtVersion: String, + classpath: Seq[File], + ) = this(directory, log, "java", launchOpts, scalaVersion, sbtVersion, classpath) + def newRemote(server: IPC.Server): Process = { val globalBase = "-Dsbt.global.base=" + new File(directory, "global").getAbsolutePath val scripted = "-Dsbt.scripted=true" @@ -58,7 +76,7 @@ final class RunFromSourceBasedRemoteSbtCreator( val cpString = classpath.mkString(java.io.File.pathSeparator) val args = List(mainClassName, directory.toString, scalaVersion, sbtVersion, cpString, "<" + server.port) - val cmd = "java" :: launchOpts.toList ::: globalBase :: scripted :: "-cp" :: cpString :: args ::: Nil + val cmd = javaCommand :: launchOpts.toList ::: globalBase :: scripted :: "-cp" :: cpString :: args ::: Nil val io = BasicIO(false, log).withInput(_.close()) val p = Process(cmd, directory) run (io) val thread = new Thread() { override def run() = { p.exitValue(); server.close() } } diff --git a/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/ScriptedTests.scala b/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/ScriptedTests.scala index cec073175..5a836b885 100644 --- a/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/ScriptedTests.scala +++ b/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/ScriptedTests.scala @@ -26,8 +26,15 @@ import scala.util.control.NonFatal final class ScriptedTests( resourceBaseDirectory: File, bufferLog: Boolean, + javaCommand: String, launchOpts: Seq[String], ) { + def this( + resourceBaseDirectory: File, + bufferLog: Boolean, + launchOpts: Seq[String], + ) = this(resourceBaseDirectory, bufferLog, "java", launchOpts) + import ScriptedTests.TestRunner private val testResources = new Resources(resourceBaseDirectory) @@ -80,11 +87,12 @@ final class ScriptedTests( val remoteSbtCreator = prop match { case LauncherBased(launcherJar) => - new LauncherBasedRemoteSbtCreator(testDir, launcherJar, buffered, launchOpts) + new LauncherBasedRemoteSbtCreator(testDir, launcherJar, buffered, javaCommand, launchOpts) case RunFromSourceBased(scalaVersion, sbtVersion, classpath) => new RunFromSourceBasedRemoteSbtCreator( testDir, buffered, + javaCommand, launchOpts, scalaVersion, sbtVersion, @@ -383,6 +391,7 @@ class ScriptedRunner { bufferLog, tests, logger, + javaCommand = "java", launchOpts, prescripted = new java.util.ArrayList[File], LauncherBased(launcherJar), @@ -411,6 +420,36 @@ class ScriptedRunner { bufferLog, tests, logger, + javaCommand = "java", + launchOpts, + prescripted, + LauncherBased(launcherJar), + Int.MaxValue, + parallelExecution = false, + ) + } + + /** + * This is the entry point used by SbtPlugin in sbt 2.0.x etc. + * Removing this method will break scripted and sbt plugin cross building. + * See https://github.com/sbt/sbt/issues/3245 + */ + def run( + resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Array[String], + launcherJar: File, + javaCommand: String, + launchOpts: Array[String], + prescripted: java.util.List[File], + ): Unit = { + val logger = TestConsoleLogger() + run( + resourceBaseDirectory, + bufferLog, + tests, + logger, + javaCommand, launchOpts, prescripted, LauncherBased(launcherJar), @@ -440,6 +479,36 @@ class ScriptedRunner { bufferLog, tests, logger, + javaCommand = "java", + launchOpts, + prescripted, + LauncherBased(launcherJar), + instance, + ) + } + + /** + * This is the entry point used by SbtPlugin in sbt 2.0.x etc. + * Removing this method will break scripted and sbt plugin cross building. + * See https://github.com/sbt/sbt/issues/3245 + */ + def runInParallel( + resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Array[String], + launcherJar: File, + javaCommand: String, + launchOpts: Array[String], + prescripted: java.util.List[File], + instance: Int, + ): Unit = { + val logger = TestConsoleLogger() + runInParallel( + resourceBaseDirectory, + bufferLog, + tests, + logger, + javaCommand, launchOpts, prescripted, LauncherBased(launcherJar), @@ -466,6 +535,7 @@ class ScriptedRunner { bufferLog, tests, logger, + javaCommand = "java", launchOpts, prescripted, RunFromSourceBased(scalaVersion, sbtVersion, classpath), @@ -477,11 +547,24 @@ class ScriptedRunner { bufferLog: Boolean, tests: Array[String], logger: Logger, + javaCommand: String, launchOpts: Array[String], prescripted: java.util.List[File], prop: RemoteSbtCreatorProp, instances: Int - ) = run(baseDir, bufferLog, tests, logger, launchOpts, prescripted, prop, instances, true) + ): Unit = + run( + baseDir, + bufferLog, + tests, + logger, + javaCommand, + launchOpts, + prescripted, + prop, + instances, + parallelExecution = true + ) @nowarn private[this] def run( @@ -489,6 +572,7 @@ class ScriptedRunner { bufferLog: Boolean, tests: Array[String], logger: Logger, + javaCommand: String, launchOpts: Array[String], prescripted: java.util.List[File], prop: RemoteSbtCreatorProp, @@ -496,7 +580,7 @@ class ScriptedRunner { parallelExecution: Boolean, ): Unit = { val addTestFile = (f: File) => { prescripted.add(f); () } - val runner = new ScriptedTests(baseDir, bufferLog, launchOpts) + val runner = new ScriptedTests(baseDir, bufferLog, javaCommand, launchOpts) val sbtVersion = prop match { case LauncherBased(launcherJar) =>