diff --git a/project/Build.scala b/project/Build.scala index d706a5a26248..1e613cf85813 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 @@ -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" } @@ -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..d423b07af9a4 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 = { - val pb = new ProcessBuilder(prepareCommand(cmd): _*).inheritIO() - if (directory != null) pb.directory(directory) + def runProcess(cmd: Seq[String], wait: Boolean = false, directory: Option[File] = None, outputCallback: Option[BufferedReader => Unit] = None): Unit = { + val pb = new ProcessBuilder(prepareCommand(cmd): _*) + + 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