From 5c394e18f6240b8b8cdad4661c080c9cc313f856 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 8 Oct 2017 01:39:38 -0400 Subject: [PATCH 01/62] implement window/logMessage This sends sbt's log message as "window/logMessage" event to LSP. --- .../scala/sbt/internal/CommandExchange.scala | 45 +++++++++++++------ .../langserver/LogMessageParams.scala | 38 ++++++++++++++++ .../langserver/codec/JsonProtocol.scala | 1 + .../codec/LogMessageParamsFormats.scala | 29 ++++++++++++ protocol/src/main/contraband/lsp.contra | 10 +++++ .../sbt/internal/langserver/MessageType.scala | 34 ++++++++++++++ 6 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/LogMessageParams.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/LogMessageParamsFormats.scala create mode 100644 protocol/src/main/scala/sbt/internal/langserver/MessageType.scala diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 9605558a4..0751576eb 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -23,9 +23,10 @@ import scala.util.{ Success, Failure } import sbt.io.syntax._ import sbt.io.Hash import sbt.internal.server._ -import sbt.internal.util.{ StringEvent, ObjectEvent, ConsoleOut, MainAppender } +import sbt.internal.langserver.{ LogMessageParams, MessageType } +import sbt.internal.util.{ StringEvent, ObjectEvent, MainAppender } import sbt.internal.util.codec.JValueFormats -import sbt.protocol.{ EventMessage, Serialization, ChannelAcceptedEvent } +import sbt.protocol.{ EventMessage, Serialization } import sbt.util.{ Level, Logger, LogExchange } /** @@ -72,7 +73,7 @@ private[sbt] final class CommandExchange { def run(s: State): State = { consoleChannel match { - case Some(x) => // do nothing + case Some(_) => // do nothing case _ => val x = new ConsoleChannel("console0") consoleChannel = Some(x) @@ -116,7 +117,7 @@ private[sbt] final class CommandExchange { subscribe(channel) } server match { - case Some(x) => // do nothing + case Some(_) => // do nothing case _ => val portfile = (new File(".")).getAbsoluteFile / "project" / "target" / "active.json" val h = Hash.halfHashString(portfile.toURI.toString) @@ -147,13 +148,13 @@ private[sbt] final class CommandExchange { private[sbt] def notifyEvent[A: JsonFormat](method: String, params: A): Unit = { val toDel: ListBuffer[CommandChannel] = ListBuffer.empty channels.foreach { - case c: ConsoleChannel => + case _: ConsoleChannel => // c.publishEvent(event) case c: NetworkChannel => try { c.notifyEvent(method, params) } catch { - case e: SocketException => + case _: SocketException => toDel += c } } @@ -167,21 +168,35 @@ private[sbt] final class CommandExchange { } def publishEvent[A: JsonFormat](event: A): Unit = { + val broadcastStringMessage = true val toDel: ListBuffer[CommandChannel] = ListBuffer.empty event match { case entry: StringEvent => + val params = toLogMessageParams(entry) channels.foreach { case c: ConsoleChannel => - if (entry.channelName.isEmpty || entry.channelName == Some(c.name)) { + if (broadcastStringMessage) { c.publishEvent(event) + } else { + if (entry.channelName.isEmpty || entry.channelName == Some(c.name)) { + c.publishEvent(event) + } } case c: NetworkChannel => try { - if (entry.channelName == Some(c.name)) { - c.publishEvent(event) + // Note that language server's LogMessageParams does not hold the execid, + // so this is weaker than the StringMessage. We might want to double-send + // in case we have a better client that can utilize the knowledge. + import sbt.internal.langserver.codec.JsonProtocol._ + if (broadcastStringMessage) { + c.langNotify("window/logMessage", params) + } else { + if (entry.channelName == Some(c.name)) { + c.langNotify("window/logMessage", params) + } } } catch { - case e: SocketException => + case _: SocketException => toDel += c } } @@ -193,7 +208,7 @@ private[sbt] final class CommandExchange { try { c.publishEvent(event) } catch { - case e: SocketException => + case _: SocketException => toDel += c } } @@ -207,6 +222,10 @@ private[sbt] final class CommandExchange { } } + private[sbt] def toLogMessageParams(event: StringEvent): LogMessageParams = { + LogMessageParams(MessageType.fromLevelString(event.level), event.message) + } + /** * This publishes object events. The type information has been * erased because it went through logging. @@ -231,7 +250,7 @@ private[sbt] final class CommandExchange { try { c.publishObjectEvent(event) } catch { - case e: SocketException => + case _: SocketException => toDel += c } } @@ -265,7 +284,7 @@ private[sbt] final class CommandExchange { try { c.publishEventMessage(event) } catch { - case e: SocketException => + case _: SocketException => toDel += c } } diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/LogMessageParams.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/LogMessageParams.scala new file mode 100644 index 000000000..a13045afa --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/LogMessageParams.scala @@ -0,0 +1,38 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver +final class LogMessageParams private ( + /** The message type. */ + val `type`: Long, + /** The actual message */ + val message: String) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: LogMessageParams => (this.`type` == x.`type`) && (this.message == x.message) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.langserver.LogMessageParams".##) + `type`.##) + message.##) + } + override def toString: String = { + "LogMessageParams(" + `type` + ", " + message + ")" + } + protected[this] def copy(`type`: Long = `type`, message: String = message): LogMessageParams = { + new LogMessageParams(`type`, message) + } + def withType(`type`: Long): LogMessageParams = { + copy(`type` = `type`) + } + def withMessage(message: String): LogMessageParams = { + copy(message = message) + } +} +object LogMessageParams { + + def apply(`type`: Long, message: String): LogMessageParams = new LogMessageParams(`type`, message) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala index a40a00bbe..f946496ec 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala @@ -16,6 +16,7 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.langserver.codec.TextDocumentSyncOptionsFormats with sbt.internal.langserver.codec.ServerCapabilitiesFormats with sbt.internal.langserver.codec.InitializeResultFormats + with sbt.internal.langserver.codec.LogMessageParamsFormats with sbt.internal.langserver.codec.PublishDiagnosticsParamsFormats with sbt.internal.langserver.codec.SbtExecParamsFormats object JsonProtocol extends JsonProtocol \ No newline at end of file diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/LogMessageParamsFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/LogMessageParamsFormats.scala new file mode 100644 index 000000000..2ffc92003 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/LogMessageParamsFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait LogMessageParamsFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val LogMessageParamsFormat: JsonFormat[sbt.internal.langserver.LogMessageParams] = new JsonFormat[sbt.internal.langserver.LogMessageParams] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.LogMessageParams = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val `type` = unbuilder.readField[Long]("type") + val message = unbuilder.readField[String]("message") + unbuilder.endObject() + sbt.internal.langserver.LogMessageParams(`type`, message) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.langserver.LogMessageParams, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("type", obj.`type`) + builder.addField("message", obj.message) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband/lsp.contra b/protocol/src/main/contraband/lsp.contra index 49853a2f4..18bd0a42c 100644 --- a/protocol/src/main/contraband/lsp.contra +++ b/protocol/src/main/contraband/lsp.contra @@ -98,6 +98,16 @@ type SaveOptions { includeText: Boolean } +# LogMessage Notification + +type LogMessageParams { + ## The message type. + type: Long! + + ## The actual message + message: String! +} + # Document # PublishDiagnostics Notification https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#textDocument_publishDiagnostics diff --git a/protocol/src/main/scala/sbt/internal/langserver/MessageType.scala b/protocol/src/main/scala/sbt/internal/langserver/MessageType.scala new file mode 100644 index 000000000..26a5e4368 --- /dev/null +++ b/protocol/src/main/scala/sbt/internal/langserver/MessageType.scala @@ -0,0 +1,34 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + +package sbt +package internal +package langserver + +object MessageType { + + /** An error message. */ + val Error = 1L + + /** A warning message. */ + val Warning = 2L + + /** An information message. */ + val Info = 3L + + /** A log message. */ + val Log = 4L + + def fromLevelString(level: String): Long = { + level.toLowerCase match { + case "debug" => Log + case "info" => Info + case "warn" => Warning + case "error" => Error + } + } +} From ae860d5a7d5379ef5ca2060de5d3bff4df1fbc83 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 20 Oct 2017 00:23:29 -0500 Subject: [PATCH 02/62] Bump to Scala 2.12.4 Uses Scala 2.12.4 for the build definition. This includes fix for runtime reflection of empty package members under Java 9. Fixes sbt/sbt#3587 --- notes/1.0.3/scala2124.markdown | 5 +++++ project/Dependencies.scala | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 notes/1.0.3/scala2124.markdown diff --git a/notes/1.0.3/scala2124.markdown b/notes/1.0.3/scala2124.markdown new file mode 100644 index 000000000..33f9829a1 --- /dev/null +++ b/notes/1.0.3/scala2124.markdown @@ -0,0 +1,5 @@ + +- Uses Scala 2.12.4 for the build definition. This includes fix for runtime reflection of empty package members under Java 9. [#3587][3587] by [@eed3si9n][@eed3si9n] + + [3587]: https://github.com/sbt/sbt/issues/3587 + [@eed3si9n]: https://github.com/eed3si9n \ No newline at end of file diff --git a/project/Dependencies.scala b/project/Dependencies.scala index b1b31a0b4..a717d7051 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -8,7 +8,7 @@ object Dependencies { val scala293 = "2.9.3" val scala210 = "2.10.6" val scala211 = "2.11.8" - val scala212 = "2.12.3" + val scala212 = "2.12.4" val baseScalaVersion = scala212 // sbt modules From 78c5ee92d355904a94ab4559fc70dd20bae0a8e3 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 11 Oct 2017 18:45:55 +0100 Subject: [PATCH 03/62] Track sources in base directory non-recursively Using a recursive Source meant that ~ looked into target. If you have any source generators and use ~ with anything the invokes them, like ~compile, that means that the act of generating sources triggers ~ to re-execute compile (perhaps only on macOS where the NIO WatchService just polls, after an initial delay). Requires sbt/io#78 Fixes #3501 --- main/src/main/scala/sbt/Defaults.scala | 8 +++++--- notes/1.0.3/watch2.md | 12 ++++++++++++ project/Dependencies.scala | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 notes/1.0.3/watch2.md diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 6664c4935..6405a2886 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -315,11 +315,13 @@ object Defaults extends BuildCommon { excludeFilter in unmanagedSources).value, watchSources in ConfigGlobal ++= { val baseDir = baseDirectory.value - val bases = unmanagedSourceDirectories.value ++ (if (sourcesInBase.value) Seq(baseDir) - else Seq.empty) + val bases = unmanagedSourceDirectories.value val include = (includeFilter in unmanagedSources).value val exclude = (excludeFilter in unmanagedSources).value - bases.map(b => new Source(b, include, exclude)) + val baseSources = + if (sourcesInBase.value) Seq(new Source(baseDir, include, exclude, recursive = false)) + else Nil + bases.map(b => new Source(b, include, exclude)) ++ baseSources }, managedSourceDirectories := Seq(sourceManaged.value), managedSources := generate(sourceGenerators).value, diff --git a/notes/1.0.3/watch2.md b/notes/1.0.3/watch2.md new file mode 100644 index 000000000..69b30cede --- /dev/null +++ b/notes/1.0.3/watch2.md @@ -0,0 +1,12 @@ +[@dwijnand]: https://github.com/dwijnand + +[#3501]: https://github.com/sbt/sbt/issues/3501 +[#3634]: https://github.com/sbt/sbt/pull/3634 + +### Fixes with compatibility implications + +### Improvements + +### Bug fixes + +- Fixes `~` to recompile on loop (when a source generator or sbt-buildinfo is present). [#3501][]/[#3634][] by [@dwijnand][] diff --git a/project/Dependencies.scala b/project/Dependencies.scala index a717d7051..4f96a8304 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -12,7 +12,7 @@ object Dependencies { val baseScalaVersion = scala212 // sbt modules - private val ioVersion = "1.0.1" + private val ioVersion = "1.0.2" private val utilVersion = "1.0.1" private val lmVersion = "1.0.2" private val zincVersion = "1.0.1" From e0d80790834e6e93b4c8c537b7e9cc863fa3a3cc Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 24 Oct 2017 12:23:37 +0100 Subject: [PATCH 04/62] Scalafmt 1.3, sbt-scalafmt 1.14 --- build.sbt | 2 +- project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 93f3e3576..85ac238c9 100644 --- a/build.sbt +++ b/build.sbt @@ -34,7 +34,7 @@ def buildLevelSettings: Seq[Setting[_]] = scmInfo := Some(ScmInfo(url("https://github.com/sbt/sbt"), "git@github.com:sbt/sbt.git")), resolvers += Resolver.mavenLocal, scalafmtOnCompile := true, - scalafmtVersion := "1.2.0", + scalafmtVersion := "1.3.0", )) def commonSettings: Seq[Setting[_]] = diff --git a/project/plugins.sbt b/project/plugins.sbt index 2ebca3cac..f5433f7cf 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -10,5 +10,5 @@ addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.17") addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.1") addSbtPlugin("org.scala-sbt" % "sbt-contraband" % "0.3.1") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0-M1") -addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.10") +addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.14") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "3.0.2") From 6ac45711979f8b844f58d8501bbdbe54b34ccc7e Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 21 Oct 2017 01:27:40 -0500 Subject: [PATCH 05/62] Adds "reboot dev" This adds a new option `dev` to the `reboot` command, which deletes the only the current sbt artifacts from the boot directory. `reboot dev` reads actively from `build.properties` instead of using the current state since `reboot` can restart into another sbt version. In general, `reboot dev` is intended for the local development of sbt. Fixes #3590 --- build.sbt | 2 ++ .../main/scala/sbt/BasicCommandStrings.scala | 11 ++++--- .../src/main/scala/sbt/BasicCommands.scala | 15 ++++++++-- main-command/src/main/scala/sbt/State.scala | 29 ++++++++++++++---- main/src/main/scala/sbt/MainLoop.scala | 30 ++++++++++++++++++- notes/1.1.0/reboot.md | 3 ++ 6 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 notes/1.1.0/reboot.md diff --git a/build.sbt b/build.sbt index 93f3e3576..5ed988166 100644 --- a/build.sbt +++ b/build.sbt @@ -309,6 +309,8 @@ lazy val commandProj = (project in file("main-command")) exclude[ReversedMissingMethodProblem]("sbt.internal.server.ServerInstance.*"), // Added method to CommandChannel. internal. exclude[ReversedMissingMethodProblem]("sbt.internal.CommandChannel.*"), + // Added an overload to reboot. The overload is private[sbt]. + exclude[ReversedMissingMethodProblem]("sbt.StateOps.reboot"), ) ) .configure( diff --git a/main-command/src/main/scala/sbt/BasicCommandStrings.scala b/main-command/src/main/scala/sbt/BasicCommandStrings.scala index 7d5f120d7..d6b8761dc 100644 --- a/main-command/src/main/scala/sbt/BasicCommandStrings.scala +++ b/main-command/src/main/scala/sbt/BasicCommandStrings.scala @@ -117,14 +117,17 @@ $HelpCommand def RebootCommand = "reboot" def RebootDetailed = - RebootCommand + """ [full] + RebootCommand + """ [dev | full] This command is equivalent to exiting sbt, restarting, and running the remaining commands with the exception that the JVM is not shut down. - If 'full' is specified, the boot directory (`~/.sbt/boot` by default) - is deleted before restarting. This forces an update of sbt and Scala - and is useful when working with development versions of sbt or Scala.""" + If 'dev' is specified, the current sbt artifacts from the boot directory + (`~/.sbt/boot` by default) are deleted before restarting. + This forces an update of sbt and Scala, which is useful when working with development + versions of sbt. + If 'full' is specified, the boot directory is wiped out before restarting. +""" def Multi = ";" def MultiBrief = diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 2ae3e6431..82bb7b883 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -175,10 +175,19 @@ object BasicCommands { } def reboot: Command = - Command(RebootCommand, Help.more(RebootCommand, RebootDetailed))(rebootParser)((s, full) => - s reboot full) + Command(RebootCommand, Help.more(RebootCommand, RebootDetailed))(rebootOptionParser) { + case (s, (full, currentOnly)) => + s.reboot(full, currentOnly) + } - def rebootParser(s: State): Parser[Boolean] = token(Space ~> "full" ^^^ true) ?? false + @deprecated("Use rebootOptionParser", "1.1.0") + def rebootParser(s: State): Parser[Boolean] = + rebootOptionParser(s) map { case (full, currentOnly) => full } + + private[sbt] def rebootOptionParser(s: State): Parser[(Boolean, Boolean)] = + token( + Space ~> (("full" ^^^ ((true, false))) | + ("dev" ^^^ ((false, true))))) ?? ((false, false)) def call: Command = Command(ApplyCommand, Help.more(ApplyCommand, ApplyDetailed))(_ => callParser) { diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index 6259a02a9..9d0e401c6 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -89,6 +89,17 @@ trait StateOps { */ def reboot(full: Boolean): State + /** + * Reboots sbt. A reboot restarts execution from the entry point of the launcher. + * A reboot is designed to be as close as possible to actually restarting the JVM without actually doing so. + * Because the JVM is not restarted, JVM exit hooks are not run. + * State.exitHooks should be used instead and those will be run before rebooting. + * If `full` is true, the boot directory is deleted before starting again. + * If `currentOnly` is true, the artifacts for the current sbt version is deleted. + * This command is currently implemented to not return, but may be implemented in the future to only reboot at the next command processing step. + */ + private[sbt] def reboot(full: Boolean, currentOnly: Boolean): State + /** Sets the next command processing action to do.*/ def setNext(n: State.Next): State @@ -248,12 +259,18 @@ object State { def baseDir: File = s.configuration.baseDirectory def setNext(n: Next) = s.copy(next = n) def continue = setNext(Continue) - def reboot(full: Boolean) = { - runExitHooks(); - throw new xsbti.FullReload( - (s.remainingCommands map { case e: Exec => e.commandLine }).toArray, - full) + + /** Implementation of reboot. */ + def reboot(full: Boolean): State = reboot(full, false) + + /** Implementation of reboot. */ + private[sbt] def reboot(full: Boolean, currentOnly: Boolean): State = { + runExitHooks() + val rs = s.remainingCommands map { case e: Exec => e.commandLine } + if (currentOnly) throw new RebootCurrent(rs) + else throw new xsbti.FullReload(rs.toArray, full) } + def reload = runExitHooks().setNext(new Return(defaultReload(s))) def clearGlobalLog = setNext(ClearGlobalLog) def keepLastLog = setNext(KeepLastLog) @@ -320,3 +337,5 @@ object State { private[sbt] def getBoolean(s: State, key: AttributeKey[Boolean], default: Boolean): Boolean = s.get(key) getOrElse default } + +private[sbt] final class RebootCurrent(val arguments: List[String]) extends RuntimeException diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index 11f2c0fb3..83b38b4c8 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -7,11 +7,12 @@ package sbt +import java.util.Properties import scala.annotation.tailrec import scala.util.control.NonFatal import jline.TerminalFactory -import sbt.io.Using +import sbt.io.{ IO, Using } import sbt.internal.util.{ ErrorHandling, GlobalLogBacking } import sbt.internal.util.complete.DefaultParsers import sbt.util.Logger @@ -58,6 +59,10 @@ object MainLoop { case e: xsbti.FullReload => deleteLastLog(logBacking) throw e // pass along a reboot request + case e: RebootCurrent => + deleteLastLog(logBacking) + deleteCurrentArtifacts(state) + throw new xsbti.FullReload(e.arguments.toArray, false) case NonFatal(e) => System.err.println( "sbt appears to be exiting abnormally.\n The log file for this session is at " + logBacking.file) @@ -69,6 +74,28 @@ object MainLoop { def deleteLastLog(logBacking: GlobalLogBacking): Unit = logBacking.last.foreach(_.delete()) + /** Deletes the current sbt artifacts from boot. */ + private[sbt] def deleteCurrentArtifacts(state: State): Unit = { + import sbt.io.syntax._ + val provider = state.configuration.provider + val appId = provider.id + // If we can obtain boot directory more accurately it'd be better. + val defaultBoot = BuildPaths.defaultGlobalBase / "boot" + val buildProps = state.baseDir / "project" / "build.properties" + // First try reading the sbt version from build.properties file. + val sbtVersionOpt = if (buildProps.exists) { + val buildProperties = new Properties() + IO.load(buildProperties, buildProps) + Option(buildProperties.getProperty("sbt.version")) + } else None + val sbtVersion = sbtVersionOpt.getOrElse(appId.version) + val currentArtDirs = defaultBoot * "*" / appId.groupID / appId.name / sbtVersion + currentArtDirs.get foreach { dir => + state.log.info(s"Deleting $dir") + IO.delete(dir) + } + } + /** Runs the next sequence of commands with global logging in place. */ def runWithNewLog(state: State, logBacking: GlobalLogBacking): RunNext = Using.fileWriter(append = true)(logBacking.file) { writer => @@ -109,6 +136,7 @@ object MainLoop { ErrorHandling.wideConvert { state.process(processCommand) } match { case Right(s) => s case Left(t: xsbti.FullReload) => throw t + case Left(t: RebootCurrent) => throw t case Left(t) => state.handleError(t) } diff --git a/notes/1.1.0/reboot.md b/notes/1.1.0/reboot.md new file mode 100644 index 000000000..0b7b06504 --- /dev/null +++ b/notes/1.1.0/reboot.md @@ -0,0 +1,3 @@ +### Improvements + +- Adds `reboot dev` command, which deletes the current artifact from the boot directory. This is useful when working with development versions of sbt. From 3cd8f4dadfce5cb6a98db088ddaa4c08e7e76c95 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 18 Oct 2017 18:41:42 -0500 Subject: [PATCH 06/62] Simplify Initialize.joinAny --- .../src/main/scala/sbt/internal/util/Settings.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala index d6090dc81..6ff3c98e6 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala @@ -577,8 +577,7 @@ trait Init[Scope] { def join[T](inits: Seq[Initialize[T]]): Initialize[Seq[T]] = uniform(inits)(idFun) def joinAny[M[_]](inits: Seq[Initialize[M[T]] forSome { type T }]): Initialize[Seq[M[_]]] = - join(inits.asInstanceOf[Seq[Initialize[M[Any]]]]) - .asInstanceOf[Initialize[Seq[M[T] forSome { type T }]]] + join(inits.asInstanceOf[Seq[Initialize[M[_]]]]) } object SettingsDefinition { From 34f9e563118fe7f94efc566d7ba298568f5758fa Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 18 Oct 2017 17:56:43 -0500 Subject: [PATCH 07/62] Introduce and use KList.Aux --- .../src/main/scala/sbt/internal/util/AList.scala | 2 +- .../src/main/scala/sbt/internal/util/KList.scala | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala b/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala index 3ab98f72e..7685f9c5f 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala @@ -59,7 +59,7 @@ object AList { } /** AList for the arbitrary arity data structure KList. */ - def klist[KL[M[_]] <: KList[M] { type Transform[N[_]] = KL[N] }]: AList[KL] = new AList[KL] { + def klist[KL[M[_]] <: KList.Aux[M, KL]]: AList[KL] = new AList[KL] { def transform[M[_], N[_]](k: KL[M], f: M ~> N) = k.transform(f) def foldr[M[_], T](k: KL[M], f: (M[_], T) => T, init: T): T = k.foldr(f, init) override def apply[M[_], C](k: KL[M], f: KL[Id] => C)(implicit app: Applicative[M]): M[C] = k.apply(f)(app) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/KList.scala b/internal/util-collection/src/main/scala/sbt/internal/util/KList.scala index 42ad69bb9..a68b61788 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/KList.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/KList.scala @@ -29,6 +29,9 @@ sealed trait KList[+M[_]] { /** Discards the heterogeneous type information and constructs a plain List from this KList's elements. */ def toList: List[M[_]] } +object KList { + type Aux[+M[_], Transform0[N[_]]] = KList[M] { type Transform[N[_]] = Transform0[N] } +} final case class KCons[H, +T <: KList[M], +M[_]](head: M[H], tail: T) extends KList[M] { final type Transform[N[_]] = KCons[H, tail.Transform[N], N] From c98449e2721e43517338523f1e484b93284a3c5c Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 18 Oct 2017 18:53:25 -0500 Subject: [PATCH 08/62] Rename & use more Instance.Aux, introduce Instance.Aux2. --- .../sbt/internal/util/appmacro/Instance.scala | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/Instance.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/Instance.scala index a14637e05..33e614eab 100644 --- a/core-macros/src/main/scala/sbt/internal/util/appmacro/Instance.scala +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/Instance.scala @@ -32,6 +32,9 @@ import scala.reflect._ import macros._ object Instance { + type Aux[M0[_]] = Instance { type M[x] = M0[x] } + type Aux2[M0[_], N[_]] = Instance { type M[x] = M0[N[x]] } + final val ApplyName = "app" final val FlattenName = "flatten" final val PureName = "pure" @@ -204,19 +207,18 @@ object Instance { import Types._ - implicit def applicativeInstance[A[_]]( - implicit ap: Applicative[A]): Instance { type M[x] = A[x] } = new Instance { - type M[x] = A[x] - def app[K[L[x]], Z](in: K[A], f: K[Id] => Z)(implicit a: AList[K]) = a.apply[A, Z](in, f) - def map[S, T](in: A[S], f: S => T) = ap.map(f, in) - def pure[S](s: () => S): M[S] = ap.pure(s()) - } + implicit def applicativeInstance[A[_]](implicit ap: Applicative[A]): Instance.Aux[A] = + new Instance { + type M[x] = A[x] + def app[K[L[x]], Z](in: K[A], f: K[Id] => Z)(implicit a: AList[K]) = a.apply[A, Z](in, f) + def map[S, T](in: A[S], f: S => T) = ap.map(f, in) + def pure[S](s: () => S): M[S] = ap.pure(s()) + } - type AI[A[_]] = Instance { type M[x] = A[x] } - def compose[A[_], B[_]](implicit a: AI[A], b: AI[B]): Instance { type M[x] = A[B[x]] } = + def compose[A[_], B[_]](implicit a: Aux[A], b: Aux[B]): Instance.Aux2[A, B] = new Composed[A, B](a, b) // made a public, named, unsealed class because of trouble with macros and inference when the Instance is not an object - class Composed[A[_], B[_]](a: AI[A], b: AI[B]) extends Instance { + class Composed[A[_], B[_]](a: Aux[A], b: Aux[B]) extends Instance { type M[x] = A[B[x]] def pure[S](s: () => S): A[B[S]] = a.pure(() => b.pure(s)) def map[S, T](in: M[S], f: S => T): M[T] = a.map(in, (bv: B[S]) => b.map(bv, f)) From 35c3542492495614ad7cb5f093162cd2cf0f626e Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 19 Oct 2017 22:18:43 -0500 Subject: [PATCH 09/62] Get rid of Fn1 --- build.sbt | 22 +++++++++++-------- .../sbt/internal/util/TypeFunctions.scala | 8 ------- sbt/src/main/scala/Import.scala | 1 - .../src/main/scala/sbt/std/System.scala | 2 +- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/build.sbt b/build.sbt index 0f1f7a292..d8dc2b577 100644 --- a/build.sbt +++ b/build.sbt @@ -143,6 +143,11 @@ val collectionProj = (project in file("internal") / "util-collection") mimaBinaryIssueFilters ++= Seq( // Added private[sbt] method to capture State attributes. exclude[ReversedMissingMethodProblem]("sbt.internal.util.AttributeMap.setCond"), + + // Dropped in favour of plain scala.Function, and its compose method + exclude[MissingClassProblem]("sbt.internal.util.Fn1"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.TypeFunctions.toFn1"), + exclude[DirectMissingMethodProblem]("sbt.internal.util.Types.toFn1"), ), ) .configure(addSbtUtilPosition) @@ -418,7 +423,14 @@ lazy val sbtProj = (project in file("sbt")) crossScalaVersions := Seq(baseScalaVersion), crossPaths := false, mimaSettings, - mimaBinaryIssueFilters ++= sbtIgnoredProblems, + mimaBinaryIssueFilters ++= Vector( + // Added more items to Import trait. + exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$WatchSource_="), + exclude[ReversedMissingMethodProblem]("sbt.Import.WatchSource"), + + // Dropped in favour of plain scala.Function, and its compose method + exclude[DirectMissingMethodProblem]("sbt.package.toFn1"), + ) ) .configure(addSbtCompilerBridge) @@ -461,14 +473,6 @@ lazy val vscodePlugin = (project in file("vscode-sbt-scala")) } ) -lazy val sbtIgnoredProblems = { - Seq( - // Added more items to Import trait. - exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$WatchSource_="), - exclude[ReversedMissingMethodProblem]("sbt.Import.WatchSource") - ) -} - def scriptedTask: Def.Initialize[InputTask[Unit]] = Def.inputTask { val result = scriptedSource(dir => (s: State) => Scripted.scriptedParser(dir)).parsed // publishLocalBinAll.value // TODO: Restore scripted needing only binary jars. diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala b/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala index c0875046c..a7ad5d70f 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala @@ -29,10 +29,6 @@ trait TypeFunctions { def apply[T](mg: M[G[T]]): N[G[T]] = f(mg) } */ - implicit def toFn1[A, B](f: A => B): Fn1[A, B] = new Fn1[A, B] { - def ∙[C](g: C => A) = f compose g - } - type Endo[T] = T => T type ~>|[A[_], B[_]] = A ~> Compose[Option, B]#Apply } @@ -52,7 +48,3 @@ object ~> { val Id: Id ~> Id = new (Id ~> Id) { def apply[T](a: T): T = a } implicit def tcIdEquals: (Id ~> Id) = Id } - -trait Fn1[A, B] { - def ∙[C](g: C => A): C => B -} diff --git a/sbt/src/main/scala/Import.scala b/sbt/src/main/scala/Import.scala index 2e318663d..5c4d71428 100644 --- a/sbt/src/main/scala/Import.scala +++ b/sbt/src/main/scala/Import.scala @@ -133,7 +133,6 @@ trait Import { type FeedbackProvidedException = sbt.internal.util.FeedbackProvidedException type FilePosition = sbt.internal.util.FilePosition type FilterLogger = sbt.internal.util.FilterLogger - type Fn1[A, B] = sbt.internal.util.Fn1[A, B] val FullLogger = sbt.internal.util.FullLogger type FullLogger = sbt.internal.util.FullLogger val FullReader = sbt.internal.util.FullReader diff --git a/tasks-standard/src/main/scala/sbt/std/System.scala b/tasks-standard/src/main/scala/sbt/std/System.scala index 7e7673a33..5beb6cd66 100644 --- a/tasks-standard/src/main/scala/sbt/std/System.scala +++ b/tasks-standard/src/main/scala/sbt/std/System.scala @@ -48,7 +48,7 @@ object Transform { case Pure(eval, _) => uniform(Nil)(_ => Right(eval())) case m: Mapped[t, k] => toNode[t, k](m.in)(right ∙ m.f)(m.alist) case m: FlatMapped[t, k] => toNode[t, k](m.in)(left ∙ m.f)(m.alist) - case DependsOn(in, deps) => uniform(existToAny(deps))(const(Left(in)) ∙ all) + case DependsOn(in, deps) => uniform(existToAny(deps))(const(Left(in)) compose all) case Join(in, f) => uniform(in)(f) } def inline[T](t: Task[T]) = t.work match { From c39e9af68c6056bec26a3cc571fcb8c4e526943a Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 18 Oct 2017 19:10:01 -0500 Subject: [PATCH 10/62] Add kind-projector --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index d8dc2b577..a2b56fed2 100644 --- a/build.sbt +++ b/build.sbt @@ -51,6 +51,7 @@ def commonSettings: Seq[Setting[_]] = resolvers += Resolver.typesafeIvyRepo("releases"), resolvers += Resolver.sonatypeRepo("snapshots"), resolvers += "bintray-sbt-maven-releases" at "https://dl.bintray.com/sbt/maven-releases/", + addCompilerPlugin("org.spire-math" % "kind-projector" % "0.9.4" cross CrossVersion.binary), concurrentRestrictions in Global += Util.testExclusiveRestriction, testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"), testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "2"), From fb17cc393f8b2e36967bc8f8e3e84180f26eb632 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 18 Oct 2017 19:10:29 -0500 Subject: [PATCH 11/62] Rewrite to inline syntax --- build.sbt | 3 +++ .../src/main/scala/sbt/internal/util/TypeFunctions.scala | 5 ++--- tasks/src/main/scala/sbt/Execute.scala | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index a2b56fed2..3958c7dbd 100644 --- a/build.sbt +++ b/build.sbt @@ -145,6 +145,9 @@ val collectionProj = (project in file("internal") / "util-collection") // Added private[sbt] method to capture State attributes. exclude[ReversedMissingMethodProblem]("sbt.internal.util.AttributeMap.setCond"), + // Dropped in favour of kind-projector's inline type lambda syntax + exclude[MissingClassProblem]("sbt.internal.util.TypeFunctions$P1of2"), + // Dropped in favour of plain scala.Function, and its compose method exclude[MissingClassProblem]("sbt.internal.util.Fn1"), exclude[DirectMissingMethodProblem]("sbt.internal.util.TypeFunctions.toFn1"), diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala b/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala index a7ad5d70f..023ab9bdf 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala @@ -13,10 +13,9 @@ trait TypeFunctions { sealed trait ConstK[A] { type l[L[x]] = A } sealed trait Compose[A[_], B[_]] { type Apply[T] = A[B[T]] } sealed trait ∙[A[_], B[_]] { type l[T] = A[B[T]] } - sealed trait P1of2[M[_, _], A] { type Apply[B] = M[A, B]; type Flip[B] = M[B, A] } - final val left = new (Id ~> P1of2[Left, Nothing]#Flip) { def apply[T](t: T) = Left(t) } - final val right = new (Id ~> P1of2[Right, Nothing]#Apply) { def apply[T](t: T) = Right(t) } + final val left = new (Id ~> Left[?, Nothing]) { def apply[T](t: T) = Left(t) } + final val right = new (Id ~> Right[Nothing, ?]) { def apply[T](t: T) = Right(t) } final val some = new (Id ~> Some) { def apply[T](t: T) = Some(t) } final def idFun[T] = (t: T) => t final def const[A, B](b: B): A => B = _ => b diff --git a/tasks/src/main/scala/sbt/Execute.scala b/tasks/src/main/scala/sbt/Execute.scala index 5a60ce706..715e00f02 100644 --- a/tasks/src/main/scala/sbt/Execute.scala +++ b/tasks/src/main/scala/sbt/Execute.scala @@ -54,7 +54,7 @@ private[sbt] final class Execute[A[_] <: AnyRef]( private[this] val reverse = idMap[A[_], Iterable[A[_]]] private[this] val callers = pMap[A, Compose[IDSet, A]#Apply] private[this] val state = idMap[A[_], State] - private[this] val viewCache = pMap[A, ({ type l[t] = Node[A, t] })#l] + private[this] val viewCache = pMap[A, Node[A, ?]] private[this] val results = pMap[A, Result] private[this] val getResult: A ~> Result = new (A ~> Result) { From f662fdda8e7aa365c2e5ead13cadf24473577d6f Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 18 Oct 2017 19:10:42 -0500 Subject: [PATCH 12/62] Rewrite to function syntax --- .../src/main/scala/sbt/internal/util/AList.scala | 8 ++++---- .../src/main/scala/sbt/internal/util/INode.scala | 2 +- .../src/main/scala/sbt/internal/util/PMap.scala | 5 ++--- .../src/main/scala/sbt/internal/util/Settings.scala | 6 +++--- main-settings/src/main/scala/sbt/Structure.scala | 2 +- tasks-standard/src/main/scala/sbt/std/System.scala | 2 +- tasks-standard/src/main/scala/sbt/std/TaskExtra.scala | 4 ++-- tasks-standard/src/test/scala/Test.scala | 4 ++-- 8 files changed, 16 insertions(+), 17 deletions(-) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala b/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala index 7685f9c5f..cc5343d2f 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/AList.scala @@ -27,7 +27,7 @@ trait AList[K[L[x]]] { } object AList { - type Empty = AList[({ type l[L[x]] = Unit })#l] + type Empty = AList[ConstK[Unit]#l] /** AList for Unit, which represents a sequence that is always empty.*/ val empty: Empty = new Empty { @@ -37,7 +37,7 @@ object AList { def traverse[M[_], N[_], P[_]](in: Unit, f: M ~> (N ∙ P)#l)(implicit np: Applicative[N]): N[Unit] = np.pure(()) } - type SeqList[T] = AList[({ type l[L[x]] = List[L[T]] })#l] + type SeqList[T] = AList[λ[L[x] => List[L[T]]]] /** AList for a homogeneous sequence. */ def seq[T]: SeqList[T] = new SeqList[T] { @@ -67,7 +67,7 @@ object AList { override def toList[M[_]](k: KL[M]) = k.toList } - type Single[A] = AList[({ type l[L[x]] = L[A] })#l] + type Single[A] = AList[λ[L[x] => L[A]]] /** AList for a single value. */ def single[A]: Single[A] = new Single[A] { @@ -76,7 +76,7 @@ object AList { def traverse[M[_], N[_], P[_]](a: M[A], f: M ~> (N ∙ P)#l)(implicit np: Applicative[N]): N[P[A]] = f(a) } - type ASplit[K[L[x]], B[x]] = AList[({ type l[L[x]] = K[(L ∙ B)#l] })#l] + type ASplit[K[L[x]], B[x]] = AList[λ[L[x] => K[(L ∙ B)#l]]] /** AList that operates on the outer type constructor `A` of a composition `[x] A[B[x]]` for type constructors `A` and `B`*/ def asplit[K[L[x]], B[x]](base: AList[K]): ASplit[K, B] = new ASplit[K, B] { diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala b/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala index 90e004efa..ca7cf5f3d 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala @@ -204,7 +204,7 @@ abstract class EvaluateSettings[Scope] { new MixedNode[ConstK[Unit]#l, T]((), _ => f(), AList.empty) private[this] def single[S, T](in: INode[S], f: S => T): INode[T] = - new MixedNode[({ type l[L[x]] = L[S] })#l, T](in, f, AList.single[S]) + new MixedNode[λ[L[x] => L[S]], T](in, f, AList.single[S]) private[this] final class BindNode[S, T](in: INode[S], f: S => INode[T]) extends INode[T] { protected def dependsOn = in :: Nil diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/PMap.scala b/internal/util-collection/src/main/scala/sbt/internal/util/PMap.scala index a488fb32f..3d8cf19fd 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/PMap.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/PMap.scala @@ -31,8 +31,7 @@ trait IMap[K[_], V[_]] extends (K ~> V) with RMap[K, V] { def remove[T](k: K[T]): IMap[K, V] def mapValue[T](k: K[T], init: V[T], f: V[T] => V[T]): IMap[K, V] def mapValues[V2[_]](f: V ~> V2): IMap[K, V2] - def mapSeparate[VL[_], VR[_]](f: V ~> ({ type l[T] = Either[VL[T], VR[T]] })#l) - : (IMap[K, VL], IMap[K, VR]) + def mapSeparate[VL[_], VR[_]](f: V ~> λ[T => Either[VL[T], VR[T]]]): (IMap[K, VL], IMap[K, VR]) } trait PMap[K[_], V[_]] extends (K ~> V) with RMap[K, V] { @@ -69,7 +68,7 @@ object IMap { def mapValues[V2[_]](f: V ~> V2) = new IMap0[K, V2](backing.mapValues(x => f(x))) - def mapSeparate[VL[_], VR[_]](f: V ~> ({ type l[T] = Either[VL[T], VR[T]] })#l) = { + def mapSeparate[VL[_], VR[_]](f: V ~> λ[T => Either[VL[T], VR[T]]]) = { val mapped = backing.iterator.map { case (k, v) => f(v) match { diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala index 6ff3c98e6..a99bf0a33 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala @@ -101,14 +101,14 @@ trait Init[Scope] { def bind[S, T](in: Initialize[S])(f: S => Initialize[T]): Initialize[T] = new Bind(f, in) def map[S, T](in: Initialize[S])(f: S => T): Initialize[T] = - new Apply[({ type l[L[x]] = L[S] })#l, T](f, in, AList.single[S]) + new Apply[λ[L[x] => L[S]], T](f, in, AList.single[S]) def app[K[L[x]], T](inputs: K[Initialize])(f: K[Id] => T)( implicit alist: AList[K] ): Initialize[T] = new Apply[K, T](f, inputs, alist) def uniform[S, T](inputs: Seq[Initialize[S]])(f: Seq[S] => T): Initialize[T] = - new Apply[({ type l[L[x]] = List[L[S]] })#l, T](f, inputs.toList, AList.seq[S]) + new Apply[λ[L[x] => List[L[S]]], T](f, inputs.toList, AList.seq[S]) /** * The result of this initialization is the validated `key`. @@ -560,7 +560,7 @@ trait Init[Scope] { def zip[S](o: Initialize[S]): Initialize[(T, S)] = zipTupled(o)(idFun) def zipWith[S, U](o: Initialize[S])(f: (T, S) => U): Initialize[U] = zipTupled(o)(f.tupled) private[this] def zipTupled[S, U](o: Initialize[S])(f: ((T, S)) => U): Initialize[U] = - new Apply[({ type l[L[x]] = (L[T], L[S]) })#l, U](f, (this, o), AList.tuple2[T, S]) + new Apply[λ[L[x] => (L[T], L[S])], U](f, (this, o), AList.tuple2[T, S]) /** A fold on the static attributes of this and nested Initializes. */ private[sbt] def processAttributes[S](init: S)(f: (S, AttributeMap) => S): S diff --git a/main-settings/src/main/scala/sbt/Structure.scala b/main-settings/src/main/scala/sbt/Structure.scala index 54a421cfe..9b1427e04 100644 --- a/main-settings/src/main/scala/sbt/Structure.scala +++ b/main-settings/src/main/scala/sbt/Structure.scala @@ -441,7 +441,7 @@ object Scoped { ) private[this] def onTasks[T](f: K[Task] => Task[T]): App[T] = - Def.app[({ type l[L[x]] = K[(L ∙ Task)#l] })#l, Task[T]](inputs)(f)(AList.asplit[K, Task](a)) + Def.app[λ[L[x] => K[(L ∙ Task)#l]], Task[T]](inputs)(f)(AList.asplit[K, Task](a)) def flatMap[T](f: Fun[Id, Task[T]]): App[T] = onTasks(_.flatMap(convert(f))) def flatMapR[T](f: Fun[Result, Task[T]]): App[T] = onTasks(_.flatMapR(convert(f))) diff --git a/tasks-standard/src/main/scala/sbt/std/System.scala b/tasks-standard/src/main/scala/sbt/std/System.scala index 5beb6cd66..2f5b65861 100644 --- a/tasks-standard/src/main/scala/sbt/std/System.scala +++ b/tasks-standard/src/main/scala/sbt/std/System.scala @@ -58,7 +58,7 @@ object Transform { } def uniform[T, D](tasks: Seq[Task[D]])(f: Seq[Result[D]] => Either[Task[T], T]): Node[Task, T] = - toNode[T, ({ type l[L[x]] = List[L[D]] })#l](tasks.toList)(f)(AList.seq[D]) + toNode[T, λ[L[x] => List[L[D]]]](tasks.toList)(f)(AList.seq[D]) def toNode[T, k[L[x]]](inputs: k[Task])(f: k[Result] => Either[Task[T], T])( implicit a: AList[k]): Node[Task, T] = new Node[Task, T] { diff --git a/tasks-standard/src/main/scala/sbt/std/TaskExtra.scala b/tasks-standard/src/main/scala/sbt/std/TaskExtra.scala index 5d7bd1b96..b94ea5e1c 100644 --- a/tasks-standard/src/main/scala/sbt/std/TaskExtra.scala +++ b/tasks-standard/src/main/scala/sbt/std/TaskExtra.scala @@ -118,7 +118,7 @@ trait TaskExtra { } final implicit def multT2Task[A, B](in: (Task[A], Task[B])) = - multInputTask[({ type l[L[x]] = (L[A], L[B]) })#l](in)(AList.tuple2[A, B]) + multInputTask[λ[L[x] => (L[A], L[B])]](in)(AList.tuple2[A, B]) final implicit def multInputTask[K[L[X]]](tasks: K[Task])(implicit a: AList[K]): MultiInTask[K] = new MultiInTask[K] { @@ -248,7 +248,7 @@ object TaskExtra extends TaskExtra { } def reducePair[S](a: Task[S], b: Task[S], f: (S, S) => S): Task[S] = - multInputTask[({ type l[L[x]] = (L[S], L[S]) })#l]((a, b))(AList.tuple2[S, S]) map f.tupled + multInputTask[λ[L[x] => (L[S], L[S])]]((a, b))(AList.tuple2[S, S]) map f.tupled def anyFailM[K[L[x]]](implicit a: AList[K]): K[Result] => Seq[Incomplete] = in => { val incs = failuresM(a)(in) diff --git a/tasks-standard/src/test/scala/Test.scala b/tasks-standard/src/test/scala/Test.scala index 07a6c11b6..0574118ca 100644 --- a/tasks-standard/src/test/scala/Test.scala +++ b/tasks-standard/src/test/scala/Test.scala @@ -11,9 +11,9 @@ import sbt.internal.util.AList object Test extends std.TaskExtra { def t2[A, B](a: Task[A], b: Task[B]) = - multInputTask[({ type l[L[x]] = (L[A], L[B]) })#l]((a, b))(AList.tuple2) + multInputTask[λ[L[x] => (L[A], L[B])]]((a, b))(AList.tuple2) def t3[A, B, C](a: Task[A], b: Task[B], c: Task[C]) = - multInputTask[({ type l[L[x]] = (L[A], L[B], L[C]) })#l]((a, b, c))(AList.tuple3) + multInputTask[λ[L[x] => (L[A], L[B], L[C])]]((a, b, c))(AList.tuple3) val a = task(3) val b = task[Boolean](sys.error("test")) From 9f1d60be602f80d66071eb22b736d8473645bc16 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 19 Oct 2017 17:19:16 -0500 Subject: [PATCH 13/62] Rewrite to polymorphic function syntax --- build.sbt | 8 ++++ .../scala/sbt/internal/util/Attributes.scala | 6 +-- .../main/scala/sbt/internal/util/INode.scala | 42 +++++++++---------- .../main/scala/sbt/internal/util/Param.scala | 32 -------------- .../scala/sbt/internal/util/Settings.scala | 36 +++++----------- .../sbt/internal/util/TypeFunctions.scala | 20 ++++----- .../src/test/scala/LiteralTest.scala | 21 ---------- .../src/main/scala/sbt/Previous.scala | 4 +- .../src/main/scala/sbt/Structure.scala | 6 +-- main/src/main/scala/sbt/Project.scala | 5 +-- main/src/main/scala/sbt/internal/Load.scala | 16 +++---- sbt/src/main/scala/Import.scala | 2 - .../src/main/scala/sbt/std/System.scala | 4 +- tasks/src/main/scala/sbt/Execute.scala | 11 ++--- tasks/src/main/scala/sbt/Result.scala | 9 ++-- 15 files changed, 71 insertions(+), 151 deletions(-) delete mode 100644 internal/util-collection/src/main/scala/sbt/internal/util/Param.scala delete mode 100644 internal/util-collection/src/test/scala/LiteralTest.scala diff --git a/build.sbt b/build.sbt index 3958c7dbd..80a62a2ed 100644 --- a/build.sbt +++ b/build.sbt @@ -148,6 +148,10 @@ val collectionProj = (project in file("internal") / "util-collection") // Dropped in favour of kind-projector's inline type lambda syntax exclude[MissingClassProblem]("sbt.internal.util.TypeFunctions$P1of2"), + // Dropped in favour of kind-projector's polymorphic lambda literals + exclude[MissingClassProblem]("sbt.internal.util.Param"), + exclude[MissingClassProblem]("sbt.internal.util.Param$"), + // Dropped in favour of plain scala.Function, and its compose method exclude[MissingClassProblem]("sbt.internal.util.Fn1"), exclude[DirectMissingMethodProblem]("sbt.internal.util.TypeFunctions.toFn1"), @@ -432,6 +436,10 @@ lazy val sbtProj = (project in file("sbt")) exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$WatchSource_="), exclude[ReversedMissingMethodProblem]("sbt.Import.WatchSource"), + // Dropped in favour of kind-projector's polymorphic lambda literals + exclude[DirectMissingMethodProblem]("sbt.Import.Param"), + exclude[DirectMissingMethodProblem]("sbt.package.Param"), + // Dropped in favour of plain scala.Function, and its compose method exclude[DirectMissingMethodProblem]("sbt.package.toFn1"), ) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala index 0fa72edc2..0dfbea73e 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Attributes.scala @@ -196,11 +196,9 @@ object AttributeMap { def apply(entries: AttributeEntry[_]*): AttributeMap = empty ++ entries /** Presents an `AttributeMap` as a natural transformation. */ - implicit def toNatTrans(map: AttributeMap): AttributeKey ~> Id = new (AttributeKey ~> Id) { - def apply[T](key: AttributeKey[T]): T = map(key) - } - + implicit def toNatTrans(map: AttributeMap): AttributeKey ~> Id = λ[AttributeKey ~> Id](map(_)) } + private class BasicAttributeMap(private val backing: Map[AttributeKey[_], Any]) extends AttributeMap { diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala b/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala index ca7cf5f3d..939bd9576 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/INode.scala @@ -32,27 +32,25 @@ abstract class EvaluateSettings[Scope] { private[this] def getStatic[T](key: ScopedKey[T]): INode[T] = static get key getOrElse sys.error("Illegal reference to key " + key) - private[this] val transform: Initialize ~> INode = new (Initialize ~> INode) { - def apply[T](i: Initialize[T]): INode[T] = i match { - case k: Keyed[s, T] @unchecked => single(getStatic(k.scopedKey), k.transform) - case a: Apply[k, T] @unchecked => - new MixedNode[k, T]( - a.alist.transform[Initialize, INode](a.inputs, transform), - a.f, - a.alist - ) - case b: Bind[s, T] @unchecked => new BindNode[s, T](transform(b.in), x => transform(b.f(x))) - case v: Value[T] @unchecked => constant(v.value) - case v: ValidationCapture[T] @unchecked => strictConstant(v.key) - case t: TransformCapture => strictConstant(t.f) - case o: Optional[s, T] @unchecked => - o.a match { - case None => constant(() => o.f(None)) - case Some(i) => single[s, T](transform(i), x => o.f(Some(x))) - } - case x if x == StaticScopes => - strictConstant(allScopes.asInstanceOf[T]) // can't convince scalac that StaticScopes => T == Set[Scope] - } + private[this] val transform: Initialize ~> INode = λ[Initialize ~> INode] { + case k: Keyed[s, A1$] @unchecked => single(getStatic(k.scopedKey), k.transform) + case a: Apply[k, A1$] @unchecked => + new MixedNode[k, A1$]( + a.alist.transform[Initialize, INode](a.inputs, transform), + a.f, + a.alist + ) + case b: Bind[s, A1$] @unchecked => new BindNode[s, A1$](transform(b.in), x => transform(b.f(x))) + case v: Value[A1$] @unchecked => constant(v.value) + case v: ValidationCapture[A1$] @unchecked => strictConstant(v.key) + case t: TransformCapture => strictConstant(t.f) + case o: Optional[s, A1$] @unchecked => + o.a match { + case None => constant(() => o.f(None)) + case Some(i) => single[s, A1$](transform(i), x => o.f(Some(x))) + } + case x if x == StaticScopes => + strictConstant(allScopes.asInstanceOf[A1$]) // can't convince scalac that StaticScopes => T == Set[Scope] } private[this] lazy val roots: Seq[INode[_]] = compiledSettings flatMap { cs => @@ -84,7 +82,7 @@ abstract class EvaluateSettings[Scope] { if (key.key.isLocal) ss else ss.set(key.scope, key.key, node.get) } - private[this] val getValue = new (INode ~> Id) { def apply[T](node: INode[T]) = node.get } + private[this] val getValue = λ[INode ~> Id](_.get) private[this] def submitEvaluate(node: INode[_]) = submit(node.evaluate()) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Param.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Param.scala deleted file mode 100644 index 201663b4e..000000000 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Param.scala +++ /dev/null @@ -1,32 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2017, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under BSD-3-Clause license (see LICENSE) - */ - -package sbt.internal.util - -// Used to emulate ~> literals -trait Param[A[_], B[_]] { - type T - def in: A[T] - def ret(out: B[T]): Unit - def ret: B[T] -} - -object Param { - implicit def pToT[A[_], B[_]](p: Param[A, B] => Unit): A ~> B = new (A ~> B) { - def apply[s](a: A[s]): B[s] = { - val v: Param[A, B] { type T = s } = new Param[A, B] { - type T = s - def in = a - private var r: B[T] = _ - def ret(b: B[T]): Unit = { r = b } - def ret: B[T] = r - } - p(v) - v.ret - } - } -} diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala index a99bf0a33..910e1c089 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Settings.scala @@ -156,9 +156,7 @@ trait Init[Scope] { def empty(implicit delegates: Scope => Seq[Scope]): Settings[Scope] = new Settings0(Map.empty, delegates) - def asTransform(s: Settings[Scope]): ScopedKey ~> Id = new (ScopedKey ~> Id) { - def apply[T](k: ScopedKey[T]): T = getValue(s, k) - } + def asTransform(s: Settings[Scope]): ScopedKey ~> Id = λ[ScopedKey ~> Id](k => getValue(s, k)) def getValue[T](s: Settings[Scope], k: ScopedKey[T]) = s.get(k.scope, k.key) getOrElse (throw new InvalidReference(k)) @@ -246,13 +244,11 @@ trait Init[Scope] { type ValidatedSettings[T] = Either[Seq[Undefined], SettingSeq[T]] - val f = new (SettingSeq ~> ValidatedSettings) { - def apply[T](ks: Seq[Setting[T]]) = { - val (undefs, valid) = Util.separate(ks.zipWithIndex) { - case (s, i) => s validateKeyReferenced refMap(s, i == 0) - } - if (undefs.isEmpty) Right(valid) else Left(undefs.flatten) + val f = λ[SettingSeq ~> ValidatedSettings] { (ks: Seq[Setting[_]]) => + val (undefs, valid) = Util.separate(ks.zipWithIndex) { + case (s, i) => s validateKeyReferenced refMap(s, i == 0) } + if (undefs.isEmpty) Right(valid) else Left(undefs.flatten) } type Undefs[_] = Seq[Undefined] @@ -685,23 +681,15 @@ trait Init[Scope] { case Right(x) => x } - private[this] lazy val getValidated = - new (ValidatedInit ~> Initialize) { def apply[T](v: ValidatedInit[T]) = handleUndefined[T](v) } + private[this] lazy val getValidated = λ[ValidatedInit ~> Initialize](handleUndefined(_)) // mainly for reducing generated class count private[this] def validateKeyReferencedT(g: ValidateKeyRef) = - new (Initialize ~> ValidatedInit) { - def apply[T](i: Initialize[T]) = i validateKeyReferenced g - } + λ[Initialize ~> ValidatedInit](_ validateKeyReferenced g) - private[this] def mapReferencedT(g: MapScoped) = - new (Initialize ~> Initialize) { def apply[T](i: Initialize[T]) = i mapReferenced g } - - private[this] def mapConstantT(g: MapConstant) = - new (Initialize ~> Initialize) { def apply[T](i: Initialize[T]) = i mapConstant g } - - private[this] def evaluateT(g: Settings[Scope]) = - new (Initialize ~> Id) { def apply[T](i: Initialize[T]) = i evaluate g } + private[this] def mapReferencedT(g: MapScoped) = λ[Initialize ~> Initialize](_ mapReferenced g) + private[this] def mapConstantT(g: MapConstant) = λ[Initialize ~> Initialize](_ mapConstant g) + private[this] def evaluateT(g: Settings[Scope]) = λ[Initialize ~> Id](_ evaluate g) private[this] def deps(ls: Seq[Initialize[_]]): Seq[ScopedKey[_]] = ls.flatMap(_.dependencies) @@ -853,9 +841,7 @@ trait Init[Scope] { def validateKeyReferenced(g: ValidateKeyRef) = { val tx = alist.transform(inputs, validateKeyReferencedT(g)) val undefs = alist.toList(tx).flatMap(_.left.toSeq.flatten) - val get = new (ValidatedInit ~> Initialize) { - def apply[B](vr: ValidatedInit[B]) = vr.right.get - } + val get = λ[ValidatedInit ~> Initialize](_.right.get) if (undefs.isEmpty) Right(new Apply(f, alist.transform(tx, get), alist)) else Left(undefs) } diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala b/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala index 023ab9bdf..49c29ff11 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/TypeFunctions.scala @@ -14,19 +14,17 @@ trait TypeFunctions { sealed trait Compose[A[_], B[_]] { type Apply[T] = A[B[T]] } sealed trait ∙[A[_], B[_]] { type l[T] = A[B[T]] } - final val left = new (Id ~> Left[?, Nothing]) { def apply[T](t: T) = Left(t) } - final val right = new (Id ~> Right[Nothing, ?]) { def apply[T](t: T) = Right(t) } - final val some = new (Id ~> Some) { def apply[T](t: T) = Some(t) } + final val left = λ[Id ~> Left[?, Nothing]](Left(_)) + final val right = λ[Id ~> Right[Nothing, ?]](Right(_)) + final val some = λ[Id ~> Some](Some(_)) final def idFun[T] = (t: T) => t final def const[A, B](b: B): A => B = _ => b - final def idK[M[_]]: M ~> M = new (M ~> M) { def apply[T](m: M[T]): M[T] = m } + final def idK[M[_]]: M ~> M = λ[M ~> M](m => m) def nestCon[M[_], N[_], G[_]](f: M ~> N): (M ∙ G)#l ~> (N ∙ G)#l = - f.asInstanceOf[(M ∙ G)#l ~> (N ∙ G)#l] // implemented with a cast to avoid extra object+method call. castless version: - - /* new ( (M ∙ G)#l ~> (N ∙ G)#l ) { - def apply[T](mg: M[G[T]]): N[G[T]] = f(mg) - } */ + f.asInstanceOf[(M ∙ G)#l ~> (N ∙ G)#l] // implemented with a cast to avoid extra object+method call. + // castless version: + // λ[(M ∙ G)#l ~> (N ∙ G)#l](f(_)) type Endo[T] = T => T type ~>|[A[_], B[_]] = A ~> Compose[Option, B]#Apply @@ -37,13 +35,13 @@ object TypeFunctions extends TypeFunctions trait ~>[-A[_], +B[_]] { outer => def apply[T](a: A[T]): B[T] // directly on ~> because of type inference limitations - final def ∙[C[_]](g: C ~> A): C ~> B = new (C ~> B) { def apply[T](c: C[T]) = outer.apply(g(c)) } + final def ∙[C[_]](g: C ~> A): C ~> B = λ[C ~> B](c => outer.apply(g(c))) final def ∙[C, D](g: C => D)(implicit ev: D <:< A[D]): C => B[D] = i => apply(ev(g(i))) final def fn[T] = (t: A[T]) => apply[T](t) } object ~> { import TypeFunctions._ - val Id: Id ~> Id = new (Id ~> Id) { def apply[T](a: T): T = a } + val Id: Id ~> Id = idK[Id] implicit def tcIdEquals: (Id ~> Id) = Id } diff --git a/internal/util-collection/src/test/scala/LiteralTest.scala b/internal/util-collection/src/test/scala/LiteralTest.scala deleted file mode 100644 index b479752f7..000000000 --- a/internal/util-collection/src/test/scala/LiteralTest.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2017, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under BSD-3-Clause license (see LICENSE) - */ - -package sbt.internal.util - -// compilation test -object LiteralTest { - def x[A[_], B[_]](f: A ~> B) = f - - import Param._ - val f = x { (p: Param[Option, List]) => - p.ret(p.in.toList) - } - - val a: List[Int] = f(Some(3)) - val b: List[String] = f(Some("aa")) -} diff --git a/main-settings/src/main/scala/sbt/Previous.scala b/main-settings/src/main/scala/sbt/Previous.scala index a40db6649..62bfd705f 100644 --- a/main-settings/src/main/scala/sbt/Previous.scala +++ b/main-settings/src/main/scala/sbt/Previous.scala @@ -21,9 +21,7 @@ import scala.util.control.NonFatal */ private[sbt] final class Previous(streams: Streams, referenced: IMap[ScopedTaskKey, Referenced]) { private[this] val map = referenced.mapValues(toValue) - private[this] def toValue = new (Referenced ~> ReferencedValue) { - def apply[T](x: Referenced[T]) = new ReferencedValue(x) - } + private[this] def toValue = λ[Referenced ~> ReferencedValue](new ReferencedValue(_)) private[this] final class ReferencedValue[T](referenced: Referenced[T]) { import referenced.{ stamped, task } diff --git a/main-settings/src/main/scala/sbt/Structure.scala b/main-settings/src/main/scala/sbt/Structure.scala index 9b1427e04..bcedc4171 100644 --- a/main-settings/src/main/scala/sbt/Structure.scala +++ b/main-settings/src/main/scala/sbt/Structure.scala @@ -434,11 +434,7 @@ object Scoped { protected def convert[M[_], Ret](f: Fun[M, Ret]): K[M] => Ret - private[this] val inputs: K[App] = - a.transform( - keys, - new (ScopedTaskable ~> App) { def apply[T](in: ScopedTaskable[T]): App[T] = in.toTask } - ) + private[this] val inputs: K[App] = a.transform(keys, λ[ScopedTaskable ~> App](_.toTask)) private[this] def onTasks[T](f: K[Task] => Task[T]): App[T] = Def.app[λ[L[x] => K[(L ∙ Task)#l]], Task[T]](inputs)(f)(AList.asplit[K, Task](a)) diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index 0c42d86b3..6ec260e86 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -515,10 +515,7 @@ object Project extends ProjectExtra { def fillTaskAxis(scoped: ScopedKey[_]): ScopedKey[_] = ScopedKey(Scope.fillTaskAxis(scoped.scope, scoped.key), scoped.key) - def mapScope(f: Scope => Scope) = new (ScopedKey ~> ScopedKey) { - def apply[T](key: ScopedKey[T]) = - ScopedKey(f(key.scope), key.key) - } + def mapScope(f: Scope => Scope) = λ[ScopedKey ~> ScopedKey](k => ScopedKey(f(k.scope), k.key)) def transform(g: Scope => Scope, ss: Seq[Def.Setting[_]]): Seq[Def.Setting[_]] = { val f = mapScope(g) diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index b10235eac..c6264f671 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -291,12 +291,12 @@ private[sbt] object Load { // 3. resolvedScoped is replaced with the defining key as a value // Note: this must be idempotent. def finalTransforms(ss: Seq[Setting[_]]): Seq[Setting[_]] = { - def mapSpecial(to: ScopedKey[_]) = new (ScopedKey ~> ScopedKey) { - def apply[T](key: ScopedKey[T]) = + def mapSpecial(to: ScopedKey[_]) = λ[ScopedKey ~> ScopedKey]( + (key: ScopedKey[_]) => if (key.key == streams.key) ScopedKey(Scope.fillTaskAxis(Scope.replaceThis(to.scope)(key.scope), to.key), key.key) else key - } + ) def setDefining[T] = (key: ScopedKey[T], value: T) => value match { @@ -304,13 +304,13 @@ private[sbt] object Load { case ik: InputTask[t] => ik.mapTask(tk => setDefinitionKey(tk, key)).asInstanceOf[T] case _ => value } - def setResolved(defining: ScopedKey[_]) = new (ScopedKey ~> Option) { - def apply[T](key: ScopedKey[T]): Option[T] = + def setResolved(defining: ScopedKey[_]) = λ[ScopedKey ~> Option]( + (key: ScopedKey[_]) => key.key match { - case resolvedScoped.key => Some(defining.asInstanceOf[T]) + case resolvedScoped.key => Some(defining.asInstanceOf[A1$]) case _ => None - } - } + } + ) ss.map(s => s mapConstant setResolved(s.key) mapReferenced mapSpecial(s.key) mapInit setDefining) } diff --git a/sbt/src/main/scala/Import.scala b/sbt/src/main/scala/Import.scala index 5c4d71428..3a0adc48a 100644 --- a/sbt/src/main/scala/Import.scala +++ b/sbt/src/main/scala/Import.scala @@ -166,8 +166,6 @@ trait Import { val NoPosition = sbt.internal.util.NoPosition val PMap = sbt.internal.util.PMap type PMap[K[_], V[_]] = sbt.internal.util.PMap[K, V] - val Param = sbt.internal.util.Param - type Param[A[_], B[_]] = sbt.internal.util.Param[A, B] type RMap[K[_], V[_]] = sbt.internal.util.RMap[K, V] val RangePosition = sbt.internal.util.RangePosition type RangePosition = sbt.internal.util.RangePosition diff --git a/tasks-standard/src/main/scala/sbt/std/System.scala b/tasks-standard/src/main/scala/sbt/std/System.scala index 2f5b65861..060bc0d48 100644 --- a/tasks-standard/src/main/scala/sbt/std/System.scala +++ b/tasks-standard/src/main/scala/sbt/std/System.scala @@ -37,9 +37,7 @@ object Transform { /** Applies `map`, returning the result if defined or returning the input unchanged otherwise.*/ implicit def getOrId(map: Task ~>| Task): Task ~> Task = - new (Task ~> Task) { - def apply[T](in: Task[T]): Task[T] = map(in).getOrElse(in) - } + λ[Task ~> Task](in => map(in).getOrElse(in)) def apply(dummies: DummyTaskMap) = taskToNode(getOrId(dummyMap(dummies))) diff --git a/tasks/src/main/scala/sbt/Execute.scala b/tasks/src/main/scala/sbt/Execute.scala index 715e00f02..267a1383a 100644 --- a/tasks/src/main/scala/sbt/Execute.scala +++ b/tasks/src/main/scala/sbt/Execute.scala @@ -57,12 +57,13 @@ private[sbt] final class Execute[A[_] <: AnyRef]( private[this] val viewCache = pMap[A, Node[A, ?]] private[this] val results = pMap[A, Result] - private[this] val getResult: A ~> Result = new (A ~> Result) { - def apply[T](a: A[T]): Result[T] = view.inline(a) match { - case Some(v) => Value(v()) - case None => results(a) + private[this] val getResult: A ~> Result = λ[A ~> Result]( + a => + view.inline(a) match { + case Some(v) => Value(v()) + case None => results(a) } - } + ) private[this] var progressState: progress.S = progress.initial private[this] type State = State.Value diff --git a/tasks/src/main/scala/sbt/Result.scala b/tasks/src/main/scala/sbt/Result.scala index 691a54f00..318027579 100644 --- a/tasks/src/main/scala/sbt/Result.scala +++ b/tasks/src/main/scala/sbt/Result.scala @@ -28,12 +28,9 @@ final case class Value[+T](value: T) extends Result[T] { object Result { type Id[X] = X - val tryValue = new (Result ~> Id) { - def apply[T](r: Result[T]): T = - r match { - case Value(v) => v - case Inc(i) => throw i - } + val tryValue = λ[Result ~> Id] { + case Value(v) => v + case Inc(i) => throw i } def tryValues[S](r: Seq[Result[Unit]], v: Result[S]): S = { r foreach tryValue[Unit] From 53ea3596eccf652b91e6dce423ee7ac048cccf33 Mon Sep 17 00:00:00 2001 From: Antonio Cunei Date: Thu, 26 Oct 2017 00:30:03 +0200 Subject: [PATCH 14/62] Interim fix for #3583, for 1.0.3 In 0.13.x, zinc would discover only top-level objects and classes containing tests to the test framework. In 1.x, however, zinc can discover also nested objects and classes; that causes the "name" of a ClassLike to no longer be usable for reflection. This change filters out nested objects/classes from the list, restoring compatibility with 0.13. A zinc extension of ClassLike will probably be introduced in 1.1 or 1.2, in order to provide the test framework with enough information to deal with nested classes. This patch unblocks https://github.com/sbt/sbt-standalone-build/issues/15 --- main-actions/src/main/scala/sbt/Tests.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/main-actions/src/main/scala/sbt/Tests.scala b/main-actions/src/main/scala/sbt/Tests.scala index e0dbbffa9..d7b8dcac5 100644 --- a/main-actions/src/main/scala/sbt/Tests.scala +++ b/main-actions/src/main/scala/sbt/Tests.scala @@ -385,7 +385,11 @@ object Tests { defined(subclasses, d.baseClasses, d.isModule) ++ defined(annotations, d.annotations, d.isModule) - val discovered = Discovery(firsts(subclasses), firsts(annotations))(definitions) + val discovered = Discovery(firsts(subclasses), firsts(annotations))(definitions.filter { + case c: ClassLike => + c.topLevel + case _ => false + }) // TODO: To pass in correct explicitlySpecified and selectors val tests = for ((df, di) <- discovered; fingerprint <- toFingerprints(di)) yield new TestDefinition(df.name, fingerprint, false, Array(new SuiteSelector)) From b19bd33dc07c55d2da1dcefee838eef22983e5e0 Mon Sep 17 00:00:00 2001 From: Antonio Cunei Date: Thu, 26 Oct 2017 01:21:14 +0200 Subject: [PATCH 15/62] Missing import --- main-actions/src/main/scala/sbt/Tests.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/main-actions/src/main/scala/sbt/Tests.scala b/main-actions/src/main/scala/sbt/Tests.scala index d7b8dcac5..97a3b2452 100644 --- a/main-actions/src/main/scala/sbt/Tests.scala +++ b/main-actions/src/main/scala/sbt/Tests.scala @@ -9,6 +9,7 @@ import sbt.internal.inc.Analysis import TaskExtra._ import sbt.internal.util.FeedbackProvidedException import xsbti.api.Definition +import xsbti.api.ClassLike import xsbti.compile.CompileAnalysis import ConcurrentRestrictions.Tag From 24a463cc6ba286ca0fca9e36b5f3b142ba90e09b Mon Sep 17 00:00:00 2001 From: Antonio Cunei Date: Thu, 26 Oct 2017 01:46:28 +0200 Subject: [PATCH 16/62] Add scripted test for nested test classes/objects --- sbt/src/sbt-test/tests/nested-tests/build.sbt | 8 ++++++ .../nested-tests/src/test/scala/q/X.scala | 25 +++++++++++++++++++ sbt/src/sbt-test/tests/nested-tests/test | 2 ++ 3 files changed, 35 insertions(+) create mode 100644 sbt/src/sbt-test/tests/nested-tests/build.sbt create mode 100644 sbt/src/sbt-test/tests/nested-tests/src/test/scala/q/X.scala create mode 100644 sbt/src/sbt-test/tests/nested-tests/test diff --git a/sbt/src/sbt-test/tests/nested-tests/build.sbt b/sbt/src/sbt-test/tests/nested-tests/build.sbt new file mode 100644 index 000000000..2e47706f6 --- /dev/null +++ b/sbt/src/sbt-test/tests/nested-tests/build.sbt @@ -0,0 +1,8 @@ +libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.13.5" % "test" + +version := "0.0.1" +name := "broken" +organization := "org.catastrophe" +//scalaVersion := "2.10.6" +scalaVersion := "2.12.3" + diff --git a/sbt/src/sbt-test/tests/nested-tests/src/test/scala/q/X.scala b/sbt/src/sbt-test/tests/nested-tests/src/test/scala/q/X.scala new file mode 100644 index 000000000..6c80f0bce --- /dev/null +++ b/sbt/src/sbt-test/tests/nested-tests/src/test/scala/q/X.scala @@ -0,0 +1,25 @@ +package q + +// +// On 1.0.3+ this test will say: +// [info] + Nesting.startsWith: OK, passed 100 tests. +// [info] Passed: Total 1, Failed 0, Errors 0, Passed 1 +// +// On 1.0.0 to 1.0.2 it will crash with: +// [error] java.lang.ClassNotFoundException: q.X.Y$ +// + +import org.scalacheck.{Prop, Properties} +import Prop.forAll + +class U extends Properties("Nesting") +object X extends U { + property("startsWith") = forAll { (a: String, b: String) => + (a+b).startsWith(a) + } + object Y extends U { + property("endsWith") = forAll { (a: String, b: String) => + (a+b).endsWith(b) + } + } +} diff --git a/sbt/src/sbt-test/tests/nested-tests/test b/sbt/src/sbt-test/tests/nested-tests/test new file mode 100644 index 000000000..c8987ae90 --- /dev/null +++ b/sbt/src/sbt-test/tests/nested-tests/test @@ -0,0 +1,2 @@ +> test + From c60a5a204eebbf3dd462553acd2d2d29add6799d Mon Sep 17 00:00:00 2001 From: Antonio Cunei Date: Thu, 26 Oct 2017 01:57:56 +0200 Subject: [PATCH 17/62] Release notes for the fix to #3583 --- notes/1.0.3/nested-tests.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 notes/1.0.3/nested-tests.md diff --git a/notes/1.0.3/nested-tests.md b/notes/1.0.3/nested-tests.md new file mode 100644 index 000000000..c34f3d41f --- /dev/null +++ b/notes/1.0.3/nested-tests.md @@ -0,0 +1,16 @@ +### Bug fixes + +In 0.13.x, zinc would discover only top-level objects and classes +containing tests, and pass them to the test framework. In 1.x, +however, zinc can discover also nested objects and classes; that +causes the "name" of a ClassLike to no longer be usable for reflection. + +Version 1.0.3 filters out nested objects/classes from the list, +restoring compatibility with 0.13. A zinc extension of ClassLike +will probably be introduced in 1.1 or 1.2, in order to provide +the test framework with enough information to deal with nested +classes. + +[@cunei]: https://github.com/cunei +[3583]: https://github.com/sbt/sbt/issues/3583 + From 5f143434a8ef1bd6f176cac4691f25814d98cb33 Mon Sep 17 00:00:00 2001 From: Roman Iakovlev Date: Thu, 26 Oct 2017 16:42:03 +0200 Subject: [PATCH 18/62] Add new setting to determine if running in CI env Fixes #3653 --- main/src/main/scala/sbt/Defaults.scala | 11 +++++------ main/src/main/scala/sbt/Keys.scala | 1 + sbt/src/sbt-test/project/inside-ci/build.sbt | 7 +++++++ sbt/src/sbt-test/project/inside-ci/test | 2 ++ 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 sbt/src/sbt-test/project/inside-ci/build.sbt create mode 100644 sbt/src/sbt-test/project/inside-ci/test diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index acbefdb16..3275fe092 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -273,6 +273,7 @@ object Defaults extends BuildCommon { .toHex(Hash(appConfiguration.value.baseDirectory.toString)) .## % 1000), serverAuthentication := Set(ServerAuthentication.Token), + insideCI :== sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI"), )) def defaultTestTasks(key: Scoped): Seq[Setting[_]] = @@ -1749,12 +1750,10 @@ object Classpaths { dependencyOverrides :== Vector.empty, libraryDependencies :== Nil, excludeDependencies :== Nil, - ivyLoggingLevel :== { - // This will suppress "Resolving..." logs on Jenkins and Travis. - if (sys.env.get("BUILD_NUMBER").isDefined || sys.env.get("CI").isDefined) - UpdateLogging.Quiet - else UpdateLogging.Default - }, + ivyLoggingLevel := (// This will suppress "Resolving..." logs on Jenkins and Travis. + if (insideCI.value) + UpdateLogging.Quiet + else UpdateLogging.Default), ivyXML :== NodeSeq.Empty, ivyValidate :== false, moduleConfigurations :== Nil, diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 2572e6cce..ca92e306b 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -445,6 +445,7 @@ object Keys { val skip = taskKey[Boolean]("For tasks that support it (currently only 'compile' and 'update'), setting skip to true will force the task to not to do its work. This exact semantics may vary by task.").withRank(BSetting) val templateResolverInfos = settingKey[Seq[TemplateResolverInfo]]("Template resolvers used for 'new'.").withRank(BSetting) val interactionService = taskKey[InteractionService]("Service used to ask for user input through the current user interface(s).").withRank(CTask) + val insideCI = SettingKey[Boolean]("insideCI", "Determines if the SBT is running in a Continuous Integration environment", AMinusSetting) // special val sessionVars = AttributeKey[SessionVar.Map]("sessionVars", "Bindings that exist for the duration of the session.", Invisible) diff --git a/sbt/src/sbt-test/project/inside-ci/build.sbt b/sbt/src/sbt-test/project/inside-ci/build.sbt new file mode 100644 index 000000000..9e772fdcd --- /dev/null +++ b/sbt/src/sbt-test/project/inside-ci/build.sbt @@ -0,0 +1,7 @@ +name := "inside-ci" + +organization := "org.example" + +val t = taskKey[Boolean]("inside-ci") + +t := insideCI.value \ No newline at end of file diff --git a/sbt/src/sbt-test/project/inside-ci/test b/sbt/src/sbt-test/project/inside-ci/test new file mode 100644 index 000000000..08a542bb3 --- /dev/null +++ b/sbt/src/sbt-test/project/inside-ci/test @@ -0,0 +1,2 @@ +# just need to verify it loads +> help \ No newline at end of file From 84f03b64bed950ca028aa86035e34fbc822a313e Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 26 Oct 2017 21:13:48 -0400 Subject: [PATCH 19/62] Bump modules Fixes #3519 Fixes #3586 Fixes #3569 Fixes #3573 --- project/Dependencies.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 4f96a8304..819094a3e 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -13,9 +13,9 @@ object Dependencies { // sbt modules private val ioVersion = "1.0.2" - private val utilVersion = "1.0.1" - private val lmVersion = "1.0.2" - private val zincVersion = "1.0.1" + private val utilVersion = "1.0.2" + private val lmVersion = "1.0.3" + private val zincVersion = "1.0.3" private val sbtIO = "org.scala-sbt" %% "io" % ioVersion From db79226c22b062e3511321557732ba4398afb612 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 26 Oct 2017 22:39:35 -0400 Subject: [PATCH 20/62] notes for 1.0.3 --- notes/0.13.17/addSbtPlugin-cross.markdown | 7 --- notes/1.0.3.markdown | 61 +++++++++++++++++++++++ notes/1.0.3/nested-tests.md | 16 ------ notes/1.0.3/scala2124.markdown | 5 -- notes/1.0.3/watch.md | 26 ---------- notes/1.0.3/watch2.md | 12 ----- 6 files changed, 61 insertions(+), 66 deletions(-) delete mode 100644 notes/0.13.17/addSbtPlugin-cross.markdown create mode 100644 notes/1.0.3.markdown delete mode 100644 notes/1.0.3/nested-tests.md delete mode 100644 notes/1.0.3/scala2124.markdown delete mode 100644 notes/1.0.3/watch.md delete mode 100644 notes/1.0.3/watch2.md diff --git a/notes/0.13.17/addSbtPlugin-cross.markdown b/notes/0.13.17/addSbtPlugin-cross.markdown deleted file mode 100644 index aba7194a4..000000000 --- a/notes/0.13.17/addSbtPlugin-cross.markdown +++ /dev/null @@ -1,7 +0,0 @@ -### Bug fixes - -- Fixes `addSbtPlugin` to use the correct version of sbt. [#3393][]/[#3397][] by [@dwijnand][] - - [#3393]: https://github.com/sbt/sbt/issues/3393 - [#3397]: https://github.com/sbt/sbt/pull/3397 - [@dwijnand]: http://github.com/dwijnand diff --git a/notes/1.0.3.markdown b/notes/1.0.3.markdown new file mode 100644 index 000000000..06624a85a --- /dev/null +++ b/notes/1.0.3.markdown @@ -0,0 +1,61 @@ +This is a hotfix release for sbt 1.0.x series. + +### Bug fixes + +- Fixes `~` recompiling in loop (when a source generator or sbt-buildinfo is present). [#3501][3501]/[#3634][3634] by [@dwijnand][@dwijnand] +- Fixes undercompilation on inheritance on same source. [zinc#424][zinc424] by [@eed3si9n][@eed3si9n] +- Fixes the compilation of package-protected objects. [zinc#431][zinc431] by [@jvican][@jvican] +- Workaround for Java returning `null` for `getGenericParameterTypes`. [zinc#446][zinc446] by [@jvican][@jvican] +- Fixes test detection regression. sbt 1.0.3 filters out nested objects/classes from the list, restoring compatibility with 0.13. [#3669][3669] by [@cunei][@cunei] +- Uses Scala 2.12.4 for the build definition. This includes fix for runtime reflection of empty package members under Java 9. [#3587][3587] by [@eed3si9n][@eed3si9n] +- Fixes extra `/` in Ivy style patterns. [lm#170][lm170] by [@laughedelic][@laughedelic] +- Fixes "destination file exist" error message by including the file name. [lm171][lm171] by [@leonardehrenfried][@leonardehrenfried] +- Fixes JDK 9 warning "Illegal reflective access" in library management module and Ivy. [lm173][lm173] by [@dwijnand][@dwijnand] + +### Improvements + +- Adds `sbt.watch.mode` system property to allow switching back to old polling behaviour for watch. See below for more details. + +#### Alternative watch mode + +sbt 1.0.0 introduced a new mechanism for watching for source changes based on the NIO `WatchService` in Java 1.7. On +some platforms (namely macOS) this has led to long delays before changes are picked up. An alternative `WatchService` +for these platforms is planned for sbt 1.1.0 ([#3527][3527]), in the meantime an option to select which watch service +has been added. + +The new `sbt.watch.mode` JVM flag has been added with the following supported values: + +- `polling`: (default for macOS) poll the filesystem for changes (mechanism used in sbt 0.13). +- `nio` (default for other platforms): use the NIO based `WatchService`. + +If you are experiencing long delays on a non-macOS machine then try adding `-Dsbt.watch.mode=polling` to your sbt +options. + +[#3597][3597] by [@stringbean][@stringbean] + +### Contributors + +A huge thank you to everyone who's helped improve sbt and Zinc 1 by using them, reporting bugs, improving our documentation, porting builds, porting plugins, and submitting and reviewing pull requests. + +This release was brought to you by 15 contributors, according to `git shortlog -sn --no-merges v1.0.2..v1.0.3` on sbt, zinc, librarymanagement, util, io, and website: Eugene Yokota, Dale Wijnand, Michael Stringer, Jorge Vicente Cantero (jvican), Alexey Alekhin, Antonio Cunei, Andrey Artemov, Jeffrey Olchovy, Kenji Yoshida (xuwei-k), Dominik Winter, Long Jinwei, Arnout Engelen, Justin Kaeser, Leonard Ehrenfried, Sakib Hadžiavdić. Thank you! + + [@dwijnand]: https://github.com/dwijnand + [@cunei]: https://github.com/cunei + [@eed3si9n]: https://github.com/eed3si9n + [@jvican]: https://github.com/jvican + [@stringbean]: https://github.com/stringbean + [@laughedelic]: https://github.com/laughedelic + [@leonardehrenfried]: https://github.com/leonardehrenfried + [3669]: https://github.com/sbt/sbt/pull/3669 + [3583]: https://github.com/sbt/sbt/issues/3583 + [3587]: https://github.com/sbt/sbt/issues/3587 + [3527]: https://github.com/sbt/sbt/issues/3527 + [3597]: https://github.com/sbt/sbt/pull/3597 + [3501]: https://github.com/sbt/sbt/issues/3501 + [3634]: https://github.com/sbt/sbt/pull/3634 + [lm170]: https://github.com/sbt/librarymanagement/pull/170 + [lm171]: https://github.com/sbt/librarymanagement/pull/171 + [lm173]: https://github.com/sbt/librarymanagement/pull/173 + [zinc424]: https://github.com/sbt/zinc/pull/424 + [zinc431]: https://github.com/sbt/zinc/pull/431 + [zinc446]: https://github.com/sbt/zinc/pull/446 diff --git a/notes/1.0.3/nested-tests.md b/notes/1.0.3/nested-tests.md deleted file mode 100644 index c34f3d41f..000000000 --- a/notes/1.0.3/nested-tests.md +++ /dev/null @@ -1,16 +0,0 @@ -### Bug fixes - -In 0.13.x, zinc would discover only top-level objects and classes -containing tests, and pass them to the test framework. In 1.x, -however, zinc can discover also nested objects and classes; that -causes the "name" of a ClassLike to no longer be usable for reflection. - -Version 1.0.3 filters out nested objects/classes from the list, -restoring compatibility with 0.13. A zinc extension of ClassLike -will probably be introduced in 1.1 or 1.2, in order to provide -the test framework with enough information to deal with nested -classes. - -[@cunei]: https://github.com/cunei -[3583]: https://github.com/sbt/sbt/issues/3583 - diff --git a/notes/1.0.3/scala2124.markdown b/notes/1.0.3/scala2124.markdown deleted file mode 100644 index 33f9829a1..000000000 --- a/notes/1.0.3/scala2124.markdown +++ /dev/null @@ -1,5 +0,0 @@ - -- Uses Scala 2.12.4 for the build definition. This includes fix for runtime reflection of empty package members under Java 9. [#3587][3587] by [@eed3si9n][@eed3si9n] - - [3587]: https://github.com/sbt/sbt/issues/3587 - [@eed3si9n]: https://github.com/eed3si9n \ No newline at end of file diff --git a/notes/1.0.3/watch.md b/notes/1.0.3/watch.md deleted file mode 100644 index ff751fdf9..000000000 --- a/notes/1.0.3/watch.md +++ /dev/null @@ -1,26 +0,0 @@ -### Fixes with compatibility implications - -### Improvements - -- Add `sbt.watch.mode` system property to allow switching back to old polling behaviour for watch. See below for more details. [#3597][3597] by [@stringbean][@stringbean] - -### Bug fixes - -#### Alternative watch mode - -sbt 1.0.0 introduced a new mechanism for watching for source changes based on the NIO `WatchService` in Java 1.7. On -some platforms (namely macOS) this has led to long delays before changes are picked up. An alternative `WatchService` -for these platforms is planned for sbt 1.1.0 ([#3527][3527]), in the meantime an option to select which watch service -has been added. - -The new `sbt.watch.mode` JVM flag has been added with the following supported values: - -- `polling`: (default for macOS) poll the filesystem for changes (mechanism used in sbt 0.13). -- `nio` (default for other platforms): use the NIO based `WatchService`. - -If you are experiencing long delays on a non-macOS machine then try adding `-Dsbt.watch.mode=polling` to your sbt -options. - -[@stringbean]: https://github.com/stringbean -[3527]: https://github.com/sbt/sbt/issues/3527 -[3597]: https://github.com/sbt/sbt/pull/3597 diff --git a/notes/1.0.3/watch2.md b/notes/1.0.3/watch2.md deleted file mode 100644 index 69b30cede..000000000 --- a/notes/1.0.3/watch2.md +++ /dev/null @@ -1,12 +0,0 @@ -[@dwijnand]: https://github.com/dwijnand - -[#3501]: https://github.com/sbt/sbt/issues/3501 -[#3634]: https://github.com/sbt/sbt/pull/3634 - -### Fixes with compatibility implications - -### Improvements - -### Bug fixes - -- Fixes `~` to recompile on loop (when a source generator or sbt-buildinfo is present). [#3501][]/[#3634][] by [@dwijnand][] From 69ae4f3073f762df53793228492474d43f56cee3 Mon Sep 17 00:00:00 2001 From: Roman Iakovlev Date: Thu, 26 Oct 2017 19:05:04 +0200 Subject: [PATCH 21/62] Add a check for a change in SBT version before reload Fixes #1055 --- main/src/main/scala/sbt/Main.scala | 22 ++++++++++++++++++- .../project/sbt-version-change/build.sbt | 8 +++++++ .../changes/build.properties | 1 + .../sbt-test/project/sbt-version-change/test | 4 ++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 sbt/src/sbt-test/project/sbt-version-change/build.sbt create mode 100644 sbt/src/sbt-test/project/sbt-version-change/changes/build.properties create mode 100644 sbt/src/sbt-test/project/sbt-version-change/test diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index f8faddff8..61fe64cd0 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -56,7 +56,8 @@ import StandardMain._ import java.io.{ File, IOException } import java.net.URI -import java.util.Locale +import java.util.{ Locale, Properties } + import scala.util.control.NonFatal import BasicCommandStrings.{ Shell, TemplateCommand } import CommandStrings.BootCommand @@ -672,7 +673,26 @@ object BuiltinCommands { def loadProjectImpl: Command = Command(LoadProjectImpl)(_ => Project.loadActionParser)(doLoadProject) + def checkSBTVersionChanged(state: State): Unit = { + import sbt.io.syntax._ + val app = state.configuration.provider + val buildProps = state.baseDir / "project" / "build.properties" + // First try reading the sbt version from build.properties file. + val sbtVersionOpt = if (buildProps.exists) { + val buildProperties = new Properties() + IO.load(buildProperties, buildProps) + Option(buildProperties.getProperty("sbt.version")) + } else None + + sbtVersionOpt.foreach(version => + if (version != app.id.version()) { + state.log.warn(s"""sbt version mismatch, current: ${app.id + .version()}, in build.properties: "$version", use 'reboot' to use the new value.""") + }) + } + def doLoadProject(s0: State, action: LoadAction.Value): State = { + checkSBTVersionChanged(s0) val (s1, base) = Project.loadAction(SessionVar.clear(s0), action) IO.createDirectory(base) val s = if (s1 has Keys.stateCompilerCache) s1 else registerCompilerCache(s1) diff --git a/sbt/src/sbt-test/project/sbt-version-change/build.sbt b/sbt/src/sbt-test/project/sbt-version-change/build.sbt new file mode 100644 index 000000000..8bc0b3ddf --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-version-change/build.sbt @@ -0,0 +1,8 @@ +TaskKey[Unit]("checkSbtVersionWarning") := { + val state = Keys.state.value + val logging = state.globalLogging + val currVersion = state.configuration.provider.id.version() + val contents = IO.read(logging.backing.file) + assert(contents.contains(s"""sbt version mismatch, current: $currVersion, in build.properties: "1.1.1", use 'reboot' to use the new value.""")) + () +} \ No newline at end of file diff --git a/sbt/src/sbt-test/project/sbt-version-change/changes/build.properties b/sbt/src/sbt-test/project/sbt-version-change/changes/build.properties new file mode 100644 index 000000000..9f782f704 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-version-change/changes/build.properties @@ -0,0 +1 @@ +sbt.version=1.1.1 \ No newline at end of file diff --git a/sbt/src/sbt-test/project/sbt-version-change/test b/sbt/src/sbt-test/project/sbt-version-change/test new file mode 100644 index 000000000..b46c8dd3c --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-version-change/test @@ -0,0 +1,4 @@ +> help +$ copy-file changes/build.properties project/build.properties +> reload +> checkSbtVersionWarning From f761dab63e31791641e41e7d021c336fc87d6546 Mon Sep 17 00:00:00 2001 From: OlegYch Date: Sun, 29 Oct 2017 16:37:53 +0300 Subject: [PATCH 22/62] Don't fail loading the build if npm is missing or we are on Windows --- build.sbt | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/build.sbt b/build.sbt index 80a62a2ed..c5b2c6cfc 100644 --- a/build.sbt +++ b/build.sbt @@ -446,6 +446,17 @@ lazy val sbtProj = (project in file("sbt")) ) .configure(addSbtCompilerBridge) +def runNpm(command: String, base: File, log: sbt.internal.util.ManagedLogger) = { + val npm = if (sbt.internal.util.Util.isWindows) "npm.cmd" else "npm" + import scala.sys.process._ + try { + val exitCode = Process(s"$npm $command", Option(base)) ! log + if (exitCode != 0) throw new Exception("Process returned exit code: " + exitCode) + } catch { + case e: java.io.IOException => log.warn("failed to run npm " + e.getMessage) + } +} + lazy val vscodePlugin = (project in file("vscode-sbt-scala")) .settings( crossPaths := false, @@ -453,12 +464,8 @@ lazy val vscodePlugin = (project in file("vscode-sbt-scala")) skip in publish := true, compile in Compile := { val u = update.value - val log = streams.value.log - import sbt.internal.inc.Analysis - import scala.sys.process._ - val exitCode = Process(s"npm run compile", Option(baseDirectory.value)) ! log - if (exitCode != 0) throw new Exception("Process returned exit code: " + exitCode) - Analysis.empty + runNpm("run compile", baseDirectory.value, streams.value.log) + sbt.internal.inc.Analysis.empty }, update := { val old = update.value @@ -467,9 +474,7 @@ lazy val vscodePlugin = (project in file("vscode-sbt-scala")) val log = streams.value.log if (t.exists) () else { - import scala.sys.process._ - val exitCode = Process("npm install", Option(base)) ! log - if (exitCode != 0) throw new Exception("Process returned exit code: " + exitCode) + runNpm("install", base, log) IO.touch(t) } old From 8757a4c6acbe3bfa2202ad8947344af9c90659da Mon Sep 17 00:00:00 2001 From: Saniya Tech Date: Tue, 31 Oct 2017 17:09:20 -0400 Subject: [PATCH 23/62] Removes reference to version 0.14.0 from a warning message --- .../src/main/scala/sbt/BasicCommandStrings.scala | 2 +- notes/1.1.0/remove-ref-to-older-vers.md | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 notes/1.1.0/remove-ref-to-older-vers.md diff --git a/main-command/src/main/scala/sbt/BasicCommandStrings.scala b/main-command/src/main/scala/sbt/BasicCommandStrings.scala index d6b8761dc..abc2bc9c1 100644 --- a/main-command/src/main/scala/sbt/BasicCommandStrings.scala +++ b/main-command/src/main/scala/sbt/BasicCommandStrings.scala @@ -194,7 +194,7 @@ $AliasCommand name= deprecatedAlias(ClearOnFailure, BasicCommandStrings.ClearOnFailure) def FailureWallDeprecated = deprecatedAlias(FailureWall, BasicCommandStrings.FailureWall) private[this] def deprecatedAlias(oldName: String, newName: String): String = - s"The `$oldName` command is deprecated in favor of `$newName` and will be removed in 0.14.0" + s"The `$oldName` command is deprecated in favor of `$newName` and will be removed in a later version" } def FailureWall = "resumeFromFailure" diff --git a/notes/1.1.0/remove-ref-to-older-vers.md b/notes/1.1.0/remove-ref-to-older-vers.md new file mode 100644 index 000000000..5b0ecf1b0 --- /dev/null +++ b/notes/1.1.0/remove-ref-to-older-vers.md @@ -0,0 +1,11 @@ +[@saniyatech]: https://github.com/saniyatech + +[#3693]: https://github.com/sbt/sbt/issues/3693 + +### Bug fixes + +- Removes reference to version 0.14.0 from a warning message. [#3693][] by [@saniyatech][] + +### Improvements + +### Fixes with compatibility implications From 8dacb72f9db46d0c2cdf8b404cc0f1fba86267f5 Mon Sep 17 00:00:00 2001 From: Miklos Martin Date: Thu, 26 Oct 2017 16:31:40 +0200 Subject: [PATCH 24/62] Fixes #3297 and #3531 Add commandName as an extension method in Command --- main-command/src/main/scala/sbt/Command.scala | 7 +++++++ sbt/src/sbt-test/actions/command-name/build.sbt | 5 +++++ sbt/src/sbt-test/actions/command-name/test | 1 + 3 files changed, 13 insertions(+) create mode 100644 sbt/src/sbt-test/actions/command-name/build.sbt create mode 100644 sbt/src/sbt-test/actions/command-name/test diff --git a/main-command/src/main/scala/sbt/Command.scala b/main-command/src/main/scala/sbt/Command.scala index f90aedc8e..05798ff55 100644 --- a/main-command/src/main/scala/sbt/Command.scala +++ b/main-command/src/main/scala/sbt/Command.scala @@ -187,6 +187,13 @@ object Command { def spacedC(name: String, c: Parser[Char]): Parser[String] = ((c & opOrIDSpaced(name)) ~ c.+) map { case (f, rem) => (f +: rem).mkString } + + implicit class CommandWithName(cmd: Command) { + def commandName: Option[String] = cmd match { + case sc: SimpleCommand => Some(sc.name) + case _ => None + } + } } trait Help { diff --git a/sbt/src/sbt-test/actions/command-name/build.sbt b/sbt/src/sbt-test/actions/command-name/build.sbt new file mode 100644 index 000000000..1a04a2dcc --- /dev/null +++ b/sbt/src/sbt-test/actions/command-name/build.sbt @@ -0,0 +1,5 @@ +val command = Command.command("noop") { s => s } + +TaskKey[Unit]("check") := { + assert(command.commandName == Some("noop"), """command.commandName should be "noop"""") +} diff --git a/sbt/src/sbt-test/actions/command-name/test b/sbt/src/sbt-test/actions/command-name/test new file mode 100644 index 000000000..a5912a391 --- /dev/null +++ b/sbt/src/sbt-test/actions/command-name/test @@ -0,0 +1 @@ +> check \ No newline at end of file From 79e75f02e5e9a42d33f226034068f48f1b5df074 Mon Sep 17 00:00:00 2001 From: Miklos Martin Date: Thu, 26 Oct 2017 16:32:54 +0200 Subject: [PATCH 25/62] add newline --- sbt/src/sbt-test/actions/command-name/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbt/src/sbt-test/actions/command-name/test b/sbt/src/sbt-test/actions/command-name/test index a5912a391..15675b169 100644 --- a/sbt/src/sbt-test/actions/command-name/test +++ b/sbt/src/sbt-test/actions/command-name/test @@ -1 +1 @@ -> check \ No newline at end of file +> check From 951c78a4d774a8f474df04553d83dd49dc88691f Mon Sep 17 00:00:00 2001 From: Miklos Martin Date: Fri, 27 Oct 2017 20:10:30 +0200 Subject: [PATCH 26/62] better naming + value class --- main-command/src/main/scala/sbt/Command.scala | 4 ++-- sbt/src/sbt-test/actions/command-name/build.sbt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/main-command/src/main/scala/sbt/Command.scala b/main-command/src/main/scala/sbt/Command.scala index 05798ff55..48324aaf5 100644 --- a/main-command/src/main/scala/sbt/Command.scala +++ b/main-command/src/main/scala/sbt/Command.scala @@ -188,8 +188,8 @@ object Command { def spacedC(name: String, c: Parser[Char]): Parser[String] = ((c & opOrIDSpaced(name)) ~ c.+) map { case (f, rem) => (f +: rem).mkString } - implicit class CommandWithName(cmd: Command) { - def commandName: Option[String] = cmd match { + implicit class CommandWithName(val cmd: Command) extends AnyVal { + def nameOption: Option[String] = cmd match { case sc: SimpleCommand => Some(sc.name) case _ => None } diff --git a/sbt/src/sbt-test/actions/command-name/build.sbt b/sbt/src/sbt-test/actions/command-name/build.sbt index 1a04a2dcc..374a7d3cd 100644 --- a/sbt/src/sbt-test/actions/command-name/build.sbt +++ b/sbt/src/sbt-test/actions/command-name/build.sbt @@ -1,5 +1,5 @@ val command = Command.command("noop") { s => s } TaskKey[Unit]("check") := { - assert(command.commandName == Some("noop"), """command.commandName should be "noop"""") + assert(command.nameOption == Some("noop"), """command.commandName should be "noop"""") } From 1f89e3dc021d65f6b2e6f084b827567a71da01db Mon Sep 17 00:00:00 2001 From: Miklos Martin Date: Thu, 2 Nov 2017 16:56:14 +0100 Subject: [PATCH 27/62] move nameOption to the trait --- main-command/src/main/scala/sbt/Command.scala | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/main-command/src/main/scala/sbt/Command.scala b/main-command/src/main/scala/sbt/Command.scala index 48324aaf5..53158daa4 100644 --- a/main-command/src/main/scala/sbt/Command.scala +++ b/main-command/src/main/scala/sbt/Command.scala @@ -25,6 +25,11 @@ sealed trait Command { def tags: AttributeMap def tag[T](key: AttributeKey[T], value: T): Command + + def nameOption: Option[String] = this match { + case sc: SimpleCommand => Some(sc.name) + case _ => None + } } private[sbt] final class SimpleCommand( @@ -187,13 +192,6 @@ object Command { def spacedC(name: String, c: Parser[Char]): Parser[String] = ((c & opOrIDSpaced(name)) ~ c.+) map { case (f, rem) => (f +: rem).mkString } - - implicit class CommandWithName(val cmd: Command) extends AnyVal { - def nameOption: Option[String] = cmd match { - case sc: SimpleCommand => Some(sc.name) - case _ => None - } - } } trait Help { From 1ca266d0f859770d3cae57a9333aeb1b40358ce5 Mon Sep 17 00:00:00 2001 From: Antonio Cunei Date: Mon, 6 Nov 2017 15:21:50 +0100 Subject: [PATCH 28/62] Do not always use Level.Debug while creating a backgroundLog The creation of a backgroundLog was always using Debug as the logging level for console and backing. This commit sets the levels to those used by the caller. Fixes #3655 --- .../main/scala/sbt/internal/LogManager.scala | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/main/src/main/scala/sbt/internal/LogManager.scala b/main/src/main/scala/sbt/internal/LogManager.scala index b29f745c5..58003f902 100644 --- a/main/src/main/scala/sbt/internal/LogManager.scala +++ b/main/src/main/scala/sbt/internal/LogManager.scala @@ -105,6 +105,14 @@ object LogManager { } } + // to change from global being the default to overriding, switch the order of state.get and data.get + def getOr[T](key: AttributeKey[T], + data: Settings[Scope], + scope: Scope, + state: State, + default: T): T = + data.get(scope, key) orElse state.get(key) getOrElse default + // This is the main function that is used to generate the logger for tasks. def defaultLogger( data: Settings[Scope], @@ -121,13 +129,10 @@ object LogManager { val execId: Option[String] = execOpt flatMap { _.execId } val log = LogExchange.logger(loggerName, channelName, execId) val scope = task.scope - // to change from global being the default to overriding, switch the order of state.get and data.get - def getOr[T](key: AttributeKey[T], default: T): T = - data.get(scope, key) orElse state.get(key) getOrElse default - val screenLevel = getOr(logLevel.key, Level.Info) - val backingLevel = getOr(persistLogLevel.key, Level.Debug) - val screenTrace = getOr(traceLevel.key, defaultTraceLevel(state)) - val backingTrace = getOr(persistTraceLevel.key, Int.MaxValue) + val screenLevel = getOr(logLevel.key, data, scope, state, Level.Info) + val backingLevel = getOr(persistLogLevel.key, data, scope, state, Level.Debug) + val screenTrace = getOr(traceLevel.key, data, scope, state, defaultTraceLevel(state)) + val backingTrace = getOr(persistTraceLevel.key, data, scope, state, Int.MaxValue) val extraBacked = state.globalLogging.backed :: relay :: Nil val consoleOpt = consoleLocally(state, console) val config = MainAppender.MainAppenderConfig( @@ -184,6 +189,9 @@ object LogManager { relay: Appender, extra: List[Appender] ): ManagedLogger = { + val scope = task.scope + val screenLevel = getOr(logLevel.key, data, scope, state, Level.Info) + val backingLevel = getOr(persistLogLevel.key, data, scope, state, Level.Debug) val execOpt = state.currentCommand val loggerName: String = s"bg-${task.key.label}-${generateId.incrementAndGet}" val channelName: Option[String] = execOpt flatMap (_.source map (_.channelName)) @@ -193,7 +201,7 @@ object LogManager { val consoleOpt = consoleLocally(state, console) LogExchange.bindLoggerAppenders( loggerName, - (consoleOpt.toList map { _ -> Level.Debug }) ::: (relay -> Level.Debug) :: Nil) + (consoleOpt.toList map { _ -> screenLevel }) ::: (relay -> backingLevel) :: Nil) log } From afe9757da0dd5e4e7f3f9fa87dab8db828a8e9eb Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Tue, 7 Nov 2017 22:54:05 -0500 Subject: [PATCH 29/62] Bump launcher to 1.0.2 Fixes #3676 --- project/Dependencies.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 819094a3e..22720347d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -6,8 +6,8 @@ object Dependencies { val scala282 = "2.8.2" val scala292 = "2.9.2" val scala293 = "2.9.3" - val scala210 = "2.10.6" - val scala211 = "2.11.8" + val scala210 = "2.10.7" + val scala211 = "2.11.12" val scala212 = "2.12.4" val baseScalaVersion = scala212 @@ -30,8 +30,8 @@ object Dependencies { private val libraryManagementCore = "org.scala-sbt" %% "librarymanagement-core" % lmVersion private val libraryManagementIvy = "org.scala-sbt" %% "librarymanagement-ivy" % lmVersion - val launcherInterface = "org.scala-sbt" % "launcher-interface" % "1.0.0" - val rawLauncher = "org.scala-sbt" % "launcher" % "1.0.0" + val launcherInterface = "org.scala-sbt" % "launcher-interface" % "1.0.2" + val rawLauncher = "org.scala-sbt" % "launcher" % "1.0.2" val testInterface = "org.scala-sbt" % "test-interface" % "1.0" private val compilerInterface = "org.scala-sbt" % "compiler-interface" % zincVersion From 6e29a1aa15232dcd74d3e8e9d875da2d39cc35cf Mon Sep 17 00:00:00 2001 From: OlegYch Date: Fri, 27 Oct 2017 19:29:19 +0300 Subject: [PATCH 30/62] Improve testQuick performance, restoring caching of stamps --- main/src/main/scala/sbt/Defaults.scala | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 6405a2886..b1fa81fab 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -766,21 +766,24 @@ object Defaults extends BuildCommon { } def intlStamp(c: String, analysis: Analysis, s: Set[String]): Long = { if (s contains c) Long.MinValue - else { - val x = { - import analysis.{ relations => rel, apis } - rel.internalClassDeps(c).map(intlStamp(_, analysis, s + c)) ++ - rel.externalDeps(c).map(stamp) + - (apis.internal.get(c) match { - case Some(x) => x.compilationTimestamp - case _ => Long.MinValue - }) - }.max - if (x != Long.MinValue) { - stamps(c) = x - } - x - } + else + stamps.getOrElse( + c, { + val x = { + import analysis.{ relations => rel, apis } + rel.internalClassDeps(c).map(intlStamp(_, analysis, s + c)) ++ + rel.externalDeps(c).map(stamp) + + (apis.internal.get(c) match { + case Some(x) => x.compilationTimestamp + case _ => Long.MinValue + }) + }.max + if (x != Long.MinValue) { + stamps(c) = x + } + x + } + ) } def noSuccessYet(test: String) = succeeded.get(test) match { case None => true From 6b6047b55ca129b0a5419fc853e62417bcac012f Mon Sep 17 00:00:00 2001 From: Alexey Alekhin Date: Tue, 14 Nov 2017 21:53:15 +0100 Subject: [PATCH 31/62] Publish ExecStatusEvent taking into account its channel name --- .../scala/sbt/internal/CommandExchange.scala | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 0751576eb..5d63c3329 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -26,7 +26,7 @@ import sbt.internal.server._ import sbt.internal.langserver.{ LogMessageParams, MessageType } import sbt.internal.util.{ StringEvent, ObjectEvent, MainAppender } import sbt.internal.util.codec.JValueFormats -import sbt.protocol.{ EventMessage, Serialization } +import sbt.protocol.{ EventMessage, ExecStatusEvent } import sbt.util.{ Level, Logger, LogExchange } /** @@ -170,10 +170,11 @@ private[sbt] final class CommandExchange { def publishEvent[A: JsonFormat](event: A): Unit = { val broadcastStringMessage = true val toDel: ListBuffer[CommandChannel] = ListBuffer.empty + event match { case entry: StringEvent => val params = toLogMessageParams(entry) - channels.foreach { + channels collect { case c: ConsoleChannel => if (broadcastStringMessage) { c.publishEvent(event) @@ -201,7 +202,7 @@ private[sbt] final class CommandExchange { } } case _ => - channels.foreach { + channels collect { case c: ConsoleChannel => c.publishEvent(event) case c: NetworkChannel => @@ -243,7 +244,7 @@ private[sbt] final class CommandExchange { JField("execId", JString(execId)) })): _* ) - channels.foreach { + channels collect { case c: ConsoleChannel => c.publishEvent(json) case c: NetworkChannel => @@ -268,16 +269,32 @@ private[sbt] final class CommandExchange { val toDel: ListBuffer[CommandChannel] = ListBuffer.empty event match { // Special treatment for ConsolePromptEvent since it's hand coded without codec. - case e: ConsolePromptEvent => + case entry: ConsolePromptEvent => channels collect { - case c: ConsoleChannel => c.publishEventMessage(e) + case c: ConsoleChannel => c.publishEventMessage(entry) } - case e: ConsoleUnpromptEvent => + case entry: ConsoleUnpromptEvent => channels collect { - case c: ConsoleChannel => c.publishEventMessage(e) + case c: ConsoleChannel => c.publishEventMessage(entry) + } + case entry: ExecStatusEvent => + channels collect { + case c: ConsoleChannel => + if (entry.channelName.isEmpty || entry.channelName == Some(c.name)) { + c.publishEventMessage(event) + } + case c: NetworkChannel => + try { + if (entry.channelName == Some(c.name)) { + c.publishEventMessage(event) + } + } catch { + case e: SocketException => + toDel += c + } } case _ => - channels.foreach { + channels collect { case c: ConsoleChannel => c.publishEventMessage(event) case c: NetworkChannel => From 0c0e0ce912471026bb1073d6149da1b73dbf7fdc Mon Sep 17 00:00:00 2001 From: Alexey Alekhin Date: Tue, 14 Nov 2017 21:53:29 +0100 Subject: [PATCH 32/62] Log StringEvents and MessageEvents to the LSP client --- .../server/LanguageServerProtocol.scala | 8 ++++++ .../sbt/internal/server/NetworkChannel.scala | 25 +++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index 4fb90f3a9..776078ae6 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -138,6 +138,14 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { publishBytes(bytes) } + def logMessage(level: String, message: String): Unit = { + import sbt.internal.langserver.codec.JsonProtocol._ + langNotify( + "window/logMessage", + LogMessageParams(MessageType.fromLevelString(level), message) + ) + } + private[sbt] lazy val serverCapabilities: ServerCapabilities = { ServerCapabilities(textDocumentSync = TextDocumentSyncOptions(true, 0, false, false, SaveOptions(false)), diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index cee95034f..26b489347 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -16,7 +16,7 @@ import sjsonnew._ import scala.annotation.tailrec import sbt.protocol._ import sbt.internal.langserver.ErrorCodes -import sbt.internal.util.ObjectEvent +import sbt.internal.util.{ ObjectEvent, StringEvent } import sbt.internal.util.codec.JValueFormats import sbt.util.Logger @@ -227,7 +227,10 @@ final class NetworkChannel(val name: String, def publishEvent[A: JsonFormat](event: A, execId: Option[String]): Unit = { if (isLanguageServerProtocol) { - langRespond(event, execId) + event match { + case entry: StringEvent => logMessage(entry.level, entry.message) + case _ => langRespond(event, execId) + } } else { contentType match { case SbtX1Protocol => @@ -241,11 +244,19 @@ final class NetworkChannel(val name: String, def publishEvent[A: JsonFormat](event: A): Unit = publishEvent(event, None) def publishEventMessage(event: EventMessage): Unit = { - contentType match { - case SbtX1Protocol => - val bytes = Serialization.serializeEventMessage(event) - publishBytes(bytes, true) - case _ => + if (isLanguageServerProtocol) { + event match { + case entry: LogEvent => logMessage(entry.level, entry.message) + case entry: ExecStatusEvent => logMessage("debug", entry.status) + case _ => () + } + } else { + contentType match { + case SbtX1Protocol => + val bytes = Serialization.serializeEventMessage(event) + publishBytes(bytes, true) + case _ => () + } } } From 8fe1934ea0c8138d5e751a913126a5c081c3eea9 Mon Sep 17 00:00:00 2001 From: Alexey Alekhin Date: Tue, 14 Nov 2017 21:53:57 +0100 Subject: [PATCH 33/62] Tweaked MessageType.fromLevelString conversion --- .../src/main/scala/sbt/internal/langserver/MessageType.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/src/main/scala/sbt/internal/langserver/MessageType.scala b/protocol/src/main/scala/sbt/internal/langserver/MessageType.scala index 26a5e4368..2d0aff8f1 100644 --- a/protocol/src/main/scala/sbt/internal/langserver/MessageType.scala +++ b/protocol/src/main/scala/sbt/internal/langserver/MessageType.scala @@ -25,10 +25,10 @@ object MessageType { def fromLevelString(level: String): Long = { level.toLowerCase match { - case "debug" => Log case "info" => Info case "warn" => Warning case "error" => Error + case _ => Log } } } From a050b73a46006343ac90f21d9ec0ce0bbe902558 Mon Sep 17 00:00:00 2001 From: Antonio Cunei Date: Tue, 14 Nov 2017 18:50:36 +0100 Subject: [PATCH 34/62] Workaround for scala/bug#10605 templateStats() is not thread-safe in 2.12.x (at least up to 2.12.4) --- main/src/main/scala/sbt/internal/parser/SbtParser.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/internal/parser/SbtParser.scala b/main/src/main/scala/sbt/internal/parser/SbtParser.scala index 0e87b2a87..b54ac503c 100644 --- a/main/src/main/scala/sbt/internal/parser/SbtParser.scala +++ b/main/src/main/scala/sbt/internal/parser/SbtParser.scala @@ -142,7 +142,9 @@ private[sbt] object SbtParser { val wrapperFile = new BatchSourceFile(reporterId, code) val unit = new CompilationUnit(wrapperFile) val parser = new syntaxAnalyzer.UnitParser(unit) - val parsedTrees = parser.templateStats() + val parsedTrees = SbtParser.synchronized { // see https://github.com/scala/bug/issues/10605 + parser.templateStats() + } parser.accept(scala.tools.nsc.ast.parser.Tokens.EOF) globalReporter.throwParserErrorsIfAny(reporter, filePath) parsedTrees -> reporterId From 43a976d42cfdd8321d2d5d29b078bf5f412fd16c Mon Sep 17 00:00:00 2001 From: Antonio Cunei Date: Thu, 16 Nov 2017 15:09:25 +0100 Subject: [PATCH 35/62] Port of sbt/zinc#441 to sbt/sbt 1.0.x --- project/Scripted.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/Scripted.scala b/project/Scripted.scala index 33e23936e..932d55a0f 100644 --- a/project/Scripted.scala +++ b/project/Scripted.scala @@ -108,6 +108,8 @@ object Scripted { prescripted: File => Unit, launchOpts: Seq[String]): Unit = { System.err.println(s"About to run tests: ${args.mkString("\n * ", "\n * ", "\n")}") + // Force Log4J to not use a thread context classloader otherwise it throws a CCE + sys.props(org.apache.logging.log4j.util.LoaderUtil.IGNORE_TCCL_PROPERTY) = "true" val noJLine = new classpath.FilteredLoader(scriptedSbtInstance.loader, "jline." :: Nil) val loader = classpath.ClasspathUtilities.toLoader(scriptedSbtClasspath.files, noJLine) val bridgeClass = Class.forName("sbt.test.ScriptedRunner", true, loader) From 238540d34a28b633570de9ae35a256ccee160211 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Tue, 21 Nov 2017 10:46:57 +0100 Subject: [PATCH 36/62] Remove 2 useless files in protocol They have been moved to Contraband since, but never removed. --- .../InitializeResultFormats.scala | 26 ------------------- .../ServerCapabilitiesFormats.scala | 26 ------------------- 2 files changed, 52 deletions(-) delete mode 100644 protocol/src/main/contraband-scala/InitializeResultFormats.scala delete mode 100644 protocol/src/main/contraband-scala/ServerCapabilitiesFormats.scala diff --git a/protocol/src/main/contraband-scala/InitializeResultFormats.scala b/protocol/src/main/contraband-scala/InitializeResultFormats.scala deleted file mode 100644 index 33827daee..000000000 --- a/protocol/src/main/contraband-scala/InitializeResultFormats.scala +++ /dev/null @@ -1,26 +0,0 @@ -/** - * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. - */ - -// DO NOT EDIT MANUALLY -import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } -trait InitializeResultFormats { self: ServerCapabilitiesFormats with sjsonnew.BasicJsonProtocol => -implicit lazy val InitializeResultFormat: JsonFormat[sbt.internal.langserver.InitializeResult] = new JsonFormat[sbt.internal.langserver.InitializeResult] { - override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.InitializeResult = { - jsOpt match { - case Some(js) => - unbuilder.beginObject(js) - val capabilities = unbuilder.readField[Option[sbt.internal.langserver.ServerCapabilities]]("capabilities") - unbuilder.endObject() - sbt.internal.langserver.InitializeResult(capabilities) - case None => - deserializationError("Expected JsObject but found None") - } - } - override def write[J](obj: sbt.internal.langserver.InitializeResult, builder: Builder[J]): Unit = { - builder.beginObject() - builder.addField("capabilities", obj.capabilities) - builder.endObject() - } -} -} diff --git a/protocol/src/main/contraband-scala/ServerCapabilitiesFormats.scala b/protocol/src/main/contraband-scala/ServerCapabilitiesFormats.scala deleted file mode 100644 index 3dbef6ccc..000000000 --- a/protocol/src/main/contraband-scala/ServerCapabilitiesFormats.scala +++ /dev/null @@ -1,26 +0,0 @@ -/** - * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. - */ - -// DO NOT EDIT MANUALLY -import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } -trait ServerCapabilitiesFormats { self: sjsonnew.BasicJsonProtocol => -implicit lazy val ServerCapabilitiesFormat: JsonFormat[sbt.internal.langserver.ServerCapabilities] = new JsonFormat[sbt.internal.langserver.ServerCapabilities] { - override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.ServerCapabilities = { - jsOpt match { - case Some(js) => - unbuilder.beginObject(js) - val hoverProvider = unbuilder.readField[Option[Boolean]]("hoverProvider") - unbuilder.endObject() - sbt.internal.langserver.ServerCapabilities(hoverProvider) - case None => - deserializationError("Expected JsObject but found None") - } - } - override def write[J](obj: sbt.internal.langserver.ServerCapabilities, builder: Builder[J]): Unit = { - builder.beginObject() - builder.addField("hoverProvider", obj.hoverProvider) - builder.endObject() - } -} -} From 8ee413778adba8504646af44ed5a8e8b90e7589e Mon Sep 17 00:00:00 2001 From: xuwei-k <6b656e6a69@gmail.com> Date: Thu, 23 Nov 2017 14:56:52 +0900 Subject: [PATCH 37/62] fix #3591. "Not a valid key: console-quick" --- main/src/main/scala/sbt/internal/IvyConsole.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/internal/IvyConsole.scala b/main/src/main/scala/sbt/internal/IvyConsole.scala index 79f6344bc..9cce3a74f 100644 --- a/main/src/main/scala/sbt/internal/IvyConsole.scala +++ b/main/src/main/scala/sbt/internal/IvyConsole.scala @@ -56,7 +56,7 @@ object IvyConsole { depSettings) val newStructure = Load.reapply(session.original ++ append, structure) - val newState = state.copy(remainingCommands = Exec("console-quick", None) :: Nil) + val newState = state.copy(remainingCommands = Exec(Keys.consoleQuick.key.label, None) :: Nil) Project.setProject(session, newStructure, newState) } From b8086e076845114d3a785382618cc408daf05724 Mon Sep 17 00:00:00 2001 From: xuwei-k <6b656e6a69@gmail.com> Date: Thu, 23 Nov 2017 15:20:15 +0900 Subject: [PATCH 38/62] make strict some methods in Completions --- .../internal/util/complete/Completions.scala | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/complete/Completions.scala b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Completions.scala index fd3df80c1..8596b96fa 100644 --- a/internal/util-complete/src/main/scala/sbt/internal/util/complete/Completions.scala +++ b/internal/util-complete/src/main/scala/sbt/internal/util/complete/Completions.scala @@ -151,16 +151,23 @@ object Completion { val empty: Completion = suggestion("") def single(c: Char): Completion = suggestion(c.toString) - // TODO: make strict in 0.13.0 to match DisplayOnly - def displayOnly(value: => String): Completion = new DisplayOnly(value) + def displayOnly(value: String): Completion = new DisplayOnly(value) - // TODO: make strict in 0.13.0 to match Token - def token(prepend: => String, append: => String): Completion = + def token(prepend: String, append: String): Completion = new Token(prepend + append, append) /** @since 0.12.1 */ def tokenDisplay(append: String, display: String): Completion = new Token(display, append) - // TODO: make strict in 0.13.0 to match Suggestion - def suggestion(value: => String): Completion = new Suggestion(value) + def suggestion(value: String): Completion = new Suggestion(value) + + @deprecated("No longer used. for binary compatibility", "1.1.0") + private[complete] def displayOnly(value: => String): Completion = new DisplayOnly(value) + + @deprecated("No longer used. for binary compatibility", "1.1.0") + private[complete] def token(prepend: => String, append: => String): Completion = + new Token(prepend + append, append) + + @deprecated("No longer used. for binary compatibility", "1.1.0") + private[complete] def suggestion(value: => String): Completion = new Suggestion(value) } From 309fdfac315d808046cab6306f7ebac879e96d6a Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 25 Nov 2017 00:27:45 -0500 Subject: [PATCH 39/62] Bump modules --- project/Dependencies.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 22720347d..e61a1a213 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -13,9 +13,9 @@ object Dependencies { // sbt modules private val ioVersion = "1.0.2" - private val utilVersion = "1.0.2" - private val lmVersion = "1.0.3" - private val zincVersion = "1.0.3" + private val utilVersion = "1.0.3" + private val lmVersion = "1.0.4" + private val zincVersion = "1.0.4" private val sbtIO = "org.scala-sbt" %% "io" % ioVersion From f44dcef14d025357d3fb589dd7d9fb2457005ecc Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 25 Nov 2017 05:26:51 -0500 Subject: [PATCH 40/62] notes --- notes/1.0.4.markdown | 51 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 notes/1.0.4.markdown diff --git a/notes/1.0.4.markdown b/notes/1.0.4.markdown new file mode 100644 index 000000000..280abf7e0 --- /dev/null +++ b/notes/1.0.4.markdown @@ -0,0 +1,51 @@ +This is a hotfix release for sbt 1.0.x series. + +### Bug fixes + +- Fixes undercompilation of value classes when the underlying type changes. [zinc#444][zinc444] by [@smarter][@smarter] +- Fixes `ArrayIndexOutOfBoundsException` on Ivy when running on Java 9. [ivy#27][ivy27] by [@xuwei-k][@xuwei-k] +- Fixes Java 9 warning by upgrading to launcher 1.0.2. [ivy#26][ivy26]/[launcher#45][launcher45] by [@dwijnand][@dwijnand] +- Fixes `run` outputing debug level logs. [#3655][3655]/[#3717][3717] by [@cunei][@cunei] +- Fixes performance regression caused by classpath hashing. [zinc#452][zinc452] by [@jvican][@jvican] +- Fixes performance regression of `testQuick`. [#3680][3680]/[#3720][3720] by [@OlegYch][@OlegYch] +- Disables Ivy log4j caller location calculation for performance regression reported in [#3711][3711]. [util#132][util132] by [@leonardehrenfried][@leonardehrenfried] +- Works around Scala compiler's `templateStats()` not being thread-safe. [#3743][3743] by [@cunei][@cunei] +- Fixes "Attempting to overwrite" error message. [lm#174][lm174] by [@dwijnand][@dwijnand] +- Fixes incorrect eviction warning message. [lm#179][lm179] by [@xuwei-k][@xuwei-k] +- Registers Ivy protocol only for `http:` and `https:` to be more plugin friendly. [lm183][lm183] by [@tpunder][@tpunder] + +### Enhancement + +- Adds Scala 2.13.0-M2 support. [zinc#453][zinc453] by [@eed3si9n][@eed3si9n] and [@jan0sch][@jan0sch] + +### Internal + +- Improves Zinc scripted testing. [zinc440][zinc#440] by [@jvican][@jvican] + + [@dwijnand]: https://github.com/dwijnand + [@cunei]: https://github.com/cunei + [@eed3si9n]: https://github.com/eed3si9n + [@jvican]: https://github.com/jvican + [@OlegYch]: https://github.com/OlegYch + [@leonardehrenfried]: https://github.com/leonardehrenfried + [@xuwei-k]: https://github.com/xuwei-k + [@tpunder]: https://github.com/tpunder + [@smarter]: https://github.com/smarter + [@jan0sch]: https://github.com/jan0sch + [3655]: https://github.com/sbt/sbt/issues/3655 + [3717]: https://github.com/sbt/sbt/pull/3717 + [ivy26]: https://github.com/sbt/ivy/pull/26 + [ivy27]: https://github.com/sbt/ivy/pull/27 + [launcher45]: https://github.com/sbt/launcher/pull/45 + [3680]: https://github.com/sbt/sbt/issues/3680 + [3720]: https://github.com/sbt/sbt/pull/3720 + [3743]: https://github.com/sbt/sbt/pull/3743 + [3711]: https://github.com/sbt/sbt/issues/3711 + [util132]: https://github.com/sbt/util/pull/132 + [lm174]: https://github.com/sbt/librarymanagement/pull/174 + [lm179]: https://github.com/sbt/librarymanagement/pull/179 + [lm183]: https://github.com/sbt/librarymanagement/pull/183 + [zinc452]: https://github.com/sbt/zinc/pull/452 + [zinc444]: https://github.com/sbt/zinc/pull/444 + [zinc453]: https://github.com/sbt/zinc/pull/453 + [zinc440]: https://github.com/sbt/zinc/pull/440 From 6b4093dc15cb2fa303a93499ab679fbd72bb80a9 Mon Sep 17 00:00:00 2001 From: xuwei-k <6b656e6a69@gmail.com> Date: Sun, 26 Nov 2017 13:59:50 +0900 Subject: [PATCH 41/62] update specs2 4.0.1 --- .../scala/sbt/internal/parser/SplitExpressionsBehavior.scala | 2 +- .../src/test/scala/sbt/internal/server/SettingQueryTest.scala | 4 ++-- project/Dependencies.scala | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/main/src/test/scala/sbt/internal/parser/SplitExpressionsBehavior.scala b/main/src/test/scala/sbt/internal/parser/SplitExpressionsBehavior.scala index 17167e024..bda88bc0d 100644 --- a/main/src/test/scala/sbt/internal/parser/SplitExpressionsBehavior.scala +++ b/main/src/test/scala/sbt/internal/parser/SplitExpressionsBehavior.scala @@ -20,7 +20,7 @@ trait SplitExpression { trait SplitExpressionsBehavior extends SplitExpression { this: SpecificationLike => - def newExpressionsSplitter(implicit splitter: SplitExpressions.SplitExpression): Unit = { + def newExpressionsSplitter(implicit splitter: SplitExpressions.SplitExpression) = { "parse a two settings without intervening blank line" in { val (imports, settings) = split("""version := "1.0" diff --git a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala index 27c7e7238..7acf7955f 100644 --- a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala +++ b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala @@ -51,7 +51,7 @@ object SettingQueryTest extends org.specs2.mutable.Specification { def apply[T](lockFile: File, run: Callable[T]) = run.call() } - lazy val structure: BuildStructure = { + lazy val buildStructure: BuildStructure = { val projectSettings: Seq[Setting[_]] = Seq(scalaVersion := "2.12.1") val appConfig: AppConfiguration = new AppConfiguration { @@ -180,7 +180,7 @@ object SettingQueryTest extends org.specs2.mutable.Specification { def query(setting: String): String = { import sbt.protocol._ val req: SettingQuery = sbt.protocol.SettingQuery(setting) - val rsp: SettingQueryResponse = server.SettingQuery.handleSettingQuery(req, structure) + val rsp: SettingQueryResponse = server.SettingQuery.handleSettingQuery(req, buildStructure) val bytes: Array[Byte] = Serialization serializeEventMessage rsp val payload: String = new String(bytes, java.nio.charset.StandardCharsets.UTF_8) payload diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f714cbc82..61bc30342 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -103,7 +103,7 @@ object Dependencies { val jline = "jline" % "jline" % "2.14.4" val scalatest = "org.scalatest" %% "scalatest" % "3.0.1" val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.13.4" - val specs2 = "org.specs2" %% "specs2" % "2.4.17" + val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1" val junit = "junit" % "junit" % "4.11" val templateResolverApi = "org.scala-sbt" % "template-resolver" % "0.1" From 1d8d1a16222856b9dd7716659394b5886610e68f Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 26 Nov 2017 02:26:04 -0500 Subject: [PATCH 42/62] Zinc 1.0.5 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index e61a1a213..f850fc073 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -15,7 +15,7 @@ object Dependencies { private val ioVersion = "1.0.2" private val utilVersion = "1.0.3" private val lmVersion = "1.0.4" - private val zincVersion = "1.0.4" + private val zincVersion = "1.0.5" private val sbtIO = "org.scala-sbt" %% "io" % ioVersion From 5b0b9e2ba254abafd9f31b22c36251315d616eb4 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 21 Nov 2017 14:43:43 +0000 Subject: [PATCH 43/62] Add node_modules to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9dfa96975..b19d061a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target/ __pycache__ toolbox.classpath +node_modules/ From 0c803214aa2d99a6f71c1d300b6ced69e3ba1f28 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 27 Nov 2017 21:36:25 -0500 Subject: [PATCH 44/62] IPC Unix Domain Socket and Windows Named Pipe sockets The Java socket implementation for IPC is lifted from facebook/nailgun. https://github.com/facebook/nailgun/tree/af623fddedfdca010df46302a0711ce0e2cc1ba6/ --- .../internal/NGUnixDomainServerSocket.java | 178 ++++++++++++++++++ .../java/sbt/internal/NGUnixDomainSocket.java | 171 +++++++++++++++++ .../internal/NGUnixDomainSocketLibrary.java | 140 ++++++++++++++ .../sbt/internal/NGWin32NamedPipeLibrary.java | 90 +++++++++ .../NGWin32NamedPipeServerSocket.java | 173 +++++++++++++++++ .../sbt/internal/NGWin32NamedPipeSocket.java | 173 +++++++++++++++++ .../ReferenceCountedFileDescriptor.java | 82 ++++++++ 7 files changed, 1007 insertions(+) create mode 100644 main-command/src/main/java/sbt/internal/NGUnixDomainServerSocket.java create mode 100644 main-command/src/main/java/sbt/internal/NGUnixDomainSocket.java create mode 100644 main-command/src/main/java/sbt/internal/NGUnixDomainSocketLibrary.java create mode 100644 main-command/src/main/java/sbt/internal/NGWin32NamedPipeLibrary.java create mode 100644 main-command/src/main/java/sbt/internal/NGWin32NamedPipeServerSocket.java create mode 100644 main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java create mode 100644 main-command/src/main/java/sbt/internal/ReferenceCountedFileDescriptor.java diff --git a/main-command/src/main/java/sbt/internal/NGUnixDomainServerSocket.java b/main-command/src/main/java/sbt/internal/NGUnixDomainServerSocket.java new file mode 100644 index 000000000..89d3bcf43 --- /dev/null +++ b/main-command/src/main/java/sbt/internal/NGUnixDomainServerSocket.java @@ -0,0 +1,178 @@ +// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGUnixDomainServerSocket.java + +/* + + Copyright 2004-2015, Martian Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package sbt.internal; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.concurrent.atomic.AtomicInteger; + +import com.sun.jna.LastErrorException; +import com.sun.jna.ptr.IntByReference; + +/** + * Implements a {@link ServerSocket} which binds to a local Unix domain socket + * and returns instances of {@link NGUnixDomainSocket} from + * {@link #accept()}. + */ +public class NGUnixDomainServerSocket extends ServerSocket { + private static final int DEFAULT_BACKLOG = 50; + + // We use an AtomicInteger to prevent a race in this situation which + // could happen if fd were just an int: + // + // Thread 1 -> NGUnixDomainServerSocket.accept() + // -> lock this + // -> check isBound and isClosed + // -> unlock this + // -> descheduled while still in method + // Thread 2 -> NGUnixDomainServerSocket.close() + // -> lock this + // -> check isClosed + // -> NGUnixDomainSocketLibrary.close(fd) + // -> now fd is invalid + // -> unlock this + // Thread 1 -> re-scheduled while still in method + // -> NGUnixDomainSocketLibrary.accept(fd, which is invalid and maybe re-used) + // + // By using an AtomicInteger, we'll set this to -1 after it's closed, which + // will cause the accept() call above to cleanly fail instead of possibly + // being called on an unrelated fd (which may or may not fail). + private final AtomicInteger fd; + + private final int backlog; + private boolean isBound; + private boolean isClosed; + + public static class NGUnixDomainServerSocketAddress extends SocketAddress { + private final String path; + + public NGUnixDomainServerSocketAddress(String path) { + this.path = path; + } + + public String getPath() { + return path; + } + } + + /** + * Constructs an unbound Unix domain server socket. + */ + public NGUnixDomainServerSocket() throws IOException { + this(DEFAULT_BACKLOG, null); + } + + /** + * Constructs an unbound Unix domain server socket with the specified listen backlog. + */ + public NGUnixDomainServerSocket(int backlog) throws IOException { + this(backlog, null); + } + + /** + * Constructs and binds a Unix domain server socket to the specified path. + */ + public NGUnixDomainServerSocket(String path) throws IOException { + this(DEFAULT_BACKLOG, path); + } + + /** + * Constructs and binds a Unix domain server socket to the specified path + * with the specified listen backlog. + */ + public NGUnixDomainServerSocket(int backlog, String path) throws IOException { + try { + fd = new AtomicInteger( + NGUnixDomainSocketLibrary.socket( + NGUnixDomainSocketLibrary.PF_LOCAL, + NGUnixDomainSocketLibrary.SOCK_STREAM, + 0)); + this.backlog = backlog; + if (path != null) { + bind(new NGUnixDomainServerSocketAddress(path)); + } + } catch (LastErrorException e) { + throw new IOException(e); + } + } + + public synchronized void bind(SocketAddress endpoint) throws IOException { + if (!(endpoint instanceof NGUnixDomainServerSocketAddress)) { + throw new IllegalArgumentException( + "endpoint must be an instance of NGUnixDomainServerSocketAddress"); + } + if (isBound) { + throw new IllegalStateException("Socket is already bound"); + } + if (isClosed) { + throw new IllegalStateException("Socket is already closed"); + } + NGUnixDomainServerSocketAddress unEndpoint = (NGUnixDomainServerSocketAddress) endpoint; + NGUnixDomainSocketLibrary.SockaddrUn address = + new NGUnixDomainSocketLibrary.SockaddrUn(unEndpoint.getPath()); + try { + int socketFd = fd.get(); + NGUnixDomainSocketLibrary.bind(socketFd, address, address.size()); + NGUnixDomainSocketLibrary.listen(socketFd, backlog); + isBound = true; + } catch (LastErrorException e) { + throw new IOException(e); + } + } + + public Socket accept() throws IOException { + // We explicitly do not make this method synchronized, since the + // call to NGUnixDomainSocketLibrary.accept() will block + // indefinitely, causing another thread's call to close() to deadlock. + synchronized (this) { + if (!isBound) { + throw new IllegalStateException("Socket is not bound"); + } + if (isClosed) { + throw new IllegalStateException("Socket is already closed"); + } + } + try { + NGUnixDomainSocketLibrary.SockaddrUn sockaddrUn = + new NGUnixDomainSocketLibrary.SockaddrUn(); + IntByReference addressLen = new IntByReference(); + addressLen.setValue(sockaddrUn.size()); + int clientFd = NGUnixDomainSocketLibrary.accept(fd.get(), sockaddrUn, addressLen); + return new NGUnixDomainSocket(clientFd); + } catch (LastErrorException e) { + throw new IOException(e); + } + } + + public synchronized void close() throws IOException { + if (isClosed) { + throw new IllegalStateException("Socket is already closed"); + } + try { + // Ensure any pending call to accept() fails. + NGUnixDomainSocketLibrary.close(fd.getAndSet(-1)); + isClosed = true; + } catch (LastErrorException e) { + throw new IOException(e); + } + } +} diff --git a/main-command/src/main/java/sbt/internal/NGUnixDomainSocket.java b/main-command/src/main/java/sbt/internal/NGUnixDomainSocket.java new file mode 100644 index 000000000..1a9942ad9 --- /dev/null +++ b/main-command/src/main/java/sbt/internal/NGUnixDomainSocket.java @@ -0,0 +1,171 @@ +// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGUnixDomainSocket.java + +/* + + Copyright 2004-2015, Martian Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package sbt.internal; + +import com.sun.jna.LastErrorException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import java.nio.ByteBuffer; + +import java.net.Socket; + +/** + * Implements a {@link Socket} backed by a native Unix domain socket. + * + * Instances of this class always return {@code null} for + * {@link Socket#getInetAddress()}, {@link Socket#getLocalAddress()}, + * {@link Socket#getLocalSocketAddress()}, {@link Socket#getRemoteSocketAddress()}. + */ +public class NGUnixDomainSocket extends Socket { + private final ReferenceCountedFileDescriptor fd; + private final InputStream is; + private final OutputStream os; + + /** + * Creates a Unix domain socket backed by a native file descriptor. + */ + public NGUnixDomainSocket(int fd) { + this.fd = new ReferenceCountedFileDescriptor(fd); + this.is = new NGUnixDomainSocketInputStream(); + this.os = new NGUnixDomainSocketOutputStream(); + } + + public InputStream getInputStream() { + return is; + } + + public OutputStream getOutputStream() { + return os; + } + + public void shutdownInput() throws IOException { + doShutdown(NGUnixDomainSocketLibrary.SHUT_RD); + } + + public void shutdownOutput() throws IOException { + doShutdown(NGUnixDomainSocketLibrary.SHUT_WR); + } + + private void doShutdown(int how) throws IOException { + try { + int socketFd = fd.acquire(); + if (socketFd != -1) { + NGUnixDomainSocketLibrary.shutdown(socketFd, how); + } + } catch (LastErrorException e) { + throw new IOException(e); + } finally { + fd.release(); + } + } + + public void close() throws IOException { + super.close(); + try { + // This might not close the FD right away. In case we are about + // to read or write on another thread, it will delay the close + // until the read or write completes, to prevent the FD from + // being re-used for a different purpose and the other thread + // reading from a different FD. + fd.close(); + } catch (LastErrorException e) { + throw new IOException(e); + } + } + + private class NGUnixDomainSocketInputStream extends InputStream { + public int read() throws IOException { + ByteBuffer buf = ByteBuffer.allocate(1); + int result; + if (doRead(buf) == 0) { + result = -1; + } else { + // Make sure to & with 0xFF to avoid sign extension + result = 0xFF & buf.get(); + } + return result; + } + + public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + ByteBuffer buf = ByteBuffer.wrap(b, off, len); + int result = doRead(buf); + if (result == 0) { + result = -1; + } + return result; + } + + private int doRead(ByteBuffer buf) throws IOException { + try { + int fdToRead = fd.acquire(); + if (fdToRead == -1) { + return -1; + } + return NGUnixDomainSocketLibrary.read(fdToRead, buf, buf.remaining()); + } catch (LastErrorException e) { + throw new IOException(e); + } finally { + fd.release(); + } + } + } + + private class NGUnixDomainSocketOutputStream extends OutputStream { + + public void write(int b) throws IOException { + ByteBuffer buf = ByteBuffer.allocate(1); + buf.put(0, (byte) (0xFF & b)); + doWrite(buf); + } + + public void write(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return; + } + ByteBuffer buf = ByteBuffer.wrap(b, off, len); + doWrite(buf); + } + + private void doWrite(ByteBuffer buf) throws IOException { + try { + int fdToWrite = fd.acquire(); + if (fdToWrite == -1) { + return; + } + int ret = NGUnixDomainSocketLibrary.write(fdToWrite, buf, buf.remaining()); + if (ret != buf.remaining()) { + // This shouldn't happen with standard blocking Unix domain sockets. + throw new IOException("Could not write " + buf.remaining() + " bytes as requested " + + "(wrote " + ret + " bytes instead)"); + } + } catch (LastErrorException e) { + throw new IOException(e); + } finally { + fd.release(); + } + } + } +} diff --git a/main-command/src/main/java/sbt/internal/NGUnixDomainSocketLibrary.java b/main-command/src/main/java/sbt/internal/NGUnixDomainSocketLibrary.java new file mode 100644 index 000000000..7e760d37a --- /dev/null +++ b/main-command/src/main/java/sbt/internal/NGUnixDomainSocketLibrary.java @@ -0,0 +1,140 @@ +// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGUnixDomainSocketLibrary.java + +/* + + Copyright 2004-2015, Martian Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package sbt.internal; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Platform; +import com.sun.jna.Structure; +import com.sun.jna.Union; +import com.sun.jna.ptr.IntByReference; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; + +/** + * Utility class to bridge native Unix domain socket calls to Java using JNA. + */ +public class NGUnixDomainSocketLibrary { + public static final int PF_LOCAL = 1; + public static final int AF_LOCAL = 1; + public static final int SOCK_STREAM = 1; + + public static final int SHUT_RD = 0; + public static final int SHUT_WR = 1; + + // Utility class, do not instantiate. + private NGUnixDomainSocketLibrary() { } + + // BSD platforms write a length byte at the start of struct sockaddr_un. + private static final boolean HAS_SUN_LEN = + Platform.isMac() || Platform.isFreeBSD() || Platform.isNetBSD() || + Platform.isOpenBSD() || Platform.iskFreeBSD(); + + /** + * Bridges {@code struct sockaddr_un} to and from native code. + */ + public static class SockaddrUn extends Structure implements Structure.ByReference { + /** + * On BSD platforms, the {@code sun_len} and {@code sun_family} values in + * {@code struct sockaddr_un}. + */ + public static class SunLenAndFamily extends Structure { + public byte sunLen; + public byte sunFamily; + + protected List getFieldOrder() { + return Arrays.asList(new String[] { "sunLen", "sunFamily" }); + } + } + + /** + * On BSD platforms, {@code sunLenAndFamily} will be present. + * On other platforms, only {@code sunFamily} will be present. + */ + public static class SunFamily extends Union { + public SunLenAndFamily sunLenAndFamily; + public short sunFamily; + } + + public SunFamily sunFamily = new SunFamily(); + public byte[] sunPath = new byte[104]; + + /** + * Constructs an empty {@code struct sockaddr_un}. + */ + public SockaddrUn() { + if (HAS_SUN_LEN) { + sunFamily.sunLenAndFamily = new SunLenAndFamily(); + sunFamily.setType(SunLenAndFamily.class); + } else { + sunFamily.setType(Short.TYPE); + } + allocateMemory(); + } + + /** + * Constructs a {@code struct sockaddr_un} with a path whose bytes are encoded + * using the default encoding of the platform. + */ + public SockaddrUn(String path) throws IOException { + byte[] pathBytes = path.getBytes(); + if (pathBytes.length > sunPath.length - 1) { + throw new IOException("Cannot fit name [" + path + "] in maximum unix domain socket length"); + } + System.arraycopy(pathBytes, 0, sunPath, 0, pathBytes.length); + sunPath[pathBytes.length] = (byte) 0; + if (HAS_SUN_LEN) { + int len = fieldOffset("sunPath") + pathBytes.length; + sunFamily.sunLenAndFamily = new SunLenAndFamily(); + sunFamily.sunLenAndFamily.sunLen = (byte) len; + sunFamily.sunLenAndFamily.sunFamily = AF_LOCAL; + sunFamily.setType(SunLenAndFamily.class); + } else { + sunFamily.sunFamily = AF_LOCAL; + sunFamily.setType(Short.TYPE); + } + allocateMemory(); + } + + protected List getFieldOrder() { + return Arrays.asList(new String[] { "sunFamily", "sunPath" }); + } + } + + static { + Native.register(Platform.C_LIBRARY_NAME); + } + + public static native int socket(int domain, int type, int protocol) throws LastErrorException; + public static native int bind(int fd, SockaddrUn address, int addressLen) + throws LastErrorException; + public static native int listen(int fd, int backlog) throws LastErrorException; + public static native int accept(int fd, SockaddrUn address, IntByReference addressLen) + throws LastErrorException; + public static native int read(int fd, ByteBuffer buffer, int count) + throws LastErrorException; + public static native int write(int fd, ByteBuffer buffer, int count) + throws LastErrorException; + public static native int close(int fd) throws LastErrorException; + public static native int shutdown(int fd, int how) throws LastErrorException; +} diff --git a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeLibrary.java b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeLibrary.java new file mode 100644 index 000000000..ba535691f --- /dev/null +++ b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeLibrary.java @@ -0,0 +1,90 @@ +// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGWin32NamedPipeLibrary.java + +/* + + Copyright 2004-2017, Martian Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package sbt.internal; + +import java.nio.ByteBuffer; + +import com.sun.jna.*; +import com.sun.jna.platform.win32.WinNT; +import com.sun.jna.platform.win32.WinNT.*; +import com.sun.jna.platform.win32.WinBase.*; +import com.sun.jna.ptr.IntByReference; + +import com.sun.jna.win32.W32APIOptions; + +public interface NGWin32NamedPipeLibrary extends WinNT { + int PIPE_ACCESS_DUPLEX = 3; + int PIPE_UNLIMITED_INSTANCES = 255; + int FILE_FLAG_FIRST_PIPE_INSTANCE = 524288; + + NGWin32NamedPipeLibrary INSTANCE = + (NGWin32NamedPipeLibrary) Native.loadLibrary( + "kernel32", + NGWin32NamedPipeLibrary.class, + W32APIOptions.UNICODE_OPTIONS); + + HANDLE CreateNamedPipe( + String lpName, + int dwOpenMode, + int dwPipeMode, + int nMaxInstances, + int nOutBufferSize, + int nInBufferSize, + int nDefaultTimeOut, + SECURITY_ATTRIBUTES lpSecurityAttributes); + boolean ConnectNamedPipe( + HANDLE hNamedPipe, + Pointer lpOverlapped); + boolean DisconnectNamedPipe( + HANDLE hObject); + boolean ReadFile( + HANDLE hFile, + Memory lpBuffer, + int nNumberOfBytesToRead, + IntByReference lpNumberOfBytesRead, + Pointer lpOverlapped); + boolean WriteFile( + HANDLE hFile, + ByteBuffer lpBuffer, + int nNumberOfBytesToWrite, + IntByReference lpNumberOfBytesWritten, + Pointer lpOverlapped); + boolean CloseHandle( + HANDLE hObject); + boolean GetOverlappedResult( + HANDLE hFile, + Pointer lpOverlapped, + IntByReference lpNumberOfBytesTransferred, + boolean wait); + boolean CancelIoEx( + HANDLE hObject, + Pointer lpOverlapped); + HANDLE CreateEvent( + SECURITY_ATTRIBUTES lpEventAttributes, + boolean bManualReset, + boolean bInitialState, + String lpName); + int WaitForSingleObject( + HANDLE hHandle, + int dwMilliseconds + ); + + int GetLastError(); +} diff --git a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeServerSocket.java b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeServerSocket.java new file mode 100644 index 000000000..137d9b5dc --- /dev/null +++ b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeServerSocket.java @@ -0,0 +1,173 @@ +// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGWin32NamedPipeServerSocket.java + +/* + + Copyright 2004-2017, Martian Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package sbt.internal; + +import com.sun.jna.platform.win32.WinBase; +import com.sun.jna.platform.win32.WinError; +import com.sun.jna.platform.win32.WinNT; +import com.sun.jna.platform.win32.WinNT.HANDLE; +import com.sun.jna.ptr.IntByReference; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +public class NGWin32NamedPipeServerSocket extends ServerSocket { + private static final NGWin32NamedPipeLibrary API = NGWin32NamedPipeLibrary.INSTANCE; + private static final String WIN32_PIPE_PREFIX = "\\\\.\\pipe\\"; + private static final int BUFFER_SIZE = 65535; + private final LinkedBlockingQueue openHandles; + private final LinkedBlockingQueue connectedHandles; + private final NGWin32NamedPipeSocket.CloseCallback closeCallback; + private final String path; + private final int maxInstances; + private final HANDLE lockHandle; + + public NGWin32NamedPipeServerSocket(String path) throws IOException { + this(NGWin32NamedPipeLibrary.PIPE_UNLIMITED_INSTANCES, path); + } + + public NGWin32NamedPipeServerSocket(int maxInstances, String path) throws IOException { + this.openHandles = new LinkedBlockingQueue<>(); + this.connectedHandles = new LinkedBlockingQueue<>(); + this.closeCallback = handle -> { + if (connectedHandles.remove(handle)) { + closeConnectedPipe(handle, false); + } + if (openHandles.remove(handle)) { + closeOpenPipe(handle); + } + }; + this.maxInstances = maxInstances; + if (!path.startsWith(WIN32_PIPE_PREFIX)) { + this.path = WIN32_PIPE_PREFIX + path; + } else { + this.path = path; + } + String lockPath = this.path + "_lock"; + lockHandle = API.CreateNamedPipe( + lockPath, + NGWin32NamedPipeLibrary.FILE_FLAG_FIRST_PIPE_INSTANCE | NGWin32NamedPipeLibrary.PIPE_ACCESS_DUPLEX, + 0, + 1, + BUFFER_SIZE, + BUFFER_SIZE, + 0, + null); + if (lockHandle == NGWin32NamedPipeLibrary.INVALID_HANDLE_VALUE) { + throw new IOException(String.format("Could not create lock for %s, error %d", lockPath, API.GetLastError())); + } else { + if (!API.DisconnectNamedPipe(lockHandle)) { + throw new IOException(String.format("Could not disconnect lock %d", API.GetLastError())); + } + } + + } + + public void bind(SocketAddress endpoint) throws IOException { + throw new IOException("Win32 named pipes do not support bind(), pass path to constructor"); + } + + public Socket accept() throws IOException { + HANDLE handle = API.CreateNamedPipe( + path, + NGWin32NamedPipeLibrary.PIPE_ACCESS_DUPLEX | WinNT.FILE_FLAG_OVERLAPPED, + 0, + maxInstances, + BUFFER_SIZE, + BUFFER_SIZE, + 0, + null); + if (handle == NGWin32NamedPipeLibrary.INVALID_HANDLE_VALUE) { + throw new IOException(String.format("Could not create named pipe, error %d", API.GetLastError())); + } + openHandles.add(handle); + + HANDLE connWaitable = API.CreateEvent(null, true, false, null); + WinBase.OVERLAPPED olap = new WinBase.OVERLAPPED(); + olap.hEvent = connWaitable; + olap.write(); + + boolean immediate = API.ConnectNamedPipe(handle, olap.getPointer()); + if (immediate) { + openHandles.remove(handle); + connectedHandles.add(handle); + return new NGWin32NamedPipeSocket(handle, closeCallback); + } + + int connectError = API.GetLastError(); + if (connectError == WinError.ERROR_PIPE_CONNECTED) { + openHandles.remove(handle); + connectedHandles.add(handle); + return new NGWin32NamedPipeSocket(handle, closeCallback); + } else if (connectError == WinError.ERROR_NO_DATA) { + // Client has connected and disconnected between CreateNamedPipe() and ConnectNamedPipe() + // connection is broken, but it is returned it avoid loop here. + // Actual error will happen for NGSession when it will try to read/write from/to pipe + return new NGWin32NamedPipeSocket(handle, closeCallback); + } else if (connectError == WinError.ERROR_IO_PENDING) { + if (!API.GetOverlappedResult(handle, olap.getPointer(), new IntByReference(), true)) { + openHandles.remove(handle); + closeOpenPipe(handle); + throw new IOException("GetOverlappedResult() failed for connect operation: " + API.GetLastError()); + } + openHandles.remove(handle); + connectedHandles.add(handle); + return new NGWin32NamedPipeSocket(handle, closeCallback); + } else { + throw new IOException("ConnectNamedPipe() failed with: " + connectError); + } + } + + public void close() throws IOException { + try { + List handlesToClose = new ArrayList<>(); + openHandles.drainTo(handlesToClose); + for (HANDLE handle : handlesToClose) { + closeOpenPipe(handle); + } + + List handlesToDisconnect = new ArrayList<>(); + connectedHandles.drainTo(handlesToDisconnect); + for (HANDLE handle : handlesToDisconnect) { + closeConnectedPipe(handle, true); + } + } finally { + API.CloseHandle(lockHandle); + } + } + + private void closeOpenPipe(HANDLE handle) throws IOException { + API.CancelIoEx(handle, null); + API.CloseHandle(handle); + } + + private void closeConnectedPipe(HANDLE handle, boolean shutdown) throws IOException { + if (!shutdown) { + API.WaitForSingleObject(handle, 10000); + } + API.DisconnectNamedPipe(handle); + API.CloseHandle(handle); + } +} diff --git a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java new file mode 100644 index 000000000..072ed3901 --- /dev/null +++ b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java @@ -0,0 +1,173 @@ +// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGWin32NamedPipeSocket.java + +/* + + Copyright 2004-2017, Martian Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package sbt.internal; + +import com.sun.jna.Memory; +import com.sun.jna.platform.win32.WinBase; +import com.sun.jna.platform.win32.WinError; +import com.sun.jna.platform.win32.WinNT.HANDLE; +import com.sun.jna.ptr.IntByReference; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; + +public class NGWin32NamedPipeSocket extends Socket { + private static final NGWin32NamedPipeLibrary API = NGWin32NamedPipeLibrary.INSTANCE; + private final HANDLE handle; + private final CloseCallback closeCallback; + private final InputStream is; + private final OutputStream os; + private final HANDLE readerWaitable; + private final HANDLE writerWaitable; + + interface CloseCallback { + void onNamedPipeSocketClose(HANDLE handle) throws IOException; + } + + public NGWin32NamedPipeSocket( + HANDLE handle, + NGWin32NamedPipeSocket.CloseCallback closeCallback) throws IOException { + this.handle = handle; + this.closeCallback = closeCallback; + this.readerWaitable = API.CreateEvent(null, true, false, null); + if (readerWaitable == null) { + throw new IOException("CreateEvent() failed "); + } + writerWaitable = API.CreateEvent(null, true, false, null); + if (writerWaitable == null) { + throw new IOException("CreateEvent() failed "); + } + this.is = new NGWin32NamedPipeSocketInputStream(handle); + this.os = new NGWin32NamedPipeSocketOutputStream(handle); + } + + @Override + public InputStream getInputStream() { + return is; + } + + @Override + public OutputStream getOutputStream() { + return os; + } + + @Override + public void close() throws IOException { + closeCallback.onNamedPipeSocketClose(handle); + } + + @Override + public void shutdownInput() throws IOException { + } + + @Override + public void shutdownOutput() throws IOException { + } + + private class NGWin32NamedPipeSocketInputStream extends InputStream { + private final HANDLE handle; + + NGWin32NamedPipeSocketInputStream(HANDLE handle) { + this.handle = handle; + } + + @Override + public int read() throws IOException { + int result; + byte[] b = new byte[1]; + if (read(b) == 0) { + result = -1; + } else { + result = 0xFF & b[0]; + } + return result; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + Memory readBuffer = new Memory(len); + + WinBase.OVERLAPPED olap = new WinBase.OVERLAPPED(); + olap.hEvent = readerWaitable; + olap.write(); + + boolean immediate = API.ReadFile(handle, readBuffer, len, null, olap.getPointer()); + if (!immediate) { + int lastError = API.GetLastError(); + if (lastError != WinError.ERROR_IO_PENDING) { + throw new IOException("ReadFile() failed: " + lastError); + } + } + + IntByReference read = new IntByReference(); + if (!API.GetOverlappedResult(handle, olap.getPointer(), read, true)) { + int lastError = API.GetLastError(); + throw new IOException("GetOverlappedResult() failed for read operation: " + lastError); + } + if (read.getValue() != len) { + throw new IOException("ReadFile() read less bytes than requested"); + } + byte[] byteArray = readBuffer.getByteArray(0, len); + System.arraycopy(byteArray, 0, b, off, len); + return len; + } + } + + private class NGWin32NamedPipeSocketOutputStream extends OutputStream { + private final HANDLE handle; + + NGWin32NamedPipeSocketOutputStream(HANDLE handle) { + this.handle = handle; + } + + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) (0xFF & b)}); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + ByteBuffer data = ByteBuffer.wrap(b, off, len); + + WinBase.OVERLAPPED olap = new WinBase.OVERLAPPED(); + olap.hEvent = writerWaitable; + olap.write(); + + boolean immediate = API.WriteFile(handle, data, len, null, olap.getPointer()); + if (!immediate) { + int lastError = API.GetLastError(); + if (lastError != WinError.ERROR_IO_PENDING) { + throw new IOException("WriteFile() failed: " + lastError); + } + } + IntByReference written = new IntByReference(); + if (!API.GetOverlappedResult(handle, olap.getPointer(), written, true)) { + int lastError = API.GetLastError(); + throw new IOException("GetOverlappedResult() failed for write operation: " + lastError); + } + if (written.getValue() != len) { + throw new IOException("WriteFile() wrote less bytes than requested"); + } + } + } +} diff --git a/main-command/src/main/java/sbt/internal/ReferenceCountedFileDescriptor.java b/main-command/src/main/java/sbt/internal/ReferenceCountedFileDescriptor.java new file mode 100644 index 000000000..7fb5d9d53 --- /dev/null +++ b/main-command/src/main/java/sbt/internal/ReferenceCountedFileDescriptor.java @@ -0,0 +1,82 @@ +// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/ReferenceCountedFileDescriptor.java + +/* + + Copyright 2004-2015, Martian Software, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package sbt.internal; + +import com.sun.jna.LastErrorException; + +import java.io.IOException; + +/** + * Encapsulates a file descriptor plus a reference count to ensure close requests + * only close the file descriptor once the last reference to the file descriptor + * is released. + * + * If not explicitly closed, the file descriptor will be closed when + * this object is finalized. + */ +public class ReferenceCountedFileDescriptor { + private int fd; + private int fdRefCount; + private boolean closePending; + + public ReferenceCountedFileDescriptor(int fd) { + this.fd = fd; + this.fdRefCount = 0; + this.closePending = false; + } + + protected void finalize() throws IOException { + close(); + } + + public synchronized int acquire() { + fdRefCount++; + return fd; + } + + public synchronized void release() throws IOException { + fdRefCount--; + if (fdRefCount == 0 && closePending && fd != -1) { + doClose(); + } + } + + public synchronized void close() throws IOException { + if (fd == -1 || closePending) { + return; + } + + if (fdRefCount == 0) { + doClose(); + } else { + // Another thread has the FD. We'll close it when they release the reference. + closePending = true; + } + } + + private void doClose() throws IOException { + try { + NGUnixDomainSocketLibrary.close(fd); + fd = -1; + } catch (LastErrorException e) { + throw new IOException(e); + } + } +} From f785750fc40515efd0614d774f2251583ac81118 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 27 Nov 2017 21:37:31 -0500 Subject: [PATCH 45/62] IPC Unix domain socket for sbt server In addition to TCP, this adds sbt server support for IPC (interprocess communication) using Unix domain socket and Windows named pipe. The use of Unix domain socket has performance and security benefits. --- build.sbt | 11 +++- .../ConnectionTypeFormats.scala | 28 +++++++++ .../contraband-scala/sbt/ConnectionType.scala | 13 +++++ main-command/src/main/contraband/state.contra | 7 +++ .../src/main/scala/sbt/BasicKeys.scala | 5 ++ .../scala/sbt/internal/server/Server.scala | 57 +++++++++++++++---- main/src/main/scala/sbt/Defaults.scala | 6 +- main/src/main/scala/sbt/Keys.scala | 2 + main/src/main/scala/sbt/Project.scala | 3 + .../scala/sbt/internal/CommandExchange.scala | 32 ++++++++--- project/Dependencies.scala | 2 + sbt/src/sbt-test/server/handshake/build.sbt | 1 + vscode-sbt-scala/client/src/extension.ts | 20 ++++--- vscode-sbt-scala/server/src/server.ts | 13 ++++- 14 files changed, 169 insertions(+), 31 deletions(-) create mode 100644 main-command/src/main/contraband-scala/ConnectionTypeFormats.scala create mode 100644 main-command/src/main/contraband-scala/sbt/ConnectionType.scala diff --git a/build.sbt b/build.sbt index c5b2c6cfc..61842c40c 100644 --- a/build.sbt +++ b/build.sbt @@ -55,7 +55,7 @@ def commonSettings: Seq[Setting[_]] = concurrentRestrictions in Global += Util.testExclusiveRestriction, testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"), testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "2"), - javacOptions in compile ++= Seq("-target", "6", "-source", "6", "-Xlint", "-Xlint:-serial"), + javacOptions in compile ++= Seq("-Xlint", "-Xlint:-serial"), crossScalaVersions := Seq(baseScalaVersion), bintrayPackage := (bintrayPackage in ThisBuild).value, bintrayRepository := (bintrayRepository in ThisBuild).value, @@ -309,7 +309,8 @@ lazy val commandProj = (project in file("main-command")) .settings( testedBaseSettings, name := "Command", - libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson.value, templateResolverApi), + libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson.value, templateResolverApi, + jna, jnaPlatform), managedSourceDirectories in Compile += baseDirectory.value / "src" / "main" / "contraband-scala", sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", @@ -324,7 +325,11 @@ lazy val commandProj = (project in file("main-command")) exclude[ReversedMissingMethodProblem]("sbt.internal.CommandChannel.*"), // Added an overload to reboot. The overload is private[sbt]. exclude[ReversedMissingMethodProblem]("sbt.StateOps.reboot"), - ) + ), + unmanagedSources in (Compile, headerCreate) := { + val old = (unmanagedSources in (Compile, headerCreate)).value + old filterNot { x => (x.getName startsWith "NG") || (x.getName == "ReferenceCountedFileDescriptor.java") } + }, ) .configure( addSbtIO, diff --git a/main-command/src/main/contraband-scala/ConnectionTypeFormats.scala b/main-command/src/main/contraband-scala/ConnectionTypeFormats.scala new file mode 100644 index 000000000..4562f75b9 --- /dev/null +++ b/main-command/src/main/contraband-scala/ConnectionTypeFormats.scala @@ -0,0 +1,28 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ConnectionTypeFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ConnectionTypeFormat: JsonFormat[sbt.ConnectionType] = new JsonFormat[sbt.ConnectionType] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.ConnectionType = { + jsOpt match { + case Some(js) => + unbuilder.readString(js) match { + case "Local" => sbt.ConnectionType.Local + case "Tcp" => sbt.ConnectionType.Tcp + } + case None => + deserializationError("Expected JsString but found None") + } + } + override def write[J](obj: sbt.ConnectionType, builder: Builder[J]): Unit = { + val str = obj match { + case sbt.ConnectionType.Local => "Local" + case sbt.ConnectionType.Tcp => "Tcp" + } + builder.writeString(str) + } +} +} diff --git a/main-command/src/main/contraband-scala/sbt/ConnectionType.scala b/main-command/src/main/contraband-scala/sbt/ConnectionType.scala new file mode 100644 index 000000000..af50ed2e9 --- /dev/null +++ b/main-command/src/main/contraband-scala/sbt/ConnectionType.scala @@ -0,0 +1,13 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt +sealed abstract class ConnectionType extends Serializable +object ConnectionType { + + /** This uses Unix domain socket on POSIX, and named pipe on Windows. */ + case object Local extends ConnectionType + case object Tcp extends ConnectionType +} diff --git a/main-command/src/main/contraband/state.contra b/main-command/src/main/contraband/state.contra index 79d0bcaab..2737ce8ab 100644 --- a/main-command/src/main/contraband/state.contra +++ b/main-command/src/main/contraband/state.contra @@ -16,3 +16,10 @@ type CommandSource { enum ServerAuthentication { Token } + +enum ConnectionType { + ## This uses Unix domain socket on POSIX, and named pipe on Windows. + Local + Tcp + # Ssh +} diff --git a/main-command/src/main/scala/sbt/BasicKeys.scala b/main-command/src/main/scala/sbt/BasicKeys.scala index ef6bb00cd..42b68daa4 100644 --- a/main-command/src/main/scala/sbt/BasicKeys.scala +++ b/main-command/src/main/scala/sbt/BasicKeys.scala @@ -33,6 +33,11 @@ object BasicKeys { "Method of authenticating server command.", 10000) + val serverConnectionType = + AttributeKey[ConnectionType]("serverConnectionType", + "The wire protocol for the server command.", + 10000) + private[sbt] val interactive = AttributeKey[Boolean]( "interactive", "True if commands are currently being entered from an interactive environment.", diff --git a/main-command/src/main/scala/sbt/internal/server/Server.scala b/main-command/src/main/scala/sbt/internal/server/Server.scala index 3a7c2785b..c4d3b542c 100644 --- a/main-command/src/main/scala/sbt/internal/server/Server.scala +++ b/main-command/src/main/scala/sbt/internal/server/Server.scala @@ -17,13 +17,14 @@ import java.security.SecureRandom import java.math.BigInteger import scala.concurrent.{ Future, Promise } import scala.util.{ Try, Success, Failure } -import sbt.internal.util.ErrorHandling import sbt.internal.protocol.{ PortFile, TokenFile } import sbt.util.Logger import sbt.io.IO import sbt.io.syntax._ import sjsonnew.support.scalajson.unsafe.{ Converter, CompactPrinter } import sbt.internal.protocol.codec._ +import sbt.internal.util.ErrorHandling +import sbt.internal.util.Util.isWindows private[sbt] sealed trait ServerInstance { def shutdown(): Unit @@ -38,31 +39,37 @@ private[sbt] object Server { with TokenFileFormats object JsonProtocol extends JsonProtocol - def start(host: String, - port: Int, + def start(connection: ServerConnection, onIncomingSocket: (Socket, ServerInstance) => Unit, - auth: Set[ServerAuthentication], - portfile: File, - tokenfile: File, log: Logger): ServerInstance = new ServerInstance { self => + import connection._ val running = new AtomicBoolean(false) val p: Promise[Unit] = Promise[Unit]() val ready: Future[Unit] = p.future private[this] val rand = new SecureRandom private[this] var token: String = nextToken + private[this] var serverSocketOpt: Option[ServerSocket] = None val serverThread = new Thread("sbt-socket-server") { override def run(): Unit = { Try { - ErrorHandling.translate(s"server failed to start on $host:$port. ") { - new ServerSocket(port, 50, InetAddress.getByName(host)) + ErrorHandling.translate(s"server failed to start on ${connection.shortName}. ") { + connection.connectionType match { + case ConnectionType.Local if isWindows => + new NGWin32NamedPipeServerSocket(pipeName) + case ConnectionType.Local => + prepareSocketfile() + new NGUnixDomainServerSocket(socketfile.getAbsolutePath) + case ConnectionType.Tcp => new ServerSocket(port, 50, InetAddress.getByName(host)) + } } } match { case Failure(e) => p.failure(e) case Success(serverSocket) => serverSocket.setSoTimeout(5000) - log.info(s"sbt server started at $host:$port") + serverSocketOpt = Option(serverSocket) + log.info(s"sbt server started at ${connection.shortName}") writePortfile() running.set(true) p.success(()) @@ -74,6 +81,7 @@ private[sbt] object Server { case _: SocketTimeoutException => // its ok } } + serverSocket.close() } } } @@ -106,7 +114,7 @@ private[sbt] object Server { private[this] def writeTokenfile(): Unit = { import JsonProtocol._ - val uri = s"tcp://$host:$port" + val uri = connection.shortName val t = TokenFile(uri, token) val jsonToken = Converter.toJson(t).get @@ -141,7 +149,7 @@ private[sbt] object Server { private[this] def writePortfile(): Unit = { import JsonProtocol._ - val uri = s"tcp://$host:$port" + val uri = connection.shortName val p = auth match { case _ if auth(ServerAuthentication.Token) => @@ -153,5 +161,32 @@ private[sbt] object Server { val json = Converter.toJson(p).get IO.write(portfile, CompactPrinter(json)) } + + private[sbt] def prepareSocketfile(): Unit = { + if (socketfile.exists) { + IO.delete(socketfile) + } + IO.createDirectory(socketfile.getParentFile) + } } } + +private[sbt] case class ServerConnection( + connectionType: ConnectionType, + host: String, + port: Int, + auth: Set[ServerAuthentication], + portfile: File, + tokenfile: File, + socketfile: File, + pipeName: String +) { + def shortName: String = { + connectionType match { + case ConnectionType.Local if isWindows => s"local:$pipeName" + case ConnectionType.Local => s"local://$socketfile" + case ConnectionType.Tcp => s"tcp://$host:$port" + // case ConnectionType.Ssh => s"ssh://$host:$port" + } + } +} diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 3275fe092..7041fe6de 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -272,7 +272,11 @@ object Defaults extends BuildCommon { serverPort := 5000 + (Hash .toHex(Hash(appConfiguration.value.baseDirectory.toString)) .## % 1000), - serverAuthentication := Set(ServerAuthentication.Token), + serverConnectionType := ConnectionType.Local, + serverAuthentication := { + if (serverConnectionType.value == ConnectionType.Tcp) Set(ServerAuthentication.Token) + else Set() + }, insideCI :== sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI"), )) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index ca92e306b..211b46b1b 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -133,6 +133,8 @@ object Keys { val serverPort = SettingKey(BasicKeys.serverPort) val serverHost = SettingKey(BasicKeys.serverHost) val serverAuthentication = SettingKey(BasicKeys.serverAuthentication) + val serverConnectionType = SettingKey(BasicKeys.serverConnectionType) + val analysis = AttributeKey[CompileAnalysis]("analysis", "Analysis of compilation, including dependencies and generated outputs.", DSetting) val watch = SettingKey(BasicKeys.watch) val suppressSbtShellNotification = settingKey[Boolean]("""True to suppress the "Executing in batch mode.." message.""").withRank(CSetting) diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index 6ec260e86..680bba811 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -23,6 +23,7 @@ import Keys.{ serverHost, serverPort, serverAuthentication, + serverConnectionType, watch } import Scope.{ Global, ThisScope } @@ -461,6 +462,7 @@ object Project extends ProjectExtra { val host: Option[String] = get(serverHost) val port: Option[Int] = get(serverPort) val authentication: Option[Set[ServerAuthentication]] = get(serverAuthentication) + val connectionType: Option[ConnectionType] = get(serverConnectionType) val commandDefs = allCommands.distinct.flatten[Command].map(_ tag (projectCommand, true)) val newDefinedCommands = commandDefs ++ BasicCommands.removeTagged(s.definedCommands, projectCommand) @@ -471,6 +473,7 @@ object Project extends ProjectExtra { .setCond(serverPort.key, port) .setCond(serverHost.key, host) .setCond(serverAuthentication.key, authentication) + .setCond(serverConnectionType.key, connectionType) .put(historyPath.key, history) .put(templateResolverInfos.key, trs) .setCond(shellPrompt.key, prompt) diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 9605558a4..4a13bdb80 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicInteger import scala.collection.mutable.ListBuffer import scala.annotation.tailrec -import BasicKeys.{ serverHost, serverPort, serverAuthentication } +import BasicKeys.{ serverHost, serverPort, serverAuthentication, serverConnectionType } import java.net.Socket import sjsonnew.JsonFormat import sjsonnew.shaded.scalajson.ast.unsafe._ @@ -83,6 +83,7 @@ private[sbt] final class CommandExchange { } private def newChannelName: String = s"channel-${nextChannelId.incrementAndGet()}" + private def newNetworkName: String = s"network-${nextChannelId.incrementAndGet()}" /** * Check if a server instance is running already, and start one if it isn't. @@ -100,19 +101,23 @@ private[sbt] final class CommandExchange { case Some(xs) => xs case None => Set(ServerAuthentication.Token) } + lazy val connectionType = (s get serverConnectionType) match { + case Some(x) => x + case None => ConnectionType.Tcp + } val serverLogLevel: Level.Value = Level.Debug def onIncomingSocket(socket: Socket, instance: ServerInstance): Unit = { - s.log.info(s"new client connected from: ${socket.getPort}") + val name = newNetworkName + s.log.info(s"new client connected: $name") val logger: Logger = { - val loggerName = s"network-${socket.getPort}" - val log = LogExchange.logger(loggerName, None, None) - LogExchange.unbindLoggerAppenders(loggerName) + val log = LogExchange.logger(name, None, None) + LogExchange.unbindLoggerAppenders(name) val appender = MainAppender.defaultScreen(s.globalLogging.console) - LogExchange.bindLoggerAppenders(loggerName, List(appender -> serverLogLevel)) + LogExchange.bindLoggerAppenders(name, List(appender -> serverLogLevel)) log } val channel = - new NetworkChannel(newChannelName, socket, Project structure s, auth, instance, logger) + new NetworkChannel(name, socket, Project structure s, auth, instance, logger) subscribe(channel) } server match { @@ -121,7 +126,18 @@ private[sbt] final class CommandExchange { val portfile = (new File(".")).getAbsoluteFile / "project" / "target" / "active.json" val h = Hash.halfHashString(portfile.toURI.toString) val tokenfile = BuildPaths.getGlobalBase(s) / "server" / h / "token.json" - val x = Server.start(host, port, onIncomingSocket, auth, portfile, tokenfile, s.log) + val socketfile = BuildPaths.getGlobalBase(s) / "server" / h / "sock" + val pipeName = "sbt-server-" + h + val connection = + ServerConnection(connectionType, + host, + port, + auth, + portfile, + tokenfile, + socketfile, + pipeName) + val x = Server.start(connection, onIncomingSocket, s.log) Await.ready(x.ready, Duration("10s")) x.ready.value match { case Some(Success(_)) => diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f714cbc82..ce9bd9223 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -106,6 +106,8 @@ object Dependencies { val specs2 = "org.specs2" %% "specs2" % "2.4.17" val junit = "junit" % "junit" % "4.11" val templateResolverApi = "org.scala-sbt" % "template-resolver" % "0.1" + val jna = "net.java.dev.jna" % "jna" % "4.1.0" + val jnaPlatform = "net.java.dev.jna" % "jna-platform" % "4.1.0" private def scala211Module(name: String, moduleVersion: String) = Def setting ( scalaBinaryVersion.value match { diff --git a/sbt/src/sbt-test/server/handshake/build.sbt b/sbt/src/sbt-test/server/handshake/build.sbt index fd924f0e4..851648f3c 100644 --- a/sbt/src/sbt-test/server/handshake/build.sbt +++ b/sbt/src/sbt-test/server/handshake/build.sbt @@ -2,6 +2,7 @@ lazy val runClient = taskKey[Unit]("") lazy val root = (project in file(".")) .settings( + serverConnectionType in Global := ConnectionType.Tcp, scalaVersion := "2.12.3", serverPort in Global := 5123, libraryDependencies += "org.scala-sbt" %% "io" % "1.0.1", diff --git a/vscode-sbt-scala/client/src/extension.ts b/vscode-sbt-scala/client/src/extension.ts index f4a3eabb4..768e0fc03 100644 --- a/vscode-sbt-scala/client/src/extension.ts +++ b/vscode-sbt-scala/client/src/extension.ts @@ -23,19 +23,25 @@ export function activate(context: ExtensionContext) { let clientOptions: LanguageClientOptions = { documentSelector: [{ language: 'scala', scheme: 'file' }, { language: 'java', scheme: 'file' }], initializationOptions: () => { - return { - token: discoverToken() - }; + return discoverToken(); } } // the port file is hardcoded to a particular location relative to the build. - function discoverToken(): String { + function discoverToken(): any { let pf = path.join(workspace.rootPath, 'project', 'target', 'active.json'); let portfile = JSON.parse(fs.readFileSync(pf)); - let tf = portfile.tokenfilePath; - let tokenfile = JSON.parse(fs.readFileSync(tf)); - return tokenfile.token; + + // if tokenfilepath exists, return the token. + if (portfile.hasOwnProperty('tokenfilePath')) { + let tf = portfile.tokenfilePath; + let tokenfile = JSON.parse(fs.readFileSync(tf)); + return { + token: tokenfile.token + }; + } else { + return {}; + } } // Create the language client and start the client. diff --git a/vscode-sbt-scala/server/src/server.ts b/vscode-sbt-scala/server/src/server.ts index bef7e50a9..05932d451 100644 --- a/vscode-sbt-scala/server/src/server.ts +++ b/vscode-sbt-scala/server/src/server.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import * as url from 'url'; let net = require('net'), fs = require('fs'), + os = require('os'), stdin = process.stdin, stdout = process.stdout; @@ -16,7 +17,17 @@ socket.on('data', (chunk: any) => { }).on('end', () => { stdin.pause(); }); -socket.connect(u.port, '127.0.0.1'); + +if (u.protocol == 'tcp:') { + socket.connect(u.port, '127.0.0.1'); +} else if (u.protocol == 'local:' && os.platform() == 'win32') { + let pipePath = '\\\\.\\pipe\\' + u.hostname; + socket.connect(pipePath); +} else if (u.protocol == 'local:') { + socket.connect(u.path); +} else { + throw 'Unknown protocol ' + u.protocol; +} stdin.resume(); stdin.on('data', (chunk: any) => { From ef61a1efa7b3be9ae296ff0a6de48e1bfe8f2269 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 27 Nov 2017 21:26:24 -0500 Subject: [PATCH 46/62] Read just the amount of bytes available --- .../java/sbt/internal/NGWin32NamedPipeSocket.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java index 072ed3901..b22bb6bbf 100644 --- a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java +++ b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java @@ -1,4 +1,5 @@ // Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGWin32NamedPipeSocket.java +// Made change in `read` to read just the amount of bytes available. /* @@ -124,12 +125,10 @@ public class NGWin32NamedPipeSocket extends Socket { int lastError = API.GetLastError(); throw new IOException("GetOverlappedResult() failed for read operation: " + lastError); } - if (read.getValue() != len) { - throw new IOException("ReadFile() read less bytes than requested"); - } - byte[] byteArray = readBuffer.getByteArray(0, len); - System.arraycopy(byteArray, 0, b, off, len); - return len; + int actualLen = read.getValue(); + byte[] byteArray = readBuffer.getByteArray(0, actualLen); + System.arraycopy(byteArray, 0, b, off, actualLen); + return actualLen; } } From ee90917cc4bb0de8294fc4d93633c798a4866ff4 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 28 Nov 2017 12:00:38 +0000 Subject: [PATCH 47/62] Alt scala parser sync workaround Apply retronym's suggestion at https://github.com/scala/bug/issues/10605 instead of the workaround merged in https://github.com/sbt/sbt/pull/3743. --- main/src/main/scala/sbt/internal/parser/SbtParser.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/src/main/scala/sbt/internal/parser/SbtParser.scala b/main/src/main/scala/sbt/internal/parser/SbtParser.scala index b54ac503c..ae8984e91 100644 --- a/main/src/main/scala/sbt/internal/parser/SbtParser.scala +++ b/main/src/main/scala/sbt/internal/parser/SbtParser.scala @@ -109,7 +109,9 @@ private[sbt] object SbtParser { scalacGlobalInitReporter = Some(new ConsoleReporter(settings)) // Mix Positions, otherwise global ignores -Yrangepos - val global = new Global(settings, globalReporter) with Positions + val global = new Global(settings, globalReporter) with Positions { + override protected def synchronizeNames = true // https://github.com/scala/bug/issues/10605 + } val run = new global.Run // Add required dummy unit for initialization... val initFile = new BatchSourceFile("", "") @@ -142,9 +144,7 @@ private[sbt] object SbtParser { val wrapperFile = new BatchSourceFile(reporterId, code) val unit = new CompilationUnit(wrapperFile) val parser = new syntaxAnalyzer.UnitParser(unit) - val parsedTrees = SbtParser.synchronized { // see https://github.com/scala/bug/issues/10605 - parser.templateStats() - } + val parsedTrees = parser.templateStats() parser.accept(scala.tools.nsc.ast.parser.Tokens.EOF) globalReporter.throwParserErrorsIfAny(reporter, filePath) parsedTrees -> reporterId From 9c32c16adee85e8ae5032ebb5140c34c50664053 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 23 Nov 2017 14:58:26 +0000 Subject: [PATCH 48/62] Create RunFromSourceMain & "sbtOn" command --- build.sbt | 9 ++ project/plugins.sbt | 1 + .../test/scala/sbt/RunFromSourceMain.scala | 110 ++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 sbt/src/test/scala/sbt/RunFromSourceMain.scala diff --git a/build.sbt b/build.sbt index 7e4e6d751..0c07705a6 100644 --- a/build.sbt +++ b/build.sbt @@ -376,17 +376,26 @@ lazy val mainProj = (project in file("main")) // with the sole purpose of providing certain identifiers without qualification (with a package object) lazy val sbtProj = (project in file("sbt")) .dependsOn(mainProj, scriptedSbtProj % "test->test") + .enablePlugins(BuildInfoPlugin) .settings( baseSettings, name := "sbt", normalizedName := "sbt", crossScalaVersions := Seq(baseScalaVersion), crossPaths := false, + javaOptions ++= Seq("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"), mimaSettings, mimaBinaryIssueFilters ++= sbtIgnoredProblems, + addBuildInfoToConfig(Test), + buildInfoObject in Test := "TestBuildInfo", + buildInfoKeys in Test := Seq[BuildInfoKey](fullClasspath in Compile), + connectInput in run in Test := true, ) .configure(addSbtCompilerBridge) +commands in Global += Command.single("sbtOn")((state, dir) => + s"sbtProj/test:runMain sbt.RunFromSourceMain $dir" :: state) + lazy val sbtIgnoredProblems = { Seq( // Added more items to Import trait. diff --git a/project/plugins.sbt b/project/plugins.sbt index 2eec0f9bd..2a33ba504 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -11,3 +11,4 @@ addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.1") addSbtPlugin("org.scala-sbt" % "sbt-contraband" % "0.3.0") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0-M1") addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.10") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.7.0") diff --git a/sbt/src/test/scala/sbt/RunFromSourceMain.scala b/sbt/src/test/scala/sbt/RunFromSourceMain.scala new file mode 100644 index 000000000..0394772b3 --- /dev/null +++ b/sbt/src/test/scala/sbt/RunFromSourceMain.scala @@ -0,0 +1,110 @@ +package sbt + +import scala.annotation.tailrec + +import xsbti._ + +object RunFromSourceMain { + private val sbtVersion = "1.0.3" // "dev" + private val scalaVersion = "2.12.4" + + def main(args: Array[String]): Unit = args match { + case Array() => sys.error(s"Must specify working directory as the first argument") + case Array(wd, args @ _*) => run(file(wd), args) + } + + // this arrangement is because Scala does not always properly optimize away + // the tail recursion in a catch statement + @tailrec private def run(baseDir: File, args: Seq[String]): Unit = + runImpl(baseDir, args) match { + case Some((baseDir, args)) => run(baseDir, args) + case None => () + } + + private def runImpl(baseDir: File, args: Seq[String]): Option[(File, Seq[String])] = + try launch(getConf(baseDir, args)) map exit + catch { + case r: xsbti.FullReload => Some((baseDir, r.arguments())) + case scala.util.control.NonFatal(e) => e.printStackTrace(); errorAndExit(e.toString) + } + + @tailrec private def launch(conf: AppConfiguration): Option[Int] = + new xMain().run(conf) match { + case e: xsbti.Exit => Some(e.code) + case _: xsbti.Continue => None + case r: xsbti.Reboot => launch(getConf(conf.baseDirectory(), r.arguments())) + case x => handleUnknownMainResult(x) + } + + private val noGlobalLock = new GlobalLock { + def apply[T](lockFile: File, run: java.util.concurrent.Callable[T]) = run.call() + } + + private def getConf(baseDir: File, args: Seq[String]): AppConfiguration = new AppConfiguration { + def baseDirectory = baseDir + def arguments = args.toArray + def provider = new AppProvider { appProvider => + def scalaProvider = new ScalaProvider { scalaProvider => + def scalaOrg = "org.scala-lang" + def launcher = new Launcher { + def getScala(version: String) = getScala(version, "") + def getScala(version: String, reason: String) = getScala(version, reason, scalaOrg) + def getScala(version: String, reason: String, scalaOrg: String) = scalaProvider + def app(id: xsbti.ApplicationID, version: String) = appProvider + def topLoader = new java.net.URLClassLoader(Array(), null) + def globalLock = noGlobalLock + def bootDirectory = file(sys.props("user.home")) / ".sbt" / "boot" + def ivyRepositories = Array() + def appRepositories = Array() + def isOverrideRepositories = false + def ivyHome = file(sys.props("user.home")) / ".ivy2" + def checksums = Array("sha1", "md5") + } + def version = scalaVersion + def libDir: File = launcher.bootDirectory / s"scala-$version" / "lib" + def jar(name: String): File = libDir / s"$name.jar" + def libraryJar = jar("scala-library") + def compilerJar = jar("scala-compiler") + def jars = libDir.listFiles(f => !f.isDirectory && f.getName.endsWith(".jar")) + def loader = new java.net.URLClassLoader(jars map (_.toURI.toURL), null) + def app(id: xsbti.ApplicationID) = appProvider + } + + def id = ApplicationID( + "org.scala-sbt", + "sbt", + sbtVersion, + "sbt.xMain", + Seq("xsbti", "extra"), + CrossValue.Disabled, + Nil + ) + + def mainClasspath = + buildinfo.TestBuildInfo.fullClasspath.iterator + .map(s => file(s.stripPrefix("Attributed(").stripSuffix(")"))) + .toArray + + def loader = new java.net.URLClassLoader(mainClasspath map (_.toURI.toURL), null) + def entryPoint = classOf[xMain] + def mainClass = classOf[xMain] + def newMain = new xMain + + def components = new ComponentProvider { + def componentLocation(id: String) = ??? + def component(componentID: String) = ??? + def defineComponent(componentID: String, components: Array[File]) = ??? + def addToComponent(componentID: String, components: Array[File]) = ??? + def lockFile = ??? + } + } + } + + private def handleUnknownMainResult(x: MainResult): Nothing = { + val clazz = if (x eq null) "" else " (class: " + x.getClass + ")" + errorAndExit("Invalid main result: " + x + clazz) + } + + private def errorAndExit(msg: String): Nothing = { System.err.println(msg); exit(1) } + private def exit(code: Int): Nothing = System.exit(code).asInstanceOf[Nothing] +} From 73b0034cfc7db7a03f84d6098829f08b0ec862db Mon Sep 17 00:00:00 2001 From: wpopielarski Date: Fri, 20 Oct 2017 17:26:23 +0200 Subject: [PATCH 49/62] textDocument/definition for LSP / VS Code This is an implementation of `textDocument/definition` request. Supports types only, and only in case when type is found in Zinc Analysis. When source(s) are found then editor opens potential source(s). This simple implementation does not use semantic data. During the processing of `textDocument/didSave`, we will start collecting the location of Analysis files via `lspCollectAnalyses`. Later on, when the user asked for `textDocument/definition`, sbt server will invoke a Future call to lspDefinition, which direct reads the files to locate the definition of a class. --- build.sbt | 2 +- main/src/main/scala/sbt/Defaults.scala | 10 +- main/src/main/scala/sbt/Keys.scala | 3 + main/src/main/scala/sbt/Main.scala | 8 +- .../sbt/internal/server/Definition.scala | 301 ++++++++++++++++++ .../server/LanguageServerProtocol.scala | 13 +- .../sbt/internal/server/DefinitionTest.scala | 187 +++++++++++ .../internal/server/SettingQueryTest.scala | 4 +- project/Dependencies.scala | 2 + .../langserver/ServerCapabilities.scala | 24 +- .../langserver/TextDocumentIdentifier.scala | 34 ++ .../TextDocumentPositionParams.scala | 39 +++ .../langserver/codec/JsonProtocol.scala | 2 + .../codec/ServerCapabilitiesFormats.scala | 4 +- .../codec/TextDocumentIdentifierFormats.scala | 27 ++ .../TextDocumentPositionParamsFormats.scala | 29 ++ protocol/src/main/contraband/lsp.contra | 18 ++ 17 files changed, 688 insertions(+), 19 deletions(-) create mode 100644 main/src/main/scala/sbt/internal/server/Definition.scala create mode 100644 main/src/test/scala/sbt/internal/server/DefinitionTest.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentIdentifier.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentIdentifierFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsFormats.scala diff --git a/build.sbt b/build.sbt index 61842c40c..0975b7d88 100644 --- a/build.sbt +++ b/build.sbt @@ -396,7 +396,7 @@ lazy val mainProj = (project in file("main")) .settings( testedBaseSettings, name := "Main", - libraryDependencies ++= scalaXml.value ++ Seq(launcherInterface) ++ log4jDependencies, + libraryDependencies ++= scalaXml.value ++ Seq(launcherInterface) ++ log4jDependencies ++ Seq(scalaCacheCaffeine), managedSourceDirectories in Compile += baseDirectory.value / "src" / "main" / "contraband-scala", sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 7041fe6de..06e6291df 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -26,7 +26,7 @@ import sbt.internal.librarymanagement.mavenint.{ PomExtraDependencyAttributes, SbtPomExtraProperties } -import sbt.internal.server.LanguageServerReporter +import sbt.internal.server.{ LanguageServerReporter, Definition } import sbt.internal.testing.TestLogger import sbt.internal.util._ import sbt.internal.util.Attributed.data @@ -132,6 +132,7 @@ object Defaults extends BuildCommon { Seq( managedDirectory := baseDirectory.value / "lib_managed" )) + import Keys.test private[sbt] lazy val globalCore: Seq[Setting[_]] = globalDefaults( defaultTestTasks(test) ++ defaultTestTasks(testOnly) ++ defaultTestTasks(testQuick) ++ Seq( excludeFilter :== HiddenFileFilter @@ -498,6 +499,7 @@ object Defaults extends BuildCommon { }, compileIncSetup := compileIncSetupTask.value, console := consoleTask.value, + collectAnalyses := Definition.collectAnalysesTask.value, consoleQuick := consoleQuickTask.value, discoveredMainClasses := (compile map discoverMainClasses storeAs discoveredMainClasses xtriggeredBy compile).value, discoveredSbtPlugins := discoverSbtPluginNames.value, @@ -643,6 +645,7 @@ object Defaults extends BuildCommon { testResultLogger :== TestResultLogger.Default, testFilter in testOnly :== (selectedFilter _) )) + import Configurations.Test lazy val testTasks : Seq[Setting[_]] = testTaskOptions(test) ++ testTaskOptions(testOnly) ++ testTaskOptions( testQuick) ++ testDefaults ++ Seq( @@ -1032,6 +1035,7 @@ object Defaults extends BuildCommon { art.value)).asFile } + import Configurations._ def artifactSetting: Initialize[Artifact] = Def.setting { val a = artifact.value @@ -1390,6 +1394,7 @@ object Defaults extends BuildCommon { } analysisResult.analysis } + def compileIncrementalTask = Def.task { // TODO - Should readAnalysis + saveAnalysis be scoped by the compile task too? compileIncrementalTaskImpl(streams.value, (compileInputs in compile).value) @@ -1674,6 +1679,7 @@ object Classpaths { } def defaultPackageKeys = Seq(packageBin, packageSrc, packageDoc) + import Configurations.Test lazy val defaultPackages: Seq[TaskKey[File]] = for (task <- defaultPackageKeys; conf <- Seq(Compile, Test)) yield (task in conf) lazy val defaultArtifactTasks: Seq[TaskKey[File]] = makePom +: defaultPackages @@ -1707,6 +1713,7 @@ object Classpaths { pkgTasks: Seq[TaskKey[_]]): Initialize[Seq[T]] = pkgTasks.map(pkg => key in pkg.scope in pkg).join + import Configurations.Test private[this] def publishGlobalDefaults = Defaults.globalDefaults( Seq( @@ -3200,6 +3207,7 @@ trait BuildExtra extends BuildCommon with DefExtra { * Disables post-compilation hook for determining tests for tab-completion (such as for 'test-only'). * This is useful for reducing test:compile time when not running test. */ + import Configurations.Test def noTestCompletion(config: Configuration = Test): Setting[_] = inConfig(config)(Seq(definedTests := detectTests.value)).head diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 211b46b1b..3df25679d 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -449,6 +449,9 @@ object Keys { val interactionService = taskKey[InteractionService]("Service used to ask for user input through the current user interface(s).").withRank(CTask) val insideCI = SettingKey[Boolean]("insideCI", "Determines if the SBT is running in a Continuous Integration environment", AMinusSetting) + // sbt server internal + val collectAnalyses = taskKey[Unit]("Collect analysis file locations for later use.") + // special val sessionVars = AttributeKey[SessionVar.Map]("sessionVars", "Bindings that exist for the duration of the session.", Invisible) val parallelExecution = settingKey[Boolean]("Enables (true) or disables (false) parallel execution of tasks.").withRank(BMinusSetting) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 61fe64cd0..09d668c4b 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -101,6 +101,9 @@ final class ConsoleMain extends xsbti.AppMain { object StandardMain { private[sbt] lazy val exchange = new CommandExchange() + import scalacache._ + import scalacache.caffeine._ + private[sbt] val cache: Cache[Any] = CaffeineCache[Any] def runManaged(s: State): xsbti.MainResult = { val previous = TrapExit.installManager() @@ -132,6 +135,9 @@ object StandardMain { Exec(x, None) } val initAttrs = BuiltinCommands.initialAttributes + import scalacache.modes.scalaFuture._ + import scala.concurrent.ExecutionContext.Implicits.global + cache.removeAll() val s = State( configuration, initialDefinitions, @@ -157,7 +163,7 @@ import TemplateCommandUtil.templateCommand object BuiltinCommands { def initialAttributes = AttributeMap.empty - + import BasicCommands.exit def ConsoleCommands: Seq[Command] = Seq(ignore, exit, IvyConsole.command, setLogLevel, early, act, nop) diff --git a/main/src/main/scala/sbt/internal/server/Definition.scala b/main/src/main/scala/sbt/internal/server/Definition.scala new file mode 100644 index 000000000..52dc7e397 --- /dev/null +++ b/main/src/main/scala/sbt/internal/server/Definition.scala @@ -0,0 +1,301 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + +package sbt +package internal +package server + +import sbt.internal.inc.MixedAnalyzingCompiler +import sbt.internal.langserver.ErrorCodes +import sbt.util.Logger +import scala.annotation.tailrec +import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.duration.Duration.Inf +import scala.util.matching.Regex.MatchIterator +import java.nio.file.{ Files, Paths } +import sbt.StandardMain + +object Definition { + import java.net.URI + import Keys._ + import sbt.internal.inc.Analysis + import sbt.internal.inc.JavaInterfaceUtil._ + val AnalysesKey = "lsp.definition.analyses.key" + + import sjsonnew.JsonFormat + def send[A: JsonFormat](source: CommandSource, execId: String)(params: A): Unit = { + for { + channel <- StandardMain.exchange.channels.collectFirst { + case c if c.name == source.channelName => c + } + } yield { + channel.publishEvent(params, Option(execId)) + } + } + + object textProcessor { + private val isIdentifier = { + import scala.tools.reflect.{ ToolBox, ToolBoxError } + lazy val tb = + scala.reflect.runtime.universe + .runtimeMirror(this.getClass.getClassLoader) + .mkToolBox() + import tb._ + lazy val check = parse _ andThen compile _ + (identifier: String) => + try { + check(s"val $identifier = 0; val ${identifier}${identifier} = $identifier") + true + } catch { + case _: ToolBoxError => false + } + } + + def identifier(line: String, point: Int): Option[String] = { + val whiteSpaceReg = "\\s+".r + val (zero, end) = fold(Seq.empty)(whiteSpaceReg.findAllIn(line)) + .collect { + case (white, ind) => (ind, ind + white.length) + } + .fold((0, line.length)) { (z, e) => + val (from, to) = e + val (left, right) = z + (if (to > left && to <= point) to else left, + if (from < right && from >= point) from else right) + } + val ranges = for { + from <- zero to point + to <- point to end + } yield (from -> to) + ranges + .sortBy { + case (from, to) => -(to - from) + } + .foldLeft(Seq.empty[String]) { (z, e) => + val (from, to) = e + val fragment = line.slice(from, to).trim + z match { + case Nil if fragment.nonEmpty && isIdentifier(fragment) => fragment +: z + case h +: _ if h.length < fragment.length && isIdentifier(fragment) => + Seq(fragment) + case h +: _ if h.length == fragment.length && isIdentifier(fragment) => + fragment +: z + case z => z + } + } + .headOption + } + + private def asClassObjectIdentifier(sym: String) = + Seq(s".$sym", s".$sym$$", s"$$$sym", s"$$$sym$$") + def potentialClsOrTraitOrObj(sym: String): PartialFunction[String, String] = { + import scala.reflect.NameTransformer + val encodedSym = NameTransformer.encode(sym) + val action: PartialFunction[String, String] = { + case potentialClassOrTraitOrObject + if asClassObjectIdentifier(encodedSym).exists(potentialClassOrTraitOrObject.endsWith) || + encodedSym == potentialClassOrTraitOrObject || + s"$encodedSym$$" == potentialClassOrTraitOrObject => + potentialClassOrTraitOrObject + } + action + } + + @tailrec + private def fold(z: Seq[(String, Int)])(it: MatchIterator): Seq[(String, Int)] = { + if (!it.hasNext) z + else fold(z :+ (it.next() -> it.start))(it) + } + + def classTraitObjectInLine(sym: String)(line: String): Seq[(String, Int)] = { + import scala.util.matching.Regex.quote + val potentials = + Seq(s"object +${quote(sym)}".r, + s"trait +${quote(sym)} *\\[?".r, + s"class +${quote(sym)} *\\[?".r) + potentials + .flatMap { reg => + fold(Seq.empty)(reg.findAllIn(line)) + } + .collect { + case (name, pos) => + (if (name.endsWith("[")) name.init.trim else name.trim) -> pos + } + } + + import java.io.File + def markPosition(file: File, sym: String): Seq[(File, Long, Long, Long)] = { + import java.nio.file._ + import scala.collection.JavaConverters._ + val findInLine = classTraitObjectInLine(sym)(_) + Files + .lines(file.toPath) + .iterator + .asScala + .zipWithIndex + .flatMap { + case (line, lineNumber) => + findInLine(line) + .collect { + case (sym, from) => + (file, lineNumber.toLong, from.toLong, from.toLong + sym.length) + } + } + .toSeq + .distinct + } + } + + import sbt.internal.langserver.TextDocumentPositionParams + import sjsonnew.shaded.scalajson.ast.unsafe.JValue + private def getDefinition(jsonDefinition: JValue): Option[TextDocumentPositionParams] = { + import sbt.internal.langserver.codec.JsonProtocol._ + import sjsonnew.support.scalajson.unsafe.Converter + Converter.fromJson[TextDocumentPositionParams](jsonDefinition).toOption + } + + import java.io.File + private def storeAnalysis(cacheFile: File, useBinary: Boolean): Option[Analysis] = + MixedAnalyzingCompiler + .staticCachedStore(cacheFile, !useBinary) + .get + .toOption + .collect { + case contents => + contents.getAnalysis + } + .collect { + case a: Analysis => a + } + + import scalacache._ + private[sbt] def updateCache[F[_]](cache: Cache[Any])(cacheFile: String, useBinary: Boolean)( + implicit + mode: Mode[F], + flags: Flags): F[Any] = { + mode.M.flatMap(cache.get(AnalysesKey)) { + case None => + cache.put(AnalysesKey)(Set(cacheFile -> useBinary -> None), Option(Inf)) + case Some(set) => + cache.put(AnalysesKey)( + set.asInstanceOf[Set[((String, Boolean), Option[Analysis])]].filterNot { + case ((file, _), _) => file == cacheFile + } + (cacheFile -> useBinary -> None), + Option(Inf)) + case _ => mode.M.pure(()) + } + } + + def collectAnalysesTask = Def.task { + val cacheFile = compileIncSetup.value.cacheFile.getAbsolutePath + val useBinary = enableBinaryCompileAnalysis.value + val s = state.value + s.log.debug(s"analysis location ${(cacheFile -> useBinary)}") + import scalacache.modes.sync._ + updateCache(StandardMain.cache)(cacheFile, useBinary) + } + + private[sbt] def getAnalyses(log: Logger): Future[Seq[Analysis]] = { + import scalacache.modes.scalaFuture._ + import scala.concurrent.ExecutionContext.Implicits.global + StandardMain.cache + .get(AnalysesKey) + .collect { + case Some(a) => a.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + } + .map { caches => + val (working, uninitialized) = caches.partition { cacheAnalysis => + cacheAnalysis._2 != None + } + val addToCache = uninitialized.collect { + case (title @ (file, useBinary), _) if Files.exists(Paths.get(file)) => + (title, storeAnalysis(Paths.get(file).toFile, !useBinary)) + } + val validCaches = working ++ addToCache + if (addToCache.nonEmpty) + StandardMain.cache.put(AnalysesKey)(validCaches, Option(Inf)) + validCaches.toSeq.collect { + case (_, Some(analysis)) => + analysis + } + } + } + + def lspDefinition(jsonDefinition: JValue, + requestId: String, + commandSource: CommandSource, + log: Logger)(implicit ec: ExecutionContext): Future[Unit] = Future { + val LspDefinitionLogHead = "lsp-definition" + import sjsonnew.support.scalajson.unsafe.CompactPrinter + log.debug(s"$LspDefinitionLogHead json request: ${CompactPrinter(jsonDefinition)}") + lazy val analyses = getAnalyses(log) + val definition = getDefinition(jsonDefinition) + definition + .flatMap { definition => + val uri = URI.create(definition.textDocument.uri) + import java.nio.file._ + Files + .lines(Paths.get(uri)) + .skip(definition.position.line) + .findFirst + .toOption + .flatMap { line => + log.debug(s"$LspDefinitionLogHead found line: $line") + textProcessor + .identifier(line, definition.position.character.toInt) + } + } + .map { sym => + log.debug(s"symbol $sym") + analyses + .map { analyses => + val locations = analyses.par.flatMap { analysis => + val selectPotentials = textProcessor.potentialClsOrTraitOrObj(sym) + val classes = + (analysis.apis.allInternalClasses ++ analysis.apis.allExternals).collect { + selectPotentials + } + log.debug(s"$LspDefinitionLogHead potentials: $classes") + classes + .flatMap { className => + analysis.relations.definesClass(className) ++ analysis.relations + .libraryDefinesClass(className) + } + .flatMap { classFile => + textProcessor.markPosition(classFile, sym).collect { + case (file, line, from, to) => + import sbt.internal.langserver.{ Location, Position, Range } + Location(file.toURI.toURL.toString, + Range(Position(line, from), Position(line, to))) + } + } + }.seq + log.debug(s"$LspDefinitionLogHead locations ${locations}") + import sbt.internal.langserver.codec.JsonProtocol._ + send(commandSource, requestId)(locations.toArray) + } + .recover { + case anyException @ _ => + log.warn( + s"Problem with processing analyses $anyException for ${CompactPrinter(jsonDefinition)}") + import sbt.internal.protocol.JsonRpcResponseError + import sbt.internal.protocol.codec.JsonRPCProtocol._ + send(commandSource, requestId)( + JsonRpcResponseError(ErrorCodes.InternalError, + "Problem with processing analyses.", + None)) + } + } + .orElse { + log.info(s"Symbol not found in definition request ${CompactPrinter(jsonDefinition)}") + import sbt.internal.langserver.Location + import sbt.internal.langserver.codec.JsonProtocol._ + send(commandSource, requestId)(Array.empty[Location]) + None + } + } +} diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index 776078ae6..5c18b926b 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -35,10 +35,8 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { protected def onSettingQuery(execId: Option[String], req: Q): Unit protected def onRequestMessage(request: JsonRpcRequestMessage): Unit = { - import sbt.internal.langserver.codec.JsonProtocol._ import internalJsonProtocol._ - def json = request.params.getOrElse( throw LangServerError(ErrorCodes.InvalidParams, @@ -57,9 +55,13 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { else throw LangServerError(ErrorCodes.InvalidRequest, "invalid token") } else () setInitialized(true) + append(Exec(s"collectAnalyses", Some(request.id), Some(CommandSource(name)))) langRespond(InitializeResult(serverCapabilities), Option(request.id)) case "textDocument/didSave" => - append(Exec("compile", Some(request.id), Some(CommandSource(name)))) + append(Exec(";compile; collectAnalyses", Some(request.id), Some(CommandSource(name)))) + case "textDocument/definition" => + import scala.concurrent.ExecutionContext.Implicits.global + Definition.lspDefinition(json, request.id, CommandSource(name), log) case "sbt/exec" => val param = Converter.fromJson[SbtExecParams](json).get append(Exec(param.commandLine, Some(request.id), Some(CommandSource(name)))) @@ -68,7 +70,7 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { val param = Converter.fromJson[Q](json).get onSettingQuery(Option(request.id), param) } - case _ => () + case unhandledRequest => log.debug(s"Unhandled request received: $unhandledRequest") } } @@ -149,6 +151,7 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { private[sbt] lazy val serverCapabilities: ServerCapabilities = { ServerCapabilities(textDocumentSync = TextDocumentSyncOptions(true, 0, false, false, SaveOptions(false)), - hoverProvider = false) + hoverProvider = false, + definitionProvider = true) } } diff --git a/main/src/test/scala/sbt/internal/server/DefinitionTest.scala b/main/src/test/scala/sbt/internal/server/DefinitionTest.scala new file mode 100644 index 000000000..6095c8ce7 --- /dev/null +++ b/main/src/test/scala/sbt/internal/server/DefinitionTest.scala @@ -0,0 +1,187 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + +package sbt +package internal +package server + +import sbt.internal.inc.Analysis + +class DefinitionTest extends org.specs2.mutable.Specification { + import Definition.textProcessor + + "text processor" should { + "find valid standard scala identifier when caret is set at the start of it" in { + textProcessor.identifier("val identifier = 0", 4) must beSome("identifier") + } + "not find valid standard scala identifier because it is '='" in { + textProcessor.identifier("val identifier = 0", 15) must beNone + } + "find valid standard scala identifier when caret is set in the middle of it" in { + textProcessor.identifier("val identifier = 0", 11) must beSome("identifier") + } + "find valid standard scala identifier with comma" in { + textProcessor.identifier("def foo(a: identifier, b: other) = ???", 13) must beSome( + "identifier") + } + "find valid standard short scala identifier when caret is set at the start of it" in { + textProcessor.identifier("val a = 0", 4) must beSome("a") + } + "find valid standard short scala identifier when caret is set at the end of it" in { + textProcessor.identifier("def foo(f: Int) = Foo(f)", 19) must beSome("Foo") + } + "find valid non-standard short scala identifier when caret is set at the start of it" in { + textProcessor.identifier("val == = 0", 4) must beSome("==") + } + "find valid non-standard short scala identifier when caret is set in the middle of it" in { + textProcessor.identifier("val == = 0", 5) must beSome("==") + } + "find valid non-standard short scala identifier when caret is set at the end of it" in { + textProcessor.identifier("val == = 0", 6) must beSome("==") + } + "choose longest valid standard scala identifier from scala keyword when caret is set at the start of it" in { + textProcessor.identifier("val k = 0", 0) must beSome("va") or beSome("al") + } + "choose longest valid standard scala identifier from scala keyword when caret is set in the middle of it" in { + textProcessor.identifier("val k = 0", 1) must beSome("va") or beSome("al") + } + "match symbol as class name" in { + textProcessor.potentialClsOrTraitOrObj("A")("com.acme.A") must be_==("com.acme.A") + } + "match symbol as object name" in { + textProcessor.potentialClsOrTraitOrObj("A")("com.acme.A$") must be_==("com.acme.A$") + } + "match symbol as inner class name" in { + textProcessor.potentialClsOrTraitOrObj("A")("com.acme.A$A") must be_==("com.acme.A$A") + } + "match symbol as inner object name" in { + textProcessor.potentialClsOrTraitOrObj("A")("com.acme.A$A$") must be_==("com.acme.A$A$") + } + "match symbol as default package class name" in { + textProcessor.potentialClsOrTraitOrObj("A")("A") must be_==("A") + } + "match symbol as default package object name" in { + textProcessor.potentialClsOrTraitOrObj("A")("A$") must be_==("A$") + } + "match object in line version 1" in { + textProcessor.classTraitObjectInLine("A")(" object A ") must contain(("object A", 3)) + } + "match object in line version 2" in { + textProcessor.classTraitObjectInLine("A")(" object A ") must contain(("object A", 3)) + } + "match object in line version 3" in { + textProcessor.classTraitObjectInLine("A")("object A {") must contain(("object A", 0)) + } + "not match object in line" in { + textProcessor.classTraitObjectInLine("B")("object A ") must be empty + } + "match class in line version 1" in { + textProcessor.classTraitObjectInLine("A")(" class A ") must contain(("class A", 3)) + } + "match class in line version 2" in { + textProcessor.classTraitObjectInLine("A")(" class A ") must contain(("class A", 3)) + } + "match class in line version 3" in { + textProcessor.classTraitObjectInLine("A")("class A {") must contain(("class A", 0)) + } + "match class in line version 4" in { + textProcessor.classTraitObjectInLine("A")(" class A[A] ") must contain( + ("class A", 3)) + } + "match class in line version 5" in { + textProcessor.classTraitObjectInLine("A")(" class A [A] ") must contain( + ("class A", 3)) + } + "match class in line version 6" in { + textProcessor.classTraitObjectInLine("A")("class A[A[_]] {") must contain(("class A", 0)) + } + "not match class in line" in { + textProcessor.classTraitObjectInLine("B")("class A ") must be empty + } + "match trait in line version 1" in { + textProcessor.classTraitObjectInLine("A")(" trait A ") must contain(("trait A", 3)) + } + "match trait in line version 2" in { + textProcessor.classTraitObjectInLine("A")(" trait A ") must contain(("trait A", 3)) + } + "match trait in line version 3" in { + textProcessor.classTraitObjectInLine("A")("trait A {") must contain(("trait A", 0)) + } + "match trait in line version 4" in { + textProcessor.classTraitObjectInLine("A")(" trait A[A] ") must contain( + ("trait A", 3)) + } + "match trait in line version 5" in { + textProcessor.classTraitObjectInLine("A")(" trait A [A] ") must contain( + ("trait A", 3)) + } + "match trait in line version 6" in { + textProcessor.classTraitObjectInLine("A")("trait A[A[_]] {") must contain(("trait A", 0)) + } + "not match trait in line" in { + textProcessor.classTraitObjectInLine("B")("trait A ") must be empty + } + } + "definition" should { + "cache data in cache" in { + import scalacache.caffeine._ + val cache = CaffeineCache[Any] + val cacheFile = "Test.scala" + val useBinary = true + + import scalacache.modes.scalaFuture._ + val actual = Definition + .updateCache(cache)(cacheFile, useBinary) + .flatMap(_ => cache.get(Definition.AnalysesKey)) + + actual.collect { + case Some(s) => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + } should contain[((String, Boolean), Option[Analysis])]("Test.scala" -> true -> None).await + } + "replace cache data in cache" in { + import scalacache.caffeine._ + val cache = CaffeineCache[Any] + val cacheFile = "Test.scala" + val useBinary = true + val falseUseBinary = false + + import scalacache.modes.scalaFuture._ + val actual = Definition + .updateCache(cache)(cacheFile, falseUseBinary) + .flatMap { _ => + Definition.updateCache(cache)(cacheFile, useBinary) + } + .flatMap(_ => cache.get(Definition.AnalysesKey)) + + actual.collect { + case Some(s) => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + } should contain[((String, Boolean), Option[Analysis])]("Test.scala" -> true -> None).await + } + "cache more data in cache" in { + import scalacache.caffeine._ + val cache = CaffeineCache[Any] + val cacheFile = "Test.scala" + val useBinary = true + val otherCacheFile = "OtherTest.scala" + val otherUseBinary = false + + import scalacache.modes.scalaFuture._ + val actual = Definition + .updateCache(cache)(otherCacheFile, otherUseBinary) + .flatMap { _ => + Definition.updateCache(cache)(cacheFile, useBinary) + } + .flatMap(_ => cache.get(Definition.AnalysesKey)) + + actual.collect { + case Some(s) => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + } should contain[((String, Boolean), Option[Analysis])]( + "Test.scala" -> true -> None, + "OtherTest.scala" -> false -> None).await + } + } +} diff --git a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala index 7acf7955f..bcb2e0d6d 100644 --- a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala +++ b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala @@ -161,11 +161,11 @@ object SettingQueryTest extends org.specs2.mutable.Specification { val settings: Seq[Setting[_]] = finalTransforms( buildConfigurations(loadedBuild, getRootProject(units), config.injectSettings)) - val delegates: Scope => Seq[Scope] = defaultDelegates(loadedBuild) + val delegates = defaultDelegates(loadedBuild) val scopeLocal: ScopeLocal = EvaluateTask.injectStreams val display: Show[ScopedKey[_]] = Project showLoadingKey loadedBuild - val data: Settings[Scope] = Def.make(settings)(delegates, scopeLocal, display) + val data = Def.make(settings)(delegates, scopeLocal, display) val extra: KeyIndex => BuildUtil[_] = index => BuildUtil(baseUri, units, index, data) val index: StructureIndex = structureIndex(data, settings, extra, units) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c9f430e17..9a0339a1e 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -125,4 +125,6 @@ object Dependencies { val log4jSlf4jImpl = "org.apache.logging.log4j" % "log4j-slf4j-impl" % log4jVersion // specify all of log4j modules to prevent misalignment val log4jDependencies = Vector(log4jApi, log4jCore, log4jSlf4jImpl) + + val scalaCacheCaffeine = "com.github.cb372" %% "scalacache-caffeine" % "0.20.0" } diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/ServerCapabilities.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/ServerCapabilities.scala index c40b895e5..802428214 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/langserver/ServerCapabilities.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/ServerCapabilities.scala @@ -7,22 +7,24 @@ package sbt.internal.langserver final class ServerCapabilities private ( val textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions], /** The server provides hover support. */ - val hoverProvider: Option[Boolean]) extends Serializable { + val hoverProvider: Option[Boolean], + /** Goto definition */ + val definitionProvider: Option[Boolean]) extends Serializable { override def equals(o: Any): Boolean = o match { - case x: ServerCapabilities => (this.textDocumentSync == x.textDocumentSync) && (this.hoverProvider == x.hoverProvider) + case x: ServerCapabilities => (this.textDocumentSync == x.textDocumentSync) && (this.hoverProvider == x.hoverProvider) && (this.definitionProvider == x.definitionProvider) case _ => false } override def hashCode: Int = { - 37 * (37 * (37 * (17 + "sbt.internal.langserver.ServerCapabilities".##) + textDocumentSync.##) + hoverProvider.##) + 37 * (37 * (37 * (37 * (17 + "sbt.internal.langserver.ServerCapabilities".##) + textDocumentSync.##) + hoverProvider.##) + definitionProvider.##) } override def toString: String = { - "ServerCapabilities(" + textDocumentSync + ", " + hoverProvider + ")" + "ServerCapabilities(" + textDocumentSync + ", " + hoverProvider + ", " + definitionProvider + ")" } - protected[this] def copy(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions] = textDocumentSync, hoverProvider: Option[Boolean] = hoverProvider): ServerCapabilities = { - new ServerCapabilities(textDocumentSync, hoverProvider) + protected[this] def copy(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions] = textDocumentSync, hoverProvider: Option[Boolean] = hoverProvider, definitionProvider: Option[Boolean] = definitionProvider): ServerCapabilities = { + new ServerCapabilities(textDocumentSync, hoverProvider, definitionProvider) } def withTextDocumentSync(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions]): ServerCapabilities = { copy(textDocumentSync = textDocumentSync) @@ -36,9 +38,15 @@ final class ServerCapabilities private ( def withHoverProvider(hoverProvider: Boolean): ServerCapabilities = { copy(hoverProvider = Option(hoverProvider)) } + def withDefinitionProvider(definitionProvider: Option[Boolean]): ServerCapabilities = { + copy(definitionProvider = definitionProvider) + } + def withDefinitionProvider(definitionProvider: Boolean): ServerCapabilities = { + copy(definitionProvider = Option(definitionProvider)) + } } object ServerCapabilities { - def apply(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions], hoverProvider: Option[Boolean]): ServerCapabilities = new ServerCapabilities(textDocumentSync, hoverProvider) - def apply(textDocumentSync: sbt.internal.langserver.TextDocumentSyncOptions, hoverProvider: Boolean): ServerCapabilities = new ServerCapabilities(Option(textDocumentSync), Option(hoverProvider)) + def apply(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions], hoverProvider: Option[Boolean], definitionProvider: Option[Boolean]): ServerCapabilities = new ServerCapabilities(textDocumentSync, hoverProvider, definitionProvider) + def apply(textDocumentSync: sbt.internal.langserver.TextDocumentSyncOptions, hoverProvider: Boolean, definitionProvider: Boolean): ServerCapabilities = new ServerCapabilities(Option(textDocumentSync), Option(hoverProvider), Option(definitionProvider)) } diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentIdentifier.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentIdentifier.scala new file mode 100644 index 000000000..bb29b3d7f --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentIdentifier.scala @@ -0,0 +1,34 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver +/** Text documents are identified using a URI. On the protocol level, URIs are passed as strings. */ +final class TextDocumentIdentifier private ( + /** The text document's URI. */ + val uri: String) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TextDocumentIdentifier => (this.uri == x.uri) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.internal.langserver.TextDocumentIdentifier".##) + uri.##) + } + override def toString: String = { + "TextDocumentIdentifier(" + uri + ")" + } + protected[this] def copy(uri: String = uri): TextDocumentIdentifier = { + new TextDocumentIdentifier(uri) + } + def withUri(uri: String): TextDocumentIdentifier = { + copy(uri = uri) + } +} +object TextDocumentIdentifier { + + def apply(uri: String): TextDocumentIdentifier = new TextDocumentIdentifier(uri) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala new file mode 100644 index 000000000..5d7d3edd8 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala @@ -0,0 +1,39 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver +/** Goto definition params model */ +final class TextDocumentPositionParams private ( + /** The text document. */ + val textDocument: sbt.internal.langserver.TextDocumentIdentifier, + /** The position inside the text document. */ + val position: sbt.internal.langserver.Position) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TextDocumentPositionParams => (this.textDocument == x.textDocument) && (this.position == x.position) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.langserver.TextDocumentPositionParams".##) + textDocument.##) + position.##) + } + override def toString: String = { + "TextDocumentPositionParams(" + textDocument + ", " + position + ")" + } + protected[this] def copy(textDocument: sbt.internal.langserver.TextDocumentIdentifier = textDocument, position: sbt.internal.langserver.Position = position): TextDocumentPositionParams = { + new TextDocumentPositionParams(textDocument, position) + } + def withTextDocument(textDocument: sbt.internal.langserver.TextDocumentIdentifier): TextDocumentPositionParams = { + copy(textDocument = textDocument) + } + def withPosition(position: sbt.internal.langserver.Position): TextDocumentPositionParams = { + copy(position = position) + } +} +object TextDocumentPositionParams { + + def apply(textDocument: sbt.internal.langserver.TextDocumentIdentifier, position: sbt.internal.langserver.Position): TextDocumentPositionParams = new TextDocumentPositionParams(textDocument, position) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala index f946496ec..7898d1d72 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala @@ -19,4 +19,6 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.langserver.codec.LogMessageParamsFormats with sbt.internal.langserver.codec.PublishDiagnosticsParamsFormats with sbt.internal.langserver.codec.SbtExecParamsFormats + with sbt.internal.langserver.codec.TextDocumentIdentifierFormats + with sbt.internal.langserver.codec.TextDocumentPositionParamsFormats object JsonProtocol extends JsonProtocol \ No newline at end of file diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/ServerCapabilitiesFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/ServerCapabilitiesFormats.scala index 4bac79256..028865f7f 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/ServerCapabilitiesFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/ServerCapabilitiesFormats.scala @@ -13,8 +13,9 @@ implicit lazy val ServerCapabilitiesFormat: JsonFormat[sbt.internal.langserver.S unbuilder.beginObject(js) val textDocumentSync = unbuilder.readField[Option[sbt.internal.langserver.TextDocumentSyncOptions]]("textDocumentSync") val hoverProvider = unbuilder.readField[Option[Boolean]]("hoverProvider") + val definitionProvider = unbuilder.readField[Option[Boolean]]("definitionProvider") unbuilder.endObject() - sbt.internal.langserver.ServerCapabilities(textDocumentSync, hoverProvider) + sbt.internal.langserver.ServerCapabilities(textDocumentSync, hoverProvider, definitionProvider) case None => deserializationError("Expected JsObject but found None") } @@ -23,6 +24,7 @@ implicit lazy val ServerCapabilitiesFormat: JsonFormat[sbt.internal.langserver.S builder.beginObject() builder.addField("textDocumentSync", obj.textDocumentSync) builder.addField("hoverProvider", obj.hoverProvider) + builder.addField("definitionProvider", obj.definitionProvider) builder.endObject() } } diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentIdentifierFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentIdentifierFormats.scala new file mode 100644 index 000000000..fd093e0cf --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentIdentifierFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TextDocumentIdentifierFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TextDocumentIdentifierFormat: JsonFormat[sbt.internal.langserver.TextDocumentIdentifier] = new JsonFormat[sbt.internal.langserver.TextDocumentIdentifier] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.TextDocumentIdentifier = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val uri = unbuilder.readField[String]("uri") + unbuilder.endObject() + sbt.internal.langserver.TextDocumentIdentifier(uri) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.langserver.TextDocumentIdentifier, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("uri", obj.uri) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsFormats.scala new file mode 100644 index 000000000..87f231e91 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TextDocumentPositionParamsFormats { self: sbt.internal.langserver.codec.TextDocumentIdentifierFormats with sbt.internal.langserver.codec.PositionFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val TextDocumentPositionParamsFormat: JsonFormat[sbt.internal.langserver.TextDocumentPositionParams] = new JsonFormat[sbt.internal.langserver.TextDocumentPositionParams] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.TextDocumentPositionParams = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val textDocument = unbuilder.readField[sbt.internal.langserver.TextDocumentIdentifier]("textDocument") + val position = unbuilder.readField[sbt.internal.langserver.Position]("position") + unbuilder.endObject() + sbt.internal.langserver.TextDocumentPositionParams(textDocument, position) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.langserver.TextDocumentPositionParams, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("textDocument", obj.textDocument) + builder.addField("position", obj.position) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband/lsp.contra b/protocol/src/main/contraband/lsp.contra index 18bd0a42c..a46d27d1d 100644 --- a/protocol/src/main/contraband/lsp.contra +++ b/protocol/src/main/contraband/lsp.contra @@ -83,6 +83,9 @@ type ServerCapabilities { ## The server provides hover support. hoverProvider: Boolean + + ## Goto definition + definitionProvider: Boolean } type TextDocumentSyncOptions { @@ -127,3 +130,18 @@ type PublishDiagnosticsParams { type SbtExecParams { commandLine: String! } + +## Goto definition params model +type TextDocumentPositionParams { + ## The text document. + textDocument: sbt.internal.langserver.TextDocumentIdentifier! + + ## The position inside the text document. + position: sbt.internal.langserver.Position! +} + +## Text documents are identified using a URI. On the protocol level, URIs are passed as strings. +type TextDocumentIdentifier { + ## The text document's URI. + uri: String! +} From 452e97e41dc3e283314e6493c5f34d4bcc46427d Mon Sep 17 00:00:00 2001 From: wpopielarski Date: Mon, 27 Nov 2017 15:04:37 +0100 Subject: [PATCH 50/62] Adds backticks to class/trait/object name. Adapts tests to changed specs2 dependency. Tiny fixes. Removes Scala IDE compiler clues. --- main/src/main/scala/sbt/Defaults.scala | 7 --- .../sbt/internal/server/Definition.scala | 42 +++++++++++++--- .../sbt/internal/server/DefinitionTest.scala | 48 ++++++++----------- .../internal/server/SettingQueryTest.scala | 4 +- 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 06e6291df..d40467dff 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -132,7 +132,6 @@ object Defaults extends BuildCommon { Seq( managedDirectory := baseDirectory.value / "lib_managed" )) - import Keys.test private[sbt] lazy val globalCore: Seq[Setting[_]] = globalDefaults( defaultTestTasks(test) ++ defaultTestTasks(testOnly) ++ defaultTestTasks(testQuick) ++ Seq( excludeFilter :== HiddenFileFilter @@ -645,7 +644,6 @@ object Defaults extends BuildCommon { testResultLogger :== TestResultLogger.Default, testFilter in testOnly :== (selectedFilter _) )) - import Configurations.Test lazy val testTasks : Seq[Setting[_]] = testTaskOptions(test) ++ testTaskOptions(testOnly) ++ testTaskOptions( testQuick) ++ testDefaults ++ Seq( @@ -1035,7 +1033,6 @@ object Defaults extends BuildCommon { art.value)).asFile } - import Configurations._ def artifactSetting: Initialize[Artifact] = Def.setting { val a = artifact.value @@ -1394,7 +1391,6 @@ object Defaults extends BuildCommon { } analysisResult.analysis } - def compileIncrementalTask = Def.task { // TODO - Should readAnalysis + saveAnalysis be scoped by the compile task too? compileIncrementalTaskImpl(streams.value, (compileInputs in compile).value) @@ -1679,7 +1675,6 @@ object Classpaths { } def defaultPackageKeys = Seq(packageBin, packageSrc, packageDoc) - import Configurations.Test lazy val defaultPackages: Seq[TaskKey[File]] = for (task <- defaultPackageKeys; conf <- Seq(Compile, Test)) yield (task in conf) lazy val defaultArtifactTasks: Seq[TaskKey[File]] = makePom +: defaultPackages @@ -1713,7 +1708,6 @@ object Classpaths { pkgTasks: Seq[TaskKey[_]]): Initialize[Seq[T]] = pkgTasks.map(pkg => key in pkg.scope in pkg).join - import Configurations.Test private[this] def publishGlobalDefaults = Defaults.globalDefaults( Seq( @@ -3207,7 +3201,6 @@ trait BuildExtra extends BuildCommon with DefExtra { * Disables post-compilation hook for determining tests for tab-completion (such as for 'test-only'). * This is useful for reducing test:compile time when not running test. */ - import Configurations.Test def noTestCompletion(config: Configuration = Test): Setting[_] = inConfig(config)(Seq(definedTests := detectTests.value)).head diff --git a/main/src/main/scala/sbt/internal/server/Definition.scala b/main/src/main/scala/sbt/internal/server/Definition.scala index 52dc7e397..4459861db 100644 --- a/main/src/main/scala/sbt/internal/server/Definition.scala +++ b/main/src/main/scala/sbt/internal/server/Definition.scala @@ -19,7 +19,7 @@ import scala.util.matching.Regex.MatchIterator import java.nio.file.{ Files, Paths } import sbt.StandardMain -object Definition { +private[sbt] object Definition { import java.net.URI import Keys._ import sbt.internal.inc.Analysis @@ -55,8 +55,33 @@ object Definition { } } - def identifier(line: String, point: Int): Option[String] = { - val whiteSpaceReg = "\\s+".r + private def findInBackticks(line: String, point: Int): Option[String] = { + val (even, odd) = line.zipWithIndex + .collect { + case (char, backtickIndex) if char == '`' => + backtickIndex + } + .zipWithIndex + .partition { bs => + val (_, index) = bs + index % 2 == 0 + } + even + .collect { + case (backtickIndex, _) => backtickIndex + } + .zip { + odd.collect { + case (backtickIndex, _) => backtickIndex + 1 + } + } + .collectFirst { + case (from, to) if from <= point && point < to => line.slice(from, to) + } + } + + def identifier(line: String, point: Int): Option[String] = findInBackticks(line, point).orElse { + val whiteSpaceReg = "(\\s|\\.)+".r val (zero, end) = fold(Seq.empty)(whiteSpaceReg.findAllIn(line)) .collect { case (white, ind) => (ind, ind + white.length) @@ -94,7 +119,10 @@ object Definition { Seq(s".$sym", s".$sym$$", s"$$$sym", s"$$$sym$$") def potentialClsOrTraitOrObj(sym: String): PartialFunction[String, String] = { import scala.reflect.NameTransformer - val encodedSym = NameTransformer.encode(sym) + val encodedSym = NameTransformer.encode(sym.toSeq match { + case '`' +: body :+ '`' => body.mkString + case noBackticked => noBackticked.mkString + }) val action: PartialFunction[String, String] = { case potentialClassOrTraitOrObject if asClassObjectIdentifier(encodedSym).exists(potentialClassOrTraitOrObject.endsWith) || @@ -114,9 +142,9 @@ object Definition { def classTraitObjectInLine(sym: String)(line: String): Seq[(String, Int)] = { import scala.util.matching.Regex.quote val potentials = - Seq(s"object +${quote(sym)}".r, - s"trait +${quote(sym)} *\\[?".r, - s"class +${quote(sym)} *\\[?".r) + Seq(s"object\\s+${quote(sym)}".r, + s"trait\\s+${quote(sym)} *\\[?".r, + s"class\\s+${quote(sym)} *\\[?".r) potentials .flatMap { reg => fold(Seq.empty)(reg.findAllIn(line)) diff --git a/main/src/test/scala/sbt/internal/server/DefinitionTest.scala b/main/src/test/scala/sbt/internal/server/DefinitionTest.scala index 6095c8ce7..534d77083 100644 --- a/main/src/test/scala/sbt/internal/server/DefinitionTest.scala +++ b/main/src/test/scala/sbt/internal/server/DefinitionTest.scala @@ -127,61 +127,51 @@ class DefinitionTest extends org.specs2.mutable.Specification { } } "definition" should { + import scalacache.caffeine._ + import scalacache.modes.sync._ "cache data in cache" in { - import scalacache.caffeine._ val cache = CaffeineCache[Any] val cacheFile = "Test.scala" val useBinary = true - import scalacache.modes.scalaFuture._ - val actual = Definition - .updateCache(cache)(cacheFile, useBinary) - .flatMap(_ => cache.get(Definition.AnalysesKey)) + Definition.updateCache(cache)(cacheFile, useBinary) + + val actual = cache.get(Definition.AnalysesKey) actual.collect { - case Some(s) => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] - } should contain[((String, Boolean), Option[Analysis])]("Test.scala" -> true -> None).await + case s => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + }.get should contain("Test.scala" -> true -> None) } "replace cache data in cache" in { - import scalacache.caffeine._ val cache = CaffeineCache[Any] val cacheFile = "Test.scala" val useBinary = true val falseUseBinary = false - import scalacache.modes.scalaFuture._ - val actual = Definition - .updateCache(cache)(cacheFile, falseUseBinary) - .flatMap { _ => - Definition.updateCache(cache)(cacheFile, useBinary) - } - .flatMap(_ => cache.get(Definition.AnalysesKey)) + Definition.updateCache(cache)(cacheFile, falseUseBinary) + Definition.updateCache(cache)(cacheFile, useBinary) + + val actual = cache.get(Definition.AnalysesKey) actual.collect { - case Some(s) => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] - } should contain[((String, Boolean), Option[Analysis])]("Test.scala" -> true -> None).await + case s => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + }.get should contain("Test.scala" -> true -> None) } "cache more data in cache" in { - import scalacache.caffeine._ val cache = CaffeineCache[Any] val cacheFile = "Test.scala" val useBinary = true val otherCacheFile = "OtherTest.scala" val otherUseBinary = false - import scalacache.modes.scalaFuture._ - val actual = Definition - .updateCache(cache)(otherCacheFile, otherUseBinary) - .flatMap { _ => - Definition.updateCache(cache)(cacheFile, useBinary) - } - .flatMap(_ => cache.get(Definition.AnalysesKey)) + Definition.updateCache(cache)(otherCacheFile, otherUseBinary) + Definition.updateCache(cache)(cacheFile, useBinary) + + val actual = cache.get(Definition.AnalysesKey) actual.collect { - case Some(s) => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] - } should contain[((String, Boolean), Option[Analysis])]( - "Test.scala" -> true -> None, - "OtherTest.scala" -> false -> None).await + case s => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + }.get should contain("Test.scala" -> true -> None, "OtherTest.scala" -> false -> None) } } } diff --git a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala index bcb2e0d6d..7acf7955f 100644 --- a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala +++ b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala @@ -161,11 +161,11 @@ object SettingQueryTest extends org.specs2.mutable.Specification { val settings: Seq[Setting[_]] = finalTransforms( buildConfigurations(loadedBuild, getRootProject(units), config.injectSettings)) - val delegates = defaultDelegates(loadedBuild) + val delegates: Scope => Seq[Scope] = defaultDelegates(loadedBuild) val scopeLocal: ScopeLocal = EvaluateTask.injectStreams val display: Show[ScopedKey[_]] = Project showLoadingKey loadedBuild - val data = Def.make(settings)(delegates, scopeLocal, display) + val data: Settings[Scope] = Def.make(settings)(delegates, scopeLocal, display) val extra: KeyIndex => BuildUtil[_] = index => BuildUtil(baseUri, units, index, data) val index: StructureIndex = structureIndex(data, settings, extra, units) From 36e079d12b6c83be81f6a09e7790d5960ae94f4b Mon Sep 17 00:00:00 2001 From: Alexey Alekhin Date: Thu, 2 Nov 2017 04:07:28 +0100 Subject: [PATCH 51/62] Added onNotification handler for the LSP CommandChannel --- .../sbt/internal/server/LanguageServerProtocol.scala | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index 5c18b926b..47ac40aee 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -34,6 +34,15 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { protected def log: Logger protected def onSettingQuery(execId: Option[String], req: Q): Unit + protected def onNotification(notification: JsonRpcNotificationMessage): Unit = { + log.debug(s"onNotification: $notification") + notification.method match { + case "textDocument/didSave" => + append(Exec(";compile; collectAnalyses", None, Some(CommandSource(name)))) + case _ => () + } + } + protected def onRequestMessage(request: JsonRpcRequestMessage): Unit = { import sbt.internal.langserver.codec.JsonProtocol._ import internalJsonProtocol._ @@ -57,8 +66,6 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { setInitialized(true) append(Exec(s"collectAnalyses", Some(request.id), Some(CommandSource(name)))) langRespond(InitializeResult(serverCapabilities), Option(request.id)) - case "textDocument/didSave" => - append(Exec(";compile; collectAnalyses", Some(request.id), Some(CommandSource(name)))) case "textDocument/definition" => import scala.concurrent.ExecutionContext.Implicits.global Definition.lspDefinition(json, request.id, CommandSource(name), log) From c6898213831da9ab66185b7074b2762c5ed47555 Mon Sep 17 00:00:00 2001 From: Alexey Alekhin Date: Thu, 2 Nov 2017 04:08:14 +0100 Subject: [PATCH 52/62] Added deserialization for NotificationMessage and used it in handleBody --- .../scala/sbt/internal/server/NetworkChannel.scala | 12 ++++++++++-- .../main/scala/sbt/protocol/Serialization.scala | 14 ++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index 26b489347..d385965f4 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -166,8 +166,8 @@ final class NetworkChannel(val name: String, def handleBody(chunk: Vector[Byte]): Unit = { if (isLanguageServerProtocol) { - Serialization.deserializeJsonRequest(chunk) match { - case Right(req) => + Serialization.deserializeJsonMessage(chunk) match { + case Right(Right(req)) => try { onRequestMessage(req) } catch { @@ -175,6 +175,14 @@ final class NetworkChannel(val name: String, log.debug(s"sending error: $code: $message") langError(Option(req.id), code, message) } + case Right(Left(ntf)) => + try { + onNotification(ntf) + } catch { + case LangServerError(code, message) => + log.debug(s"sending error: $code: $message") + langError(None, code, message) // new id? + } case Left(errorDesc) => val msg = s"Got invalid chunk from client (${new String(chunk.toArray, "UTF-8")}): " + errorDesc langError(None, ErrorCodes.ParseError, msg) diff --git a/protocol/src/main/scala/sbt/protocol/Serialization.scala b/protocol/src/main/scala/sbt/protocol/Serialization.scala index 2a6e174b4..c193dec81 100644 --- a/protocol/src/main/scala/sbt/protocol/Serialization.scala +++ b/protocol/src/main/scala/sbt/protocol/Serialization.scala @@ -143,15 +143,21 @@ object Serialization { } } - private[sbt] def deserializeJsonRequest( - bytes: Seq[Byte]): Either[String, JsonRpcRequestMessage] = { + private[sbt] def deserializeJsonMessage(bytes: Seq[Byte]) + : Either[String, Either[JsonRpcNotificationMessage, JsonRpcRequestMessage]] = { val buffer = ByteBuffer.wrap(bytes.toArray) Parser.parseFromByteBuffer(buffer) match { case Success(json) => import sbt.internal.protocol.codec.JsonRPCProtocol._ Converter.fromJson[JsonRpcRequestMessage](json) match { - case Success(msg) => Right(msg) - case Failure(e) => throw e + case Success(request) if (request.id.nonEmpty) => Right(Right(request)) + case Failure(e) => throw e + case _ => { + Converter.fromJson[JsonRpcNotificationMessage](json) match { + case Success(notification) => Right(Left(notification)) + case Failure(e) => throw e + } + } } case Failure(e) => Left(s"Parse error: ${e.getMessage}") From e4dd090d0cf3c46f46602cabe83bb331e15a4c24 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 29 Nov 2017 00:51:16 -0500 Subject: [PATCH 53/62] Add debug log on unhandled notification --- .../main/scala/sbt/internal/server/LanguageServerProtocol.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index 47ac40aee..ce1131d0f 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -39,7 +39,7 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { notification.method match { case "textDocument/didSave" => append(Exec(";compile; collectAnalyses", None, Some(CommandSource(name)))) - case _ => () + case u => log.debug(s"Unhandled notification received: $u") } } From 6a996378c713bddf7afc61e8aac9949c88c365f7 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 29 Nov 2017 01:00:46 -0500 Subject: [PATCH 54/62] return Either[String, JsonRpcMessage] --- .../main/scala/sbt/internal/server/NetworkChannel.scala | 7 +++++-- protocol/src/main/scala/sbt/protocol/Serialization.scala | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index d385965f4..7c27063de 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -18,6 +18,7 @@ import sbt.protocol._ import sbt.internal.langserver.ErrorCodes import sbt.internal.util.{ ObjectEvent, StringEvent } import sbt.internal.util.codec.JValueFormats +import sbt.internal.protocol.{ JsonRpcRequestMessage, JsonRpcNotificationMessage } import sbt.util.Logger final class NetworkChannel(val name: String, @@ -167,7 +168,7 @@ final class NetworkChannel(val name: String, def handleBody(chunk: Vector[Byte]): Unit = { if (isLanguageServerProtocol) { Serialization.deserializeJsonMessage(chunk) match { - case Right(Right(req)) => + case Right(req: JsonRpcRequestMessage) => try { onRequestMessage(req) } catch { @@ -175,7 +176,7 @@ final class NetworkChannel(val name: String, log.debug(s"sending error: $code: $message") langError(Option(req.id), code, message) } - case Right(Left(ntf)) => + case Right(ntf: JsonRpcNotificationMessage) => try { onNotification(ntf) } catch { @@ -183,6 +184,8 @@ final class NetworkChannel(val name: String, log.debug(s"sending error: $code: $message") langError(None, code, message) // new id? } + case Right(msg) => + log.debug(s"Unhandled message: $msg") case Left(errorDesc) => val msg = s"Got invalid chunk from client (${new String(chunk.toArray, "UTF-8")}): " + errorDesc langError(None, ErrorCodes.ParseError, msg) diff --git a/protocol/src/main/scala/sbt/protocol/Serialization.scala b/protocol/src/main/scala/sbt/protocol/Serialization.scala index c193dec81..75b9e7c83 100644 --- a/protocol/src/main/scala/sbt/protocol/Serialization.scala +++ b/protocol/src/main/scala/sbt/protocol/Serialization.scala @@ -15,6 +15,7 @@ import java.nio.ByteBuffer import scala.util.{ Success, Failure } import sbt.internal.util.StringEvent import sbt.internal.protocol.{ + JsonRpcMessage, JsonRpcRequestMessage, JsonRpcResponseMessage, JsonRpcNotificationMessage @@ -143,18 +144,17 @@ object Serialization { } } - private[sbt] def deserializeJsonMessage(bytes: Seq[Byte]) - : Either[String, Either[JsonRpcNotificationMessage, JsonRpcRequestMessage]] = { + private[sbt] def deserializeJsonMessage(bytes: Seq[Byte]): Either[String, JsonRpcMessage] = { val buffer = ByteBuffer.wrap(bytes.toArray) Parser.parseFromByteBuffer(buffer) match { case Success(json) => import sbt.internal.protocol.codec.JsonRPCProtocol._ Converter.fromJson[JsonRpcRequestMessage](json) match { - case Success(request) if (request.id.nonEmpty) => Right(Right(request)) + case Success(request) if (request.id.nonEmpty) => Right(request) case Failure(e) => throw e case _ => { Converter.fromJson[JsonRpcNotificationMessage](json) match { - case Success(notification) => Right(Left(notification)) + case Success(notification) => Right(notification) case Failure(e) => throw e } } From ff004993e0decd39a2f2a0d808402333ad6e49b3 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 29 Nov 2017 16:53:40 -0500 Subject: [PATCH 55/62] Bump modules --- project/Dependencies.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index ee3a894fb..c955c4efb 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -12,10 +12,10 @@ object Dependencies { val baseScalaVersion = scala212 // sbt modules - private val ioVersion = "1.1.0" - private val utilVersion = "1.0.3" + private val ioVersion = "1.1.1" + private val utilVersion = "1.1.0" private val lmVersion = "1.0.4" - private val zincVersion = "1.0.5" + private val zincVersion = "1.1.0-RC1" private val sbtIO = "org.scala-sbt" %% "io" % ioVersion @@ -101,7 +101,7 @@ object Dependencies { } val jline = "jline" % "jline" % "2.14.4" - val scalatest = "org.scalatest" %% "scalatest" % "3.0.1" + val scalatest = "org.scalatest" %% "scalatest" % "3.0.4" val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.13.4" val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1" val junit = "junit" % "junit" % "4.11" From 824cfdf527e01eb6e3affe41fa1d83bed3ab4ad2 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 29 Nov 2017 17:12:40 -0500 Subject: [PATCH 56/62] Add header --- sbt/src/test/scala/sbt/RunFromSourceMain.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sbt/src/test/scala/sbt/RunFromSourceMain.scala b/sbt/src/test/scala/sbt/RunFromSourceMain.scala index 0394772b3..be79cc54a 100644 --- a/sbt/src/test/scala/sbt/RunFromSourceMain.scala +++ b/sbt/src/test/scala/sbt/RunFromSourceMain.scala @@ -1,3 +1,10 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + package sbt import scala.annotation.tailrec From d193d302f0f0b8b7343518caa2e2383e7d8760b5 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 29 Nov 2017 22:31:57 -0500 Subject: [PATCH 57/62] lm 1.1.0 --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c955c4efb..a628f6aaf 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -14,7 +14,7 @@ object Dependencies { // sbt modules private val ioVersion = "1.1.1" private val utilVersion = "1.1.0" - private val lmVersion = "1.0.4" + private val lmVersion = "1.1.0" private val zincVersion = "1.1.0-RC1" private val sbtIO = "org.scala-sbt" %% "io" % ioVersion From c5d578815c543c7322af61e608d7cb11ae79b982 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 29 Nov 2017 22:42:58 -0500 Subject: [PATCH 58/62] catch IOException intead of SocketException `NGUnixDomainSocket` throws `java.io.IOException` instead of `SocketException`, probably because `SocketException` does not expose the contructor with a `Throwable` parameter. To allow clients to disconnect, we need to catch `IOException`. --- .../scala/sbt/internal/client/NetworkClient.scala | 5 +++-- .../main/scala/sbt/internal/CommandExchange.scala | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index 477df7238..ff5c5661b 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -9,7 +9,8 @@ package sbt package internal package client -import java.net.{ URI, Socket, InetAddress, SocketException } +import java.io.IOException +import java.net.{ URI, Socket, InetAddress } import java.util.UUID import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import scala.collection.mutable.ListBuffer @@ -111,7 +112,7 @@ class NetworkClient(arguments: List[String]) { self => try { connection.publish(bytes) } catch { - case _: SocketException => + case _: IOException => // log.debug(e.getMessage) // toDel += client } diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index f03546b64..7bbb2c38b 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -8,7 +8,7 @@ package sbt package internal -import java.net.SocketException +import java.io.IOException import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicInteger import scala.collection.mutable.ListBuffer @@ -170,7 +170,7 @@ private[sbt] final class CommandExchange { try { c.notifyEvent(method, params) } catch { - case _: SocketException => + case _: IOException => toDel += c } } @@ -213,7 +213,7 @@ private[sbt] final class CommandExchange { } } } catch { - case _: SocketException => + case _: IOException => toDel += c } } @@ -225,7 +225,7 @@ private[sbt] final class CommandExchange { try { c.publishEvent(event) } catch { - case _: SocketException => + case _: IOException => toDel += c } } @@ -267,7 +267,7 @@ private[sbt] final class CommandExchange { try { c.publishObjectEvent(event) } catch { - case _: SocketException => + case _: IOException => toDel += c } } @@ -305,7 +305,7 @@ private[sbt] final class CommandExchange { c.publishEventMessage(event) } } catch { - case e: SocketException => + case e: IOException => toDel += c } } @@ -317,7 +317,7 @@ private[sbt] final class CommandExchange { try { c.publishEventMessage(event) } catch { - case _: SocketException => + case _: IOException => toDel += c } } From 8031bb0f83c0f4070e2115caa43260a40ab641ed Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 29 Nov 2017 23:45:28 -0500 Subject: [PATCH 59/62] notes --- notes/1.0.3/escape-imports-from-sbt-file.md | 9 -- notes/1.1.0.markdown | 153 ++++++++++++++++++++ notes/1.1.0/filter-scripted-tests.md | 14 -- notes/1.1.0/new-version-default.md | 3 - notes/1.1.0/project-id.md | 11 -- notes/1.1.0/reboot.md | 3 - notes/1.1.0/remove-ref-to-older-vers.md | 11 -- notes/1.1.0/unified-shell.markdown | 49 ------- 8 files changed, 153 insertions(+), 100 deletions(-) delete mode 100644 notes/1.0.3/escape-imports-from-sbt-file.md create mode 100644 notes/1.1.0.markdown delete mode 100644 notes/1.1.0/filter-scripted-tests.md delete mode 100644 notes/1.1.0/new-version-default.md delete mode 100644 notes/1.1.0/project-id.md delete mode 100644 notes/1.1.0/reboot.md delete mode 100644 notes/1.1.0/remove-ref-to-older-vers.md delete mode 100644 notes/1.1.0/unified-shell.markdown diff --git a/notes/1.0.3/escape-imports-from-sbt-file.md b/notes/1.0.3/escape-imports-from-sbt-file.md deleted file mode 100644 index 065dad76e..000000000 --- a/notes/1.0.3/escape-imports-from-sbt-file.md +++ /dev/null @@ -1,9 +0,0 @@ -[@panaeon]: https://github.com/panaeon - -[#3464]: https://github.com/sbt/sbt/issues/3464 -[#3566]: https://github.com/sbt/sbt/pull/3566 - -### Bug fixes - -- Escape imports from sbt files, so if user creates a backquoted definition then task evalution will not fail. - diff --git a/notes/1.1.0.markdown b/notes/1.1.0.markdown new file mode 100644 index 000000000..edcaacccb --- /dev/null +++ b/notes/1.1.0.markdown @@ -0,0 +1,153 @@ +### Features, fixes, changes with compatibility implications + +- sbt server feature is reworked in sbt 1.1.0. See below. +- Changes `version` setting default to `0.1.0-SNAPSHOT` for compatibility with Semantic Versioning. [#3577][3577] by [@laughedelic][@laughedelic] + +### Features + +- Unifies sbt shell and build.sbt syntax. See below. + +### Fixes + +- Fixes handling of `ThisProject`. [#3609][3609] by [@dwijnand][@dwijnand] +- Escapes imports from sbt files, so if user creates a backquoted definition then task evalution will not fail. [#3635][3635] by [@panaeon][@panaeon] +- Removes reference to version 0.14.0 from a warning message. [#3693][3693] by [@saniyatech][@saniyatech] +- Fixes screpl throwing "Not a valid key: console-quick". [#3762][3762] by [@xuwei-k][@xuwei-k] + +### Improvements + +- Filters scripted tests based on optional `project/build.properties`. See below. +- Adds `Project#withId` to change a project's id. [#3601][3601] by [@dwijnand][@dwijnand] +- Adds `reboot dev` command, which deletes the current artifact from the boot directory. This is useful when working with development versions of sbt. [#3659][3659] by [@eed3si9n][@eed3si9n] +- Adds a check for a change in sbt version before `reload`. [#1055][1055]/[#3673][3673] by [@RomanIakovlev][@RomanIakovlev] +- Adds a new setting `insideCI`, which indicates that sbt is likely running in an Continuous Integration environment. [#3672][3672] by [@RomanIakovlev][@RomanIakovlev] +- Adds `nameOption` to `Command` trait. [#3671][3671] by [@miklos-martin][@miklos-martin] +- Uses kind-projector in the code. [#3650][3650] by [@dwijnand][@dwijnand] +- Make `displayOnly` etc methods strict in `Completions`. [#3763][3763] by [@xuwei-k][@xuwei-k] + +### Unified slash syntax for sbt shell and build.sbt + +This adds unified slash syntax for both sbt shell and the build.sbt DSL. +Instead of the current `/config:intask::key`, this adds +`//intask/key` where `` is the Scala identifier +notation for the configurations like `Compile` and `Test`. (The old shell syntax will continue to function) + +These examples work both from the shell and in build.sbt. + + Global / cancelable + ThisBuild / scalaVersion + Test / test + root / Compile / compile / scalacOptions + ProjectRef(uri("file:/xxx/helloworld/"),"root")/Compile/scalacOptions + Zero / Zero / name + +The inspect command now outputs something that can be copy-pasted: + + > inspect compile + [info] Task: sbt.inc.Analysis + [info] Description: + [info] Compiles sources. + [info] Provided by: + [info] ProjectRef(uri("file:/xxx/helloworld/"),"root")/Compile/compile + [info] Defined at: + [info] (sbt.Defaults) Defaults.scala:326 + [info] Dependencies: + [info] Compile/manipulateBytecode + [info] Compile/incCompileSetup + .... + +[#1812][1812]/[#3434][3434]/[#3617][3617]/[#3620][3620] by [@eed3si9n][@eed3si9n] and [@dwijnand][@dwijnand] + +### sbt server + +sbt server feature was reworked to use Language Server Protocol 3.0 (LSP) as the wire protocol, a protocol created by Microsoft for Visual Studio Code. + +To discover a running server, sbt 1.1.0 creates a port file at `./project/target/active.json` relative to a build: + +``` +{"uri":"local:///Users/foo/.sbt/1.0/server/0845deda85cb41abcdef/sock"} +``` + +`local:` indicates a UNIX domain socket. Here's how we can say hello to the server using `nc`. (`^M` can be sent `Ctrl-V` then `Return`): + +``` +$ nc -U /Users/foo/.sbt/1.0/server/0845deda85cb41abcdef/sock +Content-Length: 99^M +^M +{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "initializationOptions": { } } }^M +``` + +sbt server adds network access to sbt's shell command so, in addition to accepting input from the terminal, server also to accepts input from the network. Here's how we can call `compile`: + +``` +Content-Length: 93^M +^M +{ "jsonrpc": "2.0", "id": 2, "method": "sbt/exec", "params": { "commandLine": "compile" } }^M +``` + +The running sbt session should now queue `compile`, and return back with compiler warnings and errors, if any: + +``` +Content-Length: 296 +Content-Type: application/vscode-jsonrpc; charset=utf-8 + +{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"file:/Users/foo/work/hellotest/Hello.scala","diagnostics":[{"range":{"start":{"line":2,"character":26},"end":{"line":2,"character":27}},"severity":1,"source":"sbt","message":"object X is not a member of package foo"}]}} +``` + +[#3524][3524]/[#3556][3556] by [@eed3si9n][@eed3si9n] + +### VS Code extension + +The primary use case we have in mind for the sbt server is tooling integration such as editors and IDEs. As a proof of concept, we created a Visual Studio Code extension called [Scala (sbt)][vscode-sbt-scala]. + +Currently this extension is able to: + +- Run `compile` at the root project when `*.scala` files are saved. [#3524][3524] by [@eed3si9n][@eed3si9n] +- Display compiler errors. +- Display log messages. [#3740][3740] by [@laughedelic][@laughedelic] +- Jump to class definitions. [#3660][3660] + +### Filtering scripted tests using `project/build.properties` + +For all scripted tests in which `project/build.properties` exist, the value of the `sbt.version` property is read. If its binary version is different from `sbtBinaryVersion in pluginCrossBuild` the test will be skipped and a message indicating this will be logged. + +This allows you to define scripted tests that track the minimum supported sbt versions, e.g. 0.13.9 and 1.0.0-RC2. [#3564][3564]/[#3566][3566] by [@jonas][@jonas] + + [@eed3si9n]: https://github.com/eed3si9n + [@dwijnand]: http://github.com/dwijnand + [@jvican]: https://github.com/jvican + [@Duhemm]: https://github.com/Duhemm + [@jonas]: https://github.com/jonas + [@laughedelic]: https://github.com/laughedelic + [@panaeon]: https://github.com/panaeon + [@RomanIakovlev]: https://github.com/RomanIakovlev + [@miklos-martin]: https://github.com/miklos-martin + [@saniyatech]: https://github.com/saniyatech + [@xuwei-k]: https://github.com/xuwei-k + [@wpopielarski]: https://github.com/wpopielarski + + [vscode-sbt-scala]: https://marketplace.visualstudio.com/items?itemName=lightbend.vscode-sbt-scala + [1812]: https://github.com/sbt/sbt/issues/1812 + [3524]: https://github.com/sbt/sbt/pull/3524 + [3556]: https://github.com/sbt/sbt/pull/3556 + [3564]: https://github.com/sbt/sbt/issues/3564 + [3566]: https://github.com/sbt/sbt/pull/3566 + [3577]: https://github.com/sbt/sbt/pull/3577 + [3434]: https://github.com/sbt/sbt/pull/3434 + [3601]: https://github.com/sbt/sbt/pull/3601 + [3609]: https://github.com/sbt/sbt/pull/3609 + [3617]: https://github.com/sbt/sbt/pull/3617 + [3620]: https://github.com/sbt/sbt/pull/3620 + [3464]: https://github.com/sbt/sbt/issues/3464 + [3635]: https://github.com/sbt/sbt/pull/3635 + [3659]: https://github.com/sbt/sbt/pull/3659 + [3650]: https://github.com/sbt/sbt/pull/3650 + [3673]: https://github.com/sbt/sbt/pull/3673 + [1055]: https://github.com/sbt/sbt/issues/1055 + [3672]: https://github.com/sbt/sbt/pull/3672 + [3671]: https://github.com/sbt/sbt/pull/3671 + [3693]: https://github.com/sbt/sbt/issues/3693 + [3763]: https://github.com/sbt/sbt/pull/3763 + [3762]: https://github.com/sbt/sbt/pull/3762 + [3740]: https://github.com/sbt/sbt/pull/3740 + [3660]: https://github.com/sbt/sbt/pull/3660 diff --git a/notes/1.1.0/filter-scripted-tests.md b/notes/1.1.0/filter-scripted-tests.md deleted file mode 100644 index 4f216e9c2..000000000 --- a/notes/1.1.0/filter-scripted-tests.md +++ /dev/null @@ -1,14 +0,0 @@ -[@jonas]: https://github.com/jonas - -[#3564]: https://github.com/sbt/sbt/issues/3564 -[#3566]: https://github.com/sbt/sbt/pull/3566 - -### Improvements - -- Filter scripted tests based on optional `project/build.properties`. [#3564]/[#3566] by [@jonas] - -### Filtering scripted tests using `project/build.properties`. - -For all scripted tests in which `project/build.properties` exist, the value of the `sbt.version` property is read. If its binary version is different from `sbtBinaryVersion in pluginCrossBuild` the test will be skipped and a message indicating this will be logged. - -This allows you to define scripted tests that track the minimum supported sbt versions, e.g. 0.13.9 and 1.0.0-RC2. diff --git a/notes/1.1.0/new-version-default.md b/notes/1.1.0/new-version-default.md deleted file mode 100644 index c9aabd112..000000000 --- a/notes/1.1.0/new-version-default.md +++ /dev/null @@ -1,3 +0,0 @@ -### Improvements - -- Changes `version` setting default to `0.1.0-SNAPSHOT` for compatibility with Semantic Versioning diff --git a/notes/1.1.0/project-id.md b/notes/1.1.0/project-id.md deleted file mode 100644 index fb408b1f8..000000000 --- a/notes/1.1.0/project-id.md +++ /dev/null @@ -1,11 +0,0 @@ -[@dwijnand]: https://github.com/dwijnand - -[#3601]: https://github.com/sbt/sbt/pull/3601 - -### Fixes with compatibility implications - -### Improvements - -- Adds `Project#withId` to change a project's id. [#3601][] by [@dwijnand][] - -### Bug fixes diff --git a/notes/1.1.0/reboot.md b/notes/1.1.0/reboot.md deleted file mode 100644 index 0b7b06504..000000000 --- a/notes/1.1.0/reboot.md +++ /dev/null @@ -1,3 +0,0 @@ -### Improvements - -- Adds `reboot dev` command, which deletes the current artifact from the boot directory. This is useful when working with development versions of sbt. diff --git a/notes/1.1.0/remove-ref-to-older-vers.md b/notes/1.1.0/remove-ref-to-older-vers.md deleted file mode 100644 index 5b0ecf1b0..000000000 --- a/notes/1.1.0/remove-ref-to-older-vers.md +++ /dev/null @@ -1,11 +0,0 @@ -[@saniyatech]: https://github.com/saniyatech - -[#3693]: https://github.com/sbt/sbt/issues/3693 - -### Bug fixes - -- Removes reference to version 0.14.0 from a warning message. [#3693][] by [@saniyatech][] - -### Improvements - -### Fixes with compatibility implications diff --git a/notes/1.1.0/unified-shell.markdown b/notes/1.1.0/unified-shell.markdown deleted file mode 100644 index f10435046..000000000 --- a/notes/1.1.0/unified-shell.markdown +++ /dev/null @@ -1,49 +0,0 @@ - -### Fixes with compatibility implications - -- - -### Improvements - -- Unifies sbt shell and build.sbt syntax. See below. - -### Bug fixes - -- - -### Unified slash syntax for sbt shell and build.sbt - -This adds unified slash syntax for both sbt shell and the build.sbt DSL. -Instead of the current `/config:intask::key`, this adds -`//intask/key` where `` is the Scala identifier -notation for the configurations like `Compile` and `Test`. (The old shell syntax will continue to function) - -These examples work both from the shell and in build.sbt. - - Global / cancelable - ThisBuild / scalaVersion - Test / test - root / Compile / compile / scalacOptions - ProjectRef(uri("file:/xxx/helloworld/"),"root")/Compile/scalacOptions - Zero / Zero / name - -The inspect command now outputs something that can be copy-pasted: - - > inspect compile - [info] Task: sbt.inc.Analysis - [info] Description: - [info] Compiles sources. - [info] Provided by: - [info] ProjectRef(uri("file:/xxx/helloworld/"),"root")/Compile/compile - [info] Defined at: - [info] (sbt.Defaults) Defaults.scala:326 - [info] Dependencies: - [info] Compile/manipulateBytecode - [info] Compile/incCompileSetup - .... - -[#3434][3434] by [@eed3si9n][@eed3si9n] - - [3434]: https://github.com/sbt/sbt/pull/3434 - [@eed3si9n]: https://github.com/eed3si9n - [@dwijnand]: http://github.com/dwijnand From 6ed3c0f5341178861c524d2736e9c01b04dbcdf2 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 30 Nov 2017 00:01:33 -0500 Subject: [PATCH 60/62] more notes --- notes/1.1.0.markdown | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/notes/1.1.0.markdown b/notes/1.1.0.markdown index edcaacccb..8bbfa3805 100644 --- a/notes/1.1.0.markdown +++ b/notes/1.1.0.markdown @@ -9,6 +9,11 @@ ### Fixes +- Fixes over-compilation bug with Java 9. [zinc#450][zinc450] by [@retronym][@retronym] +- Fixes handling of deeply nested Java classes. [zinc#423][zinc423] by [@romanowski][@romanowski] +- Fixes JavaDoc not printing all errors. [zinc#415][zinc415] by [@raboof][@raboof] +- Preserves JAR order in `ScalaInstance.otherJars`. [zinc#411][zinc411] by [@dwijnand][@dwijnand] +- Fixes used name when it contains NL. [zinc#449][zinc449] by [@jilen][@jilen] - Fixes handling of `ThisProject`. [#3609][3609] by [@dwijnand][@dwijnand] - Escapes imports from sbt files, so if user creates a backquoted definition then task evalution will not fail. [#3635][3635] by [@panaeon][@panaeon] - Removes reference to version 0.14.0 from a warning message. [#3693][3693] by [@saniyatech][@saniyatech] @@ -22,6 +27,8 @@ - Adds a check for a change in sbt version before `reload`. [#1055][1055]/[#3673][3673] by [@RomanIakovlev][@RomanIakovlev] - Adds a new setting `insideCI`, which indicates that sbt is likely running in an Continuous Integration environment. [#3672][3672] by [@RomanIakovlev][@RomanIakovlev] - Adds `nameOption` to `Command` trait. [#3671][3671] by [@miklos-martin][@miklos-martin] +- Adds POSIX persmission operations in IO, such as `IO.chmod(..)`. [io#76][io76] by [@eed3si9n][@eed3si9n] +- Treat sbt 1 modules using Semantic Versioning in the eviction warning. [lm#188][lm188] by [@eed3si9n][@eed3si9n] - Uses kind-projector in the code. [#3650][3650] by [@dwijnand][@dwijnand] - Make `displayOnly` etc methods strict in `Completions`. [#3763][3763] by [@xuwei-k][@xuwei-k] @@ -125,7 +132,10 @@ This allows you to define scripted tests that track the minimum supported sbt ve [@saniyatech]: https://github.com/saniyatech [@xuwei-k]: https://github.com/xuwei-k [@wpopielarski]: https://github.com/wpopielarski - + [@retronym]: https://github.com/retronym + [@romanowski]: https://github.com/romanowski + [@raboof]: https://github.com/raboof + [@jilen]: https://github.com/jilen [vscode-sbt-scala]: https://marketplace.visualstudio.com/items?itemName=lightbend.vscode-sbt-scala [1812]: https://github.com/sbt/sbt/issues/1812 [3524]: https://github.com/sbt/sbt/pull/3524 @@ -151,3 +161,10 @@ This allows you to define scripted tests that track the minimum supported sbt ve [3762]: https://github.com/sbt/sbt/pull/3762 [3740]: https://github.com/sbt/sbt/pull/3740 [3660]: https://github.com/sbt/sbt/pull/3660 + [io76]: https://github.com/sbt/io/pull/76 + [lm188]: https://github.com/sbt/librarymanagement/pull/188 + [zinc450]: https://github.com/sbt/zinc/pull/450 + [zinc423]: https://github.com/sbt/zinc/pull/423 + [zinc415]: https://github.com/sbt/zinc/issues/415 + [zinc411]: https://github.com/sbt/zinc/pull/411 + [zinc449]: https://github.com/sbt/zinc/pull/449 From 62ecd417da1acb9d9b98efd8fcf64b97452ecdaf Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 30 Nov 2017 12:35:40 -0500 Subject: [PATCH 61/62] Bump vscode-sbt-scala version to 0.1.0 --- CONTRIBUTING.md | 19 +++++++++++++++++++ vscode-sbt-scala/client/README.md | 5 ++++- vscode-sbt-scala/client/package.json | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad9d2c193..0b6b0d37d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -200,3 +200,22 @@ Building Documentation ====================== The scala-sbt.org site documentation is a separate project [website](https://github.com/sbt/website). Follow [the steps in the README](https://github.com/sbt/website#scala-sbtorg) to generate the documentation. + + +Note for maintainers +==================== + +Publishing VS Code Extensions +----------------------------- + +https://code.visualstudio.com/docs/extensions/publish-extension + +``` +$ sbt +> vscodePlugin/compile +> exit +cd vscode-sbt-scala/client +# update version number in vscode-sbt-scala/client/package.json +$ vsce package +$ vsce publish +``` diff --git a/vscode-sbt-scala/client/README.md b/vscode-sbt-scala/client/README.md index 5c1838a76..01de59573 100644 --- a/vscode-sbt-scala/client/README.md +++ b/vscode-sbt-scala/client/README.md @@ -3,4 +3,7 @@ Scala language support using sbt This is an experimental Scala language support using sbt as the language server. -To try this, use sbt 1.1.0-M1 and above. Saving `*.scala` will trigger `compile` task. +To try this, use sbt 1.1.0-RC1 or above. + +- Saving `*.scala` will trigger `compile` task. +- Jump to definition support for class names. diff --git a/vscode-sbt-scala/client/package.json b/vscode-sbt-scala/client/package.json index 70bacbdd6..c8d8a4472 100644 --- a/vscode-sbt-scala/client/package.json +++ b/vscode-sbt-scala/client/package.json @@ -1,7 +1,7 @@ { "name": "vscode-sbt-scala", "displayName": "Scala (sbt)", - "version": "0.0.2", + "version": "0.1.0", "author": "Lightbend, Inc.", "license": "BSD-3-Clause", "publisher": "lightbend", From 832360905324289ba56ef8095c6f70a9e1bf855a Mon Sep 17 00:00:00 2001 From: kenji yoshida <6b656e6a69@gmail.com> Date: Tue, 5 Dec 2017 16:27:32 +0900 Subject: [PATCH 62/62] update mimaPreviousArtifacts --- build.sbt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 3ebd1cbdf..9a1e8b923 100644 --- a/build.sbt +++ b/build.sbt @@ -75,10 +75,9 @@ def testedBaseSettings: Seq[Setting[_]] = baseSettings ++ testDependencies val mimaSettings = Def settings ( - mimaPreviousArtifacts := Set( - organization.value % moduleName.value % "1.0.0" - cross (if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled) - ) + mimaPreviousArtifacts := (0 to 4).map { v => + organization.value % moduleName.value % s"1.0.$v" cross (if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled) + }.toSet ) lazy val sbtRoot: Project = (project in file("."))