From 88f54cc356cf6b59e2cbe8381ddcb01667f72ccb Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Tue, 21 Feb 2017 15:57:41 +0100 Subject: [PATCH] Add sbt-launcher --- build.sbt | 14 + project/generate-sbt-launcher.sh | 11 + .../sbtlauncher/AppConfiguration.scala | 9 + .../coursier/sbtlauncher/AppProvider.scala | 18 + .../coursier/sbtlauncher/ApplicationID.scala | 54 ++ .../sbtlauncher/ComponentProvider.scala | 70 ++ .../sbtlauncher/DummyGlobalLock.scala | 21 + .../scala/coursier/sbtlauncher/Launcher.scala | 638 ++++++++++++++++++ .../scala/coursier/sbtlauncher/MainApp.scala | 110 +++ .../coursier/sbtlauncher/Repository.scala | 19 + .../coursier/sbtlauncher/SbtConfig.scala | 112 +++ .../coursier/sbtlauncher/ScalaProvider.scala | 16 + sbt.properties | 16 + 13 files changed, 1108 insertions(+) create mode 100755 project/generate-sbt-launcher.sh create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/AppConfiguration.scala create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/AppProvider.scala create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/ApplicationID.scala create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/ComponentProvider.scala create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/DummyGlobalLock.scala create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/Launcher.scala create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/MainApp.scala create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/Repository.scala create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/SbtConfig.scala create mode 100644 sbt-launcher/src/main/scala/coursier/sbtlauncher/ScalaProvider.scala create mode 100644 sbt.properties diff --git a/build.sbt b/build.sbt index 5ede4bd5a..129738960 100644 --- a/build.sbt +++ b/build.sbt @@ -382,6 +382,19 @@ lazy val `sbt-shading` = project libraryDependencies += "org.anarres.jarjar" % "jarjar-core" % "1.0.0" ) +lazy val `sbt-launcher` = project + .dependsOn(cache) + .settings(commonSettings) + .settings(packAutoSettings) + .settings( + libraryDependencies ++= Seq( + "com.github.alexarchambault" %% "case-app" % "1.1.3", + "org.scala-sbt" % "launcher-interface" % "1.0.0", + "com.typesafe" % "config" % "1.3.1" + ), + packExcludeArtifactTypes += "pom" + ) + val http4sVersion = "0.8.6" lazy val `http-server` = project @@ -426,6 +439,7 @@ lazy val `coursier` = project.in(file(".")) cli, `sbt-coursier`, `sbt-shading`, + `sbt-launcher`, web, doc, `http-server`, diff --git a/project/generate-sbt-launcher.sh b/project/generate-sbt-launcher.sh new file mode 100755 index 000000000..c376262d9 --- /dev/null +++ b/project/generate-sbt-launcher.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e + +COURSIER_VERSION=1.0.0-SNAPSHOT + +"$(dirname "$0")/../coursier" bootstrap \ + "io.get-coursier:sbt-launcher_2.12:$COURSIER_VERSION" \ + -i launcher \ + -I launcher:org.scala-sbt:launcher-interface:1.0.0 \ + -o csbt \ + -J -Djline.shutdownhook=false diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/AppConfiguration.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/AppConfiguration.scala new file mode 100644 index 000000000..7fadd73f4 --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/AppConfiguration.scala @@ -0,0 +1,9 @@ +package coursier.sbtlauncher + +import java.io.File + +final case class AppConfiguration( + arguments: Array[String], + baseDirectory: File, + provider: xsbti.AppProvider +) extends xsbti.AppConfiguration diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/AppProvider.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/AppProvider.scala new file mode 100644 index 000000000..ac671f138 --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/AppProvider.scala @@ -0,0 +1,18 @@ +package coursier.sbtlauncher + +import java.io.File + +final case class AppProvider( + scalaProvider: xsbti.ScalaProvider, + id: xsbti.ApplicationID, + loader: ClassLoader, + mainClass: Class[_ <: xsbti.AppMain], + createMain: () => xsbti.AppMain, + mainClasspath: Array[File], + components: xsbti.ComponentProvider +) extends xsbti.AppProvider { + def entryPoint: Class[_] = + mainClass + def newMain(): xsbti.AppMain = + createMain() +} diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/ApplicationID.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/ApplicationID.scala new file mode 100644 index 000000000..423eb35ea --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/ApplicationID.scala @@ -0,0 +1,54 @@ +package coursier.sbtlauncher + +import java.io.File + +final case class ApplicationID( + groupID: String, + name: String, + version: String, + mainClass: String, + mainComponents: Array[String], + crossVersioned: Boolean, + crossVersionedValue: xsbti.CrossValue, + classpathExtra: Array[File] +) extends xsbti.ApplicationID { + + assert(crossVersioned == (crossVersionedValue != xsbti.CrossValue.Disabled)) + + def disableCrossVersion(scalaVersion: String): ApplicationID = + crossVersionedValue match { + case xsbti.CrossValue.Disabled => + this + case xsbti.CrossValue.Binary => + val scalaBinaryVersion = scalaVersion.split('.').take(2).mkString(".") + copy( + crossVersioned = false, + crossVersionedValue = xsbti.CrossValue.Disabled, + version = s"${version}_$scalaBinaryVersion" + ) + case xsbti.CrossValue.Full => + copy( + crossVersioned = false, + crossVersionedValue = xsbti.CrossValue.Disabled, + version = s"${version}_$scalaVersion" + ) + } +} + +object ApplicationID { + def apply(id: xsbti.ApplicationID): ApplicationID = + id match { + case id0: ApplicationID => id0 + case _ => + ApplicationID( + id.groupID(), + id.name(), + id.version(), + id.mainClass(), + id.mainComponents(), + id.crossVersionedValue() != xsbti.CrossValue.Disabled, + id.crossVersionedValue(), + id.classpathExtra() + ) + } +} diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/ComponentProvider.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/ComponentProvider.scala new file mode 100644 index 000000000..923de660d --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/ComponentProvider.scala @@ -0,0 +1,70 @@ +package coursier.sbtlauncher + +import java.io.File +import java.nio.file.{Files, StandardCopyOption} + +import scala.collection.mutable + +final class ComponentProvider(cacheDir: File) extends xsbti.ComponentProvider { + + private val components0 = new mutable.HashMap[String, Array[File]] + + def componentLocation(id: String): File = + new File(cacheDir, id) + + def component(componentId: String): Array[File] = { + val res = components0.getOrElse[Array[File]]( + componentId, + { + val dir = componentLocation(componentId) + if (dir.exists()) + Option(dir.listFiles()).getOrElse(Array()) + else + Array() + } + ) + res + } + + private def clear(componentId: String): Unit = { + + def deleteRecursively(f: File): Unit = + if (f.isFile) + f.delete() + else + Option(f.listFiles()) + .getOrElse(Array()) + .foreach(deleteRecursively) + + val dir = componentLocation(componentId) + deleteRecursively(dir) + } + + private def copying(componentId: String, f: File): File = { + + // TODO Use some locking mechanisms here + + val dir = componentLocation(componentId) + dir.mkdirs() + val dest = new File(dir, f.getName) + Files.copy(f.toPath, dest.toPath, StandardCopyOption.REPLACE_EXISTING) + dest + } + + def defineComponentNoCopy(componentId: String, components: Array[File]): Unit = { + components0 += componentId -> components.distinct + } + def defineComponent(componentId: String, components: Array[File]): Unit = { + clear(componentId) + components0 += componentId -> components.distinct.map(copying(componentId, _)) + } + def addToComponent(componentId: String, components: Array[File]): Boolean = { + val previousFiles = components0.getOrElse(componentId, Array.empty[File]) + val newFiles = (previousFiles ++ components.distinct.map(copying(componentId, _))).distinct + components0 += componentId -> newFiles + newFiles.length != previousFiles.length + } + + def lockFile: File = new File("/component-lock") + +} diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/DummyGlobalLock.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/DummyGlobalLock.scala new file mode 100644 index 000000000..21584e1d7 --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/DummyGlobalLock.scala @@ -0,0 +1,21 @@ +package coursier.sbtlauncher + +import java.io.File +import java.util.concurrent.{Callable, ConcurrentHashMap} + +case object DummyGlobalLock extends xsbti.GlobalLock { + + private val locks = new ConcurrentHashMap[File, AnyRef] + + def apply[T](lockFile: File, run: Callable[T]): T = + Option(lockFile) match { + case None => run.call() + case Some(lockFile0) => + val lock0 = new AnyRef + val lock = Option(locks.putIfAbsent(lockFile0, lock0)).getOrElse(lock0) + + lock.synchronized { + run.call() + } + } +} diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/Launcher.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/Launcher.scala new file mode 100644 index 000000000..d591a9dd8 --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/Launcher.scala @@ -0,0 +1,638 @@ +package coursier.sbtlauncher + +import java.io.{File, OutputStreamWriter} +import java.net.{URL, URLClassLoader} +import java.util.concurrent.ConcurrentHashMap + +import coursier.Cache.Logger +import coursier._ +import coursier.ivy.IvyRepository +import coursier.maven.MavenSource + +import scala.annotation.tailrec +import scalaz.{-\/, \/-} +import scalaz.concurrent.Task + +class Launcher( + scalaVersion: String, + componentsCache: File, + val ivyHome: File +) extends xsbti.Launcher { + + val componentProvider = new ComponentProvider(componentsCache) + + lazy val baseLoader = { + + @tailrec + def rootLoader(cl: ClassLoader): ClassLoader = + if (cl == null) + sys.error("Cannot find base loader") + else { + val isLauncherLoader = + try { + cl + .asInstanceOf[AnyRef { def getIsolationTargets: Array[String] }] + .getIsolationTargets + .contains("launcher") + } catch { + case _: Throwable => false + } + + if (isLauncherLoader) + cl + else + rootLoader(cl.getParent) + } + + rootLoader(Thread.currentThread().getContextClassLoader) + } + + val repositoryIdPrefix = "coursier-launcher-" + + val repositories = Seq( + // mmh, ID "local" seems to be required for publishLocal to be fine if we're launching sbt + "local" -> Cache.ivy2Local, + s"${repositoryIdPrefix}central" -> MavenRepository("https://repo1.maven.org/maven2", sbtAttrStub = true), + s"${repositoryIdPrefix}typesafe-ivy-releases" -> IvyRepository.parse( + "https://repo.typesafe.com/typesafe/ivy-releases/[organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext]" + ).leftMap(sys.error).merge, + s"${repositoryIdPrefix}sbt-plugin-releases" -> IvyRepository.parse( + "https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/[organization]/[module](/scala_[scalaVersion])(/sbt_[sbtVersion])/[revision]/[type]s/[artifact](-[classifier]).[ext]" + ).leftMap(sys.error).merge + ) + + assert(!repositories.groupBy(_._1).exists(_._2.lengthCompare(1) > 0)) + + val cachePolicies = CachePolicy.default + + def fetch(logger: Option[Logger]) = { + def helper(policy: CachePolicy) = + Cache.fetch(cachePolicy = policy, logger = logger) + + val f = cachePolicies.map(helper) + + Fetch.from(repositories.map(_._2), f.head, f.tail: _*) + } + + val keepArtifactTypes = Set("jar", "bundle") + + def tasks(res: Resolution, logger: Option[Logger], classifiersOpt: Option[Seq[String]] = None) = { + val a = classifiersOpt + .fold(res.dependencyArtifacts.map(_._2))(res.dependencyClassifiersArtifacts(_).map(_._2)) + + val keepArtifactTypes = classifiersOpt.fold(Set("jar", "bundle"))(c => c.map(c => MavenSource.classifierExtensionDefaultTypes.getOrElse((c, "jar"), ???)).toSet) + + a.collect { + case artifact if keepArtifactTypes(artifact.`type`) => + def file(policy: CachePolicy) = Cache.file( + artifact, + cachePolicy = policy, + logger = logger + ) + + (file(cachePolicies.head) /: cachePolicies.tail)(_ orElse file(_)) + .run + .map(artifact.->) + } + } + + + def isOverrideRepositories = false // ??? + + def bootDirectory: File = ??? + + def getScala(version: String): xsbti.ScalaProvider = + getScala(version, "") + + def getScala(version: String, reason: String): xsbti.ScalaProvider = + getScala(version, reason, "org.scala-lang") + + def getScala(version: String, reason: String, scalaOrg: String): xsbti.ScalaProvider = { + + val key = (version, scalaOrg) + + Option(scalaProviderCache.get(key)).getOrElse { + val prov = getScala0(version, reason, scalaOrg) + val previous = Option(scalaProviderCache.putIfAbsent(key, prov)) + previous.getOrElse(prov) + } + } + + private val scalaProviderCache = new ConcurrentHashMap[(String, String), xsbti.ScalaProvider] + + private def getScala0(version: String, reason: String, scalaOrg: String): xsbti.ScalaProvider = { + + val files = getScalaFiles(version, reason, scalaOrg) + + val libraryJar = files.find(_.getName.startsWith("scala-library")).getOrElse { + throw new NoSuchElementException("scala-library JAR") + } + val compilerJar = files.find(_.getName.startsWith("scala-compiler")).getOrElse { + throw new NoSuchElementException("scala-compiler JAR") + } + + val scalaLoader = new URLClassLoader(files.map(_.toURI.toURL).toArray, baseLoader) + + ScalaProvider( + this, + version, + scalaLoader, + files.toArray, + libraryJar, + compilerJar, + id => app(id, id.version()) + ) + } + + private def getScalaFiles(version: String, reason: String, scalaOrg: String): Seq[File] = { + + val initialRes = Resolution( + Set( + Dependency(Module(scalaOrg, "scala-library"), version), + Dependency(Module(scalaOrg, "scala-compiler"), version) + ), + forceVersions = Map( + Module(scalaOrg, "scala-library") -> version, + Module(scalaOrg, "scala-compiler") -> version, + Module(scalaOrg, "scala-reflect") -> version + ) + ) + + val logger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + logger.foreach(_.init { + System.err.println(s"Resolving Scala $version (organization $scalaOrg)") + }) + + val res = initialRes.process.run(fetch(logger)).unsafePerformSync + + logger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Resolved Scala $version (organization $scalaOrg)") + } + + if (res.errors.nonEmpty) { + Console.err.println(s"Errors:\n${res.errors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + if (res.conflicts.nonEmpty) { + Console.err.println(s"Conflicts:\n${res.conflicts.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + if (!res.isDone) { + Console.err.println("Did not converge") + sys.exit(1) + } + + val artifactLogger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + artifactLogger.foreach(_.init { + System.err.println(s"Fetching Scala $version artifacts (organization $scalaOrg)") + }) + + val results = Task.gatherUnordered(tasks(res, artifactLogger)).unsafePerformSync + + artifactLogger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Fetched Scala $version artifacts (organization $scalaOrg)") + } + + val errors = results.collect { case (a, -\/(err)) => (a, err) } + val files = results.collect { case (_, \/-(f)) => f } + + if (errors.nonEmpty) { + Console.err.println(s"Error downloading artifacts:\n${errors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + files + } + + def topLoader: ClassLoader = baseLoader + + def appRepositories: Array[xsbti.Repository] = + repositories.map { + case (id, m: MavenRepository) => + Repository.Maven(id, new URL(m.root)) + case (id, i: IvyRepository) => + + assert(i.metadataPatternOpt.forall(_ == i.pattern)) + + val (base, pat) = i.pattern.string.span(c => c != '[' && c != '$' && c != '(') + + assert(base.nonEmpty, i.pattern.string) + + Repository.Ivy( + id, + new URL(base), + pat, + pat, + mavenCompatible = false, + skipConsistencyCheck = true, // ??? + descriptorOptional = true // ??? + ) + }.toArray + + def ivyRepositories: Array[xsbti.Repository] = + appRepositories // ??? + + def globalLock = DummyGlobalLock + + // See https://github.com/sbt/ivy/blob/2cf13e211b2cb31f0d3b317289dca70eca3362f6/src/java/org/apache/ivy/util/ChecksumHelper.java + def checksums: Array[String] = Array("sha1", "md5") + + def app(id: xsbti.ApplicationID, version: String): xsbti.AppProvider = + app(ApplicationID(id).copy(version = version)) + + def app(id: xsbti.ApplicationID, extra: Dependency*): xsbti.AppProvider = { + + val (scalaFiles, files) = appFiles(id, extra: _*) + + val scalaLoader = new URLClassLoader(scalaFiles.map(_.toURI.toURL).toArray, baseLoader) + + val libraryJar = scalaFiles.find(_.getName.startsWith("scala-library")).getOrElse { + throw new NoSuchElementException("scala-library JAR") + } + val compilerJar = scalaFiles.find(_.getName.startsWith("scala-compiler")).getOrElse { + throw new NoSuchElementException("scala-compiler JAR") + } + + val scalaProvider = ScalaProvider( + this, + scalaVersion, + scalaLoader, + scalaFiles.toArray, + libraryJar, + compilerJar, + id => app(id, id.version()) + ) + + val loader = new URLClassLoader(files.filterNot(scalaFiles.toSet).map(_.toURI.toURL).toArray, scalaLoader) + val mainClass0 = loader.loadClass(id.mainClass).asSubclass(classOf[xsbti.AppMain]) + + AppProvider( + scalaProvider, + id, + loader, + mainClass0, + () => mainClass0.newInstance(), + files.toArray, + componentProvider + ) + } + + private def appFiles(id: xsbti.ApplicationID, extra: Dependency*): (Seq[File], Seq[File]) = { + + val id0 = ApplicationID(id).disableCrossVersion(scalaVersion) + + val initialRes = Resolution( + Set( + Dependency(Module("org.scala-lang", "scala-library"), scalaVersion), + Dependency(Module("org.scala-lang", "scala-compiler"), scalaVersion), + Dependency(Module(id0.groupID, id0.name), id0.version) + ) ++ extra, + forceVersions = Map( + Module("org.scala-lang", "scala-library") -> scalaVersion, + Module("org.scala-lang", "scala-compiler") -> scalaVersion, + Module("org.scala-lang", "scala-reflect") -> scalaVersion + ) + ) + + val logger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + val extraMsg = + if (extra.isEmpty) + "" + else + s" (plus ${extra.length} dependencies)" + + logger.foreach(_.init { + System.err.println(s"Resolving ${id0.groupID}:${id0.name}:${id0.version}$extraMsg") + }) + + val res = initialRes.process.run(fetch(logger)).unsafePerformSync + + logger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Resolved ${id0.groupID}:${id0.name}:${id0.version}$extraMsg") + } + + if (res.errors.nonEmpty) { + Console.err.println(s"Errors:\n${res.errors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + if (res.conflicts.nonEmpty) { + Console.err.println(s"Conflicts:\n${res.conflicts.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + if (!res.isDone) { + Console.err.println("Did not converge") + sys.exit(1) + } + + val artifactLogger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + artifactLogger.foreach(_.init { + System.err.println(s"Fetching ${id0.groupID}:${id0.name}:${id0.version} artifacts") + }) + + val results = Task.gatherUnordered(tasks(res, artifactLogger)).unsafePerformSync + + artifactLogger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Fetched ${id0.groupID}:${id0.name}:${id0.version} artifacts") + } + + val errors = results.collect { case (a, -\/(err)) => (a, err) } + val files = results.collect { case (_, \/-(f)) => f } + + if (errors.nonEmpty) { + Console.err.println(s"Error downloading artifacts:\n${errors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + val scalaSubRes = res.subset( + Set( + Dependency(Module("org.scala-lang", "scala-library"), scalaVersion), + Dependency(Module("org.scala-lang", "scala-compiler"), scalaVersion) + ) + ) + + val scalaArtifactLogger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + scalaArtifactLogger.foreach(_.init { + System.err.println(s"Fetching ${id0.groupID}:${id0.name}:${id0.version} Scala artifacts") + }) + + val scalaResults = Task.gatherUnordered(tasks(scalaSubRes, scalaArtifactLogger)).unsafePerformSync + + scalaArtifactLogger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Fetched ${id0.groupID}:${id0.name}:${id0.version} Scala artifacts") + } + + val scalaErrors = scalaResults.collect { case (a, -\/(err)) => (a, err) } + val scalaFiles = scalaResults.collect { case (_, \/-(f)) => f } + + if (scalaErrors.nonEmpty) { + Console.err.println(s"Error downloading artifacts:\n${scalaErrors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + (scalaFiles, files) + } + + def registerScalaComponents(scalaVersion: String = scalaVersion): Unit = { + + lazy val prov = getScala(scalaVersion) + lazy val jars = prov.jars() + + lazy val libraryJar = jars.find(_.getName.startsWith("scala-library")).getOrElse { + throw new NoSuchElementException("scala-library JAR") + } + lazy val compilerJar = jars.find(_.getName.startsWith("scala-compiler")).getOrElse { + throw new NoSuchElementException("scala-compiler JAR") + } + lazy val reflectJar = jars.find(_.getName.startsWith("scala-reflect")).getOrElse { + throw new NoSuchElementException("scala-reflect JAR") + } + + if (componentProvider.component("library").isEmpty) + componentProvider.defineComponentNoCopy("library", Array(libraryJar)) + if (componentProvider.component("compiler").isEmpty) + componentProvider.defineComponentNoCopy("compiler", Array(compilerJar)) + if (componentProvider.component("reflect").isEmpty) + componentProvider.defineComponentNoCopy("reflect", Array(reflectJar)) + } + + def registerSbtInterfaceComponents(sbtVersion: String): Unit = { + + lazy val (interfaceJar, _) = sbtInterfaceComponentFiles(sbtVersion) + lazy val compilerInterfaceSourceJar = sbtCompilerInterfaceSrcComponentFile(sbtVersion) + + if (componentProvider.component("xsbti").isEmpty) + componentProvider.defineComponentNoCopy("xsbti", Array(interfaceJar)) + if (componentProvider.component("compiler-interface").isEmpty) + componentProvider.defineComponentNoCopy("compiler-interface", Array(compilerInterfaceSourceJar)) + if (componentProvider.component("compiler-interface-src").isEmpty) + componentProvider.defineComponentNoCopy("compiler-interface-src", Array(compilerInterfaceSourceJar)) + } + + private def sbtInterfaceComponentFiles(sbtVersion: String): (File, File) = { + + lazy val res = { + + val initialRes = Resolution( + Set( + Dependency(Module("org.scala-sbt", "interface"), sbtVersion, transitive = false) + ) + ) + + val logger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + logger.foreach(_.init { + System.err.println(s"Resolving org.scala-sbt:interface:$sbtVersion") + }) + + val res = initialRes.process.run(fetch(logger)).unsafePerformSync + + logger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Resolved org.scala-sbt:interface:$sbtVersion") + } + + if (res.errors.nonEmpty) { + Console.err.println(s"Errors:\n${res.errors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + if (res.conflicts.nonEmpty) { + Console.err.println(s"Conflicts:\n${res.conflicts.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + if (!res.isDone) { + Console.err.println("Did not converge") + sys.exit(1) + } + + res + } + + lazy val interfaceJar = { + + val artifactLogger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + artifactLogger.foreach(_.init { + System.err.println(s"Fetching org.scala-sbt:interface:$sbtVersion artifacts") + }) + + val results = Task.gatherUnordered(tasks(res, artifactLogger)).unsafePerformSync + + artifactLogger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Fetched org.scala-sbt:interface:$sbtVersion artifacts") + } + + val errors = results.collect { case (a, -\/(err)) => (a, err) } + val files = results.collect { case (_, \/-(f)) => f } + + if (errors.nonEmpty) { + Console.err.println(s"Error downloading artifacts:\n${errors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + files match { + case Nil => + throw new NoSuchElementException(s"interface JAR for sbt $sbtVersion") + case List(jar) => + jar + case _ => + sys.error(s"Too many interface JAR for sbt $sbtVersion: ${files.mkString(", ")}") + } + } + + lazy val compilerInterfaceSourcesJar = { + + val artifactLogger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + artifactLogger.foreach(_.init { + System.err.println(s"Fetching org.scala-sbt:interface:$sbtVersion source artifacts") + }) + + val results = Task.gatherUnordered(tasks(res, artifactLogger, Some(Seq("sources")))).unsafePerformSync + + artifactLogger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Fetched org.scala-sbt:interface:$sbtVersion source artifacts") + } + + val errors = results.collect { case (a, -\/(err)) => (a, err) } + val files = results.collect { case (_, \/-(f)) => f } + + if (errors.nonEmpty) { + Console.err.println(s"Error downloading artifacts:\n${errors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + files match { + case Nil => + throw new NoSuchElementException(s"compiler-interface source JAR for sbt $sbtVersion") + case List(jar) => + jar + case _ => + sys.error(s"Too many compiler-interface source JAR for sbt $sbtVersion: ${files.mkString(", ")}") + } + } + + (interfaceJar, compilerInterfaceSourcesJar) + } + + private def sbtCompilerInterfaceSrcComponentFile(sbtVersion: String): File = { + + val res = { + + val initialRes = Resolution( + Set( + Dependency(Module("org.scala-sbt", "compiler-interface"), sbtVersion, transitive = false) + ) + ) + + val logger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + logger.foreach(_.init { + System.err.println(s"Resolving org.scala-sbt:compiler-interface:$sbtVersion") + }) + + val res = initialRes.process.run(fetch(logger)).unsafePerformSync + + logger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Resolved org.scala-sbt:compiler-interface:$sbtVersion") + } + + if (res.errors.nonEmpty) { + Console.err.println(s"Errors:\n${res.errors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + if (res.conflicts.nonEmpty) { + Console.err.println(s"Conflicts:\n${res.conflicts.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + if (!res.isDone) { + Console.err.println("Did not converge") + sys.exit(1) + } + + res + } + + val files = { + + val artifactLogger = + Some(new TermDisplay( + new OutputStreamWriter(System.err) + )) + + artifactLogger.foreach(_.init { + System.err.println(s"Fetching org.scala-sbt:compiler-interface:$sbtVersion source artifacts") + }) + + val results = Task.gatherUnordered( + tasks(res, artifactLogger, None) ++ + tasks(res, artifactLogger, Some(Seq("sources"))) + ).unsafePerformSync + + artifactLogger.foreach { l => + if (l.stopDidPrintSomething()) + System.err.println(s"Fetched org.scala-sbt:compiler-interface:$sbtVersion source artifacts") + } + + val errors = results.collect { case (a, -\/(err)) => (a, err) } + + if (errors.nonEmpty) { + Console.err.println(s"Error downloading artifacts:\n${errors.map(" " + _).mkString("\n")}") + sys.exit(1) + } + + results.collect { case (_, \/-(f)) => f } + } + + files.find(f => f.getName == "compiler-interface-src.jar" || f.getName == "compiler-interface-sources.jar").getOrElse { + sys.error("compiler-interface-src not found") + } + } +} diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/MainApp.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/MainApp.scala new file mode 100644 index 000000000..c83b7b1d6 --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/MainApp.scala @@ -0,0 +1,110 @@ +package coursier.sbtlauncher + +import java.io.File +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +import caseapp._ +import com.typesafe.config.ConfigFactory +import coursier.Dependency + +final case class MainApp( + @ExtraName("org") + organization: String, + name: String, + version: String, + scalaVersion: String, + sbtVersion: String, + mainClass: String, + mainComponents: List[String], + classpathExtra: List[String], + extra: List[String] +) extends App { + + val sbtPropFile = new File(sys.props("user.dir") + "/sbt.properties") + val buildPropFile = new File(sys.props("user.dir") + "/project/build.properties") + + val propFileOpt = Some(sbtPropFile).filter(_.exists()) + .orElse(Some(buildPropFile).filter(_.exists())) + + val (org0, name0, ver0, scalaVer0, extraDeps0, mainClass0, sbtVersion0) = + propFileOpt match { + case Some(propFile) => + // can't get ConfigFactory.parseFile to work fine here + val conf = ConfigFactory.parseString(new String(Files.readAllBytes(propFile.toPath), StandardCharsets.UTF_8)) + .withFallback(ConfigFactory.defaultReference(Thread.currentThread().getContextClassLoader)) + .resolve() + val sbtConfig = SbtConfig.fromConfig(conf) + + (sbtConfig.organization, sbtConfig.moduleName, sbtConfig.version, sbtConfig.scalaVersion, sbtConfig.dependencies, sbtConfig.mainClass, sbtConfig.version) + case None => + require(scalaVersion.nonEmpty, "No scala version specified") + (organization, name, version, scalaVersion, Nil, mainClass, sbtVersion) + } + + val (extraParseErrors, extraModuleVersions) = coursier.util.Parse.moduleVersions(extra, scalaVersion) + + if (extraParseErrors.nonEmpty) { + ??? + } + + val extraDeps = extraModuleVersions.map { + case (mod, ver) => + Dependency(mod, ver) + } + + val launcher = new Launcher( + scalaVer0, + // FIXME Add org & moduleName in this path + new File(s"${sys.props("user.dir")}/target/sbt-components/components_scala$scalaVer0${if (sbtVersion0.isEmpty) "" else "_sbt" + sbtVersion0}"), + new File(s"${sys.props("user.dir")}/target/ivy2") + ) + + launcher.registerScalaComponents() + + if (sbtVersion0.nonEmpty) + launcher.registerSbtInterfaceComponents(sbtVersion0) + + val appId = ApplicationID( + org0, + name0, + ver0, + mainClass0, + mainComponents.toArray, + crossVersioned = false, + xsbti.CrossValue.Disabled, + classpathExtra.map(new File(_)).toArray + ) + + val appProvider = launcher.app(appId, extraDeps0 ++ extraDeps: _*) + + val appMain = appProvider.newMain() + + val appConfig = AppConfiguration( + remainingArgs.toArray, + new File(sys.props("user.dir")), + appProvider + ) + + val thread = Thread.currentThread() + val previousLoader = thread.getContextClassLoader + + val result = + try { + thread.setContextClassLoader(appProvider.loader()) + appMain.run(appConfig) + } finally { + thread.setContextClassLoader(previousLoader) + } + + result match { + case _: xsbti.Continue => + case e: xsbti.Exit => + sys.exit(e.code()) + case _: xsbti.Reboot => + sys.error("Not able to reboot yet") + } + +} + +object Main extends AppOf[MainApp] diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/Repository.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/Repository.scala new file mode 100644 index 000000000..c77e0acee --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/Repository.scala @@ -0,0 +1,19 @@ +package coursier.sbtlauncher + +import java.net.URL + +object Repository { + + final case class Maven(id: String, url: URL) extends xsbti.MavenRepository + + final case class Ivy( + id: String, + url: URL, + ivyPattern: String, + artifactPattern: String, + mavenCompatible: Boolean, + skipConsistencyCheck: Boolean, + descriptorOptional: Boolean + ) extends xsbti.IvyRepository + +} diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/SbtConfig.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/SbtConfig.scala new file mode 100644 index 000000000..1806ecf19 --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/SbtConfig.scala @@ -0,0 +1,112 @@ +package coursier.sbtlauncher + +import com.typesafe.config.Config +import coursier.Dependency +import coursier.util.Parse + +import scala.collection.JavaConverters._ + +final case class SbtConfig( + organization: String, + moduleName: String, + version: String, + scalaVersion: String, + mainClass: String, + dependencies: Seq[Dependency] +) + +object SbtConfig { + + def defaultOrganization = "org.scala-sbt" + def defaultModuleName = "sbt" + def defaultMainClass = "sbt.xMain" + + def fromConfig(config: Config): SbtConfig = { + + val version = config.getString("sbt.version") + + val scalaVersion = + if (config.hasPath("scala.version")) + config.getString("scala.version") + else if (version.startsWith("0.13.")) + "2.10.6" + else if (version.startsWith("1.0.")) + "2.12.1" + else + throw new Exception(s"Don't know what Scala version should be used for sbt version '$version'") + + val org = + if (config.hasPath("sbt.organization")) + config.getString("sbt.organization") + else + defaultOrganization + + val name = + if (config.hasPath("sbt.module-name")) + config.getString("sbt.module-name") + else + defaultModuleName + + val mainClass = + if (config.hasPath("sbt.main-class")) + config.getString("sbt.main-class") + else + defaultMainClass + + val scalaBinaryVersion = scalaVersion.split('.').take(2).mkString(".") + val sbtBinaryVersion = version.split('.').take(2).mkString(".") + + val rawPlugins = + if (config.hasPath("plugins")) + config.getStringList("plugins").asScala + else + Nil + + val (pluginErrors, pluginsModuleVersions) = Parse.moduleVersions(rawPlugins, scalaVersion) + + if (pluginErrors.nonEmpty) { + ??? + } + + val pluginDependencies = + pluginsModuleVersions.map { + case (mod, ver) => + Dependency( + mod.copy( + attributes = mod.attributes ++ Seq( + "scalaVersion" -> scalaBinaryVersion, + "sbtVersion" -> sbtBinaryVersion + ) + ), + ver + ) + } + + val rawDeps = + if (config.hasPath("dependencies")) + config.getStringList("dependencies").asScala + else + Nil + + val (depsErrors, depsModuleVersions) = Parse.moduleVersions(rawDeps, scalaVersion) + + if (depsErrors.nonEmpty) { + ??? + } + + val dependencies = + depsModuleVersions.map { + case (mod, ver) => + Dependency(mod, ver) + } + + SbtConfig( + org, + name, + version, + scalaVersion, + mainClass, + pluginDependencies ++ dependencies + ) + } +} diff --git a/sbt-launcher/src/main/scala/coursier/sbtlauncher/ScalaProvider.scala b/sbt-launcher/src/main/scala/coursier/sbtlauncher/ScalaProvider.scala new file mode 100644 index 000000000..d29ae0e2a --- /dev/null +++ b/sbt-launcher/src/main/scala/coursier/sbtlauncher/ScalaProvider.scala @@ -0,0 +1,16 @@ +package coursier.sbtlauncher + +import java.io.File + +final case class ScalaProvider( + launcher: xsbti.Launcher, + version: String, + loader: ClassLoader, + jars: Array[File], + libraryJar: File, + compilerJar: File, + createApp: xsbti.ApplicationID => xsbti.AppProvider +) extends xsbti.ScalaProvider { + def app(id: xsbti.ApplicationID): xsbti.AppProvider = + createApp(id) +} diff --git a/sbt.properties b/sbt.properties new file mode 100644 index 000000000..d42ee8c7a --- /dev/null +++ b/sbt.properties @@ -0,0 +1,16 @@ +sbt.version=0.13.8 + +plugins = [ + "io.get-coursier:sbt-coursier:1.0.0-M15-1" + "org.xerial.sbt:sbt-pack:0.8.2" + "org.scala-js:sbt-scalajs:0.6.14" + "com.jsuereth:sbt-pgp:1.0.0" + "org.scoverage:sbt-scoverage:1.4.0" + "org.tpolecat:tut-plugin:0.4.8" + "com.typesafe.sbt:sbt-proguard:0.2.2" + "com.typesafe:sbt-mima-plugin:0.1.13" +] + +dependencies = [ + "org.scala-sbt:scripted-plugin:"${sbt.version} +]