fix: Fixes client-side run

**Problem**
Client-side run currently fails on JDK 8 because sbtn
creates args file even though JDK 8 does not support it.
This is likely because sbtn is compiled using GraalVM on a modern JDK.

**Solution**
This adds a new fork option canUseArgumentsFile to delegate the args file decision
to the server, and default to false if the value is missing.
This retroactively fixes sbt 2.x client-side run.
This commit is contained in:
Eugene Yokota 2025-09-02 03:51:01 -04:00
parent b5ab0e137e
commit 3a8a891d71
4 changed files with 30 additions and 12 deletions

View File

@ -1460,6 +1460,9 @@ object Defaults extends BuildCommon {
} }
def forkOptionsTask: Initialize[Task[ForkOptions]] = def forkOptionsTask: Initialize[Task[ForkOptions]] =
Def.task { Def.task {
val canUseArgumentsFile = sys.props
.getOrElse("java.vm.specification.version", "1")
.toFloat >= 9.0
ForkOptions( ForkOptions(
javaHome = javaHome.value, javaHome = javaHome.value,
outputStrategy = outputStrategy.value, outputStrategy = outputStrategy.value,
@ -1468,7 +1471,8 @@ object Defaults extends BuildCommon {
workingDirectory = Some(baseDirectory.value), workingDirectory = Some(baseDirectory.value),
runJVMOptions = javaOptions.value.toVector, runJVMOptions = javaOptions.value.toVector,
connectInput = connectInput.value, connectInput = connectInput.value,
envVars = envVars.value envVars = envVars.value,
canUseArgumentsFile = Some(canUseArgumentsFile)
) )
} }

View File

@ -17,6 +17,7 @@ package sbt
* @param connectInput If true, the standard input of the forked process is connected to the standard input of this process. Otherwise, it is connected to an empty input stream. * @param connectInput If true, the standard input of the forked process is connected to the standard input of this process. Otherwise, it is connected to an empty input stream.
Connecting input streams can be problematic, especially on versions before Java 7. Connecting input streams can be problematic, especially on versions before Java 7.
* @param envVars The environment variables to provide to the forked process. By default, none are provided. * @param envVars The environment variables to provide to the forked process. By default, none are provided.
* @param canUseArgumentsFile Use arguments file
*/ */
final class ForkOptions private ( final class ForkOptions private (
val javaHome: Option[java.io.File], val javaHome: Option[java.io.File],
@ -25,22 +26,24 @@ final class ForkOptions private (
val workingDirectory: Option[java.io.File], val workingDirectory: Option[java.io.File],
val runJVMOptions: Vector[String], val runJVMOptions: Vector[String],
val connectInput: Boolean, val connectInput: Boolean,
val envVars: scala.collection.immutable.Map[String, String]) extends Serializable { val envVars: scala.collection.immutable.Map[String, String],
val canUseArgumentsFile: Option[Boolean]) extends Serializable {
private def this() = this(None, None, Vector(), None, Vector(), false, Map()) private def this() = this(None, None, Vector(), None, Vector(), false, Map(), None)
private def this(javaHome: Option[java.io.File], outputStrategy: Option[sbt.OutputStrategy], bootJars: Vector[java.io.File], workingDirectory: Option[java.io.File], runJVMOptions: Vector[String], connectInput: Boolean, envVars: scala.collection.immutable.Map[String, String]) = this(javaHome, outputStrategy, bootJars, workingDirectory, runJVMOptions, connectInput, envVars, None)
override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match {
case x: ForkOptions => (this.javaHome == x.javaHome) && (this.outputStrategy == x.outputStrategy) && (this.bootJars == x.bootJars) && (this.workingDirectory == x.workingDirectory) && (this.runJVMOptions == x.runJVMOptions) && (this.connectInput == x.connectInput) && (this.envVars == x.envVars) case x: ForkOptions => (this.javaHome == x.javaHome) && (this.outputStrategy == x.outputStrategy) && (this.bootJars == x.bootJars) && (this.workingDirectory == x.workingDirectory) && (this.runJVMOptions == x.runJVMOptions) && (this.connectInput == x.connectInput) && (this.envVars == x.envVars) && (this.canUseArgumentsFile == x.canUseArgumentsFile)
case _ => false case _ => false
}) })
override def hashCode: Int = { override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.ForkOptions".##) + javaHome.##) + outputStrategy.##) + bootJars.##) + workingDirectory.##) + runJVMOptions.##) + connectInput.##) + envVars.##) 37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.ForkOptions".##) + javaHome.##) + outputStrategy.##) + bootJars.##) + workingDirectory.##) + runJVMOptions.##) + connectInput.##) + envVars.##) + canUseArgumentsFile.##)
} }
override def toString: String = { override def toString: String = {
"ForkOptions(" + javaHome + ", " + outputStrategy + ", " + bootJars + ", " + workingDirectory + ", " + runJVMOptions + ", " + connectInput + ", " + envVars + ")" "ForkOptions(" + javaHome + ", " + outputStrategy + ", " + bootJars + ", " + workingDirectory + ", " + runJVMOptions + ", " + connectInput + ", " + envVars + ", " + canUseArgumentsFile + ")"
} }
private[this] def copy(javaHome: Option[java.io.File] = javaHome, outputStrategy: Option[sbt.OutputStrategy] = outputStrategy, bootJars: Vector[java.io.File] = bootJars, workingDirectory: Option[java.io.File] = workingDirectory, runJVMOptions: Vector[String] = runJVMOptions, connectInput: Boolean = connectInput, envVars: scala.collection.immutable.Map[String, String] = envVars): ForkOptions = { private[this] def copy(javaHome: Option[java.io.File] = javaHome, outputStrategy: Option[sbt.OutputStrategy] = outputStrategy, bootJars: Vector[java.io.File] = bootJars, workingDirectory: Option[java.io.File] = workingDirectory, runJVMOptions: Vector[String] = runJVMOptions, connectInput: Boolean = connectInput, envVars: scala.collection.immutable.Map[String, String] = envVars, canUseArgumentsFile: Option[Boolean] = canUseArgumentsFile): ForkOptions = {
new ForkOptions(javaHome, outputStrategy, bootJars, workingDirectory, runJVMOptions, connectInput, envVars) new ForkOptions(javaHome, outputStrategy, bootJars, workingDirectory, runJVMOptions, connectInput, envVars, canUseArgumentsFile)
} }
def withJavaHome(javaHome: Option[java.io.File]): ForkOptions = { def withJavaHome(javaHome: Option[java.io.File]): ForkOptions = {
copy(javaHome = javaHome) copy(javaHome = javaHome)
@ -72,10 +75,18 @@ final class ForkOptions private (
def withEnvVars(envVars: scala.collection.immutable.Map[String, String]): ForkOptions = { def withEnvVars(envVars: scala.collection.immutable.Map[String, String]): ForkOptions = {
copy(envVars = envVars) copy(envVars = envVars)
} }
def withCanUseArgumentsFile(canUseArgumentsFile: Option[Boolean]): ForkOptions = {
copy(canUseArgumentsFile = canUseArgumentsFile)
}
def withCanUseArgumentsFile(canUseArgumentsFile: Boolean): ForkOptions = {
copy(canUseArgumentsFile = Option(canUseArgumentsFile))
}
} }
object ForkOptions { object ForkOptions {
def apply(): ForkOptions = new ForkOptions() def apply(): ForkOptions = new ForkOptions()
def apply(javaHome: Option[java.io.File], outputStrategy: Option[sbt.OutputStrategy], bootJars: Vector[java.io.File], workingDirectory: Option[java.io.File], runJVMOptions: Vector[String], connectInput: Boolean, envVars: scala.collection.immutable.Map[String, String]): ForkOptions = new ForkOptions(javaHome, outputStrategy, bootJars, workingDirectory, runJVMOptions, connectInput, envVars) def apply(javaHome: Option[java.io.File], outputStrategy: Option[sbt.OutputStrategy], bootJars: Vector[java.io.File], workingDirectory: Option[java.io.File], runJVMOptions: Vector[String], connectInput: Boolean, envVars: scala.collection.immutable.Map[String, String]): ForkOptions = new ForkOptions(javaHome, outputStrategy, bootJars, workingDirectory, runJVMOptions, connectInput, envVars)
def apply(javaHome: java.io.File, outputStrategy: sbt.OutputStrategy, bootJars: Vector[java.io.File], workingDirectory: java.io.File, runJVMOptions: Vector[String], connectInput: Boolean, envVars: scala.collection.immutable.Map[String, String]): ForkOptions = new ForkOptions(Option(javaHome), Option(outputStrategy), bootJars, Option(workingDirectory), runJVMOptions, connectInput, envVars) def apply(javaHome: java.io.File, outputStrategy: sbt.OutputStrategy, bootJars: Vector[java.io.File], workingDirectory: java.io.File, runJVMOptions: Vector[String], connectInput: Boolean, envVars: scala.collection.immutable.Map[String, String]): ForkOptions = new ForkOptions(Option(javaHome), Option(outputStrategy), bootJars, Option(workingDirectory), runJVMOptions, connectInput, envVars)
def apply(javaHome: Option[java.io.File], outputStrategy: Option[sbt.OutputStrategy], bootJars: Vector[java.io.File], workingDirectory: Option[java.io.File], runJVMOptions: Vector[String], connectInput: Boolean, envVars: scala.collection.immutable.Map[String, String], canUseArgumentsFile: Option[Boolean]): ForkOptions = new ForkOptions(javaHome, outputStrategy, bootJars, workingDirectory, runJVMOptions, connectInput, envVars, canUseArgumentsFile)
def apply(javaHome: java.io.File, outputStrategy: sbt.OutputStrategy, bootJars: Vector[java.io.File], workingDirectory: java.io.File, runJVMOptions: Vector[String], connectInput: Boolean, envVars: scala.collection.immutable.Map[String, String], canUseArgumentsFile: Boolean): ForkOptions = new ForkOptions(Option(javaHome), Option(outputStrategy), bootJars, Option(workingDirectory), runJVMOptions, connectInput, envVars, Option(canUseArgumentsFile))
} }

View File

@ -27,4 +27,7 @@ type ForkOptions {
## The environment variables to provide to the forked process. By default, none are provided. ## The environment variables to provide to the forked process. By default, none are provided.
envVars: StringStringMap! = raw"Map()" @since("0.1.0") envVars: StringStringMap! = raw"Map()" @since("0.1.0")
## Use arguments file
canUseArgumentsFile: Boolean @since("1.11.6")
} }

View File

@ -49,7 +49,9 @@ final class Fork(val commandName: String, val runnerClass: Option[String]) {
val (classpathEnv, options) = Fork.fitClasspath(preOptions) val (classpathEnv, options) = Fork.fitClasspath(preOptions)
val command = executable +: options val command = executable +: options
val jpb = val jpb =
if (Fork.shouldUseArgumentsFile(options)) if (config.canUseArgumentsFile.getOrElse(false) &&
Fork.booleanOpt("sbt.argsfile").getOrElse(true) &&
Fork.shouldUseArgumentsFile(options))
new JProcessBuilder(executable, Fork.createArgumentsFile(options)) new JProcessBuilder(executable, Fork.createArgumentsFile(options))
else else
new JProcessBuilder(command.toArray: _*) new JProcessBuilder(command.toArray: _*)
@ -137,9 +139,7 @@ object Fork {
* - the command line length would exceed MaxConcatenatedOptionLength * - the command line length would exceed MaxConcatenatedOptionLength
*/ */
private def shouldUseArgumentsFile(options: Seq[String]): Boolean = private def shouldUseArgumentsFile(options: Seq[String]): Boolean =
(sys.props.getOrElse("java.vm.specification.version", "1").toFloat >= 9.0) && options.mkString.length > MaxConcatenatedOptionLength
booleanOpt("sbt.argsfile").getOrElse(true) &&
(options.mkString.length > MaxConcatenatedOptionLength)
/** /**
* Create an arguments file from a sequence of command line arguments * Create an arguments file from a sequence of command line arguments