From 53c9b848580376544543b0732848005b37d97fd7 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 30 Sep 2018 20:59:19 -0400 Subject: [PATCH 1/4] add sbt.color flag This implements a new sbt.color flag that takes always/auto/never/true/false value as a replacement of current sbt.log.format=true/false flag. When neither flags are set, the default behavior is to enable color when the terminal supports ANSI and it detects an stdout console (as opposed to redirects). Fixes https://github.com/sbt/sbt/issues/4284 --- .../sbt/internal/util/LogOption.scala | 15 +++++++ .../internal/util/codec/JsonProtocol.scala | 1 + .../util/codec/LogOptionFormats.scala | 31 +++++++++++++ .../src/main/contraband/logging.contra | 7 +++ .../sbt/internal/util/ConsoleAppender.scala | 44 +++++++++++++++++-- 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 internal/util-logging/src/main/contraband-scala/sbt/internal/util/LogOption.scala create mode 100644 internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/LogOptionFormats.scala diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/LogOption.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/LogOption.scala new file mode 100644 index 000000000..769da7982 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/LogOption.scala @@ -0,0 +1,15 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util +/** value for logging options like color */ +sealed abstract class LogOption extends Serializable +object LogOption { + + + case object Always extends LogOption + case object Never extends LogOption + case object Auto extends LogOption +} diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/JsonProtocol.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/JsonProtocol.scala index a94906dda..15e4d9cb2 100644 --- a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/JsonProtocol.scala +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/JsonProtocol.scala @@ -9,4 +9,5 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.util.codec.TraceEventFormats with sbt.internal.util.codec.AbstractEntryFormats with sbt.internal.util.codec.SuccessEventFormats + with sbt.internal.util.codec.LogOptionFormats object JsonProtocol extends JsonProtocol \ No newline at end of file diff --git a/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/LogOptionFormats.scala b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/LogOptionFormats.scala new file mode 100644 index 000000000..e52700c19 --- /dev/null +++ b/internal/util-logging/src/main/contraband-scala/sbt/internal/util/codec/LogOptionFormats.scala @@ -0,0 +1,31 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.util.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait LogOptionFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val LogOptionFormat: JsonFormat[sbt.internal.util.LogOption] = new JsonFormat[sbt.internal.util.LogOption] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.LogOption = { + jsOpt match { + case Some(js) => + unbuilder.readString(js) match { + case "Always" => sbt.internal.util.LogOption.Always + case "Never" => sbt.internal.util.LogOption.Never + case "Auto" => sbt.internal.util.LogOption.Auto + } + case None => + deserializationError("Expected JsString but found None") + } + } + override def write[J](obj: sbt.internal.util.LogOption, builder: Builder[J]): Unit = { + val str = obj match { + case sbt.internal.util.LogOption.Always => "Always" + case sbt.internal.util.LogOption.Never => "Never" + case sbt.internal.util.LogOption.Auto => "Auto" + } + builder.writeString(str) + } +} +} diff --git a/internal/util-logging/src/main/contraband/logging.contra b/internal/util-logging/src/main/contraband/logging.contra index 19b019c66..73d0b1a56 100644 --- a/internal/util-logging/src/main/contraband/logging.contra +++ b/internal/util-logging/src/main/contraband/logging.contra @@ -25,3 +25,10 @@ type TraceEvent implements sbt.internal.util.AbstractEntry { type SuccessEvent { message: String! } + +## value for logging options like color +enum LogOption { + Always + Never + Auto +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala index d5f6f7919..f070a1252 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala @@ -101,13 +101,49 @@ object ConsoleAppender { /** Hide stack trace altogether. */ val noSuppressedMessage = (_: SuppressedTraceContext) => None - /** Indicates whether formatting has been disabled in environment variables. */ + /** + * Indicates whether formatting has been disabled in environment variables. + * 1. -Dsbt.log.noformat=true means no formatting. + * 2. -Dsbt.color=always/auto/never/true/false + * 3. -Dsbt.colour=always/auto/never/true/false + * 4. -Dsbt.log.format=always/auto/never/true/false + */ val formatEnabledInEnv: Boolean = { - import java.lang.Boolean.{ getBoolean, parseBoolean } - val value = System.getProperty("sbt.log.format") - if (value eq null) (ansiSupported && !getBoolean("sbt.log.noformat")) else parseBoolean(value) + def useColorDefault: Boolean = { + // This approximates that both stdin and stdio are connected, + // so by default color will be turned off for pipes and redirects. + val hasConsole = Option(java.lang.System.console).isDefined + ansiSupported && hasConsole + } + sys.props.get("sbt.log.noformat") match { + case Some(_) => !java.lang.Boolean.getBoolean("sbt.log.noformat") + case _ => + sys.props + .get("sbt.color") + .orElse(sys.props.get("sbt.colour")) + .orElse(sys.props.get("sbt.log.format")) + .flatMap({ s => + parseLogOption(s) match { + case LogOption.Always => Some(true) + case LogOption.Never => Some(false) + case _ => None + } + }) + .getOrElse(useColorDefault) + } } + private[sbt] def parseLogOption(s: String): LogOption = + s.toLowerCase match { + case "always" => LogOption.Always + case "auto" => LogOption.Auto + case "never" => LogOption.Never + case "true" => LogOption.Always + case "false" => LogOption.Never + case "default" => LogOption.Auto + case _ => LogOption.Auto + } + private[this] val generateId: AtomicInteger = new AtomicInteger /** From 9bb244314dc8e0c371dd39a1fd178ab7fc33937d Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 30 Sep 2018 21:43:39 -0400 Subject: [PATCH 2/4] implement sbt.progress This implements a logger that grows upward, instead towards bottom. --- build.sbt | 1 + .../sbt/internal/util/ConsoleAppender.scala | 29 ++++++++++++++++--- .../scala/sbt/internal/util/ConsoleOut.scala | 18 ++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/build.sbt b/build.sbt index 58c1dcdc6..84fac6e31 100644 --- a/build.sbt +++ b/build.sbt @@ -126,6 +126,7 @@ lazy val utilLogging = (project in internalPath / "util-logging") // Private final class constructors changed exclude[DirectMissingMethodProblem]("sbt.util.InterfaceUtil#ConcretePosition.this"), exclude[DirectMissingMethodProblem]("sbt.util.InterfaceUtil#ConcreteProblem.this"), + exclude[ReversedMissingMethodProblem]("sbt.internal.util.ConsoleOut.flush"), ), ) .configure(addSbtIO) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala index f070a1252..79e210b62 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala @@ -133,6 +133,21 @@ object ConsoleAppender { } } + /** + * Indicates whether the super shell is enabled. + */ + lazy val showProgress: Boolean = + formatEnabledInEnv && sys.props + .get("sbt.progress") + .flatMap({ s => + parseLogOption(s) match { + case LogOption.Always => Some(true) + case LogOption.Never => Some(false) + case _ => None + } + }) + .getOrElse(true) + private[sbt] def parseLogOption(s: String): LogOption = s.toLowerCase match { case "always" => LogOption.Always @@ -443,11 +458,17 @@ class ConsoleAppender private[ConsoleAppender] ( appendLog(SUCCESS_LABEL_COLOR, Level.SuccessLabel, SUCCESS_MESSAGE_COLOR, message) } + private final val ScrollUp = "\u001B[S" + private final val DeleteLine = "\u001B[2K" + private final val CursorLeft1000 = "\u001B[1000D" private def write(msg: String): Unit = { - val cleanedMsg = - if (!useFormat || !ansiCodesSupported) EscHelpers.removeEscapeSequences(msg) - else msg - out.println(cleanedMsg) + if (!useFormat || !ansiCodesSupported) out.println(EscHelpers.removeEscapeSequences(msg)) + else { + if (ConsoleAppender.showProgress) { + out.print(s"$ScrollUp$DeleteLine$msg${CursorLeft1000}") + out.flush() + } else out.println(msg) + } } private def appendMessage(level: Level.Value, msg: Message): Unit = diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala index 717be2cfd..7edefebd7 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleOut.scala @@ -7,6 +7,7 @@ sealed trait ConsoleOut { def print(s: String): Unit def println(s: String): Unit def println(): Unit + def flush(): Unit } object ConsoleOut { @@ -39,6 +40,14 @@ object ConsoleOut { last = Some(s) current.setLength(0) } + def flush(): Unit = synchronized { + val s = current.toString + if (ConsoleAppender.formatEnabledInEnv && last.exists(lmsg => f(s, lmsg))) + lockObject.print(OverwriteLine) + lockObject.print(s) + last = Some(s) + current.setLength(0) + } } def printStreamOut(out: PrintStream): ConsoleOut = new ConsoleOut { @@ -46,17 +55,20 @@ object ConsoleOut { def print(s: String) = out.print(s) def println(s: String) = out.println(s) def println() = out.println() + def flush() = out.flush() } def printWriterOut(out: PrintWriter): ConsoleOut = new ConsoleOut { val lockObject = out def print(s: String) = out.print(s) - def println(s: String) = { out.println(s); out.flush() } - def println() = { out.println(); out.flush() } + def println(s: String) = { out.println(s); flush() } + def println() = { out.println(); flush() } + def flush() = { out.flush() } } def bufferedWriterOut(out: BufferedWriter): ConsoleOut = new ConsoleOut { val lockObject = out def print(s: String) = out.write(s) def println(s: String) = { out.write(s); println() } - def println() = { out.newLine(); out.flush() } + def println() = { out.newLine(); flush() } + def flush() = { out.flush() } } } From 458675239c5d01f83ae00166861fce82d7b5a6db Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Tue, 2 Oct 2018 08:17:08 -0400 Subject: [PATCH 3/4] Add mima exclusion for JsonProtocol.LogOptionFormat According to Travis CI only Scala 2.11 seems to be affected. --- build.sbt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sbt b/build.sbt index 84fac6e31..6267773b1 100644 --- a/build.sbt +++ b/build.sbt @@ -127,6 +127,8 @@ lazy val utilLogging = (project in internalPath / "util-logging") exclude[DirectMissingMethodProblem]("sbt.util.InterfaceUtil#ConcretePosition.this"), exclude[DirectMissingMethodProblem]("sbt.util.InterfaceUtil#ConcreteProblem.this"), exclude[ReversedMissingMethodProblem]("sbt.internal.util.ConsoleOut.flush"), + // This affects Scala 2.11 only it seems, so it's ok? + exclude[InheritedNewAbstractMethodProblem]("sbt.internal.util.codec.JsonProtocol.LogOptionFormat"), ), ) .configure(addSbtIO) From efe04c1cdecc5e43521c9b12565426d52f518b3d Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Tue, 2 Oct 2018 08:51:17 -0400 Subject: [PATCH 4/4] Cleaning up code --- .../sbt/internal/util/ConsoleAppender.scala | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala index 79e210b62..fedb80f9f 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala @@ -150,13 +150,12 @@ object ConsoleAppender { private[sbt] def parseLogOption(s: String): LogOption = s.toLowerCase match { - case "always" => LogOption.Always - case "auto" => LogOption.Auto - case "never" => LogOption.Never - case "true" => LogOption.Always - case "false" => LogOption.Never - case "default" => LogOption.Auto - case _ => LogOption.Auto + case "always" => LogOption.Always + case "auto" => LogOption.Auto + case "never" => LogOption.Never + case "true" => LogOption.Always + case "false" => LogOption.Never + case _ => LogOption.Auto } private[this] val generateId: AtomicInteger = new AtomicInteger @@ -462,12 +461,13 @@ class ConsoleAppender private[ConsoleAppender] ( private final val DeleteLine = "\u001B[2K" private final val CursorLeft1000 = "\u001B[1000D" private def write(msg: String): Unit = { - if (!useFormat || !ansiCodesSupported) out.println(EscHelpers.removeEscapeSequences(msg)) - else { - if (ConsoleAppender.showProgress) { - out.print(s"$ScrollUp$DeleteLine$msg${CursorLeft1000}") - out.flush() - } else out.println(msg) + if (!useFormat || !ansiCodesSupported) { + out.println(EscHelpers.removeEscapeSequences(msg)) + } else if (ConsoleAppender.showProgress) { + out.print(s"$ScrollUp$DeleteLine$msg${CursorLeft1000}") + out.flush() + } else { + out.println(msg) } }