diff --git a/.travis.yml b/.travis.yml index 451d7b8a9..7c92f1ad4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ matrix: include: ## build using JDK 8, test using JDK 8 - script: - - sbt -Dsbt.build.version=$SBT_VER universal:packageBin + - sbt -Dsbt.build.version=$SBT_VER universal:packageBin universal:stage integrationTest/test - cd citest && ./test.sh jdk: oraclejdk8 @@ -33,7 +33,7 @@ matrix: - unset _JAVA_OPTIONS - curl -sL https://raw.githubusercontent.com/shyiko/jabba/0.10.1/install.sh | bash && . ~/.jabba/jabba.sh script: - - sbt -Dsbt.build.version=$SBT_VER universal:packageBin + - sbt -Dsbt.build.version=$SBT_VER universal:packageBin universal:stage integrationTest/test - $JABBA_HOME/bin/jabba install $TRAVIS_JDK && export JAVA_HOME="$JABBA_HOME/jdk/$TRAVIS_JDK" && export PATH="$JAVA_HOME/bin:$PATH" && java -Xmx32m -version - cd citest && ./test.sh jdk: oraclejdk8 diff --git a/build.sbt b/build.sbt index bbcb335da..96777b968 100644 --- a/build.sbt +++ b/build.sbt @@ -68,6 +68,7 @@ val root = (project in file(".")). // TODO - GPG Trust validation. file }, + // GENERAL LINUX PACKAGING STUFFS maintainer := "Eugene Yokota ", packageSummary := "sbt, the interactive build tool", @@ -78,11 +79,10 @@ val root = (project in file(".")). val links = linuxPackageSymlinks.value for { link <- links - if !(link.destination endsWith "sbt-launch-lib.bash") if !(link.destination endsWith "sbt-launch.jar") } yield link }, - + // DEBIAN SPECIFIC debianBuildId := 0, version in Debian := { @@ -150,15 +150,8 @@ val root = (project in file(".")). mappings in Universal := { val t = (target in Universal).value val prev = (mappings in Universal).value - val BinBash = "bin" + java.io.File.separator + "sbt-launch-lib.bash" val BinBat = "bin" + java.io.File.separator + "sbt.bat" prev.toList map { - case (k, BinBash) => - val x = IO.read(k) - IO.write(t / "sbt-launch-lib.bash", x.replaceAllLiterally( - "declare init_sbt_version=_to_be_replaced", - s"""declare init_sbt_version="$sbtVersionToRelease"""")) - (t / "sbt-launch-lib.bash", BinBash) case (k, BinBat) => val x = IO.read(k) IO.write(t / "sbt.bat", x.replaceAllLiterally( @@ -202,6 +195,18 @@ val root = (project in file(".")). } ) +lazy val integrationTest = (project in file("integration-test")) + .settings( + name := "integration-test", + scalaVersion := "2.12.8", + libraryDependencies ++= Seq( + "io.monix" %% "minitest" % "2.3.2" % Test, + "com.eed3si9n.expecty" %% "expecty" % "0.11.0" % Test, + "org.scala-sbt" %% "io" % "1.2.2" % Test + ), + testFrameworks += new TestFramework("minitest.runner.Framework") + ) + lazy val java9rtexport = (project in file("java9-rt-export")) .settings( name := "java9-rt-export", diff --git a/citest/test.sh b/citest/test.sh index 4699a876a..53c9c6db6 100755 --- a/citest/test.sh +++ b/citest/test.sh @@ -11,7 +11,7 @@ unzip -qo ../target/universal/sbt.zip -d ./freshly-baked export SBT_OPTS=-Dfile.encoding=UTF-8 -./freshly-baked/sbt/bin/sbt about run +./freshly-baked/sbt/bin/sbt about run -v export SBT_OPTS="-Dfile.encoding=UTF-8 -Xms2048M -Xmx2048M -Xss2M -XX:MaxPermSize=512M" diff --git a/integration-test/src/test/scala/InheritInput.scala b/integration-test/src/test/scala/InheritInput.scala new file mode 100644 index 000000000..d8304926d --- /dev/null +++ b/integration-test/src/test/scala/InheritInput.scala @@ -0,0 +1,17 @@ +package sbt.internal + +import java.lang.{ ProcessBuilder => JProcessBuilder } + +private[sbt] object InheritInput { + def apply(p: JProcessBuilder): Boolean = (redirectInput, inherit) match { + case (Some(m), Some(f)) => + m.invoke(p, f); true + case _ => false + } + + private[this] val pbClass = Class.forName("java.lang.ProcessBuilder") + private[this] val redirectClass = pbClass.getClasses find (_.getSimpleName == "Redirect") + + private[this] val redirectInput = redirectClass map (pbClass.getMethod("redirectInput", _)) + private[this] val inherit = redirectClass map (_ getField "INHERIT" get null) +} diff --git a/integration-test/src/test/scala/PowerAssertions.scala b/integration-test/src/test/scala/PowerAssertions.scala new file mode 100644 index 000000000..eb3671b76 --- /dev/null +++ b/integration-test/src/test/scala/PowerAssertions.scala @@ -0,0 +1,7 @@ +package example.test + +import com.eed3si9n.expecty.Expecty + +trait PowerAssertions { + lazy val assert: Expecty = new Expecty() +} diff --git a/integration-test/src/test/scala/Process.scala b/integration-test/src/test/scala/Process.scala new file mode 100644 index 000000000..40798e209 --- /dev/null +++ b/integration-test/src/test/scala/Process.scala @@ -0,0 +1,216 @@ +package sbt.internal + +import java.lang.{ Process => JProcess, ProcessBuilder => JProcessBuilder } +import java.io.{ Closeable, File, IOException } +import java.io.{ BufferedReader, InputStream, InputStreamReader, OutputStream, PipedInputStream, PipedOutputStream } +import java.net.URL + +trait ProcessExtra { + import Process._ + implicit def builderToProcess(builder: JProcessBuilder): ProcessBuilder = apply(builder) + implicit def fileToProcess(file: File): FilePartialBuilder = apply(file) + implicit def urlToProcess(url: URL): URLPartialBuilder = apply(url) + + implicit def buildersToProcess[T](builders: Seq[T])(implicit convert: T => SourcePartialBuilder): Seq[SourcePartialBuilder] = applySeq(builders) + + implicit def stringToProcess(command: String): ProcessBuilder = apply(command) + implicit def stringSeqToProcess(command: Seq[String]): ProcessBuilder = apply(command) +} + +/** Methods for constructing simple commands that can then be combined. */ +object Process extends ProcessExtra { + def apply(command: String): ProcessBuilder = apply(command, None) + + def apply(command: Seq[String]): ProcessBuilder = apply(command.toArray, None) + + def apply(command: String, arguments: Seq[String]): ProcessBuilder = apply(command :: arguments.toList, None) + /** create ProcessBuilder with working dir set to File and extra environment variables */ + def apply(command: String, cwd: File, extraEnv: (String, String)*): ProcessBuilder = + apply(command, Some(cwd), extraEnv: _*) + /** create ProcessBuilder with working dir set to File and extra environment variables */ + def apply(command: Seq[String], cwd: File, extraEnv: (String, String)*): ProcessBuilder = + apply(command, Some(cwd), extraEnv: _*) + /** create ProcessBuilder with working dir optionally set to File and extra environment variables */ + def apply(command: String, cwd: Option[File], extraEnv: (String, String)*): ProcessBuilder = { + apply(command.split("""\s+"""), cwd, extraEnv: _*) + // not smart to use this on windows, because CommandParser uses \ to escape ". + /*CommandParser.parse(command) match { + case Left(errorMsg) => error(errorMsg) + case Right((cmd, args)) => apply(cmd :: args, cwd, extraEnv : _*) + }*/ + } + /** create ProcessBuilder with working dir optionally set to File and extra environment variables */ + def apply(command: Seq[String], cwd: Option[File], extraEnv: (String, String)*): ProcessBuilder = { + val jpb = new JProcessBuilder(command.toArray: _*) + cwd.foreach(jpb directory _) + extraEnv.foreach { case (k, v) => jpb.environment.put(k, v) } + apply(jpb) + } + def apply(builder: JProcessBuilder): ProcessBuilder = new SimpleProcessBuilder(builder) + def apply(file: File): FilePartialBuilder = new FileBuilder(file) + def apply(url: URL): URLPartialBuilder = new URLBuilder(url) + + def applySeq[T](builders: Seq[T])(implicit convert: T => SourcePartialBuilder): Seq[SourcePartialBuilder] = builders.map(convert) + + def apply(value: Boolean): ProcessBuilder = apply(value.toString, if (value) 0 else 1) + def apply(name: String, exitValue: => Int): ProcessBuilder = new DummyProcessBuilder(name, exitValue) + + def cat(file: SourcePartialBuilder, files: SourcePartialBuilder*): ProcessBuilder = cat(file :: files.toList) + def cat(files: Seq[SourcePartialBuilder]): ProcessBuilder = + { + require(files.nonEmpty) + files.map(_.cat).reduceLeft(_ #&& _) + } +} + +trait SourcePartialBuilder extends NotNull { + /** Writes the output stream of this process to the given file. */ + def #>(f: File): ProcessBuilder = toFile(f, false) + /** Appends the output stream of this process to the given file. */ + def #>>(f: File): ProcessBuilder = toFile(f, true) + /** + * Writes the output stream of this process to the given OutputStream. The + * argument is call-by-name, so the stream is recreated, written, and closed each + * time this process is executed. + */ + def #>(out: => OutputStream): ProcessBuilder = #>(new OutputStreamBuilder(out)) + def #>(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(toSource, b, false, ExitCodes.firstIfNonzero) + private def toFile(f: File, append: Boolean) = #>(new FileOutput(f, append)) + def cat = toSource + protected def toSource: ProcessBuilder +} +trait SinkPartialBuilder extends NotNull { + /** Reads the given file into the input stream of this process. */ + def #<(f: File): ProcessBuilder = #<(new FileInput(f)) + /** Reads the given URL into the input stream of this process. */ + def #<(f: URL): ProcessBuilder = #<(new URLInput(f)) + /** + * Reads the given InputStream into the input stream of this process. The + * argument is call-by-name, so the stream is recreated, read, and closed each + * time this process is executed. + */ + def #<(in: => InputStream): ProcessBuilder = #<(new InputStreamBuilder(in)) + def #<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, toSink, false, ExitCodes.firstIfNonzero) + protected def toSink: ProcessBuilder +} + +trait URLPartialBuilder extends SourcePartialBuilder +trait FilePartialBuilder extends SinkPartialBuilder with SourcePartialBuilder { + def #<<(f: File): ProcessBuilder + def #<<(u: URL): ProcessBuilder + def #<<(i: => InputStream): ProcessBuilder + def #<<(p: ProcessBuilder): ProcessBuilder +} + +/** + * Represents a process that is running or has finished running. + * It may be a compound process with several underlying native processes (such as 'a #&& b`). + */ +trait Process extends NotNull { + /** Blocks until this process exits and returns the exit code.*/ + def exitValue(): Int + /** Destroys this process. */ + def destroy(): Unit +} +/** Represents a runnable process. */ +trait ProcessBuilder extends SourcePartialBuilder with SinkPartialBuilder { + /** + * Starts the process represented by this builder, blocks until it exits, and returns the output as a String. Standard error is + * sent to the console. If the exit code is non-zero, an exception is thrown. + */ + def !! : String + /** + * Starts the process represented by this builder, blocks until it exits, and returns the output as a String. Standard error is + * sent to the provided ProcessLogger. If the exit code is non-zero, an exception is thrown. + */ + def !!(log: ProcessLogger): String + /** + * Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the console. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination and then throw an exception. + */ + def lines: Stream[String] + /** + * Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the provided ProcessLogger. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination but will not throw an exception. + */ + def lines(log: ProcessLogger): Stream[String] + /** + * Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the console. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination but will not throw an exception. + */ + def lines_! : Stream[String] + /** + * Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the provided ProcessLogger. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination but will not throw an exception. + */ + def lines_!(log: ProcessLogger): Stream[String] + /** + * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the console. + */ + def ! : Int + /** + * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the given ProcessLogger. + */ + def !(log: ProcessLogger): Int + /** + * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the console. The newly started process reads from standard input of the current process. + */ + def !< : Int + /** + * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the given ProcessLogger. The newly started process reads from standard input of the current process. + */ + def !<(log: ProcessLogger): Int + /** Starts the process represented by this builder. Standard output and error are sent to the console.*/ + def run(): Process + /** Starts the process represented by this builder. Standard output and error are sent to the given ProcessLogger.*/ + def run(log: ProcessLogger): Process + /** Starts the process represented by this builder. I/O is handled by the given ProcessIO instance.*/ + def run(io: ProcessIO): Process + /** + * Starts the process represented by this builder. Standard output and error are sent to the console. + * The newly started process reads from standard input of the current process if `connectInput` is true. + */ + def run(connectInput: Boolean): Process + /** + * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are + * sent to the given ProcessLogger. + * The newly started process reads from standard input of the current process if `connectInput` is true. + */ + def run(log: ProcessLogger, connectInput: Boolean): Process + + def runBuffered(log: ProcessLogger, connectInput: Boolean): Process + + /** Constructs a command that runs this command first and then `other` if this command succeeds.*/ + def #&&(other: ProcessBuilder): ProcessBuilder + /** Constructs a command that runs this command first and then `other` if this command does not succeed.*/ + def #||(other: ProcessBuilder): ProcessBuilder + /** + * Constructs a command that will run this command and pipes the output to `other`. + * `other` must be a simple command. + * The exit code will be that of `other` regardless of whether this command succeeds. + */ + def #|(other: ProcessBuilder): ProcessBuilder + /** Constructs a command that will run this command and then `other`. The exit code will be the exit code of `other`.*/ + def ###(other: ProcessBuilder): ProcessBuilder + + def canPipeTo: Boolean +} +/** Each method will be called in a separate thread.*/ +final class ProcessIO(val writeInput: OutputStream => Unit, val processOutput: InputStream => Unit, val processError: InputStream => Unit, val inheritInput: JProcessBuilder => Boolean) extends NotNull { + def withOutput(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, process, processError, inheritInput) + def withError(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, processOutput, process, inheritInput) + def withInput(write: OutputStream => Unit): ProcessIO = new ProcessIO(write, processOutput, processError, inheritInput) +} +trait ProcessLogger { + def info(s: => String): Unit + def error(s: => String): Unit + def buffer[T](f: => T): T +} diff --git a/integration-test/src/test/scala/ProcessImpl.scala b/integration-test/src/test/scala/ProcessImpl.scala new file mode 100644 index 000000000..7c8e4bc01 --- /dev/null +++ b/integration-test/src/test/scala/ProcessImpl.scala @@ -0,0 +1,433 @@ +package sbt.internal + +import java.lang.{ Process => JProcess, ProcessBuilder => JProcessBuilder } +import java.io.{ BufferedReader, Closeable, InputStream, InputStreamReader, IOException, OutputStream, PrintStream } +import java.io.{ FilterInputStream, FilterOutputStream, PipedInputStream, PipedOutputStream } +import java.io.{ File, FileInputStream, FileOutputStream } +import java.net.URL + +/** Runs provided code in a new Thread and returns the Thread instance. */ +private object Spawn { + def apply(f: => Unit): Thread = apply(f, false) + def apply(f: => Unit, daemon: Boolean): Thread = + { + val thread = new Thread() { override def run() = { f } } + thread.setDaemon(daemon) + thread.start() + thread + } +} +private object Future { + def apply[T](f: => T): () => T = + { + val result = new SyncVar[Either[Throwable, T]] + def run(): Unit = + try { result.set(Right(f)) } + catch { case e: Exception => result.set(Left(e)) } + Spawn(run) + () => + result.get match { + case Right(value) => value + case Left(exception) => throw exception + } + } +} + +object BasicIO { + def apply(buffer: StringBuffer, log: Option[ProcessLogger], withIn: Boolean) = new ProcessIO(input(withIn), processFully(buffer), getErr(log), inheritInput(withIn)) + def apply(log: ProcessLogger, withIn: Boolean) = new ProcessIO(input(withIn), processInfoFully(log), processErrFully(log), inheritInput(withIn)) + + def getErr(log: Option[ProcessLogger]) = log match { case Some(lg) => processErrFully(lg); case None => toStdErr } + + private def processErrFully(log: ProcessLogger) = processFully(s => log.error(s)) + private def processInfoFully(log: ProcessLogger) = processFully(s => log.info(s)) + + def closeOut = (_: OutputStream).close() + final val BufferSize = 8192 + final val Newline = System.getProperty("line.separator") + + def close(c: java.io.Closeable) = try { c.close() } catch { case _: java.io.IOException => () } + def processFully(buffer: Appendable): InputStream => Unit = processFully(appendLine(buffer)) + def processFully(processLine: String => Unit): InputStream => Unit = + in => + { + val reader = new BufferedReader(new InputStreamReader(in)) + processLinesFully(processLine)(reader.readLine) + reader.close() + } + def processLinesFully(processLine: String => Unit)(readLine: () => String): Unit = { + def readFully(): Unit = { + val line = readLine() + if (line != null) { + processLine(line) + readFully() + } + } + readFully() + } + def connectToIn(o: OutputStream): Unit = transferFully(Uncloseable protect System.in, o) + def input(connect: Boolean): OutputStream => Unit = if (connect) connectToIn else closeOut + def standard(connectInput: Boolean): ProcessIO = standard(input(connectInput), inheritInput(connectInput)) + def standard(in: OutputStream => Unit, inheritIn: JProcessBuilder => Boolean): ProcessIO = new ProcessIO(in, toStdOut, toStdErr, inheritIn) + + def toStdErr = (in: InputStream) => transferFully(in, System.err) + def toStdOut = (in: InputStream) => transferFully(in, System.out) + + def transferFully(in: InputStream, out: OutputStream): Unit = + try { transferFullyImpl(in, out) } + catch { case _: InterruptedException => () } + + private[this] def appendLine(buffer: Appendable): String => Unit = + line => + { + buffer.append(line) + buffer.append(Newline) + } + + private[this] def transferFullyImpl(in: InputStream, out: OutputStream): Unit = { + val continueCount = 1 //if(in.isInstanceOf[PipedInputStream]) 1 else 0 + val buffer = new Array[Byte](BufferSize) + def read(): Unit = { + val byteCount = in.read(buffer) + if (byteCount >= continueCount) { + out.write(buffer, 0, byteCount) + out.flush() + read + } + } + read + in.close() + } + + def inheritInput(connect: Boolean) = { p: JProcessBuilder => if (connect) InheritInput(p) else false } +} +private[sbt] object ExitCodes { + def ignoreFirst: (Int, Int) => Int = (a, b) => b + def firstIfNonzero: (Int, Int) => Int = (a, b) => if (a != 0) a else b +} + +private[sbt] abstract class AbstractProcessBuilder extends ProcessBuilder with SinkPartialBuilder with SourcePartialBuilder { + def #&&(other: ProcessBuilder): ProcessBuilder = new AndProcessBuilder(this, other) + def #||(other: ProcessBuilder): ProcessBuilder = new OrProcessBuilder(this, other) + def #|(other: ProcessBuilder): ProcessBuilder = + { + require(other.canPipeTo, "Piping to multiple processes is not supported.") + new PipedProcessBuilder(this, other, false, exitCode = ExitCodes.ignoreFirst) + } + def ###(other: ProcessBuilder): ProcessBuilder = new SequenceProcessBuilder(this, other) + + protected def toSource = this + protected def toSink = this + + def run(): Process = run(false) + def run(connectInput: Boolean): Process = run(BasicIO.standard(connectInput)) + def run(log: ProcessLogger): Process = run(log, false) + def run(log: ProcessLogger, connectInput: Boolean): Process = run(BasicIO(log, connectInput)) + + private[this] def getString(log: Option[ProcessLogger], withIn: Boolean): String = + { + val buffer = new StringBuffer + val code = this ! BasicIO(buffer, log, withIn) + if (code == 0) buffer.toString else sys.error("Nonzero exit value: " + code) + } + def !! = getString(None, false) + def !!(log: ProcessLogger) = getString(Some(log), false) + def !!< = getString(None, true) + def !!<(log: ProcessLogger) = getString(Some(log), true) + + def lines: Stream[String] = lines(false, true, None) + def lines(log: ProcessLogger): Stream[String] = lines(false, true, Some(log)) + def lines_! : Stream[String] = lines(false, false, None) + def lines_!(log: ProcessLogger): Stream[String] = lines(false, false, Some(log)) + + private[this] def lines(withInput: Boolean, nonZeroException: Boolean, log: Option[ProcessLogger]): Stream[String] = + { + val streamed = Streamed[String](nonZeroException) + val process = run(new ProcessIO(BasicIO.input(withInput), BasicIO.processFully(streamed.process), BasicIO.getErr(log), BasicIO.inheritInput(withInput))) + Spawn { streamed.done(process.exitValue()) } + streamed.stream() + } + + def ! = run(false).exitValue() + def !< = run(true).exitValue() + def !(log: ProcessLogger) = runBuffered(log, false).exitValue() + def !<(log: ProcessLogger) = runBuffered(log, true).exitValue() + def runBuffered(log: ProcessLogger, connectInput: Boolean) = + log.buffer { run(log, connectInput) } + def !(io: ProcessIO) = run(io).exitValue() + + def canPipeTo = false +} + +private[sbt] class URLBuilder(url: URL) extends URLPartialBuilder with SourcePartialBuilder { + protected def toSource = new URLInput(url) +} +private[sbt] class FileBuilder(base: File) extends FilePartialBuilder with SinkPartialBuilder with SourcePartialBuilder { + protected def toSource = new FileInput(base) + protected def toSink = new FileOutput(base, false) + def #<<(f: File): ProcessBuilder = #<<(new FileInput(f)) + def #<<(u: URL): ProcessBuilder = #<<(new URLInput(u)) + def #<<(s: => InputStream): ProcessBuilder = #<<(new InputStreamBuilder(s)) + def #<<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, new FileOutput(base, true), false, ExitCodes.firstIfNonzero) +} + +private abstract class BasicBuilder extends AbstractProcessBuilder { + protected[this] def checkNotThis(a: ProcessBuilder) = require(a != this, "Compound process '" + a + "' cannot contain itself.") + final def run(io: ProcessIO): Process = + { + val p = createProcess(io) + p.start() + p + } + protected[this] def createProcess(io: ProcessIO): BasicProcess +} +private abstract class BasicProcess extends Process { + def start(): Unit +} + +private abstract class CompoundProcess extends BasicProcess { + def destroy(): Unit = destroyer() + def exitValue() = getExitValue().getOrElse(sys.error("No exit code: process destroyed.")) + + def start() = getExitValue + + protected lazy val (getExitValue, destroyer) = + { + val code = new SyncVar[Option[Int]]() + code.set(None) + val thread = Spawn(code.set(runAndExitValue())) + + ( + Future { thread.join(); code.get }, + () => thread.interrupt() + ) + } + + /** Start and block until the exit value is available and then return it in Some. Return None if destroyed (use 'run')*/ + protected[this] def runAndExitValue(): Option[Int] + + protected[this] def runInterruptible[T](action: => T)(destroyImpl: => Unit): Option[T] = + { + try { Some(action) } + catch { case _: InterruptedException => destroyImpl; None } + } +} + +private abstract class SequentialProcessBuilder(a: ProcessBuilder, b: ProcessBuilder, operatorString: String) extends BasicBuilder { + checkNotThis(a) + checkNotThis(b) + override def toString = " ( " + a + " " + operatorString + " " + b + " ) " +} +private class PipedProcessBuilder(first: ProcessBuilder, second: ProcessBuilder, toError: Boolean, exitCode: (Int, Int) => Int) extends SequentialProcessBuilder(first, second, if (toError) "#|!" else "#|") { + override def createProcess(io: ProcessIO) = new PipedProcesses(first, second, io, toError, exitCode) +} +private class AndProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#&&") { + override def createProcess(io: ProcessIO) = new AndProcess(first, second, io) +} +private class OrProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#||") { + override def createProcess(io: ProcessIO) = new OrProcess(first, second, io) +} +private class SequenceProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "###") { + override def createProcess(io: ProcessIO) = new ProcessSequence(first, second, io) +} + +private class SequentialProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO, evaluateSecondProcess: Int => Boolean) extends CompoundProcess { + protected[this] override def runAndExitValue() = + { + val first = a.run(io) + runInterruptible(first.exitValue)(first.destroy()) flatMap + { codeA => + if (evaluateSecondProcess(codeA)) { + val second = b.run(io) + runInterruptible(second.exitValue)(second.destroy()) + } else + Some(codeA) + } + } +} +private class AndProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, _ == 0) +private class OrProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, _ != 0) +private class ProcessSequence(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, ignore => true) + +private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: ProcessIO, toError: Boolean, exitCode: (Int, Int) => Int) extends CompoundProcess { + protected[this] override def runAndExitValue() = + { + val currentSource = new SyncVar[Option[InputStream]] + val pipeOut = new PipedOutputStream + val source = new PipeSource(currentSource, pipeOut, a.toString) + source.start() + + val pipeIn = new PipedInputStream(pipeOut) + val currentSink = new SyncVar[Option[OutputStream]] + val sink = new PipeSink(pipeIn, currentSink, b.toString) + sink.start() + + def handleOutOrError(fromOutput: InputStream) = currentSource.put(Some(fromOutput)) + + val firstIO = + if (toError) + defaultIO.withError(handleOutOrError) + else + defaultIO.withOutput(handleOutOrError) + val secondIO = defaultIO.withInput(toInput => currentSink.put(Some(toInput))) + + val second = b.run(secondIO) + val first = a.run(firstIO) + try { + runInterruptible { + val firstResult = first.exitValue + currentSource.put(None) + currentSink.put(None) + val secondResult = second.exitValue + exitCode(firstResult, secondResult) + } { + first.destroy() + second.destroy() + } + } finally { + BasicIO.close(pipeIn) + BasicIO.close(pipeOut) + } + } +} +private class PipeSource(currentSource: SyncVar[Option[InputStream]], pipe: PipedOutputStream, label: => String) extends Thread { + final override def run(): Unit = { + currentSource.get match { + case Some(source) => + try { BasicIO.transferFully(source, pipe) } + catch { case e: IOException => println("I/O error " + e.getMessage + " for process: " + label); e.printStackTrace() } + finally { + BasicIO.close(source) + currentSource.unset() + } + run() + case None => + currentSource.unset() + BasicIO.close(pipe) + } + } +} +private class PipeSink(pipe: PipedInputStream, currentSink: SyncVar[Option[OutputStream]], label: => String) extends Thread { + final override def run(): Unit = { + currentSink.get match { + case Some(sink) => + try { BasicIO.transferFully(pipe, sink) } + catch { case e: IOException => println("I/O error " + e.getMessage + " for process: " + label); e.printStackTrace() } + finally { + BasicIO.close(sink) + currentSink.unset() + } + run() + case None => + currentSink.unset() + } + } +} + +private[sbt] class DummyProcessBuilder(override val toString: String, exitValue: => Int) extends AbstractProcessBuilder { + override def run(io: ProcessIO): Process = new DummyProcess(exitValue) + override def canPipeTo = true +} +/** + * A thin wrapper around a java.lang.Process. `ioThreads` are the Threads created to do I/O. + * The implementation of `exitValue` waits until these threads die before returning. + */ +private class DummyProcess(action: => Int) extends Process { + private[this] val exitCode = Future(action) + override def exitValue() = exitCode() + override def destroy(): Unit = () +} +/** Represents a simple command without any redirection or combination. */ +private[sbt] class SimpleProcessBuilder(p: JProcessBuilder) extends AbstractProcessBuilder { + override def run(io: ProcessIO): Process = + { + import io._ + val inherited = inheritInput(p) + val process = p.start() + + // spawn threads that process the output and error streams, and also write input if not inherited. + if (!inherited) + Spawn(writeInput(process.getOutputStream)) + val outThread = Spawn(processOutput(process.getInputStream)) + val errorThread = + if (!p.redirectErrorStream) + Spawn(processError(process.getErrorStream)) :: Nil + else + Nil + new SimpleProcess(process, outThread :: errorThread) + } + override def toString = p.command.toString + override def canPipeTo = true +} + +/** + * A thin wrapper around a java.lang.Process. `outputThreads` are the Threads created to read from the + * output and error streams of the process. + * The implementation of `exitValue` wait for the process to finish and then waits until the threads reading output and error streams die before + * returning. Note that the thread that reads the input stream cannot be interrupted, see https://github.com/sbt/sbt/issues/327 and + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4514257 + */ +private class SimpleProcess(p: JProcess, outputThreads: List[Thread]) extends Process { + override def exitValue() = + { + try { + p.waitFor() + } catch { + case _: InterruptedException => p.destroy() + } + outputThreads.foreach(_.join()) // this ensures that all output is complete before returning (waitFor does not ensure this) + p.exitValue() + } + override def destroy() = p.destroy() +} + +private[sbt] class FileOutput(file: File, append: Boolean) extends OutputStreamBuilder(new FileOutputStream(file, append), file.getAbsolutePath) +private[sbt] class URLInput(url: URL) extends InputStreamBuilder(url.openStream, url.toString) +private[sbt] class FileInput(file: File) extends InputStreamBuilder(new FileInputStream(file), file.getAbsolutePath) + +import Uncloseable.protect +private[sbt] class OutputStreamBuilder(stream: => OutputStream, label: String) extends ThreadProcessBuilder(label, _.writeInput(protect(stream))) { + def this(stream: => OutputStream) = this(stream, "") +} +private[sbt] class InputStreamBuilder(stream: => InputStream, label: String) extends ThreadProcessBuilder(label, _.processOutput(protect(stream))) { + def this(stream: => InputStream) = this(stream, "") +} + +private[sbt] abstract class ThreadProcessBuilder(override val toString: String, runImpl: ProcessIO => Unit) extends AbstractProcessBuilder { + override def run(io: ProcessIO): Process = + { + val success = new SyncVar[Boolean] + success.put(false) + new ThreadProcess(Spawn { runImpl(io); success.set(true) }, success) + } +} +private[sbt] final class ThreadProcess(thread: Thread, success: SyncVar[Boolean]) extends Process { + override def exitValue() = + { + thread.join() + if (success.get) 0 else 1 + } + override def destroy(): Unit = thread.interrupt() +} + +object Uncloseable { + def apply(in: InputStream): InputStream = new FilterInputStream(in) { override def close(): Unit = () } + def apply(out: OutputStream): OutputStream = new FilterOutputStream(out) { override def close(): Unit = () } + def protect(in: InputStream): InputStream = if (in eq System.in) Uncloseable(in) else in + def protect(out: OutputStream): OutputStream = if ((out eq System.out) || (out eq System.err)) Uncloseable(out) else out +} +private[sbt] object Streamed { + def apply[T](nonzeroException: Boolean): Streamed[T] = + { + val q = new java.util.concurrent.LinkedBlockingQueue[Either[Int, T]] + def next(): Stream[T] = + q.take match { + case Left(0) => Stream.empty + case Left(code) => if (nonzeroException) sys.error("Nonzero exit code: " + code) else Stream.empty + case Right(s) => Stream.cons(s, next) + } + new Streamed((s: T) => q.put(Right(s)), code => q.put(Left(code)), () => next()) + } +} + +private[sbt] final class Streamed[T](val process: T => Unit, val done: Int => Unit, val stream: () => Stream[T]) extends NotNull \ No newline at end of file diff --git a/integration-test/src/test/scala/RunnerTest.scala b/integration-test/src/test/scala/RunnerTest.scala new file mode 100644 index 000000000..ab238243c --- /dev/null +++ b/integration-test/src/test/scala/RunnerTest.scala @@ -0,0 +1,42 @@ +package example.test + +import minitest._ +import scala.sys.process._ +import java.io.File + +object SbtRunnerTest extends SimpleTestSuite with PowerAssertions { + lazy val sbtScript = new File("target/universal/stage/bin/sbt") + def sbtProcess(arg: String) = + sbt.internal.Process(sbtScript.getAbsolutePath + " " + arg, new File("citest"), + "JAVA_OPTS" -> "", + "SBT_OPTS" -> "") + def sbtProcessWithOpts(arg: String) = + sbt.internal.Process(sbtScript.getAbsolutePath + " " + arg, new File("citest"), + "JAVA_OPTS" -> "-Xmx1024m", + "SBT_OPTS" -> "") + + test("sbt runs") { + assert(sbtScript.exists) + val out = sbtProcess("compile -v").! + assert(out == 0) + () + } + + test("sbt -no-colors") { + val out = sbtProcess("compile -no-colors -v").!!.linesIterator.toList + assert(out.contains[String]("-Dsbt.log.noformat=true")) + () + } + + test("sbt -mem 503") { + val out = sbtProcess("compile -mem 503 -v").!!.linesIterator.toList + assert(out.contains[String]("-Xmx503m")) + () + } + + test("sbt -mem 503 with JAVA_OPTS") { + val out = sbtProcessWithOpts("compile -mem 503 -v").!!.linesIterator.toList + assert(out.contains[String]("-Xmx503m")) + () + } +} diff --git a/integration-test/src/test/scala/SyncVar.scala b/integration-test/src/test/scala/SyncVar.scala new file mode 100644 index 000000000..5754e6da0 --- /dev/null +++ b/integration-test/src/test/scala/SyncVar.scala @@ -0,0 +1,38 @@ +package sbt.internal + +// minimal copy of scala.concurrent.SyncVar since that version deprecated put and unset +private[sbt] final class SyncVar[A] { + private[this] var isDefined: Boolean = false + private[this] var value: Option[A] = None + + /** Waits until a value is set and then gets it. Does not clear the value */ + def get: A = synchronized { + while (!isDefined) wait() + value.get + } + + /** Waits until a value is set, gets it, and finally clears the value. */ + def take(): A = synchronized { + try get finally unset() + } + + /** Sets the value, whether or not it is currently defined. */ + def set(x: A): Unit = synchronized { + isDefined = true + value = Some(x) + notifyAll() + } + + /** Sets the value, first waiting until it is undefined if it is currently defined. */ + def put(x: A): Unit = synchronized { + while (isDefined) wait() + set(x) + } + + /** Clears the value, whether or not it is current defined. */ + def unset(): Unit = synchronized { + isDefined = false + value = None + notifyAll() + } +} diff --git a/src/test/scala/RunnerTest.scala b/src/test/scala/RunnerTest.scala deleted file mode 100644 index 9db52d254..000000000 --- a/src/test/scala/RunnerTest.scala +++ /dev/null @@ -1,51 +0,0 @@ -package org.improving - -import scala.tools.nsc.io._ -import org.specs._ - -object SbtRunnerTest extends Specification { - val scripts = { - import Predef._ - List[String]( - """|sbt -sbt-create -sbt-snapshot -210 - |sbt update - |sbt about - """, - """|sbt -sbt-create -sbt-snapshot -29 - |sbt update - |sbt version - """, - """|sbt -sbt-create -sbt-version 0.7.7 -28 - |sbt help - |sbt -h - """ - ) map (_.trim.stripMargin.lines.toList) - } - - val singles = """ -sbt -v -d -no-colors update package -sbt -verbose -210 -debug -ivy /tmp update -""".trim.lines - - import scala.sys.process._ - - def sbtProjectLines(lines: List[String]) = { - println("Running: " + lines.mkString(", ")) - - val dir = Directory.makeTemp("sbt-runner-test").jfile - val result = lines map (x => Process(x, dir)) reduceLeft (_ #&& _) ! ; - - result == 0 - } - def sbtProjectLine(line: String) = - sbtProjectLines(List("sbt -sbt-create version", line)) - - "Sbt Runner" should { - "deal with lots of different command lines" in { - singles foreach (x => sbtProjectLine(x) mustEqual true) - } - "handle various command sequences" in { - scripts foreach (xs => sbtProjectLines(xs) mustEqual true) - } - } -} diff --git a/src/universal/bin/sbt b/src/universal/bin/sbt index dd16fa77a..3c60188b0 100755 --- a/src/universal/bin/sbt +++ b/src/universal/bin/sbt @@ -1,38 +1,22 @@ #!/usr/bin/env bash set +e +declare -a residual_args +declare -a java_args +declare -a scalac_args +declare -a sbt_commands +declare -a sbt_options +declare java_cmd=java +declare java_version +declare init_sbt_version=_to_be_replaced +declare sbt_default_mem=1024 +declare -r default_sbt_opts="" +declare -r default_java_opts="-Dfile.encoding=UTF-8" ### ------------------------------- ### ### Helper methods for BASH scripts ### ### ------------------------------- ### -realpath () { -( - TARGET_FILE="$1" - FIX_CYGPATH="$2" - - cd "$(dirname "$TARGET_FILE")" - TARGET_FILE=$(basename "$TARGET_FILE") - - COUNT=0 - while [ -L "$TARGET_FILE" -a $COUNT -lt 100 ] - do - TARGET_FILE=$(readlink "$TARGET_FILE") - cd "$(dirname "$TARGET_FILE")" - TARGET_FILE=$(basename "$TARGET_FILE") - COUNT=$(($COUNT + 1)) - done - - # make sure we grab the actual windows path, instead of cygwin's path. - if [[ "x$FIX_CYGPATH" != "x" ]]; then - echo "$(cygwinpath "$(pwd -P)/$TARGET_FILE")" - else - echo "$(pwd -P)/$TARGET_FILE" - fi -) -} - - # Uses uname to detect if we're in the odd cygwin environment. is_cygwin() { local os=$(uname -s) @@ -47,7 +31,6 @@ is_cygwin() { # TODO - Use nicer bash-isms here. CYGWIN_FLAG=$(if is_cygwin; then echo true; else echo false; fi) - # This can fix cygwin style /cygdrive paths so we get the # windows style paths. cygwinpath() { @@ -59,8 +42,374 @@ cygwinpath() { fi } -. "$(dirname "$(realpath "$0")")/sbt-launch-lib.bash" +declare SCRIPT=$0 +while [ -h "$SCRIPT" ] ; do + ls=$(ls -ld "$SCRIPT") + # Drop everything prior to -> + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=$(dirname "$SCRIPT")/"$link" + fi +done +declare -r sbt_bin_dir="$(dirname "$SCRIPT")" +declare -r sbt_home="$(dirname "$sbt_bin_dir")" + +echoerr () { + echo 1>&2 "$@" +} +vlog () { + [[ $verbose || $debug ]] && echoerr "$@" +} +dlog () { + [[ $debug ]] && echoerr "$@" +} + +jar_file () { + echo "$(cygwinpath "${sbt_home}/bin/sbt-launch.jar")" +} + +acquire_sbt_jar () { + sbt_jar="$(jar_file)" + + if [[ ! -f "$sbt_jar" ]]; then + echoerr "Could not find launcher jar: $sbt_jar" + exit 2 + fi +} + +rt_export_file () { + echo "${sbt_bin_dir}/java9-rt-export.jar" +} + +execRunner () { + # print the arguments one to a line, quoting any containing spaces + [[ $verbose || $debug ]] && echo "# Executing command line:" && { + for arg; do + if printf "%s\n" "$arg" | grep -q ' '; then + printf "\"%s\"\n" "$arg" + else + printf "%s\n" "$arg" + fi + done + echo "" + } + + # THis used to be exec, but we loose the ability to re-hook stty then + # for cygwin... Maybe we should flag the feature here... + "$@" +} + +addJava () { + dlog "[addJava] arg = '$1'" + java_args=( "${java_args[@]}" "$1" ) +} +addSbt () { + dlog "[addSbt] arg = '$1'" + sbt_commands=( "${sbt_commands[@]}" "$1" ) +} +addResidual () { + dlog "[residual] arg = '$1'" + residual_args=( "${residual_args[@]}" "$1" ) +} +addDebugger () { + addJava "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$1" +} + +addMemory () { + dlog "[addMemory] arg = '$1'" + # evict memory related options + local xs=("${java_args[@]}") + java_args=() + for i in "${xs[@]}"; do + if ! [[ "${i}" == *-Xmx* ]] && ! [[ "${i}" == *-Xms* ]] && ! [[ "${i}" == *-XX:MaxPermSize* ]] && ! [[ "${i}" == *-XX:MaxMetaspaceSize* ]] && ! [[ "${i}" == *-XX:ReservedCodeCacheSize* ]]; then + java_args+=("${i}") + fi + done + local ys=("${sbt_options[@]}") + sbt_options=() + for i in "${ys[@]}"; do + if ! [[ "${i}" == *-Xmx* ]] && ! [[ "${i}" == *-Xms* ]] && ! [[ "${i}" == *-XX:MaxPermSize* ]] && ! [[ "${i}" == *-XX:MaxMetaspaceSize* ]] && ! [[ "${i}" == *-XX:ReservedCodeCacheSize* ]]; then + sbt_options+=("${i}") + fi + done + # a ham-fisted attempt to move some memory settings in concert + local mem=$1 + local codecache=$(( $mem / 8 )) + (( $codecache > 128 )) || codecache=128 + (( $codecache < 512 )) || codecache=512 + local class_metadata_size=$(( $codecache * 2 )) + if [[ -z $java_version ]]; then + java_version=$(jdk_version) + fi + local class_metadata_opt=$((( $java_version < 8 )) && echo "MaxPermSize" || echo "MaxMetaspaceSize") + + addJava "-Xms${mem}m" + addJava "-Xmx${mem}m" + addJava "-Xss2M" + addJava "-XX:ReservedCodeCacheSize=${codecache}m" + if [[ (( $java_version > 7 )) ]]; then + addJava "-XX:${class_metadata_opt}=${class_metadata_size}m" + fi +} + +addDefaultMemory() { + # if we detect any of these settings in ${JAVA_OPTS} or ${JAVA_TOOL_OPTIONS} we need to NOT output our settings. + # The reason is the Xms/Xmx, if they don't line up, cause errors. + if [[ "${java_args[@]}" == *-Xmx* ]] || [[ "${java_args[@]}" == *-Xms* ]]; then + : + elif [[ "${JAVA_TOOL_OPTIONS}" == *-Xmx* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-Xms* ]]; then + : + elif [[ "${sbt_options[@]}" == *-Xmx* ]] || [[ "${sbt_options[@]}" == *-Xms* ]]; then + : + else + addMemory $sbt_default_mem + fi +} + +get_gc_opts () { + local older_than_9=$(( $java_version < 9 )) + + if [[ "$older_than_9" == "1" ]]; then + # don't need to worry about gc + echo "" + elif [[ "${JAVA_OPTS}" =~ Use.*GC ]] || [[ "${JAVA_TOOL_OPTIONS}" =~ Use.*GC ]] || [[ "${SBT_OPTS}" =~ Use.*GC ]] ; then + # GC arg has been passed in - don't change + echo "" + else + # Java 9+ so revert to old + echo "-XX:+UseParallelGC" + fi +} + +require_arg () { + local type="$1" + local opt="$2" + local arg="$3" + if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then + echo "$opt requires <$type> argument" + exit 1 + fi +} + +is_function_defined() { + declare -f "$1" > /dev/null +} + +# parses JDK version from the -version output line. +# 8 for 1.8.0_nn, 9 for 9-ea etc, and "no_java" for undetected +jdk_version() { + local result + local lines=$("$java_cmd" -Xms32M -Xmx32M -version 2>&1 | tr '\r' '\n') + local IFS=$'\n' + for line in $lines; do + if [[ (-z $result) && ($line = *"version \""*) ]] + then + local ver=$(echo $line | sed -e 's/.*version "\(.*\)"\(.*\)/\1/; 1q') + # on macOS sed doesn't support '?' + if [[ $ver = "1."* ]] + then + result=$(echo $ver | sed -e 's/1\.\([0-9]*\)\(.*\)/\1/; 1q') + else + result=$(echo $ver | sed -e 's/\([0-9]*\)\(.*\)/\1/; 1q') + fi + fi + done + if [[ -z $result ]] + then + result=no_java + fi + echo "$result" +} + +process_args () { + while [[ $# -gt 0 ]]; do + case "$1" in + -h|-help) usage; exit 1 ;; + -v|-verbose) verbose=1 && shift ;; + -d|-debug) debug=1 && addSbt "-debug" && shift ;; + + -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; + -mem) require_arg integer "$1" "$2" && addMemory "$2" && shift 2 ;; + -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;; + -batch) exec /dev/null 2>&1 && { + mkdir -p "$target_preloaded" + rsync -a --ignore-existing "$source_preloaded" "$target_preloaded" + } + } + } +} + +# Detect that we have java installed. +checkJava() { + local required_version="$1" + # Now check to see if it's a good enough version + local good_enough="$(expr $java_version ">=" $required_version)" + if [[ "$java_version" == "" ]]; then + echo + echo "No Java Development Kit (JDK) installation was detected." + echo Please go to http://www.oracle.com/technetwork/java/javase/downloads/ and download. + echo + exit 1 + elif [[ "$good_enough" != "1" ]]; then + echo + echo "The Java Development Kit (JDK) installation you have is not up to date." + echo $script_name requires at least version $required_version+, you have + echo version $java_version + echo + echo Please go to http://www.oracle.com/technetwork/java/javase/downloads/ and download + echo a valid JDK and install before running $script_name. + echo + exit 1 + fi +} + +copyRt() { + local at_least_9="$(expr $java_version ">=" 9)" + if [[ "$at_least_9" == "1" ]]; then + rtexport=$(rt_export_file) + # The grep for java9-rt-ext- matches the filename prefix printed in Export.java + java9_ext=$("$java_cmd" ${sbt_options[@]} ${java_args[@]} \ + -jar "$rtexport" --rt-ext-dir | grep java9-rt-ext-) + java9_rt=$(echo "$java9_ext/rt.jar") + vlog "[copyRt] java9_rt = '$java9_rt'" + if [[ ! -f "$java9_rt" ]]; then + echo Copying runtime jar. + mkdir -p "$java9_ext" + execRunner "$java_cmd" \ + ${sbt_options[@]} \ + ${java_args[@]} \ + -jar "$rtexport" \ + "${java9_rt}" + fi + addJava "-Dscala.ext.dirs=${java9_ext}" + fi +} + +run() { + java_args=($JAVA_OPTS) + sbt_options=(${SBT_OPTS:-$default_sbt_opts}) + + # process the combined args, then reset "$@" to the residuals + process_args "$@" + addDefaultMemory + set -- "${residual_args[@]}" + argumentCount=$# + + # Copy preloaded repo to user's preloaded directory + syncPreloaded + + # no jar? download it. + [[ -f "$sbt_jar" ]] || acquire_sbt_jar "$sbt_version" || { + # still no jar? uh-oh. + echo "Download failed. Obtain the sbt-launch.jar manually and place it at $sbt_jar" + exit 1 + } + + # TODO - java check should be configurable... + checkJava "6" + + # Java 9 support + copyRt + + #If we're in cygwin, we should use the windows config, and terminal hacks + if [[ "$CYGWIN_FLAG" == "true" ]]; then + stty -icanon min 1 -echo > /dev/null 2>&1 + addJava "-Djline.terminal=jline.UnixTerminal" + addJava "-Dsbt.cygwin=true" + fi + + # run sbt + execRunner "$java_cmd" \ + $(get_gc_opts) \ + ${java_args[@]} \ + ${sbt_options[@]} \ + -jar "$sbt_jar" \ + "${sbt_commands[@]}" \ + "${residual_args[@]}" + + exit_code=$? + + # Clean up the terminal from cygwin hacks. + if [[ "$CYGWIN_FLAG" == "true" ]]; then + stty icanon echo > /dev/null 2>&1 + fi + exit $exit_code +} declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" declare -r sbt_opts_file=".sbtopts" @@ -96,7 +445,7 @@ Usage: `basename "$0"` [options] -java-home alternate JAVA_HOME # jvm options and output control - JAVA_OPTS environment variable, if unset uses "$java_opts" + JAVA_OPTS environment variable, if unset uses "$default_java_opts" .jvmopts if this file exists in the current directory, its contents are appended to JAVA_OPTS SBT_OPTS environment variable, if unset uses "$default_sbt_opts" @@ -133,7 +482,7 @@ process_my_args () { *) addResidual "$1" && shift ;; esac done - + # Now, ensure sbt version is used. [[ "${sbt_version}XXX" != "XXX" ]] && addJava "-Dsbt.version=$sbt_version" @@ -168,11 +517,13 @@ loadConfigFile() { # Here we pull in the global settings configuration. [[ -f "$etc_sbt_opts_file" ]] && set -- $(loadConfigFile "$etc_sbt_opts_file") "$@" -# Pull in the project-level config file, if it exists. +# Pull in the project-level config file, if it exists. [[ -f "$sbt_opts_file" ]] && set -- $(loadConfigFile "$sbt_opts_file") "$@" -# Pull in the project-level java config, if it exists. +# Pull in the project-level java config, if it exists. [[ -f ".jvmopts" ]] && export JAVA_OPTS="$JAVA_OPTS $(loadConfigFile .jvmopts)" -run "$@" +# Pull in default JAVA_OPTS +[[ -z "${JAVA_OPTS// }" ]] && export JAVA_OPTS="$default_java_opts" +run "$@" diff --git a/src/universal/bin/sbt.bat b/src/universal/bin/sbt.bat index fbe0704e2..f7f1dd7fb 100644 --- a/src/universal/bin/sbt.bat +++ b/src/universal/bin/sbt.bat @@ -13,6 +13,8 @@ set SBT_HOME=%~dp0 set SBT_ARGS= +set DEFAULT_JAVA_OPTS=-Dfile.encoding=UTF-8 + rem FIRST we load the config file of extra options. set FN=%SBT_HOME%\..\conf\sbtconfig.txt set CFG_OPTS= @@ -55,6 +57,8 @@ rem We use the value of the JAVA_OPTS environment variable if defined, rather th set _JAVA_OPTS=%JAVA_OPTS% if "%_JAVA_OPTS%"=="" set _JAVA_OPTS=%CFG_OPTS% +if "%_JAVA_OPTS%"=="" set _JAVA_OPTS=%DEFAULT_JAVA_OPTS% + set INIT_SBT_VERSION=_TO_BE_REPLACED :args_loop