mirror of https://github.com/sbt/sbt.git
Implement sbtw Windows drop-in launcher
This commit is contained in:
parent
b2fea15030
commit
0c017d2e52
|
|
@ -0,0 +1,53 @@
|
|||
name: sbtw Release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-and-upload:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "23"
|
||||
cache: sbt
|
||||
|
||||
- name: Setup sbt
|
||||
uses: sbt/setup-sbt@v1
|
||||
|
||||
- name: Setup Windows C++ toolchain
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
|
||||
- name: Build sbtw native image
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 15
|
||||
max_attempts: 3
|
||||
shell: bash
|
||||
command: sbt -Dsbt.io.virtual=false sbtw/nativeImage
|
||||
env:
|
||||
JAVA_OPTS: -Xms800M -Xmx2G -Xss6M -XX:ReservedCodeCacheSize=128M -server -Dfile.encoding=UTF-8
|
||||
|
||||
- name: Upload sbtw to Release
|
||||
if: github.event_name == 'release'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.release.tag_name }}
|
||||
files: sbtw/target/bin/sbtw*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload sbtw artifact (manual run)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sbtw-windows-${{ github.sha }}
|
||||
path: sbtw/target/bin/sbtw*
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
val sbtwScalaVersion = "3.3.7"
|
||||
|
||||
lazy val sbtwProj = (project in file("."))
|
||||
.enablePlugins(NativeImagePlugin)
|
||||
.settings(
|
||||
commonSettings,
|
||||
name := "sbtw",
|
||||
description := "Windows drop-in launcher for sbt (replaces sbt.bat)",
|
||||
scalaVersion := sbtwScalaVersion,
|
||||
crossPaths := false,
|
||||
Compile / mainClass := Some("sbtw.Main"),
|
||||
libraryDependencies += "com.github.scopt" %% "scopt" % "4.1.0",
|
||||
nativeImageVersion := "23.0",
|
||||
nativeImageJvm := "graalvm-java23",
|
||||
nativeImageOutput := (target.value / "bin" / "sbtw").toPath.toFile,
|
||||
nativeImageOptions ++= Seq(
|
||||
"--no-fallback",
|
||||
s"--initialize-at-run-time=sbtw",
|
||||
"-H:+ReportExceptionStackTraces",
|
||||
s"-H:Name=${(target.value / "bin" / "sbtw").getAbsolutePath}",
|
||||
),
|
||||
Utils.noPublish,
|
||||
)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package sbtw
|
||||
|
||||
import scopt.OParser
|
||||
|
||||
object ArgParser {
|
||||
|
||||
def parse(args: Array[String]): Option[LauncherOptions] = {
|
||||
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[Int]("jvm-debug").action((x, c) => c.copy(jvmDebug = Some(x))),
|
||||
opt[String]("java-home").action((x, c) => c.copy(javaHome = Some(x))),
|
||||
arg[String]("<arg>").unbounded().optional().action((x, c) =>
|
||||
c.copy(residual = c.residual :+ x)
|
||||
),
|
||||
)
|
||||
}
|
||||
OParser.parse(parser, args, LauncherOptions()).map { opts =>
|
||||
val sbtNew = opts.residual.contains("new") || opts.residual.contains("init")
|
||||
opts.copy(sbtNew = sbtNew)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package sbtw
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.{ Files, Paths }
|
||||
import scala.io.Source
|
||||
import scala.util.Using
|
||||
|
||||
object ConfigLoader {
|
||||
|
||||
def loadLines(file: File): Seq[String] =
|
||||
if (!file.isFile) Nil
|
||||
else
|
||||
try
|
||||
Using.resource(Source.fromFile(file))(_.getLines().toList.flatMap { line =>
|
||||
val trimmed = line.trim
|
||||
if (trimmed.isEmpty || trimmed.startsWith("#")) Nil
|
||||
else Seq(trimmed)
|
||||
})
|
||||
catch { case _: Exception => Nil }
|
||||
|
||||
def loadSbtOpts(cwd: File, sbtHome: File): Seq[String] = {
|
||||
val fromProject = new File(cwd, ".sbtopts")
|
||||
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)
|
||||
(fromEtcLines ++ fromConfigLines ++ fromSbtConfigLines ++ fromEnv ++ fromProjectLines).toSeq
|
||||
}
|
||||
|
||||
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))
|
||||
fromEnv ++ loadLines(fromProject)
|
||||
}
|
||||
|
||||
private def stripJ(s: String): String = if (s.startsWith("-J")) s.substring(2).trim else s
|
||||
|
||||
def defaultJavaOpts: Seq[String] = Seq("-Dfile.encoding=UTF-8")
|
||||
def defaultSbtOpts: Seq[String] = Nil
|
||||
|
||||
def sbtVersionFromBuildProperties(projectDir: File): Option[String] = {
|
||||
val f = new File(projectDir, "project/build.properties")
|
||||
if (!f.isFile) return None
|
||||
try
|
||||
Using.resource(Source.fromFile(f)) { src =>
|
||||
src.getLines()
|
||||
.map(_.trim)
|
||||
.filterNot(_.startsWith("#"))
|
||||
.find(_.startsWith("sbt.version="))
|
||||
.map(_.drop("sbt.version=".length).trim)
|
||||
.filter(_.nonEmpty)
|
||||
}
|
||||
catch { case _: Exception => None }
|
||||
}
|
||||
|
||||
def isSbtProjectDir(dir: File): Boolean =
|
||||
new File(dir, "build.sbt").isFile || new File(dir, "project/build.properties").isFile
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package sbtw
|
||||
|
||||
case class LauncherOptions(
|
||||
help: Boolean = false,
|
||||
verbose: Boolean = false,
|
||||
debug: Boolean = false,
|
||||
version: Boolean = false,
|
||||
numericVersion: Boolean = false,
|
||||
scriptVersion: Boolean = false,
|
||||
shutdownAll: Boolean = false,
|
||||
allowEmpty: Boolean = false,
|
||||
client: Boolean = false,
|
||||
jvmClient: Boolean = false,
|
||||
noServer: Boolean = false,
|
||||
noColors: Boolean = false,
|
||||
noGlobal: Boolean = false,
|
||||
noShare: Boolean = false,
|
||||
noHideJdkWarnings: Boolean = false,
|
||||
debugInc: Boolean = false,
|
||||
timings: Boolean = false,
|
||||
traces: Boolean = false,
|
||||
batch: Boolean = false,
|
||||
sbtDir: Option[String] = None,
|
||||
sbtBoot: Option[String] = None,
|
||||
sbtCache: Option[String] = None,
|
||||
sbtJar: Option[String] = None,
|
||||
sbtVersion: Option[String] = None,
|
||||
ivy: Option[String] = None,
|
||||
mem: Option[Int] = None,
|
||||
supershell: Option[String] = None,
|
||||
color: Option[String] = None,
|
||||
jvmDebug: Option[Int] = None,
|
||||
javaHome: Option[String] = None,
|
||||
server: Boolean = false,
|
||||
residual: Seq[String] = Nil,
|
||||
sbtNew: Boolean = false,
|
||||
)
|
||||
|
||||
object LauncherOptions {
|
||||
val defaultMemMb = 1024
|
||||
val initSbtVersion = "_to_be_replaced"
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
package sbtw
|
||||
|
||||
import java.io.File
|
||||
import scala.sys.process.*
|
||||
|
||||
object Main {
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
val cwd = new File(sys.props("user.dir"))
|
||||
val sbtHome = new File(sys.env.get("SBT_HOME").getOrElse {
|
||||
sys.env.get("SBT_BIN_DIR").map(d => new File(d).getParent).getOrElse(cwd.getAbsolutePath)
|
||||
})
|
||||
val sbtBinDir = new File(sbtHome, "bin")
|
||||
|
||||
val fileSbtOpts = ConfigLoader.loadSbtOpts(cwd, sbtHome)
|
||||
val fileArgs = fileSbtOpts.flatMap(_.split("\\s+").filter(_.nonEmpty))
|
||||
val allArgs = fileArgs ++ args
|
||||
|
||||
ArgParser.parse(allArgs.toArray) match {
|
||||
case None => System.exit(1)
|
||||
case Some(opts) =>
|
||||
val exitCode = run(cwd, sbtHome, sbtBinDir, opts)
|
||||
System.exit(if (exitCode == 0) 0 else 1)
|
||||
}
|
||||
}
|
||||
|
||||
private def run(cwd: File, sbtHome: File, sbtBinDir: File, opts: LauncherOptions): Int = {
|
||||
if (opts.help) return printUsage()
|
||||
if (opts.version || opts.numericVersion || opts.scriptVersion) {
|
||||
return handleVersionCommands(cwd, sbtHome, sbtBinDir, opts)
|
||||
}
|
||||
if (opts.shutdownAll) {
|
||||
val javaCmd = Runner.findJavaCmd(opts.javaHome)
|
||||
return Runner.shutdownAll(javaCmd)
|
||||
}
|
||||
|
||||
if (!opts.allowEmpty && !opts.sbtNew && !ConfigLoader.isSbtProjectDir(cwd)) {
|
||||
System.err.println("[error] Neither build.sbt nor a 'project' directory in the current directory: " + cwd)
|
||||
System.err.println("[error] run 'sbt new', touch build.sbt, or run 'sbt --allow-empty'.")
|
||||
return 1
|
||||
}
|
||||
|
||||
val buildPropsVersion = ConfigLoader.sbtVersionFromBuildProperties(cwd)
|
||||
val clientOpt = opts.client || sys.env.get("SBT_NATIVE_CLIENT").contains("true")
|
||||
val useNativeClient = shouldRunNativeClient(opts.copy(client = clientOpt), buildPropsVersion)
|
||||
|
||||
if (useNativeClient) {
|
||||
val scriptPath = sbtBinDir.getAbsolutePath.replace("\\", "/") + "/sbt.bat"
|
||||
return Runner.runNativeClient(sbtBinDir, scriptPath, opts)
|
||||
}
|
||||
|
||||
val javaCmd = Runner.findJavaCmd(opts.javaHome)
|
||||
val javaVer = Runner.javaVersion(javaCmd)
|
||||
if (javaVer > 0 && javaVer < 8) {
|
||||
System.err.println("[error] sbt requires at least JDK 8+, you have " + javaVer)
|
||||
return 1
|
||||
}
|
||||
|
||||
val sbtJar = opts.sbtJar
|
||||
.filter(p => new File(p).isFile)
|
||||
.getOrElse(new File(sbtBinDir, "sbt-launch.jar").getAbsolutePath)
|
||||
if (!new File(sbtJar).isFile) {
|
||||
System.err.println("[error] Launcher jar not found: " + sbtJar)
|
||||
return 1
|
||||
}
|
||||
|
||||
var javaOpts = ConfigLoader.loadJvmOpts(cwd)
|
||||
if (javaOpts.isEmpty) javaOpts = ConfigLoader.defaultJavaOpts
|
||||
var sbtOpts = Runner.buildSbtOpts(opts)
|
||||
|
||||
val (residualJava, bootArgs) = Runner.splitResidual(opts.residual)
|
||||
javaOpts = javaOpts ++ residualJava
|
||||
|
||||
val (finalJava, finalSbt) = if (opts.mem.isDefined) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
private def shouldRunNativeClient(opts: LauncherOptions, buildPropsVersion: Option[String]): Boolean = {
|
||||
if (opts.sbtNew) return false
|
||||
if (opts.jvmClient) return false
|
||||
val version = buildPropsVersion.getOrElse(LauncherOptions.initSbtVersion)
|
||||
val parts = version.split("[.-]").take(2).flatMap(s => scala.util.Try(s.toInt).toOption)
|
||||
val (major, minor) = (parts.lift(0).getOrElse(0), parts.lift(1).getOrElse(0))
|
||||
if (major >= 2) !opts.server
|
||||
else if (major >= 1 && minor >= 4) opts.client
|
||||
else false
|
||||
}
|
||||
|
||||
private def handleVersionCommands(cwd: File, sbtHome: File, sbtBinDir: File, opts: LauncherOptions): Int = {
|
||||
if (opts.scriptVersion) {
|
||||
println(LauncherOptions.initSbtVersion)
|
||||
return 0
|
||||
}
|
||||
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) {
|
||||
System.err.println("[error] Launcher jar not found for version check")
|
||||
return 1
|
||||
}
|
||||
if (opts.numericVersion) {
|
||||
try {
|
||||
val out = Process(Seq(javaCmd, "-jar", sbtJar, "sbtVersion")).!!
|
||||
println(out.linesIterator.lastOption.map(_.trim).getOrElse(""))
|
||||
return 0
|
||||
} catch { case _: Exception => return 1 }
|
||||
}
|
||||
if (opts.version) {
|
||||
if (ConfigLoader.isSbtProjectDir(cwd)) {
|
||||
val out = try Process(Seq(javaCmd, "-jar", sbtJar, "sbtVersion")).!! catch { case _: Exception => "" }
|
||||
val ver = out.linesIterator.lastOption.map(_.trim).getOrElse("")
|
||||
println("sbt version in this project: " + ver)
|
||||
}
|
||||
println("sbt runner version: " + LauncherOptions.initSbtVersion)
|
||||
System.err.println("[info] sbt runner (sbtw) is a runner to run any declared version of sbt.")
|
||||
System.err.println("[info] Actual version of sbt is declared using project\\build.properties for each build.")
|
||||
return 0
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
private def printUsage(): Int = {
|
||||
println("""
|
||||
|Usage: sbtw [options]
|
||||
|
|
||||
| -h | --help print this message
|
||||
| -v | --verbose this runner is chattier
|
||||
| -V | --version print sbt version information
|
||||
| --numeric-version print the numeric sbt version
|
||||
| --script-version print the version of sbt script
|
||||
| shutdownall shutdown all running sbt processes
|
||||
| -d | --debug set sbt log level to debug
|
||||
| --allow-empty start sbt even if current directory contains no sbt project
|
||||
| --client run native client (sbt 1.4+)
|
||||
| --server run JVM launcher (disable native client, sbt 2.x)
|
||||
| --jvm-client run JVM client
|
||||
| --mem <integer> set memory options (default: 1024)
|
||||
| --sbt-version <v> use the specified version of sbt
|
||||
| --sbt-jar <path> use the specified jar as the sbt launcher
|
||||
| --java-home <path> alternate JAVA_HOME
|
||||
| -Dkey=val pass -Dkey=val to the JVM
|
||||
| -X<flag> pass -X<flag> to the JVM (e.g. -Xmx1G)
|
||||
| -J-X pass -X to the JVM (-J is stripped)
|
||||
|""".stripMargin)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package sbtw
|
||||
|
||||
object Memory {
|
||||
|
||||
private val memoryOptPrefixes = Set(
|
||||
"-Xmx", "-Xms", "-Xss",
|
||||
"-XX:MaxPermSize", "-XX:MaxMetaspaceSize", "-XX:ReservedCodeCacheSize",
|
||||
"-XX:+UseCGroupMemoryLimitForHeap", "-XX:MaxRAM", "-XX:InitialRAMPercentage",
|
||||
"-XX:MaxRAMPercentage", "-XX:MinRAMPercentage"
|
||||
)
|
||||
|
||||
def hasMemoryOpts(opts: Seq[String]): Boolean =
|
||||
opts.exists(o => memoryOptPrefixes.exists(p => o.startsWith(p)))
|
||||
|
||||
def evictMemoryOpts(opts: Seq[String]): Seq[String] =
|
||||
opts.filter(o => !memoryOptPrefixes.exists(p => o.startsWith(p)))
|
||||
|
||||
def addMemory(memMb: Int, javaVersion: Int): Seq[String] = {
|
||||
var codecache = memMb / 8
|
||||
if (codecache > 512) codecache = 512
|
||||
if (codecache < 128) codecache = 128
|
||||
val classMetadataSize = codecache * 2
|
||||
val base = Seq(
|
||||
s"-Xms${memMb}m",
|
||||
s"-Xmx${memMb}m",
|
||||
"-Xss4M",
|
||||
s"-XX:ReservedCodeCacheSize=${codecache}m"
|
||||
)
|
||||
if (javaVersion < 8) base :+ s"-XX:MaxPermSize=${classMetadataSize}m"
|
||||
else base
|
||||
}
|
||||
|
||||
def addDefaultMemory(
|
||||
javaOpts: Seq[String],
|
||||
sbtOpts: Seq[String],
|
||||
javaVersion: Int,
|
||||
defaultMemMb: Int
|
||||
): (Seq[String], Seq[String]) = {
|
||||
val fromJava = hasMemoryOpts(javaOpts)
|
||||
val fromTool = sys.env.get("JAVA_TOOL_OPTIONS").exists(s => hasMemoryOpts(s.split("\\s+").toSeq))
|
||||
val fromJdk = sys.env.get("JDK_JAVA_OPTIONS").exists(s => hasMemoryOpts(s.split("\\s+").toSeq))
|
||||
val fromSbt = hasMemoryOpts(sbtOpts)
|
||||
if (fromJava || fromTool || fromJdk || fromSbt)
|
||||
(javaOpts, sbtOpts)
|
||||
else {
|
||||
val evictedJava = evictMemoryOpts(javaOpts)
|
||||
val evictedSbt = evictMemoryOpts(sbtOpts)
|
||||
val memOpts = addMemory(defaultMemMb, javaVersion)
|
||||
(evictedJava ++ memOpts, evictedSbt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
package sbtw
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.{ Paths, Files }
|
||||
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) 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 = {
|
||||
try {
|
||||
val pb = Process(Seq(javaCmd, "-Xms32M", "-Xmx32M", "-version"))
|
||||
val out = pb.!!
|
||||
val line = out.linesIterator.find(_.contains("version")).getOrElse("")
|
||||
val quoted = line.split("\"").lift(1).getOrElse("")
|
||||
val parts = quoted.replaceFirst("^1\\.", "").split("[.-_]")
|
||||
val major = parts.headOption.flatMap(s => scala.util.Try(s.toInt).toOption).getOrElse(0)
|
||||
if (quoted.startsWith("1.") && parts.nonEmpty)
|
||||
scala.util.Try(parts(0).toInt).toOption.getOrElse(major)
|
||||
else major
|
||||
} catch { case _: Exception => 0 }
|
||||
}
|
||||
|
||||
def buildSbtOpts(opts: LauncherOptions): Seq[String] = {
|
||||
var s: Seq[String] = Nil
|
||||
if (opts.debug) s = s :+ "-debug"
|
||||
if (opts.debugInc) s = s :+ "-Dxsbt.inc.debug=true"
|
||||
if (opts.noColors) s = s :+ "-Dsbt.log.noformat=true"
|
||||
if (opts.noGlobal) s = s :+ "-Dsbt.global.base=project/.sbtboot"
|
||||
if (opts.noShare) s = s ++ Seq("-Dsbt.global.base=project/.sbtboot", "-Dsbt.boot.directory=project/.boot", "-Dsbt.ivy.home=project/.ivy")
|
||||
opts.supershell.foreach(v => s = s :+ s"-Dsbt.supershell=$v")
|
||||
opts.sbtVersion.foreach(v => s = s :+ s"-Dsbt.version=$v")
|
||||
opts.sbtDir.foreach(v => s = s :+ s"-Dsbt.global.base=$v")
|
||||
opts.sbtBoot.foreach(v => s = s :+ s"-Dsbt.boot.directory=$v")
|
||||
opts.sbtCache.foreach(v => s = s :+ s"-Dsbt.global.localcache=$v")
|
||||
opts.ivy.foreach(v => s = s :+ s"-Dsbt.ivy.home=$v")
|
||||
opts.color.foreach(v => s = s :+ s"-Dsbt.color=$v")
|
||||
if (opts.timings) s = s ++ Seq("-Dsbt.task.timings=true", "-Dsbt.task.timings.on.shutdown=true")
|
||||
if (opts.traces) s = s :+ "-Dsbt.traces=true"
|
||||
if (opts.noServer) s = s ++ Seq("-Dsbt.io.virtual=false", "-Dsbt.server.autostart=false")
|
||||
if (opts.jvmClient) s = s :+ "--client"
|
||||
s
|
||||
}
|
||||
|
||||
def runNativeClient(sbtBinDir: File, scriptPath: String, opts: LauncherOptions): Int = {
|
||||
val sbtn = new File(sbtBinDir, "sbtn-x86_64-pc-win32.exe")
|
||||
if (!sbtn.isFile) {
|
||||
System.err.println("[error] sbtn-x86_64-pc-win32.exe not found in " + sbtBinDir)
|
||||
return 1
|
||||
}
|
||||
val args = Seq("--sbt-script=" + scriptPath.replace(" ", "%20")) ++
|
||||
(if (opts.verbose) Seq("-v") else Nil) ++
|
||||
opts.residual
|
||||
val cmd = sbtn.getAbsolutePath +: args
|
||||
if (opts.verbose) {
|
||||
System.err.println("# running native client")
|
||||
cmd.foreach(a => System.err.println(a))
|
||||
}
|
||||
val proc = Process(cmd, None, "SBT_SCRIPT" -> scriptPath)
|
||||
proc.!
|
||||
}
|
||||
|
||||
def runJvm(
|
||||
javaCmd: String,
|
||||
javaOpts: Seq[String],
|
||||
sbtOpts: Seq[String],
|
||||
sbtJar: String,
|
||||
bootArgs: Seq[String],
|
||||
verbose: Boolean
|
||||
): Int = {
|
||||
val toolOpts = 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
|
||||
if (verbose) {
|
||||
System.err.println("# Executing command line:")
|
||||
cmd.foreach(a => System.err.println(if (a.contains(" ")) s""""$a"""" else a))
|
||||
}
|
||||
Process(cmd).!
|
||||
}
|
||||
|
||||
def shutdownAll(javaCmd: String): Int = {
|
||||
try {
|
||||
val jpsOut = Process(Seq("jps", "-lv")).!!
|
||||
val pids = jpsOut.linesIterator
|
||||
.filter(_.contains("xsbt.boot.Boot"))
|
||||
.flatMap(line => scala.util.Try(line.takeWhile(_.isDigit).toLong).toOption)
|
||||
.toList
|
||||
pids.foreach { pid =>
|
||||
try Process(Seq("taskkill", "/F", "/PID", pid.toString)).!
|
||||
catch { case _: Exception => }
|
||||
}
|
||||
System.err.println(s"shutdown ${pids.size} sbt processes")
|
||||
0
|
||||
} catch { case _: Exception => 1 }
|
||||
}
|
||||
|
||||
def splitResidual(residual: Seq[String]): (Seq[String], Seq[String]) = {
|
||||
var javaOpts: Seq[String] = Nil
|
||||
var bootArgs: Seq[String] = Nil
|
||||
var i = 0
|
||||
while (i < residual.size) {
|
||||
val a = residual(i)
|
||||
if (a.startsWith("-J")) javaOpts = javaOpts :+ a.drop(2)
|
||||
else if (a.startsWith("-X")) javaOpts = javaOpts :+ a
|
||||
else if (a.startsWith("-D") && a.contains("=")) bootArgs = bootArgs :+ a
|
||||
else if (a.startsWith("-D") && i + 1 < residual.size) {
|
||||
bootArgs = bootArgs :+ s"$a=${residual(i + 1)}"
|
||||
i += 1
|
||||
} else if (a.startsWith("-XX") && a.contains("=")) bootArgs = bootArgs :+ a
|
||||
else if (a.startsWith("-XX") && i + 1 < residual.size) {
|
||||
bootArgs = bootArgs :+ s"$a=${residual(i + 1)}"
|
||||
i += 1
|
||||
} else bootArgs = bootArgs :+ a
|
||||
i += 1
|
||||
}
|
||||
(javaOpts, bootArgs)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue