package cbt import java.io.{File, FileNotFoundException} import java.lang.management.ManagementFactory import java.util.{Map => JMap} import scala.collection.JavaConverters._ import scala.sys.process._ import scala.util.Try import scala.util.control.NonFatal /** * Plugin that provides ENSIME (http://ensime.org/) support. * * The central method provided by this plugin is `ensime`, which will generate an ENSIME * configuration file describing the current build. This configuration file may be used by ENSIME * server and clients, adding IDE capabilities to plain text editors. * * Most methods and helper classes were copied verbatim or with minor changes from the * ensime-related projects, ensime-server (https://github.com/ensime/ensime-server) and ensime-sbt * (https://github.com/ensime/ensime-sbt). Licensed under the Apache 2.0 License. */ trait Ensime extends BaseBuild { /** The version of ensime to use. */ def ensimeServerVersion: String = "2.0.0-SNAPSHOT" /** ENSIME server and client configuration. */ def ensimeConfig: Ensime.EnsimeConfig = Ensime.EnsimeConfig( scalaCompilerJars = { new ScalaDependencies( context.cbtLastModified, context.paths.mavenCache, scalaVersion ).dependencies.flatMap(_.exportedClasspathArray) }, ensimeServerJars = { val deps = Dependencies(Resolver(mavenCentral, sonatypeReleases, sonatypeSnapshots).bind( MavenDependency("org.ensime", s"server_$scalaMajorVersion", ensimeServerVersion) )) (deps.dependencies ++ deps.transitiveDependencies).flatMap(_.exportedClasspathArray) }, ensimeServerVersion = scalaVersion, rootDir = projectDirectory, cacheDir = projectDirectory / ".ensime_cache", javaHome = Ensime.jdkHome, name = name, scalaVersion = scalaVersion, javaSources = Ensime.jdkSource.toSeq, javaFlags = Ensime.baseJavaFlags(ensimeServerVersion), projects = Ensime.findProjects(this) ) /** Generate an ENSIME configuration file for this build. */ def ensime: File = lib.writeIfChanged( projectDirectory / ".ensime", ensimeConfig.toSexp ) } object Ensime { def jdkHome: File = List( // manual sys.env.get("JDK_HOME"), sys.env.get("JAVA_HOME"), // fallback sys.props.get("java.home").map(new File(_).getParent), sys.props.get("java.home"), // osx Try("/usr/libexec/java_home".!!.trim).toOption ).flatten.filter { n => new File(n + "/lib/tools.jar").exists }.headOption.map(new File(_).getCanonicalFile).getOrElse( throw new FileNotFoundException( """Could not automatically find the JDK/lib/tools.jar. |You must explicitly set JDK_HOME or JAVA_HOME.""".stripMargin ) ) def jdkSource: Option[File] = { val src = new File(jdkHome, "src.zip") if (src.exists) Some(src) else None } def baseJavaFlags(serverVersion: String): Seq[String] = { val raw = ManagementFactory.getRuntimeMXBean.getInputArguments.asScala.toList // WORKAROUND https://github.com/ensime/ensime-sbt/issues/91 // WORKAROUND https://github.com/ensime/ensime-server/issues/1756 val StackSize = "-Xss[^ ]+".r val MinHeap = "-Xms[^ ]+".r val MaxHeap = "-Xmx[^ ]+".r val MaxPerm = "-XX:MaxPermSize=[^ ]+".r val corrected = raw.filter { case StackSize() => false case MinHeap() => false case MaxHeap() => false case MaxPerm() => false case other => true } val memory = Seq( "-Xss2m", "-Xms512m", "-Xmx4g" ) val server = serverVersion.substring(0, 3) val java = sys.props("java.version").substring(0, 3) val versioned = (java, server) match { case (_, "1.0") | ("1.6" | "1.7", _) => Seq( "-XX:MaxPermSize=256m" ) case _ => List( "-XX:MaxMetaspaceSize=256m", // these improve ensime-server performance "-XX:StringTableSize=1000003", "-XX:+UnlockExperimentalVMOptions", "-XX:SymbolTableSize=1000003" ) } // WORKAROUND: https://github.com/scala/scala/pull/5592 val zipFix = Seq("-Dscala.classpath.closeZip=true") corrected ++ memory ++ versioned ++ zipFix } /** Attempt to download a maven dependency with a given classifer. Warn if it is not available. */ def resolveWithClassifier(dependencies: Seq[Dependency], classifier: Classifier)(implicit logger: Logger, cache: JMap[AnyRef, AnyRef], classLoaderCache: ClassLoaderCache): Seq[File] = { val classifierName = classifier.name.getOrElse("") val classified = dependencies.collect{ case m: BoundMavenDependency => m.copy(mavenDependency = m.mavenDependency.copy(classifier = classifier)) } classified.flatMap{ dependency => try { dependency.exportedJars } catch { case NonFatal(_) => logger.resolver( s"ensime: could not find $classifierName of ${dependency.mavenDependency.serialize}") Seq.empty } } } /** Find and convert all (transitive) dependencies of a build to a sequence of ENSIME projects. */ def findProjects(root: BaseBuild)(implicit logger: Logger, cache: JMap[AnyRef, AnyRef], classLoaderCache: ClassLoaderCache): Seq[EnsimeProject] = { def asProject(base: BaseBuild) = EnsimeProject( id = EnsimeProjectId( project = base.name, config = "compile" ), depends = base.transitiveDependencies.collect{ case b: BaseBuild => EnsimeProjectId(b.name, "compile") }, sources = base.sources.filter(_.isDirectory), targets = Seq(base.compileTarget), scalacOptions = base.scalacOptions.toList, javacOptions = Nil, // TODO get javac options from base build libraryJars = base.transitiveDependencies.flatMap(_.exportedClasspathArray).toList, librarySources = resolveWithClassifier(base.transitiveDependencies, Classifier.sources), libraryDocs = resolveWithClassifier(base.transitiveDependencies, Classifier.javadoc) ) Seq(asProject(root)) ++ root.transitiveDependencies.collect{ case b: BaseBuild => asProject(b) } } final case class EnsimeProjectId( project: String, config: String ) final case class EnsimeProject( id: EnsimeProjectId, depends: Seq[EnsimeProjectId], sources: Seq[File], targets: Seq[File], scalacOptions: Seq[String], javacOptions: Seq[String], libraryJars: Seq[File], librarySources: Seq[File], libraryDocs: Seq[File] ) /* EnsimeModules are required for backwards-compatibility with clients. * They can automatically be derived from projects (see method EnsimeModule.fromProjects). */ private final case class EnsimeModule( name: String, mainRoots: Set[File], testRoots: Set[File], targets: Set[File], testTargets: Set[File], dependsOnNames: Set[String], compileJars: Set[File], runtimeJars: Set[File], testJars: Set[File], sourceJars: Set[File], docJars: Set[File] ) private object EnsimeModule { def fromProjects(p: Iterable[EnsimeProject]): EnsimeModule = { val name = p.head.id.project val deps = for { s <- p d <- s.depends if d.project != name } yield d.project val (mains, tests) = p.toSet.partition(_.id.config == "compile") val mainSources = mains.flatMap(_.sources) val mainTargets = mains.flatMap(_.targets) val mainJars = mains.flatMap(_.libraryJars) val testSources = tests.flatMap(_.sources) val testTargets = tests.flatMap(_.targets) val testJars = tests.flatMap(_.libraryJars).toSet -- mainJars val sourceJars = p.flatMap(_.librarySources).toSet val docJars = p.flatMap(_.libraryDocs).toSet EnsimeModule( name, mainSources, testSources, mainTargets, testTargets, deps.toSet, mainJars, Set.empty, testJars, sourceJars, docJars ) } } final case class EnsimeConfig( scalaCompilerJars: Seq[File], ensimeServerJars: Seq[File], ensimeServerVersion: String, rootDir: File, cacheDir: File, javaHome: File, name: String, scalaVersion: String, javaSources: Seq[File], javaFlags: Seq[String], projects: Seq[EnsimeProject] ) { def toSexp: String = EnsimeConfig.sexp(this) } object EnsimeConfig { private def sexp(value: Any): String = value match { case s: String => s""""$s"""" case f: File => sexp(f.getAbsolutePath) case (k, v) => s":$k ${sexp(v)}\n" case xss: Traversable[_] if xss.isEmpty => "nil" case xss: Traversable[_] => xss.map(sexp(_)).mkString("(", " ", ")") case EnsimeProjectId(project, config) => sexp(List( "project" -> project, "config" -> config )) case proj: EnsimeProject => sexp(List( "id" -> proj.id, "depends" -> proj.depends, "sources" -> proj.sources, "targets" -> proj.targets, "scalac-options" -> proj.scalacOptions, "javac-options" -> proj.javacOptions, "library-jars" -> proj.libraryJars, "library-sources" -> proj.librarySources, "library-docs" -> proj.libraryDocs )) case conf: EnsimeConfig => sexp(List( "root-dir" -> conf.rootDir, "cache-dir" -> conf.cacheDir, "scala-compiler-jars" -> conf.scalaCompilerJars, "ensime-server-jars" -> conf.ensimeServerJars, "ensime-server-version" -> conf.ensimeServerVersion, "name" -> conf.name, "scala-version" -> conf.scalaVersion, "java-home" -> conf.javaHome, "java-flags" -> conf.javaFlags, "java-sources" -> conf.javaSources, "projects" -> conf.projects, // subprojects are required for backwards-compatibility with older clients // (ensime-server does not require them) "subprojects" -> conf.projects.groupBy(_.id.project).mapValues(EnsimeModule.fromProjects)//conf.projects.flatMap(proj => EnsimeModule.fromProjects(conf.projects)) )) case module: EnsimeModule => sexp(List( "name" -> module.name, "source-roots" -> (module.mainRoots ++ module.testRoots), "targets" -> module.targets, "test-targets" -> module.testTargets, "depends-on-modules" -> module.dependsOnNames, "compile-deps" -> module.compileJars, "runtime-deps" -> module.runtimeJars, "test-deps" -> module.testJars, "doc-jars"-> module.docJars, "reference-source-roots" -> module.sourceJars )) } } }