From 3a8a891d714bad8010a7b5d758dc7e2aa7f1205e Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Tue, 2 Sep 2025 03:51:01 -0400 Subject: [PATCH] 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. --- main/src/main/scala/sbt/Defaults.scala | 6 ++++- .../contraband-scala/sbt/ForkOptions.scala | 25 +++++++++++++------ run/src/main/contraband/run.contra | 3 +++ run/src/main/scala/sbt/Fork.scala | 8 +++--- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 6c53b78b5..fd49fdcc3 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1460,6 +1460,9 @@ object Defaults extends BuildCommon { } def forkOptionsTask: Initialize[Task[ForkOptions]] = Def.task { + val canUseArgumentsFile = sys.props + .getOrElse("java.vm.specification.version", "1") + .toFloat >= 9.0 ForkOptions( javaHome = javaHome.value, outputStrategy = outputStrategy.value, @@ -1468,7 +1471,8 @@ object Defaults extends BuildCommon { workingDirectory = Some(baseDirectory.value), runJVMOptions = javaOptions.value.toVector, connectInput = connectInput.value, - envVars = envVars.value + envVars = envVars.value, + canUseArgumentsFile = Some(canUseArgumentsFile) ) } diff --git a/run/src/main/contraband-scala/sbt/ForkOptions.scala b/run/src/main/contraband-scala/sbt/ForkOptions.scala index 6f083bc93..ec1009c58 100644 --- a/run/src/main/contraband-scala/sbt/ForkOptions.scala +++ b/run/src/main/contraband-scala/sbt/ForkOptions.scala @@ -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. 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 canUseArgumentsFile Use arguments file */ final class ForkOptions private ( val javaHome: Option[java.io.File], @@ -25,22 +26,24 @@ final class ForkOptions private ( val workingDirectory: Option[java.io.File], val runJVMOptions: Vector[String], 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 { - 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 }) 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 = { - "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 = { - new ForkOptions(javaHome, outputStrategy, bootJars, workingDirectory, runJVMOptions, connectInput, envVars) + 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, canUseArgumentsFile) } def withJavaHome(javaHome: Option[java.io.File]): ForkOptions = { copy(javaHome = javaHome) @@ -72,10 +75,18 @@ final class ForkOptions private ( def withEnvVars(envVars: scala.collection.immutable.Map[String, String]): ForkOptions = { copy(envVars = envVars) } + def withCanUseArgumentsFile(canUseArgumentsFile: Option[Boolean]): ForkOptions = { + copy(canUseArgumentsFile = canUseArgumentsFile) + } + def withCanUseArgumentsFile(canUseArgumentsFile: Boolean): ForkOptions = { + copy(canUseArgumentsFile = Option(canUseArgumentsFile)) + } } object 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: 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)) } diff --git a/run/src/main/contraband/run.contra b/run/src/main/contraband/run.contra index 0cb61c7c3..a38e6df11 100644 --- a/run/src/main/contraband/run.contra +++ b/run/src/main/contraband/run.contra @@ -27,4 +27,7 @@ type ForkOptions { ## The environment variables to provide to the forked process. By default, none are provided. envVars: StringStringMap! = raw"Map()" @since("0.1.0") + + ## Use arguments file + canUseArgumentsFile: Boolean @since("1.11.6") } diff --git a/run/src/main/scala/sbt/Fork.scala b/run/src/main/scala/sbt/Fork.scala index 07fc33924..cb4f664d0 100644 --- a/run/src/main/scala/sbt/Fork.scala +++ b/run/src/main/scala/sbt/Fork.scala @@ -49,7 +49,9 @@ final class Fork(val commandName: String, val runnerClass: Option[String]) { val (classpathEnv, options) = Fork.fitClasspath(preOptions) val command = executable +: options 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)) else new JProcessBuilder(command.toArray: _*) @@ -137,9 +139,7 @@ object Fork { * - the command line length would exceed MaxConcatenatedOptionLength */ private def shouldUseArgumentsFile(options: Seq[String]): Boolean = - (sys.props.getOrElse("java.vm.specification.version", "1").toFloat >= 9.0) && - booleanOpt("sbt.argsfile").getOrElse(true) && - (options.mkString.length > MaxConcatenatedOptionLength) + options.mkString.length > MaxConcatenatedOptionLength /** * Create an arguments file from a sequence of command line arguments