This commit is contained in:
Jozef Koval 2026-07-14 18:29:25 +00:00 committed by GitHub
commit 9125887856
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 591 additions and 148 deletions

View File

@ -0,0 +1,36 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package example.test
import java.io.File
/**
* First-hop propagation guard: an explicit `-java-home` must be the JDK the Windows launcher runs
* (echoed on the verbose command line). Not the full sbt/sbt#963 regression the
* `.java-version`-override case needs two JDK paths + the native client, so it is verified manually
* on Windows.
*/
object JavaHomeScriptTest extends verify.BasicTestSuite with ShellScriptUtil:
private val jdkHome =
new File(sys.env.getOrElse("JAVA_HOME", System.getProperty("java.home"))).getAbsolutePath
testOutput("sbt -java-home selects the JDK for the launcher")(
"-java-home",
jdkHome,
"compile",
"-v"
): (out: List[String]) =>
if !isWindows then cancel("`-java-home` bin/java.exe selection is Windows-specific")
else
val javaExe = new File(new File(jdkHome, "bin"), "java.exe").getAbsolutePath
assert(
out.exists(_.contains(javaExe)),
s"launcher should run $javaExe; command echo was: ${out.mkString(" | ")}"
)
end JavaHomeScriptTest

View File

@ -87,7 +87,8 @@ if exist "!SBT_CONFIG!" (
)
rem poor man's jenv (which is not available on Windows)
if defined JAVA_HOMES (
rem explicit -java-home wins over the project .java-version
if not defined SBT_EXPLICIT_JAVA_HOME if defined JAVA_HOMES (
if exist .java-version for /F %%A in (.java-version) do (
set JAVA_HOME=%JAVA_HOMES%\%%A
set JDK_HOME=%JAVA_HOMES%\%%A
@ -503,8 +504,11 @@ if defined _java_home_arg (
if not "%~1" == "" (
if exist "%~1\bin\java.exe" (
set "_JAVACMD=%~1\bin\java.exe"
set "JAVACMD=%~1\bin\java.exe"
set "JAVA_HOME=%~1"
set "JDK_HOME=%~1"
set "SBT_EXPLICIT_JAVA_HOME=1"
set "PATH=%~1\bin;!PATH!"
shift
goto args_loop
) else (
@ -786,6 +790,9 @@ if defined sbt_args_verbose (
echo.
)
rem one-hop marker: drop it before the server JVM
set "SBT_EXPLICIT_JAVA_HOME="
"!_JAVACMD!" !_JAVA_OPTS! !_SBT_OPTS! %JAVA_TOOL_OPTIONS% %JDK_JAVA_OPTIONS% -cp "!sbt_jar!" xsbt.boot.Boot %*
goto :eof

View File

@ -4,56 +4,89 @@ import scopt.OParser
object ArgParser:
def parse(args: Array[String]): Option[LauncherOptions] =
private val parser =
val b = OParser.builder[LauncherOptions]
val parser =
import b.*
OParser.sequence(
programName("sbtw"),
head("sbtw", "Windows launcher for sbt"),
opt[Unit]('h', "help").action((_, c) => c.copy(help = true)),
opt[Unit]('v', "verbose").action((_, c) => c.copy(verbose = true)),
opt[Unit]('d', "debug").action((_, c) => c.copy(debug = true)),
opt[Unit]('V', "version").action((_, c) => c.copy(version = true)),
opt[Unit]("numeric-version").action((_, c) => c.copy(numericVersion = true)),
opt[Unit]("script-version").action((_, c) => c.copy(scriptVersion = true)),
opt[Unit]("shutdownall").action((_, c) => c.copy(shutdownAll = true)),
opt[Unit]("allow-empty").action((_, c) => c.copy(allowEmpty = true)),
opt[Unit]("sbt-create").action((_, c) => c.copy(allowEmpty = true)),
opt[Unit]("client").action((_, c) => c.copy(client = true)),
opt[Unit]("server").action((_, c) => c.copy(server = true)),
opt[Unit]("jvm-client").action((_, c) => c.copy(jvmClient = true)),
opt[Unit]("no-server").action((_, c) => c.copy(noServer = true)),
opt[Unit]("no-colors").action((_, c) => c.copy(noColors = true)),
opt[Unit]("no-global").action((_, c) => c.copy(noGlobal = true)),
opt[Unit]("no-share").action((_, c) => c.copy(noShare = true)),
opt[Unit]("no-hide-jdk-warnings").action((_, c) => c.copy(noHideJdkWarnings = true)),
opt[Unit]("debug-inc").action((_, c) => c.copy(debugInc = true)),
opt[Unit]("timings").action((_, c) => c.copy(timings = true)),
opt[Unit]("traces").action((_, c) => c.copy(traces = true)),
opt[Unit]("batch").action((_, c) => c.copy(batch = true)),
opt[String]("sbt-dir").action((x, c) => c.copy(sbtDir = Some(x))),
opt[String]("sbt-boot").action((x, c) => c.copy(sbtBoot = Some(x))),
opt[String]("sbt-cache").action((x, c) => c.copy(sbtCache = Some(x))),
opt[String]("sbt-jar").action((x, c) => c.copy(sbtJar = Some(x))),
opt[String]("sbt-version").action((x, c) => c.copy(sbtVersion = Some(x))),
opt[String]("ivy").action((x, c) => c.copy(ivy = Some(x))),
opt[Int]("mem").action((x, c) => c.copy(mem = Some(x))),
opt[String]("supershell").action((x, c) => c.copy(supershell = Some(x))),
opt[String]("color").action((x, c) => c.copy(color = Some(x))),
opt[String]("autostart").action((x, c) => c.copy(autostart = Some(x))),
opt[Int]("jvm-debug").action((x, c) => c.copy(jvmDebug = Some(x))),
opt[String]("java-home").action((x, c) => c.copy(javaHome = Some(x))),
opt[String]("experimental_execution_log").action((x, c) =>
c.copy(experimentalExecutionLog = Some(x))
),
arg[String]("<arg>")
.unbounded()
.optional()
.action((x, c) => c.copy(residual = c.residual :+ x)),
)
import b.*
OParser.sequence(
programName("sbtw"),
head("sbtw", "Windows launcher for sbt"),
opt[Unit]('h', "help").action((_, c) => c.copy(help = true)),
opt[Unit]('v', "verbose").action((_, c) => c.copy(verbose = true)),
opt[Unit]('d', "debug").action((_, c) => c.copy(debug = true)),
opt[Unit]('V', "version").action((_, c) => c.copy(version = true)),
opt[Unit]("numeric-version").action((_, c) => c.copy(numericVersion = true)),
opt[Unit]("script-version").action((_, c) => c.copy(scriptVersion = true)),
opt[Unit]("shutdownall").action((_, c) => c.copy(shutdownAll = true)),
opt[Unit]("allow-empty").action((_, c) => c.copy(allowEmpty = true)),
opt[Unit]("sbt-create").action((_, c) => c.copy(allowEmpty = true)),
opt[Unit]("client").action((_, c) => c.copy(client = true)),
opt[Unit]("server").action((_, c) => c.copy(server = true)),
opt[Unit]("jvm-client").action((_, c) => c.copy(jvmClient = true)),
opt[Unit]("no-server").action((_, c) => c.copy(noServer = true)),
opt[Unit]("no-colors").action((_, c) => c.copy(noColors = true)),
opt[Unit]("no-global").action((_, c) => c.copy(noGlobal = true)),
opt[Unit]("no-share").action((_, c) => c.copy(noShare = true)),
opt[Unit]("no-hide-jdk-warnings").action((_, c) => c.copy(noHideJdkWarnings = true)),
opt[Unit]("debug-inc").action((_, c) => c.copy(debugInc = true)),
opt[Unit]("timings").action((_, c) => c.copy(timings = true)),
opt[Unit]("traces").action((_, c) => c.copy(traces = true)),
opt[Unit]("batch").action((_, c) => c.copy(batch = true)),
opt[String]("sbt-dir").action((x, c) => c.copy(sbtDir = Some(x))),
opt[String]("sbt-boot").action((x, c) => c.copy(sbtBoot = Some(x))),
opt[String]("sbt-cache").action((x, c) => c.copy(sbtCache = Some(x))),
opt[String]("sbt-jar").action((x, c) => c.copy(sbtJar = Some(x))),
opt[String]("sbt-version").action((x, c) => c.copy(sbtVersion = Some(x))),
opt[String]("ivy").action((x, c) => c.copy(ivy = Some(x))),
opt[Int]("mem").action((x, c) => c.copy(mem = Some(x))),
opt[String]("supershell").action((x, c) => c.copy(supershell = Some(x))),
opt[String]("color").action((x, c) => c.copy(color = Some(x))),
opt[String]("autostart").action((x, c) => c.copy(autostart = Some(x))),
opt[Int]("jvm-debug").action((x, c) => c.copy(jvmDebug = Some(x))),
opt[String]("java-home").action((x, c) => c.copy(javaHome = Some(x))),
opt[String]("experimental_execution_log").action((x, c) =>
c.copy(experimentalExecutionLog = Some(x))
),
arg[String]("<arg>")
.unbounded()
.optional()
.action((x, c) => c.copy(residual = c.residual :+ x)),
)
/** Long-option names, derived from the parser (`fullName` is `--name` for options) — one source of truth. */
private[sbtw] val longNames: Set[String] =
parser.toList
.map(_.fullName)
.collect {
case n if n.startsWith("--") => n.drop(2)
}
.toSet
/** Long options that take a value, probed from the parser so it stays derived. */
private val valueTakingNames: Set[String] =
longNames.filter(n => OParser.runParser(parser, Seq(s"--$n"), LauncherOptions())._1.isEmpty)
/** Rewrites single-dash `-name` to `--name`, stopping at `--` and skipping option value slots. */
private[sbtw] def normalizeArgs(args: Seq[String]): Seq[String] =
val (options, passthrough) = args.span(_ != "--")
val out = List.newBuilder[String]
var expectValue = false
options.foreach: arg =>
if expectValue then
out += arg
expectValue = false
else
val normalized = alias(arg)
out += normalized
expectValue = normalized.startsWith("--") && valueTakingNames(normalized.drop(2))
out.result() ++ passthrough
private def alias(arg: String): String =
if arg.startsWith("-") && !arg.startsWith("--") && longNames(arg.drop(1)) then "-" + arg
else arg
def parse(args: Array[String]): Option[LauncherOptions] =
OParser
.parse(parser, args, LauncherOptions())
.parse(parser, normalizeArgs(args.toSeq), LauncherOptions())
.map: opts =>
val sbtNew = opts.residual.contains("new") || opts.residual.contains("init")
val isScript = opts.residual.exists(_.startsWith("-Dsbt.main.class=sbt.ScriptMain"))

View File

@ -21,13 +21,31 @@ object ConfigLoader:
val fromConfig = new File(sbtHome, "conf/sbtopts")
val fromEtc = new File("/etc/sbt/sbtopts")
val fromSbtConfig = new File(sbtHome, "conf/sbtconfig.txt")
val fromEnv = sys.env.get("SBT_OPTS").toSeq.flatMap(_.split("\\s+").filter(_.nonEmpty))
val fromProjectLines = loadLines(fromProject).map(stripJ)
val fromConfigLines = loadLines(fromConfig)
val fromEtcLines = loadLines(fromEtc)
val fromSbtConfigLines = loadLines(fromSbtConfig)
val fromEnv = sys.env.get("SBT_OPTS").toSeq.flatMap(tokenize)
val fromProjectLines = loadLines(fromProject).flatMap(tokenize).map(stripJ)
val fromConfigLines = loadLines(fromConfig).flatMap(tokenize)
val fromEtcLines = loadLines(fromEtc).flatMap(tokenize)
val fromSbtConfigLines = loadLines(fromSbtConfig).flatMap(tokenize)
(fromEtcLines ++ fromConfigLines ++ fromSbtConfigLines ++ fromEnv ++ fromProjectLines)
/** Splits an sbtopts line on unquoted whitespace, honoring `'`/`"` (like the bash `parseLineIntoWords`). */
private[sbtw] def tokenize(line: String): Seq[String] =
val out = scala.collection.mutable.ListBuffer.empty[String]
val word = new StringBuilder
var inDouble = false
var inSingle = false
line.foreach: c =>
if inDouble then if c == '"' then inDouble = false else word += c
else if inSingle then if c == '\'' then inSingle = false else word += c
else
c match
case '"' => inDouble = true
case '\'' => inSingle = true
case w if w.isWhitespace => if word.nonEmpty then { out += word.toString; word.clear() }
case other => word += other
if word.nonEmpty then out += word.toString
out.toList
def loadJvmOpts(cwd: File): Seq[String] =
val fromProject = new File(cwd, ".jvmopts")
val fromEnv = sys.env.get("JAVA_OPTS").toSeq.flatMap(_.split("\\s+").filter(_.nonEmpty))

View File

@ -16,8 +16,7 @@ object Main:
)
val sbtBinDir = new File(sbtHome, "bin")
val fileSbtOpts = ConfigLoader.loadSbtOpts(cwd, sbtHome)
val fileArgs = fileSbtOpts.flatMap(_.split("\\s+").filter(_.nonEmpty))
val fileArgs = ConfigLoader.loadSbtOpts(cwd, sbtHome)
val allArgs = fileArgs ++ args
ArgParser.parse(allArgs.toArray) match
@ -31,8 +30,9 @@ object Main:
else if opts.version || opts.numericVersion || opts.scriptVersion then
handleVersionCommands(cwd, sbtHome, sbtBinDir, opts)
else if opts.shutdownAll then
val javaCmd = Runner.findJavaCmd(opts.javaHome)
Runner.shutdownAll(javaCmd)
SelectedJava.resolve(opts.javaHome) match
case Left(error) => reportJavaHomeError(error)
case Right(selected) => Runner.shutdownAll(selected)
else if !opts.allowEmpty && !opts.sbtNew && !ConfigLoader.isSbtProjectDir(cwd) then
System.err.println(
"[error] Neither build.sbt nor a 'project' directory in the current directory: " + cwd
@ -40,61 +40,80 @@ object Main:
System.err.println("[error] run 'sbt new', touch build.sbt, or run 'sbt --allow-empty'.")
1
else
val buildPropsVersion = ConfigLoader.sbtVersionFromBuildProperties(cwd)
SelectedJava.resolve(opts.javaHome) match
case Left(error) => reportJavaHomeError(error)
case Right(selected) => launch(cwd, sbtBinDir, opts, selected)
val javaCmd = Runner.findJavaCmd(opts.javaHome)
val javaVer = Runner.javaVersion(javaCmd)
val minJdk = Runner.minimumJdkVersion(buildPropsVersion)
if javaVer > 0 && javaVer < minJdk then
if minJdk >= 17 then
System.err.println(
"[error] sbt 2.x requires JDK 17 or above, but you have JDK " + javaVer
)
else System.err.println("[error] sbt requires at least JDK 8+, you have " + javaVer)
1
private def reportJavaHomeError(error: String): Int =
System.err.println(error)
1
private def launch(
cwd: File,
sbtBinDir: File,
opts: LauncherOptions,
selected: SelectedJava
): Int =
val buildPropsVersion = ConfigLoader.sbtVersionFromBuildProperties(cwd)
val javaVer = Runner.javaVersion(selected.javaCmd)
val minJdk = Runner.minimumJdkVersion(buildPropsVersion)
if javaVer > 0 && javaVer < minJdk then
if minJdk >= 17 then
System.err.println(
"[error] sbt 2.x requires JDK 17 or above, but you have JDK " + javaVer
)
else System.err.println("[error] sbt requires at least JDK 8+, you have " + javaVer)
1
else
val bspMode = opts.residual.exists(a => a == "bsp" || a == "-bsp" || a == "--bsp")
val clientOpt = opts.client || sys.env.get("SBT_NATIVE_CLIENT").contains("true")
val useNativeClient =
if bspMode then false
else shouldRunNativeClient(opts.copy(client = clientOpt), buildPropsVersion)
if useNativeClient then
val scriptPath = sbtBinDir.getAbsolutePath.replace("\\", "/") + "/sbt.bat"
Runner.runNativeClient(sbtBinDir, scriptPath, opts, selected)
else
val bspMode = opts.residual.exists(a => a == "bsp" || a == "-bsp" || a == "--bsp")
val clientOpt = opts.client || sys.env.get("SBT_NATIVE_CLIENT").contains("true")
val useNativeClient =
if bspMode then false
else shouldRunNativeClient(opts.copy(client = clientOpt), buildPropsVersion)
if useNativeClient then
val scriptPath = sbtBinDir.getAbsolutePath.replace("\\", "/") + "/sbt.bat"
Runner.runNativeClient(sbtBinDir, scriptPath, opts)
val sbtJar = opts.sbtJar
.filter(p => new File(p).isFile)
.getOrElse(new File(sbtBinDir, "sbt-launch.jar").getAbsolutePath)
if !new File(sbtJar).isFile then
System.err.println("[error] Launcher jar not found: " + sbtJar)
1
else
val sbtJar = opts.sbtJar
.filter(p => new File(p).isFile)
.getOrElse(new File(sbtBinDir, "sbt-launch.jar").getAbsolutePath)
if !new File(sbtJar).isFile then
System.err.println("[error] Launcher jar not found: " + sbtJar)
1
else
var javaOpts = ConfigLoader.loadJvmOpts(cwd)
if javaOpts.isEmpty then javaOpts = ConfigLoader.defaultJavaOpts
var sbtOpts = Runner.buildSbtOpts(opts)
var javaOpts = ConfigLoader.loadJvmOpts(cwd)
if javaOpts.isEmpty then javaOpts = ConfigLoader.defaultJavaOpts
var sbtOpts = Runner.buildSbtOpts(opts)
val (residualJava, bootArgs) = Runner.splitResidual(opts.residual)
javaOpts = javaOpts ++ residualJava
val (residualJava, bootArgs) = Runner.splitResidual(opts.residual)
javaOpts = javaOpts ++ residualJava
val (finalJava, finalSbt) = if opts.mem.isDefined then
val evictedJava = Memory.evictMemoryOpts(javaOpts)
val evictedSbt = Memory.evictMemoryOpts(sbtOpts)
val memOpts = Memory.addMemory(opts.mem.get, javaVer)
(evictedJava ++ memOpts, evictedSbt)
else Memory.addDefaultMemory(javaOpts, sbtOpts, javaVer, LauncherOptions.defaultMemMb)
sbtOpts = finalSbt
val (finalJava, finalSbt) = if opts.mem.isDefined then
val evictedJava = Memory.evictMemoryOpts(javaOpts)
val evictedSbt = Memory.evictMemoryOpts(sbtOpts)
val memOpts = Memory.addMemory(opts.mem.get, javaVer)
(evictedJava ++ memOpts, evictedSbt)
else Memory.addDefaultMemory(javaOpts, sbtOpts, javaVer, LauncherOptions.defaultMemMb)
sbtOpts = finalSbt
if !opts.noHideJdkWarnings && javaVer >= 25 then
sbtOpts = sbtOpts ++ Seq(
"--sun-misc-unsafe-memory-access=allow",
"--enable-native-access=ALL-UNNAMED"
)
val javaOptsWithDebug = opts.jvmDebug.fold(finalJava)(port =>
finalJava :+ s"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$port"
if !opts.noHideJdkWarnings && javaVer >= 25 then
sbtOpts = sbtOpts ++ Seq(
"--sun-misc-unsafe-memory-access=allow",
"--enable-native-access=ALL-UNNAMED"
)
val javaOptsWithDebug = opts.jvmDebug.fold(finalJava)(port =>
finalJava :+ s"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$port"
)
Runner.runJvm(javaCmd, javaOptsWithDebug, sbtOpts, sbtJar, bootArgs, opts.verbose)
Runner.runJvm(
selected,
javaOptsWithDebug,
sbtOpts,
sbtJar,
bootArgs,
opts.verbose
)
private def shouldRunNativeClient(
opts: LauncherOptions,
@ -130,19 +149,22 @@ object Main:
)
0
else if opts.numericVersion then
val javaCmd = Runner.findJavaCmd(opts.javaHome)
val sbtJar = opts.sbtJar
.filter(p => new File(p).isFile)
.getOrElse(new File(sbtBinDir, "sbt-launch.jar").getAbsolutePath)
if !new File(sbtJar).isFile then
System.err.println("[error] Launcher jar not found for version check")
1
else
try
val out = Process(Seq(javaCmd, "-jar", sbtJar, "sbtVersion")).!!
println(out.linesIterator.toSeq.lastOption.map(_.trim).getOrElse(""))
0
catch { case _: Exception => 1 }
SelectedJava.resolve(opts.javaHome) match
case Left(error) => reportJavaHomeError(error)
case Right(selected) =>
val sbtJar = opts.sbtJar
.filter(p => new File(p).isFile)
.getOrElse(new File(sbtBinDir, "sbt-launch.jar").getAbsolutePath)
if !new File(sbtJar).isFile then
System.err.println("[error] Launcher jar not found for version check")
1
else
try
val cmd = Seq(selected.javaCmd, "-jar", sbtJar, "sbtVersion")
val out = Process(cmd, None, selected.envOverlay*).!!
println(out.linesIterator.toSeq.lastOption.map(_.trim).getOrElse(""))
0
catch { case _: Exception => 1 }
else 0
private def projectSbtVersion(cwd: File): Option[String] =

View File

@ -6,26 +6,7 @@ import scala.sys.process.*
object Runner:
def findJavaCmd(javaHome: Option[String]): String =
val cmd = javaHome match
case Some(h) =>
val exe = new File(h, "bin/java.exe")
if exe.isFile then exe.getAbsolutePath
else
sys.env
.get("JAVACMD")
.orElse(
sys.env.get("JAVA_HOME").map(h0 => new File(h0, "bin/java.exe").getAbsolutePath)
)
.getOrElse("java")
case None =>
sys.env
.get("JAVACMD")
.orElse(sys.env.get("JAVA_HOME").map(h => new File(h, "bin/java.exe").getAbsolutePath))
.getOrElse("java")
cmd.replace("\"", "")
def javaVersion(javaCmd: String): Int =
private[sbtw] def javaVersion(javaCmd: String): Int =
try
val pb = Process(Seq(javaCmd, "-Xms32M", "-Xmx32M", "-version"))
val out = pb.!!
@ -39,11 +20,11 @@ object Runner:
catch { case _: Exception => 0 }
/** Returns the minimum JDK version required for the given sbt version. */
def minimumJdkVersion(sbtVersion: Option[String]): Int =
private[sbtw] def minimumJdkVersion(sbtVersion: Option[String]): Int =
val isSbt2 = sbtVersion.exists(v => v.takeWhile(_.isDigit).toIntOption.exists(_ >= 2))
if isSbt2 then 17 else 8
def buildSbtOpts(opts: LauncherOptions): Seq[String] =
private[sbtw] def buildSbtOpts(opts: LauncherOptions): Seq[String] =
var s: Seq[String] = Nil
if opts.debug then s = s :+ "-debug"
if opts.debugInc then s = s :+ "-Dxsbt.inc.debug=true"
@ -71,7 +52,12 @@ object Runner:
if opts.jvmClient then s = s :+ "--client"
s
def runNativeClient(sbtBinDir: File, scriptPath: String, opts: LauncherOptions): Int =
private[sbtw] def runNativeClient(
sbtBinDir: File,
scriptPath: String,
opts: LauncherOptions,
selected: SelectedJava
): Int =
val sbtn = new File(sbtBinDir, "sbtn-x86_64-pc-win32.exe")
if !sbtn.isFile then
System.err.println("[error] sbtn-x86_64-pc-win32.exe not found in " + sbtBinDir)
@ -84,11 +70,12 @@ object Runner:
if opts.verbose then
System.err.println("# running native client")
cmd.foreach(a => System.err.println(a))
val proc = Process(cmd, None, "SBT_SCRIPT" -> scriptPath)
val extraEnv = ("SBT_SCRIPT" -> scriptPath) +: selected.handoffEnv
val proc = Process(cmd, None, extraEnv*)
proc.!
def runJvm(
javaCmd: String,
private[sbtw] def runJvm(
selected: SelectedJava,
javaOpts: Seq[String],
sbtOpts: Seq[String],
sbtJar: String,
@ -99,21 +86,23 @@ object Runner:
sys.env.get("JAVA_TOOL_OPTIONS").toSeq.flatMap(_.split("\\s+").filter(_.nonEmpty))
val jdkOpts = sys.env.get("JDK_JAVA_OPTIONS").toSeq.flatMap(_.split("\\s+").filter(_.nonEmpty))
val fullJavaOpts = javaOpts ++ sbtOpts ++ toolOpts ++ jdkOpts
val cmd = Seq(javaCmd) ++ fullJavaOpts ++ Seq("-cp", sbtJar, "xsbt.boot.Boot") ++ bootArgs
val cmd =
Seq(selected.javaCmd) ++ fullJavaOpts ++ Seq("-cp", sbtJar, "xsbt.boot.Boot") ++ bootArgs
if verbose then
System.err.println("# Executing command line:")
cmd.foreach(a => System.err.println(if a.contains(" ") then s""""$a"""" else a))
val jpb = new JProcessBuilder(cmd*)
jpb.inheritIO()
selected.envOverlay.foreach((k, v) => jpb.environment().put(k, v))
val p = jpb.start()
try
p.waitFor()
p.exitValue()
finally if p.isAlive then p.destroy()
def shutdownAll(javaCmd: String): Int =
private[sbtw] def shutdownAll(selected: SelectedJava): Int =
try
val jpsOut = Process(Seq("jps", "-lv")).!!
val jpsOut = Process(Seq("jps", "-lv"), None, selected.envOverlay*).!!
val pids = jpsOut.linesIterator
.filter(_.contains("xsbt.boot.Boot"))
.flatMap: line =>
@ -127,7 +116,7 @@ object Runner:
0
catch { case _: Exception => 1 }
def splitResidual(residual: Seq[String]): (Seq[String], Seq[String]) =
private[sbtw] def splitResidual(residual: Seq[String]): (Seq[String], Seq[String]) =
var javaOpts: Seq[String] = Nil
var bootArgs: Seq[String] = Nil
var i = 0

View File

@ -0,0 +1,70 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbtw
import java.io.File
/** The JDK chosen to launch sbt: `javaCmd` to run, plus `envOverlay` so indirect launches use it too. */
private[sbtw] final case class SelectedJava(
javaCmd: String,
envOverlay: Seq[(String, String)],
explicit: Boolean = false
):
/** Overlay for the native-client handoff, plus the one-hop explicit marker. */
def handoffEnv: Seq[(String, String)] =
if explicit then envOverlay :+ (SelectedJava.explicitMarker -> "1") else envOverlay
end SelectedJava
private[sbtw] object SelectedJava:
/** One-hop marker telling `sbt.bat`'s `.java-version` block to defer to an explicit `--java-home`. */
private[sbtw] val explicitMarker = "SBT_EXPLICIT_JAVA_HOME"
/** Resolves `--java-home` > `JAVACMD` > `JAVA_HOME` > `PATH`; an invalid `--java-home` is an error. */
def resolve(
javaHome: Option[String],
env: Map[String, String] = sys.env
): Either[String, SelectedJava] =
javaHome match
case Some(home) =>
val exe = new File(home, "bin/java.exe")
if exe.isFile then Right(fromHome(home, env, explicit = true))
else Left(s"""[error] Directory "$home" for JAVA_HOME is not valid""")
case None =>
getIgnoreCase(env, "JAVACMD").map(stripQuotes) match
case Some(cmd) => Right(SelectedJava(cmd, Nil))
case None =>
getIgnoreCase(env, "JAVA_HOME") match
case Some(home) => Right(fromHome(home, env, explicit = false))
case None => Right(SelectedJava("java", Nil))
private def fromHome(home: String, env: Map[String, String], explicit: Boolean): SelectedJava =
val absoluteHome = new File(home).getAbsolutePath
val cmd = new File(absoluteHome, "bin/java.exe").getAbsolutePath
val bin = absoluteHome + File.separator + "bin"
val path = getIgnoreCase(env, "PATH") match
case Some(old) => bin + File.pathSeparator + old
case None => bin
val overlay = Seq(
keyFor(env, "JAVACMD") -> cmd,
keyFor(env, "JAVA_HOME") -> absoluteHome,
keyFor(env, "JDK_HOME") -> absoluteHome,
keyFor(env, "PATH") -> path
)
SelectedJava(cmd, overlay, explicit)
private def getIgnoreCase(env: Map[String, String], key: String): Option[String] =
env.collectFirst { case (k, v) if k.equalsIgnoreCase(key) => v }
/** Existing env key matching `name` case-insensitively, so overlays replace rather than duplicate. */
private def keyFor(env: Map[String, String], name: String): String =
env.keys.find(_.equalsIgnoreCase(name)).getOrElse(name)
private def stripQuotes(s: String): String = s.replace("\"", "")
end SelectedJava

View File

@ -0,0 +1,85 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbtw
object ArgParserSpec extends verify.BasicTestSuite:
test("parse should accept --java-home") {
val opts = ArgParser.parse(Array("--java-home", "C:\\jdk", "compile")).get
assert(opts.javaHome == Some("C:\\jdk"))
assert(opts.residual == Seq("compile"))
}
test("parse should accept single-dash -java-home") {
val opts = ArgParser.parse(Array("-java-home", "C:\\jdk", "compile")).get
assert(opts.javaHome == Some("C:\\jdk"))
assert(opts.residual == Seq("compile"))
}
test("parse should accept single-dash -sbt-version") {
val opts = ArgParser.parse(Array("-sbt-version", "1.10.7")).get
assert(opts.sbtVersion == Some("1.10.7"))
}
test("parse should accept single-dash -mem") {
val opts = ArgParser.parse(Array("-mem", "2048")).get
assert(opts.mem == Some(2048))
}
test("parse should accept single-dash -no-colors") {
val opts = ArgParser.parse(Array("-no-colors")).get
assert(opts.noColors)
}
test("parse should accept single-dash -jvm-debug") {
val opts = ArgParser.parse(Array("-jvm-debug", "5005")).get
assert(opts.jvmDebug == Some(5005))
}
test("parse should detect new alongside -java-home") {
val opts = ArgParser.parse(Array("-java-home", "C:\\jdk", "new")).get
assert(opts.sbtNew)
}
test("longNames should be derived from the parser grammar") {
assert(ArgParser.longNames.contains("java-home"))
assert(ArgParser.longNames.contains("client"))
assert(ArgParser.longNames.contains("sbt-version"))
assert(!ArgParser.longNames.contains("h"))
assert(!ArgParser.longNames.contains("<arg>"))
assert(!ArgParser.longNames.contains(""))
}
test("normalizeArgs should rewrite single-dash long options to double-dash") {
assert(ArgParser.normalizeArgs(Seq("-java-home")) == Seq("--java-home"))
assert(ArgParser.normalizeArgs(Seq("-sbt-version")) == Seq("--sbt-version"))
assert(ArgParser.normalizeArgs(Seq("-timings")) == Seq("--timings"))
}
test("normalizeArgs should leave other tokens untouched") {
val args = Seq("-Dfoo=bar", "-Xmx1G", "-J-Xss2m", "-v", "--java-home", "compile")
assert(ArgParser.normalizeArgs(args) == args)
}
test("normalizeArgs should not rewrite past a -- terminator") {
assert(ArgParser.normalizeArgs(Seq("--", "-server")) == Seq("--", "-server"))
assert(
ArgParser.normalizeArgs(Seq("-java-home", "X", "--", "-server")) ==
Seq("--java-home", "X", "--", "-server")
)
}
test("normalizeArgs should not rewrite a value-taking option's value") {
assert(ArgParser.normalizeArgs(Seq("--sbt-dir", "-server")) == Seq("--sbt-dir", "-server"))
assert(ArgParser.normalizeArgs(Seq("-sbt-dir", "-server")) == Seq("--sbt-dir", "-server"))
}
test("normalizeArgs should still rewrite the token after a value-less flag") {
assert(ArgParser.normalizeArgs(Seq("-no-colors", "-server")) == Seq("--no-colors", "--server"))
}
end ArgParserSpec

View File

@ -0,0 +1,36 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbtw
object ConfigLoaderSpec extends verify.BasicTestSuite:
test("tokenize keeps a double-quoted path together") {
assert(
ConfigLoader.tokenize("""-java-home "C:\Program Files\jdk"""") == Seq(
"-java-home",
"C:\\Program Files\\jdk"
)
)
}
test("tokenize keeps a single-quoted path together") {
assert(ConfigLoader.tokenize("-sbt-dir '/Users/a dog'") == Seq("-sbt-dir", "/Users/a dog"))
}
test("tokenize splits on unquoted whitespace") {
assert(ConfigLoader.tokenize("-mem 2048") == Seq("-mem", "2048"))
}
test("tokenize returns a single token for a bare flag") {
assert(ConfigLoader.tokenize("-java-home") == Seq("-java-home"))
}
test("tokenize returns nothing for blank input") {
assert(ConfigLoader.tokenize(" ") == Seq.empty)
}
end ConfigLoaderSpec

View File

@ -0,0 +1,147 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbtw
import java.io.File
import java.nio.file.Files
object SelectedJavaSpec extends verify.BasicTestSuite:
private def jdkHome(): File =
val home = Files.createTempDirectory("sbtw-jdk").toFile
val exe = new File(home, "bin/java.exe")
exe.getParentFile.mkdirs()
exe.createNewFile()
home
test("resolve should use a valid --java-home") {
val home = jdkHome()
val selected = SelectedJava.resolve(Some(home.getPath), Map.empty).toOption.get
assert(selected.javaCmd == new File(home, "bin/java.exe").getAbsolutePath)
}
test("resolve should reject an invalid --java-home instead of falling back") {
val result =
SelectedJava.resolve(Some("no-such-dir"), Map("JAVACMD" -> "C:\\jdk\\bin\\java.exe"))
assert(result.isLeft)
assert(result.swap.exists(_.contains("for JAVA_HOME is not valid")))
}
test("--java-home overlay makes JAVACMD authoritative over an inherited one") {
val home = jdkHome()
val exe = new File(home, "bin/java.exe").getAbsolutePath
val overlay =
SelectedJava
.resolve(Some(home.getPath), Map("JAVACMD" -> "C:\\old\\bin\\java.exe"))
.toOption
.get
.envOverlay
.toMap
assert(overlay.get("JAVACMD") == Some(exe))
assert(overlay.get("JAVA_HOME") == Some(home.getPath))
assert(overlay.get("JDK_HOME") == Some(home.getPath))
val expectedPath = home.getPath + File.separator + "bin" + File.pathSeparator + "C:\\old"
assert(overlay.get("PATH") == Some(home.getPath + File.separator + "bin"))
assert(
SelectedJava
.resolve(Some(home.getPath), Map("PATH" -> "C:\\old"))
.toOption
.get
.envOverlay
.toMap
.get("PATH") == Some(expectedPath)
)
}
test("resolve should prefer JAVACMD over JAVA_HOME with no overlay") {
val env = Map("JAVACMD" -> "C:\\some\\java.exe", "JAVA_HOME" -> "C:\\jdk")
val selected = SelectedJava.resolve(None, env).toOption.get
assert(selected.javaCmd == "C:\\some\\java.exe")
assert(selected.envOverlay.isEmpty)
}
test("resolve should strip quotes from JAVACMD") {
val selected =
SelectedJava.resolve(None, Map("JAVACMD" -> "\"C:\\some\\java.exe\"")).toOption.get
assert(selected.javaCmd == "C:\\some\\java.exe")
}
test("a JDK derived from JAVA_HOME still gets a full overlay (PATH prepend)") {
val home = jdkHome().getPath
val selected =
SelectedJava.resolve(None, Map("JAVA_HOME" -> home, "PATH" -> "/old")).toOption.get
assert(selected.javaCmd == new File(home, "bin/java.exe").getAbsolutePath)
val overlay = selected.envOverlay.toMap
assert(overlay.get("JAVA_HOME") == Some(home))
assert(overlay.get("PATH") == Some(home + File.separator + "bin" + File.pathSeparator + "/old"))
}
test("a relative home is exported as an absolute path") {
val overlay =
SelectedJava.resolve(None, Map("JAVA_HOME" -> "rel-jdk")).toOption.get.envOverlay.toMap
assert(new File(overlay("JAVA_HOME")).isAbsolute)
assert(new File(overlay("JDK_HOME")).isAbsolute)
}
test("resolve should read JAVACMD case-insensitively") {
val selected = SelectedJava.resolve(None, Map("JavaCmd" -> "C:\\x\\java.exe")).toOption.get
assert(selected.javaCmd == "C:\\x\\java.exe")
assert(selected.envOverlay.isEmpty)
}
test("resolve should read JAVA_HOME case-insensitively") {
val home = jdkHome().getPath
val selected = SelectedJava.resolve(None, Map("Java_Home" -> home)).toOption.get
assert(selected.javaCmd == new File(home, "bin/java.exe").getAbsolutePath)
}
test("resolve should fall back to bare java with no overlay") {
val selected = SelectedJava.resolve(None, Map.empty).toOption.get
assert(selected.javaCmd == "java")
assert(selected.envOverlay.isEmpty)
}
test("overlay should reuse the existing PATH key case") {
val overlay = SelectedJava
.resolve(Some(jdkHome().getPath), Map("Path" -> "C:\\old"))
.toOption
.get
.envOverlay
.toMap
assert(overlay.contains("Path"))
assert(!overlay.contains("PATH"))
}
test("an explicit --java-home carries the provenance marker only on the handoff") {
val selected = SelectedJava.resolve(Some(jdkHome().getPath), Map.empty).toOption.get
assert(!selected.envOverlay.toMap.contains(SelectedJava.explicitMarker))
assert(selected.handoffEnv.toMap.get(SelectedJava.explicitMarker) == Some("1"))
}
test("a JAVA_HOME-derived selection never carries the provenance marker") {
val selected = SelectedJava.resolve(None, Map("JAVA_HOME" -> jdkHome().getPath)).toOption.get
assert(!selected.envOverlay.toMap.contains(SelectedJava.explicitMarker))
assert(!selected.handoffEnv.toMap.contains(SelectedJava.explicitMarker))
}
test("overlay should replace any-cased alias rather than duplicate it") {
val home = jdkHome().getPath
val overlay =
SelectedJava
.resolve(Some(home), Map("javacmd" -> "old", "JavaCmd2" -> "x"))
.toOption
.get
.envOverlay
.toMap
assert(overlay.contains("javacmd"))
assert(!overlay.contains("JAVACMD"))
assert(overlay.get("javacmd") == Some(new File(home, "bin/java.exe").getAbsolutePath))
assert(overlay.contains("JAVA_HOME"))
}
end SelectedJavaSpec