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 =
|
||||
loadForParser(discoveredMainClasses)((s, names) => runMainParser(s, names getOrElse Nil))
|
||||
Def.inputTask {
|
||||
val (mainClass, args) = parser.parsed
|
||||
val (mainClass, allArgs) = parser.parsed
|
||||
val (jvmArgs, appArgs) = RunUtil.splitArgs(allArgs)
|
||||
val cp = classpath.value
|
||||
val fo = (run / forkOptions).value
|
||||
val log = streams.value.log
|
||||
given FileConverter = fileConverter.value
|
||||
scalaRun.value
|
||||
.run(mainClass, cp.files, args, streams.value.log)
|
||||
val (modifiedRun, _) = RunUtil.applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||
modifiedRun
|
||||
.run(mainClass, cp.files, appArgs, log)
|
||||
.get
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,43 @@ import sbt.internal.worker.{ ClientJobParams, FilePath, JvmRunInfo, RunInfo }
|
|||
import sbt.io.IO
|
||||
import sbt.protocol.Serialization
|
||||
import sbt.util.CacheImplicits.given
|
||||
import sbt.util.Logger
|
||||
import xsbti.FileConverter
|
||||
|
||||
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 =
|
||||
if System.console() != null && System.getenv("TERM") != null then
|
||||
scala.Console.print(s"\u001b]0;$title\u0007")
|
||||
|
|
@ -38,11 +72,14 @@ object RunUtil:
|
|||
): Initialize[InputTask[Unit]] =
|
||||
val parser = Def.spaceDelimited()
|
||||
Def.inputTask {
|
||||
val in = parser.parsed
|
||||
val (jvmArgs, appArgs) = splitArgs(parser.parsed)
|
||||
val mainClass = getMainClass(mainClassTask.value)
|
||||
val cp = classpath.value
|
||||
val fo = (run / forkOptions).value
|
||||
val log = streams.value.log
|
||||
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(
|
||||
|
|
@ -136,9 +173,12 @@ object RunUtil:
|
|||
val conv = fileConverter.value
|
||||
given FileConverter = conv
|
||||
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 fo = (run / forkOptions).value
|
||||
val log = streams.value.log
|
||||
val (modifiedRun, modifiedFo) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||
val state = Keys.state.value
|
||||
val windowTitle = mkWindowTitle("runMain", organization.value, name.value, version.value)
|
||||
if clientRun.value && state.isNetworkCommand then
|
||||
|
|
@ -149,7 +189,8 @@ object RunUtil:
|
|||
workingDir,
|
||||
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(
|
||||
runInfo = info
|
||||
)
|
||||
|
|
@ -168,15 +209,15 @@ object RunUtil:
|
|||
hashClasspath,
|
||||
conv,
|
||||
)
|
||||
scalaRun.value match
|
||||
modifiedRun match
|
||||
case r: Run =>
|
||||
val loader = r.newLoader(cp.files)
|
||||
(
|
||||
Some(loader),
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||
)
|
||||
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
|
||||
()
|
||||
}
|
||||
|
|
@ -192,11 +233,13 @@ object RunUtil:
|
|||
Def.inputTask {
|
||||
val conv = fileConverter.value
|
||||
given FileConverter = conv
|
||||
val args = parser.parsed
|
||||
val (jvmArgs, appArgs) = splitArgs(parser.parsed)
|
||||
val service = bgJobService.value
|
||||
val mainClass = getMainClass(mainClassTask.value)
|
||||
val hashClasspath = (bgRun / bgHashClasspath).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 windowTitle = mkWindowTitle("run", organization.value, name.value, version.value)
|
||||
if clientRun.value && state.isNetworkCommand then
|
||||
|
|
@ -207,7 +250,7 @@ object RunUtil:
|
|||
workingDir,
|
||||
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(
|
||||
runInfo = info
|
||||
)
|
||||
|
|
@ -226,15 +269,15 @@ object RunUtil:
|
|||
hashClasspath,
|
||||
conv
|
||||
)
|
||||
scalaRun.value match
|
||||
modifiedRun match
|
||||
case r: Run =>
|
||||
val loader = r.newLoader(cp.files)
|
||||
(
|
||||
Some(loader),
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||
)
|
||||
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
|
||||
()
|
||||
}
|
||||
|
|
@ -250,8 +293,12 @@ object RunUtil:
|
|||
)
|
||||
Def.inputTask {
|
||||
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 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 converter = fileConverter.value
|
||||
setWindowTitle(mkWindowTitle("bgRunMain", organization.value, name.value, version.value))
|
||||
|
|
@ -268,15 +315,15 @@ object RunUtil:
|
|||
)
|
||||
else classpath.value
|
||||
given FileConverter = fileConverter.value
|
||||
scalaRun.value match
|
||||
modifiedRun match
|
||||
case r: Run =>
|
||||
val loader = r.newLoader(cp.files)
|
||||
(
|
||||
Some(loader),
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||
)
|
||||
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]] =
|
||||
val parser = Def.spaceDelimited()
|
||||
Def.inputTask {
|
||||
val args = parser.parsed
|
||||
val (jvmArgs, appArgs) = splitArgs(parser.parsed)
|
||||
val service = bgJobService.value
|
||||
val mainClass = getMainClass(mainClassTask.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 converter = fileConverter.value
|
||||
setWindowTitle(mkWindowTitle("bgRun", organization.value, name.value, version.value))
|
||||
|
|
@ -309,15 +359,15 @@ object RunUtil:
|
|||
)
|
||||
else classpath.value
|
||||
given FileConverter = converter
|
||||
scalaRun.value match
|
||||
modifiedRun match
|
||||
case r: Run =>
|
||||
val loader = r.newLoader(cp.files)
|
||||
(
|
||||
Some(loader),
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||
)
|
||||
case sr =>
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, appArgs, logger).get))
|
||||
}
|
||||
}
|
||||
end RunUtil
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ object RunUtilTest extends Properties:
|
|||
override def tests: List[Test] = List(
|
||||
property("mkWindowTitle formats title correctly", testMkWindowTitle),
|
||||
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 =
|
||||
|
|
@ -34,4 +41,48 @@ object RunUtilTest extends Properties:
|
|||
yield
|
||||
val result = RunUtil.mkWindowTitle("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
|
||||
|
|
|
|||
|
|
@ -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