Eclipse + Typelevel compiler: put it under ~/.sbt/1.0/plugins/ and use the 'exlipse' command (requires sbteclipse 5.2.4)

December 6, 2017 ยท View on GitHub

/*

  • Copyright 2016-2017 Daniel Urban
  • 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. */

import sbt._ import sbt.Keys._

import scala.xml.{ Attribute, Elem, MetaData, Node, NodeSeq, Null, Text, NamespaceBinding } import scala.xml.transform.RewriteRule

import com.typesafe.sbteclipse.core.EclipsePlugin.EclipseTransformerFactory import com.typesafe.sbteclipse.core.EclipsePlugin.EclipseKeys._

import scalaz.{ Validation, NonEmptyList }

/**

  • This is a terrible hack to allow sbteclipse to cope
  • with slightly nonstandard projects.
  • It works around the following issues:
    • no "-Ypartial-unification" in the presentation compiler
  • (this is worked around by including the si2712fix-plugin
  • in the generated Eclipse projects);
    • no "-Xstrict-patmat-analysis" in the presentation compiler
  • (this is simply removed from the options);
    • "-Ymacro-expand:normal" is added to the options,
  • because the presentation compiler defaults to "discard";
    • compiler plugins which depend on a patch-level version of
  • the Scala compiler (e.g., macro paradise) don't work if
  • scalaVersion is not exactly the same as the presentation
  • compiler version (this is worked around by replacing scalaVersion);
    • duplicated Scala container classpath entries (probably due
  • to the use of the Typelevel compiler; this is worked
  • around by filtering these).
  • All of the setting modifications are only performed to generate
  • the Eclipse projects, and then they are reset to their original
  • values.
  • Usage:
    • put this file under ~/.sbt/1.0/plugins/
    • use the exlipse command in sbt to generate project files
    • import the projects into Scala IDE 4.7.1 RC1. */ object EclipseHack extends AutoPlugin {

private val saveKey = AttributeKeySeq[Setting[_]]

override def requires = com.typesafe.sbteclipse.plugin.EclipsePlugin

override def trigger = allRequirements

override def projectSettings: Seq[Setting[_]] = Seq( classpathTransformerFactories += HackyFactory, commands += exlipseCommand )

private lazy val exlipseCommand: Command = Command.command("exlipse") { st => val modified: State = modifyOptions(st) modified.log.info("Running sbteclipse ...") val generated: State = modified.copy(remainingCommands = Exec("eclipse with-source=true", Some(Exec.newExecId), None) +: modified.remainingCommands) val resetted: State = resetOptions(generated) resetted }

private def modifyOptions(state: State): State = { state.log.info("Remembering current settings ...") val savedState = state.put(saveKey, Project.extract(state).session.rawAppend) savedState.log.info("Modifying scalacOptions ...") val extracted = Project.extract(savedState) val structure = extracted.structure val newSettings = structure.allProjectRefs.flatMap { ref => val scope = Scope(Select(ref), Zero, Zero, Zero) Seq( // this is only in TLS: scalacOptions in scope -= "-Xstrict-patmat-analysis", scalaOrganization in scope := "org.scala-lang", // for some compiler plugins: scalaVersion in scope := { val sv = (scalaVersion in scope).value if (sv.startsWith("2.12")) "2.12.3" else if (sv.startsWith("2.11")) "2.11.8" else sv }, // this is only in 2.11.11 and 2.12 and TLS: scalacOptions in scope --= (if (!(scalaVersion in scope).value.startsWith("2.12")) { Seq("-Ypartial-unification") } else { Seq() }), libraryDependencies in scope ++= (if (!(scalaVersion in scope).value.startsWith("2.12")) { Seq(compilerPlugin("com.milessabin" % "si2712fix-plugin" % "1.2.0" cross CrossVersion.patch)) } else { Seq() }), // changed in 2.12.3: scalacOptions in scope := { val opts = (scalacOptions in scope).value opts.filter { case "-opt:l:inline" => state.log.info("Leaving out -opt:l:inline for Eclipse") false case s if s.startsWith("-opt-inline-from:") => state.log.info("Leaving out -opt-inline-from:... for Eclipse") false case x => true } }, // the PC defaults to "discard", but this causes less invalid errors: scalacOptions in scope += "-Ymacro-expand:normal" ) } val newSession = extracted.session.copy(rawAppend = extracted.session.rawAppend ++ newSettings) BuiltinCommands.reapply(newSession, structure, savedState) }

private def resetOptions(state: State): State = { state.get(saveKey) match { case Some(rawAppend) => state.log.info("Resetting modified settings ...") val extracted = Project.extract(state) val resettedSession = extracted.session.copy(rawAppend = rawAppend) BuiltinCommands.reapply(resettedSession, extracted.structure, state).remove(saveKey) case None => state.log.warn("No modified settings found!") state } }

private object HackyFactory extends EclipseTransformerFactory[RewriteRule] {

private val CpEntry = "classpathentry"
private val Cpath = "classpath"
private val ScalaContainer = "org.scala-ide.sdt.launching.SCALA_CONTAINER"
private val ScalaCompiler = "org.scala-ide.sdt.launching.SCALA_COMPILER_CONTAINER"

override def createTransformer(ref: ProjectRef, state: State): Validation[NonEmptyList[String], RewriteRule] = {

  val rr: RewriteRule = new RewriteRule {

    override def transform(node: Node): Seq[Node] = node match {
      case Elem(pf, CpEntry, attrs, scope, children @ _*) if isScalaContainer(attrs) || isScalaCompiler(attrs) =>
        NodeSeq.Empty
      case Elem(pf, Cpath, attrs, scope, children @ _*) =>
        val newChildren = children :+
          container(ScalaContainer, pf, scope) :+
          container(ScalaCompiler, pf, scope)
        Elem(pf, Cpath, attrs, scope, newChildren: _*)
      case x =>
        x
    }

    private def container(name: String, pf: String, scope: NamespaceBinding): Elem =
      Elem(pf, CpEntry, Attribute("kind", Text("con"), Attribute("path", Text(name), Null)), scope)

    private def isScalaContainer(metaData: MetaData): Boolean =
      isContainer(metaData, ScalaContainer)

    private def isScalaCompiler(metaData: MetaData): Boolean =
      isContainer(metaData, ScalaCompiler)

    private def isContainer(metaData: MetaData, typ: String): Boolean = {
      (metaData("kind") == Text("con")) &&
      (Option(metaData("path").text) map (_ == typ) getOrElse false)
    }
  }

  Validation.success(rr)
}

} }