diff --git a/.github/workflows/sbtw-release.yml b/.github/workflows/sbtw-release.yml new file mode 100644 index 000000000..bbc66ecd1 --- /dev/null +++ b/.github/workflows/sbtw-release.yml @@ -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* diff --git a/sbtw/build.sbt b/sbtw/build.sbt new file mode 100644 index 000000000..afa931e77 --- /dev/null +++ b/sbtw/build.sbt @@ -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, + ) diff --git a/sbtw/src/main/scala/sbtw/ArgParser.scala b/sbtw/src/main/scala/sbtw/ArgParser.scala new file mode 100644 index 000000000..7ee35826f --- /dev/null +++ b/sbtw/src/main/scala/sbtw/ArgParser.scala @@ -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]("").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) + } + } +} diff --git a/sbtw/src/main/scala/sbtw/ConfigLoader.scala b/sbtw/src/main/scala/sbtw/ConfigLoader.scala new file mode 100644 index 000000000..9cd82eb82 --- /dev/null +++ b/sbtw/src/main/scala/sbtw/ConfigLoader.scala @@ -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 +} diff --git a/sbtw/src/main/scala/sbtw/LauncherOptions.scala b/sbtw/src/main/scala/sbtw/LauncherOptions.scala new file mode 100644 index 000000000..3006e0a4f --- /dev/null +++ b/sbtw/src/main/scala/sbtw/LauncherOptions.scala @@ -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" +} diff --git a/sbtw/src/main/scala/sbtw/Main.scala b/sbtw/src/main/scala/sbtw/Main.scala new file mode 100644 index 000000000..cc592ed17 --- /dev/null +++ b/sbtw/src/main/scala/sbtw/Main.scala @@ -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 set memory options (default: 1024) + | --sbt-version use the specified version of sbt + | --sbt-jar use the specified jar as the sbt launcher + | --java-home alternate JAVA_HOME + | -Dkey=val pass -Dkey=val to the JVM + | -X pass -X to the JVM (e.g. -Xmx1G) + | -J-X pass -X to the JVM (-J is stripped) + |""".stripMargin) + 0 + } +} diff --git a/sbtw/src/main/scala/sbtw/Memory.scala b/sbtw/src/main/scala/sbtw/Memory.scala new file mode 100644 index 000000000..155f58950 --- /dev/null +++ b/sbtw/src/main/scala/sbtw/Memory.scala @@ -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) + } + } +} diff --git a/sbtw/src/main/scala/sbtw/Runner.scala b/sbtw/src/main/scala/sbtw/Runner.scala new file mode 100644 index 000000000..64332cbc2 --- /dev/null +++ b/sbtw/src/main/scala/sbtw/Runner.scala @@ -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) + } +}