From 05498c284746eb40699f4dfdecb5f9b8ddf2afd6 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 20 Sep 2018 22:27:47 +0200 Subject: [PATCH 1/3] sbt-dotty: Make launchIDE work offline For each Code extension we require, we start by trying to install or upgrade the extension. If this fails we check if an existing version of the extension exists. If this also fails we throw an exception. This ensures that we're always running the latest version of the extension but that we can still work offline. --- project/Build.scala | 12 ++-- .../tools/sbtplugin/DottyIDEPlugin.scala | 65 ++++++++++++++++--- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index d706a5a26248..a8e3871b0078 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -16,7 +16,7 @@ import xerial.sbt.pack.PackPlugin import xerial.sbt.pack.PackPlugin.autoImport._ import dotty.tools.sbtplugin.DottyPlugin.autoImport._ -import dotty.tools.sbtplugin.DottyIDEPlugin.{ prepareCommand, runProcess } +import dotty.tools.sbtplugin.DottyIDEPlugin.{ installCodeExtension, prepareCommand, runProcess } import dotty.tools.sbtplugin.DottyIDEPlugin.autoImport._ import sbtbuildinfo.BuildInfoPlugin @@ -988,27 +988,27 @@ object Build { val coursier = workingDir / "out" / "coursier" val packageJson = workingDir / "package.json" if (!coursier.exists || packageJson.lastModified > coursier.lastModified) - runProcess(Seq("npm", "install"), wait = true, directory = workingDir) + runProcess(Seq("npm", "install"), wait = true, directory = Some(workingDir)) val tsc = workingDir / "node_modules" / ".bin" / "tsc" runProcess(Seq(tsc.getAbsolutePath, "--pretty", "--project", workingDir.getAbsolutePath), wait = true) // Currently, vscode-dotty depends on daltonjorge.scala for syntax highlighting, // this is not automatically installed when starting the extension in development mode // (--extensionDevelopmentPath=...) - runProcess(codeCommand.value ++ Seq("--install-extension", "daltonjorge.scala"), wait = true) + installCodeExtension(codeCommand.value, "daltonjorge.scala") sbt.internal.inc.Analysis.Empty }.dependsOn(managedResources in Compile).value, sbt.Keys.`package`:= { - runProcess(Seq("vsce", "package"), wait = true, directory = baseDirectory.value) + runProcess(Seq("vsce", "package"), wait = true, directory = Some(baseDirectory.value)) baseDirectory.value / s"dotty-${version.value}.vsix" }, unpublish := { - runProcess(Seq("vsce", "unpublish"), wait = true, directory = baseDirectory.value) + runProcess(Seq("vsce", "unpublish"), wait = true, directory = Some(baseDirectory.value)) }, publish := { - runProcess(Seq("vsce", "publish"), wait = true, directory = baseDirectory.value) + runProcess(Seq("vsce", "publish"), wait = true, directory = Some(baseDirectory.value)) }, run := Def.inputTask { val inputArgs = spaceDelimited("").parsed diff --git a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala index 5df06e48ca68..7ff6c1d2fab3 100644 --- a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala +++ b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala @@ -5,6 +5,7 @@ import sbt.Def.Initialize import sbt.Keys._ import java.io._ import java.lang.ProcessBuilder +import java.lang.ProcessBuilder.Redirect import scala.collection.mutable import scala.util.Properties.{ isWin, isMac } @@ -146,23 +147,66 @@ object DottyIDEPlugin extends AutoPlugin { if (isWin) Seq("cmd.exe", "/C") ++ cmd else cmd - /** Run `cmd`. - * @param wait If true, wait for `cmd` to return and throw an exception if the exit code is non-zero. - * @param directory If not null, run `cmd` in this directory. + /** Run the command `cmd`. + * + * @param wait If true, wait for the command to return and throw an exception if the exit code is non-zero. + * @param directory If not None, run the command in this directory. + * @param outputCallback If not None, pass the command output to this callback instead of writing it to stdout. */ - def runProcess(cmd: Seq[String], wait: Boolean = false, directory: File = null): Unit = { + def runProcess(cmd: Seq[String], wait: Boolean = false, directory: Option[File] = None, outputCallback: Option[BufferedReader => Unit] = None): Unit = { val pb = new ProcessBuilder(prepareCommand(cmd): _*).inheritIO() - if (directory != null) pb.directory(directory) + + directory match { + case Some(dir) => + pb.directory(dir) + case None => + } + + pb.redirectInput(Redirect.INHERIT) + .redirectError(Redirect.INHERIT) + .redirectOutput( + outputCallback match { + case Some(_) => + Redirect.PIPE + case None => + Redirect.INHERIT + }) + + val process = pb.start() + outputCallback match { + case Some(callback) => + callback(new BufferedReader(new InputStreamReader(process.getInputStream))) + case None => + } if (wait) { - val exitCode = pb.start().waitFor() + val exitCode = process.waitFor() if (exitCode != 0) { val cmdString = cmd.mkString(" ") val description = if (directory != null) s""" in directory "$directory"""" else "" throw new MessageOnlyException(s"""Running command "${cmdString}"${description} failed.""") } } - else - pb.start() + } + + /** Install or upgrade Code extension `name`. + * + * We start by trying to install or upgrade the extension. If this fails we + * check if an existing version of the extension exists. If this also fails + * we throw an exception. This ensures that we're always running the latest + * version of the extension but that we can still work offline. + */ + def installCodeExtension(codeCmd: Seq[String], name: String): Unit = { + try { + runProcess(codeCmd ++ Seq("--install-extension", name), wait = true) + } catch { + case e: Exception => + var alreadyInstalled: Boolean = false + runProcess(codeCmd ++ Seq("--list-extensions"), wait = true, outputCallback = Some({ br => + alreadyInstalled = br.lines.filter(_ == name).findFirst.isPresent + })) + if (!alreadyInstalled) + throw e + } } private val projectConfig = taskKey[Option[ProjectConfig]]("") @@ -270,8 +314,9 @@ object DottyIDEPlugin extends AutoPlugin { runCode := { try { - runProcess(codeCommand.value ++ Seq("--install-extension", "lampepfl.dotty"), wait = true) - runProcess(codeCommand.value ++ Seq("."), directory = baseDirectory.value) + installCodeExtension(codeCommand.value, "lampepfl.dotty") + + runProcess(codeCommand.value ++ Seq("."), directory = Some(baseDirectory.value)) } catch { case ioex: IOException if ioex.getMessage.startsWith("""Cannot run program "code"""") => val log = streams.value.log From 35b9d5386a0f8413754ac7e5f736c5aa40172ec0 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 20 Sep 2018 23:46:47 +0200 Subject: [PATCH 2/3] Bump to sbt-dotty 0.2.5-SNAPSHOT 0.2.4 has been released. --- project/Build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index a8e3871b0078..1e613cf85813 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -56,7 +56,7 @@ object Build { val sbtDottyName = "sbt-dotty" val sbtDottyVersion = { - val base = "0.2.4" + val base = "0.2.5" if (isRelease) base else base + "-SNAPSHOT" } From 1073a799cbe2e2e979a479b94f0b680fcdc35b19 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Fri, 21 Sep 2018 14:03:22 +0200 Subject: [PATCH 3/3] sbt-dotty: Remove no longer needed .inheritIO We now explicitly set .redirect{Input,Output,Error} --- sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala index 7ff6c1d2fab3..d423b07af9a4 100644 --- a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala +++ b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala @@ -154,7 +154,7 @@ object DottyIDEPlugin extends AutoPlugin { * @param outputCallback If not None, pass the command output to this callback instead of writing it to stdout. */ def runProcess(cmd: Seq[String], wait: Boolean = false, directory: Option[File] = None, outputCallback: Option[BufferedReader => Unit] = None): Unit = { - val pb = new ProcessBuilder(prepareCommand(cmd): _*).inheritIO() + val pb = new ProcessBuilder(prepareCommand(cmd): _*) directory match { case Some(dir) =>