mirror of https://github.com/sbt/sbt.git
[2.x] feat: support passing JVM arguments via -- delimiter in run commands (#8868)
Summary - Adds support for passing JVM arguments inline to `run`, `runMain`, `bgRun`, `bgRunMain`, and `fgRun`/`fgRunMain` using `--` as a delimiter - Syntax: `run <jvmArgs> -- <appArgs>` (e.g., `run -Xmx2G -Dapp.mode=debug -- arg1 arg2`) - Fully backward compatible — without `--`, all arguments are treated as app args as before - When `fork` is `false`, a warning is logged that JVM arguments will be ignored
This commit is contained in:
parent
09c4856409
commit
b1db6ba44d
|
|
@ -1953,11 +1953,15 @@ object Defaults extends BuildCommon {
|
||||||
val parser =
|
val parser =
|
||||||
loadForParser(discoveredMainClasses)((s, names) => runMainParser(s, names getOrElse Nil))
|
loadForParser(discoveredMainClasses)((s, names) => runMainParser(s, names getOrElse Nil))
|
||||||
Def.inputTask {
|
Def.inputTask {
|
||||||
val (mainClass, args) = parser.parsed
|
val (mainClass, allArgs) = parser.parsed
|
||||||
|
val (jvmArgs, appArgs) = RunUtil.splitArgs(allArgs)
|
||||||
val cp = classpath.value
|
val cp = classpath.value
|
||||||
|
val fo = (run / forkOptions).value
|
||||||
|
val log = streams.value.log
|
||||||
given FileConverter = fileConverter.value
|
given FileConverter = fileConverter.value
|
||||||
scalaRun.value
|
val (modifiedRun, _) = RunUtil.applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||||
.run(mainClass, cp.files, args, streams.value.log)
|
modifiedRun
|
||||||
|
.run(mainClass, cp.files, appArgs, log)
|
||||||
.get
|
.get
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,43 @@ import sbt.internal.worker.{ ClientJobParams, FilePath, JvmRunInfo, RunInfo }
|
||||||
import sbt.io.IO
|
import sbt.io.IO
|
||||||
import sbt.protocol.Serialization
|
import sbt.protocol.Serialization
|
||||||
import sbt.util.CacheImplicits.given
|
import sbt.util.CacheImplicits.given
|
||||||
|
import sbt.util.Logger
|
||||||
import xsbti.FileConverter
|
import xsbti.FileConverter
|
||||||
|
|
||||||
object RunUtil:
|
object RunUtil:
|
||||||
|
/**
|
||||||
|
* Split arguments at the first `--` delimiter.
|
||||||
|
* Tokens before `--` are JVM args; tokens after are app args.
|
||||||
|
* If no `--` is present, all tokens are app args (backward compatible).
|
||||||
|
*/
|
||||||
|
private[sbt] def splitArgs(args: Seq[String]): (Seq[String], Seq[String]) =
|
||||||
|
val idx = args.indexOf("--")
|
||||||
|
if idx < 0 then (Nil, args)
|
||||||
|
else (args.take(idx), args.drop(idx + 1))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply CLI JVM args to the ScalaRun and ForkOptions.
|
||||||
|
* For ForkRun: creates new ForkRun with augmented runJVMOptions.
|
||||||
|
* For Run (non-fork): warns and returns unchanged.
|
||||||
|
*/
|
||||||
|
private[sbt] def applyJvmArgs(
|
||||||
|
scalaRun: ScalaRun,
|
||||||
|
jvmArgs: Seq[String],
|
||||||
|
fo: ForkOptions,
|
||||||
|
log: Logger,
|
||||||
|
): (ScalaRun, ForkOptions) =
|
||||||
|
if jvmArgs.isEmpty then (scalaRun, fo)
|
||||||
|
else
|
||||||
|
scalaRun match
|
||||||
|
case _: ForkRun =>
|
||||||
|
val newFo = fo.withRunJVMOptions(fo.runJVMOptions ++ jvmArgs)
|
||||||
|
(new ForkRun(newFo), newFo)
|
||||||
|
case _ =>
|
||||||
|
log.warn(
|
||||||
|
s"JVM options (${jvmArgs.mkString(" ")}) will be ignored, fork is set to false"
|
||||||
|
)
|
||||||
|
(scalaRun, fo)
|
||||||
|
|
||||||
private def setWindowTitle(title: String): Unit =
|
private def setWindowTitle(title: String): Unit =
|
||||||
if System.console() != null && System.getenv("TERM") != null then
|
if System.console() != null && System.getenv("TERM") != null then
|
||||||
scala.Console.print(s"\u001b]0;$title\u0007")
|
scala.Console.print(s"\u001b]0;$title\u0007")
|
||||||
|
|
@ -38,11 +72,14 @@ object RunUtil:
|
||||||
): Initialize[InputTask[Unit]] =
|
): Initialize[InputTask[Unit]] =
|
||||||
val parser = Def.spaceDelimited()
|
val parser = Def.spaceDelimited()
|
||||||
Def.inputTask {
|
Def.inputTask {
|
||||||
val in = parser.parsed
|
val (jvmArgs, appArgs) = splitArgs(parser.parsed)
|
||||||
val mainClass = getMainClass(mainClassTask.value)
|
val mainClass = getMainClass(mainClassTask.value)
|
||||||
val cp = classpath.value
|
val cp = classpath.value
|
||||||
|
val fo = (run / forkOptions).value
|
||||||
|
val log = streams.value.log
|
||||||
given FileConverter = fileConverter.value
|
given FileConverter = fileConverter.value
|
||||||
scalaRun.value.run(mainClass, cp.files, in, streams.value.log).get
|
val (modifiedRun, _) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||||
|
modifiedRun.run(mainClass, cp.files, appArgs, log).get
|
||||||
}
|
}
|
||||||
|
|
||||||
def configTasks(c: ScopeAxis[ConfigKey]): Seq[Setting[?]] = Seq(
|
def configTasks(c: ScopeAxis[ConfigKey]): Seq[Setting[?]] = Seq(
|
||||||
|
|
@ -136,9 +173,12 @@ object RunUtil:
|
||||||
val conv = fileConverter.value
|
val conv = fileConverter.value
|
||||||
given FileConverter = conv
|
given FileConverter = conv
|
||||||
val service = bgJobService.value
|
val service = bgJobService.value
|
||||||
val (mainClass, args) = parser.parsed
|
val (mainClass, allArgs) = parser.parsed
|
||||||
|
val (jvmArgs, appArgs) = splitArgs(allArgs)
|
||||||
val hashClasspath = (bgRunMain / bgHashClasspath).value
|
val hashClasspath = (bgRunMain / bgHashClasspath).value
|
||||||
val fo = (run / forkOptions).value
|
val fo = (run / forkOptions).value
|
||||||
|
val log = streams.value.log
|
||||||
|
val (modifiedRun, modifiedFo) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||||
val state = Keys.state.value
|
val state = Keys.state.value
|
||||||
val windowTitle = mkWindowTitle("runMain", organization.value, name.value, version.value)
|
val windowTitle = mkWindowTitle("runMain", organization.value, name.value, version.value)
|
||||||
if clientRun.value && state.isNetworkCommand then
|
if clientRun.value && state.isNetworkCommand then
|
||||||
|
|
@ -149,7 +189,8 @@ object RunUtil:
|
||||||
workingDir,
|
workingDir,
|
||||||
conv,
|
conv,
|
||||||
)
|
)
|
||||||
val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv, Some(windowTitle))
|
val info =
|
||||||
|
mkRunInfo(appArgs.toVector, mainClass, cp, modifiedFo, conv, Some(windowTitle))
|
||||||
val result = ClientJobParams(
|
val result = ClientJobParams(
|
||||||
runInfo = info
|
runInfo = info
|
||||||
)
|
)
|
||||||
|
|
@ -168,15 +209,15 @@ object RunUtil:
|
||||||
hashClasspath,
|
hashClasspath,
|
||||||
conv,
|
conv,
|
||||||
)
|
)
|
||||||
scalaRun.value match
|
modifiedRun match
|
||||||
case r: Run =>
|
case r: Run =>
|
||||||
val loader = r.newLoader(cp.files)
|
val loader = r.newLoader(cp.files)
|
||||||
(
|
(
|
||||||
Some(loader),
|
Some(loader),
|
||||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||||
)
|
)
|
||||||
case sr =>
|
case sr =>
|
||||||
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
|
(None, wrapper(() => sr.run(mainClass, cp.files, appArgs, logger).get))
|
||||||
service.waitForTry(handle).get
|
service.waitForTry(handle).get
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
|
|
@ -192,11 +233,13 @@ object RunUtil:
|
||||||
Def.inputTask {
|
Def.inputTask {
|
||||||
val conv = fileConverter.value
|
val conv = fileConverter.value
|
||||||
given FileConverter = conv
|
given FileConverter = conv
|
||||||
val args = parser.parsed
|
val (jvmArgs, appArgs) = splitArgs(parser.parsed)
|
||||||
val service = bgJobService.value
|
val service = bgJobService.value
|
||||||
val mainClass = getMainClass(mainClassTask.value)
|
val mainClass = getMainClass(mainClassTask.value)
|
||||||
val hashClasspath = (bgRun / bgHashClasspath).value
|
val hashClasspath = (bgRun / bgHashClasspath).value
|
||||||
val fo = (run / forkOptions).value
|
val fo = (run / forkOptions).value
|
||||||
|
val log = streams.value.log
|
||||||
|
val (modifiedRun, modifiedFo) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||||
val state = Keys.state.value
|
val state = Keys.state.value
|
||||||
val windowTitle = mkWindowTitle("run", organization.value, name.value, version.value)
|
val windowTitle = mkWindowTitle("run", organization.value, name.value, version.value)
|
||||||
if clientRun.value && state.isNetworkCommand then
|
if clientRun.value && state.isNetworkCommand then
|
||||||
|
|
@ -207,7 +250,7 @@ object RunUtil:
|
||||||
workingDir,
|
workingDir,
|
||||||
conv,
|
conv,
|
||||||
)
|
)
|
||||||
val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv, Some(windowTitle))
|
val info = mkRunInfo(appArgs.toVector, mainClass, cp, modifiedFo, conv, Some(windowTitle))
|
||||||
val result = ClientJobParams(
|
val result = ClientJobParams(
|
||||||
runInfo = info
|
runInfo = info
|
||||||
)
|
)
|
||||||
|
|
@ -226,15 +269,15 @@ object RunUtil:
|
||||||
hashClasspath,
|
hashClasspath,
|
||||||
conv
|
conv
|
||||||
)
|
)
|
||||||
scalaRun.value match
|
modifiedRun match
|
||||||
case r: Run =>
|
case r: Run =>
|
||||||
val loader = r.newLoader(cp.files)
|
val loader = r.newLoader(cp.files)
|
||||||
(
|
(
|
||||||
Some(loader),
|
Some(loader),
|
||||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||||
)
|
)
|
||||||
case sr =>
|
case sr =>
|
||||||
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
|
(None, wrapper(() => sr.run(mainClass, cp.files, appArgs, logger).get))
|
||||||
service.waitForTry(handle).get
|
service.waitForTry(handle).get
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
|
|
@ -250,8 +293,12 @@ object RunUtil:
|
||||||
)
|
)
|
||||||
Def.inputTask {
|
Def.inputTask {
|
||||||
val service = bgJobService.value
|
val service = bgJobService.value
|
||||||
val (mainClass, args) = parser.parsed
|
val (mainClass, allArgs) = parser.parsed
|
||||||
|
val (jvmArgs, appArgs) = splitArgs(allArgs)
|
||||||
val hashClasspath = (bgRunMain / bgHashClasspath).value
|
val hashClasspath = (bgRunMain / bgHashClasspath).value
|
||||||
|
val fo = (run / forkOptions).value
|
||||||
|
val log = streams.value.log
|
||||||
|
val (modifiedRun, _) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||||
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
||||||
val converter = fileConverter.value
|
val converter = fileConverter.value
|
||||||
setWindowTitle(mkWindowTitle("bgRunMain", organization.value, name.value, version.value))
|
setWindowTitle(mkWindowTitle("bgRunMain", organization.value, name.value, version.value))
|
||||||
|
|
@ -268,15 +315,15 @@ object RunUtil:
|
||||||
)
|
)
|
||||||
else classpath.value
|
else classpath.value
|
||||||
given FileConverter = fileConverter.value
|
given FileConverter = fileConverter.value
|
||||||
scalaRun.value match
|
modifiedRun match
|
||||||
case r: Run =>
|
case r: Run =>
|
||||||
val loader = r.newLoader(cp.files)
|
val loader = r.newLoader(cp.files)
|
||||||
(
|
(
|
||||||
Some(loader),
|
Some(loader),
|
||||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||||
)
|
)
|
||||||
case sr =>
|
case sr =>
|
||||||
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
|
(None, wrapper(() => sr.run(mainClass, cp.files, appArgs, logger).get))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,10 +336,13 @@ object RunUtil:
|
||||||
): Initialize[InputTask[JobHandle]] =
|
): Initialize[InputTask[JobHandle]] =
|
||||||
val parser = Def.spaceDelimited()
|
val parser = Def.spaceDelimited()
|
||||||
Def.inputTask {
|
Def.inputTask {
|
||||||
val args = parser.parsed
|
val (jvmArgs, appArgs) = splitArgs(parser.parsed)
|
||||||
val service = bgJobService.value
|
val service = bgJobService.value
|
||||||
val mainClass = getMainClass(mainClassTask.value)
|
val mainClass = getMainClass(mainClassTask.value)
|
||||||
val hashClasspath = (bgRun / bgHashClasspath).value
|
val hashClasspath = (bgRun / bgHashClasspath).value
|
||||||
|
val fo = (run / forkOptions).value
|
||||||
|
val log = streams.value.log
|
||||||
|
val (modifiedRun, _) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||||
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
||||||
val converter = fileConverter.value
|
val converter = fileConverter.value
|
||||||
setWindowTitle(mkWindowTitle("bgRun", organization.value, name.value, version.value))
|
setWindowTitle(mkWindowTitle("bgRun", organization.value, name.value, version.value))
|
||||||
|
|
@ -309,15 +359,15 @@ object RunUtil:
|
||||||
)
|
)
|
||||||
else classpath.value
|
else classpath.value
|
||||||
given FileConverter = converter
|
given FileConverter = converter
|
||||||
scalaRun.value match
|
modifiedRun match
|
||||||
case r: Run =>
|
case r: Run =>
|
||||||
val loader = r.newLoader(cp.files)
|
val loader = r.newLoader(cp.files)
|
||||||
(
|
(
|
||||||
Some(loader),
|
Some(loader),
|
||||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||||
)
|
)
|
||||||
case sr =>
|
case sr =>
|
||||||
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
|
(None, wrapper(() => sr.run(mainClass, cp.files, appArgs, logger).get))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end RunUtil
|
end RunUtil
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ object RunUtilTest extends Properties:
|
||||||
override def tests: List[Test] = List(
|
override def tests: List[Test] = List(
|
||||||
property("mkWindowTitle formats title correctly", testMkWindowTitle),
|
property("mkWindowTitle formats title correctly", testMkWindowTitle),
|
||||||
property("mkWindowTitle handles empty strings", testMkWindowTitleEmpty),
|
property("mkWindowTitle handles empty strings", testMkWindowTitleEmpty),
|
||||||
|
property("splitArgs with no -- treats all as app args", testSplitArgsNoDash),
|
||||||
|
property("splitArgs splits at first --", testSplitArgsWithDash),
|
||||||
|
property("splitArgs with only -- gives empty jvm and app args", testSplitArgsOnlyDash),
|
||||||
|
property("splitArgs with -- at start gives empty jvm args", testSplitArgsDashAtStart),
|
||||||
|
property("splitArgs with -- at end gives empty app args", testSplitArgsDashAtEnd),
|
||||||
|
property("splitArgs with multiple -- splits at first only", testSplitArgsMultipleDash),
|
||||||
|
property("splitArgs with empty input", testSplitArgsEmpty),
|
||||||
)
|
)
|
||||||
|
|
||||||
def testMkWindowTitle: Property =
|
def testMkWindowTitle: Property =
|
||||||
|
|
@ -34,4 +41,48 @@ object RunUtilTest extends Properties:
|
||||||
yield
|
yield
|
||||||
val result = RunUtil.mkWindowTitle("run", "", "", "")
|
val result = RunUtil.mkWindowTitle("run", "", "", "")
|
||||||
Result.assert(result == "sbt run: % % ")
|
Result.assert(result == "sbt run: % % ")
|
||||||
|
|
||||||
|
def testSplitArgsNoDash: Property =
|
||||||
|
for _ <- Gen.constant(()).forAll
|
||||||
|
yield
|
||||||
|
val (jvm, app) = RunUtil.splitArgs(Seq("arg1", "arg2"))
|
||||||
|
Result.assert(jvm.isEmpty).and(Result.assert(app == Seq("arg1", "arg2")))
|
||||||
|
|
||||||
|
def testSplitArgsWithDash: Property =
|
||||||
|
for _ <- Gen.constant(()).forAll
|
||||||
|
yield
|
||||||
|
val (jvm, app) = RunUtil.splitArgs(Seq("-Xmx2G", "-ea", "--", "arg1", "arg2"))
|
||||||
|
Result.assert(jvm == Seq("-Xmx2G", "-ea")).and(Result.assert(app == Seq("arg1", "arg2")))
|
||||||
|
|
||||||
|
def testSplitArgsOnlyDash: Property =
|
||||||
|
for _ <- Gen.constant(()).forAll
|
||||||
|
yield
|
||||||
|
val (jvm, app) = RunUtil.splitArgs(Seq("--"))
|
||||||
|
Result.assert(jvm.isEmpty).and(Result.assert(app.isEmpty))
|
||||||
|
|
||||||
|
def testSplitArgsDashAtStart: Property =
|
||||||
|
for _ <- Gen.constant(()).forAll
|
||||||
|
yield
|
||||||
|
val (jvm, app) = RunUtil.splitArgs(Seq("--", "arg1"))
|
||||||
|
Result.assert(jvm.isEmpty).and(Result.assert(app == Seq("arg1")))
|
||||||
|
|
||||||
|
def testSplitArgsDashAtEnd: Property =
|
||||||
|
for _ <- Gen.constant(()).forAll
|
||||||
|
yield
|
||||||
|
val (jvm, app) = RunUtil.splitArgs(Seq("-Xmx2G", "--"))
|
||||||
|
Result.assert(jvm == Seq("-Xmx2G")).and(Result.assert(app.isEmpty))
|
||||||
|
|
||||||
|
def testSplitArgsMultipleDash: Property =
|
||||||
|
for _ <- Gen.constant(()).forAll
|
||||||
|
yield
|
||||||
|
val (jvm, app) = RunUtil.splitArgs(Seq("-Xmx2G", "--", "arg1", "--", "arg2"))
|
||||||
|
Result
|
||||||
|
.assert(jvm == Seq("-Xmx2G"))
|
||||||
|
.and(Result.assert(app == Seq("arg1", "--", "arg2")))
|
||||||
|
|
||||||
|
def testSplitArgsEmpty: Property =
|
||||||
|
for _ <- Gen.constant(()).forAll
|
||||||
|
yield
|
||||||
|
val (jvm, app) = RunUtil.splitArgs(Seq.empty)
|
||||||
|
Result.assert(jvm.isEmpty).and(Result.assert(app.isEmpty))
|
||||||
end RunUtilTest
|
end RunUtilTest
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
scalaVersion := "3.6.4"
|
||||||
|
run / fork := true
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
object Main {
|
||||||
|
def main(args: Array[String]): Unit = {
|
||||||
|
val prop = System.getProperty("test.prop")
|
||||||
|
assert(prop == "hello", s"Expected system property test.prop=hello but got '$prop'")
|
||||||
|
assert(args.toList == List("arg1", "arg2"), s"Expected args [arg1, arg2] but got ${args.toList}")
|
||||||
|
println("OK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Verify that JVM args can be passed via the -- delimiter
|
||||||
|
> run -Dtest.prop=hello -- arg1 arg2
|
||||||
|
|
||||||
|
# Verify runMain also supports JVM args via --
|
||||||
|
> runMain Main -Dtest.prop=hello -- arg1 arg2
|
||||||
Loading…
Reference in New Issue