mirror of https://github.com/sbt/sbt.git
Add bootstrap app
This commit is contained in:
parent
b5529679cf
commit
a6091c3df1
|
|
@ -0,0 +1,143 @@
|
|||
package coursier
|
||||
|
||||
import java.io.{ ByteArrayOutputStream, InputStream, File }
|
||||
import java.net.{ URI, URLClassLoader }
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.{ Executors, ThreadFactory }
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
import scala.concurrent.{ ExecutionContext, Future, Await }
|
||||
|
||||
import scala.util.{ Try, Success, Failure }
|
||||
|
||||
object Bootstrap extends App {
|
||||
|
||||
val concurrentDownloadCount = 6
|
||||
val threadFactory = new ThreadFactory {
|
||||
// from scalaz Strategy.DefaultDaemonThreadFactory
|
||||
val defaultThreadFactory = Executors.defaultThreadFactory()
|
||||
def newThread(r: Runnable) = {
|
||||
val t = defaultThreadFactory.newThread(r)
|
||||
t.setDaemon(true)
|
||||
t
|
||||
}
|
||||
}
|
||||
val defaultPool = Executors.newFixedThreadPool(concurrentDownloadCount, threadFactory)
|
||||
implicit val ec = ExecutionContext.fromExecutorService(defaultPool)
|
||||
|
||||
private def readFullySync(is: InputStream) = {
|
||||
val buffer = new ByteArrayOutputStream()
|
||||
val data = Array.ofDim[Byte](16384)
|
||||
|
||||
var nRead = is.read(data, 0, data.length)
|
||||
while (nRead != -1) {
|
||||
buffer.write(data, 0, nRead)
|
||||
nRead = is.read(data, 0, data.length)
|
||||
}
|
||||
|
||||
buffer.flush()
|
||||
buffer.toByteArray
|
||||
}
|
||||
|
||||
private def errPrintln(s: String): Unit =
|
||||
Console.err.println(s)
|
||||
|
||||
private def exit(msg: String = ""): Nothing = {
|
||||
if (msg.nonEmpty)
|
||||
errPrintln(msg)
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
args match {
|
||||
case Array(mainClass0, jarDir0, remainingArgs @ _*) =>
|
||||
val jarDir = new File(jarDir0)
|
||||
|
||||
if (jarDir.exists()) {
|
||||
if (!jarDir.isDirectory)
|
||||
exit(s"Error: $jarDir0 is not a directory")
|
||||
} else if (!jarDir.mkdirs())
|
||||
errPrintln(s"Warning: cannot create $jarDir0, continuing anyway.")
|
||||
|
||||
val splitIdx = remainingArgs.indexOf("--")
|
||||
val (jarStrUrls, userArgs) =
|
||||
if (splitIdx < 0)
|
||||
(remainingArgs, Nil)
|
||||
else
|
||||
(remainingArgs.take(splitIdx), remainingArgs.drop(splitIdx + 1))
|
||||
|
||||
val tryUrls = jarStrUrls.map(urlStr => urlStr -> Try(URI.create(urlStr).toURL))
|
||||
|
||||
val failedUrls = tryUrls.collect {
|
||||
case (strUrl, Failure(t)) => strUrl -> t
|
||||
}
|
||||
if (failedUrls.nonEmpty)
|
||||
exit(
|
||||
s"Error parsing ${failedUrls.length} URL(s):\n" +
|
||||
failedUrls.map { case (s, t) => s"$s: ${t.getMessage}" }.mkString("\n")
|
||||
)
|
||||
|
||||
val jarUrls = tryUrls.collect {
|
||||
case (_, Success(url)) => url
|
||||
}
|
||||
|
||||
val jarLocalUrlFutures = jarUrls.map { url =>
|
||||
if (url.getProtocol == "file")
|
||||
Future.successful(url)
|
||||
else
|
||||
Future {
|
||||
val path = url.getPath
|
||||
val idx = path.lastIndexOf('/')
|
||||
// FIXME Add other components in path to prevent conflicts?
|
||||
val fileName = path.drop(idx + 1)
|
||||
val dest = new File(jarDir, fileName)
|
||||
|
||||
// FIXME If dest exists, do a HEAD request and check that its size or last modified time is OK?
|
||||
|
||||
if (!dest.exists()) {
|
||||
Console.err.println(s"Downloading $url")
|
||||
try {
|
||||
val conn = url.openConnection()
|
||||
val lastModified = conn.getLastModified
|
||||
val s = conn.getInputStream
|
||||
val b = readFullySync(s)
|
||||
Files.write(dest.toPath, b)
|
||||
dest.setLastModified(lastModified)
|
||||
} catch { case e: Exception =>
|
||||
Console.err.println(s"Error while downloading $url: ${e.getMessage}, ignoring it")
|
||||
}
|
||||
}
|
||||
|
||||
dest.toURI.toURL
|
||||
}
|
||||
}
|
||||
|
||||
val jarLocalUrls = Await.result(Future.sequence(jarLocalUrlFutures), Duration.Inf)
|
||||
|
||||
val thread = Thread.currentThread()
|
||||
val parentClassLoader = thread.getContextClassLoader
|
||||
|
||||
val classLoader = new URLClassLoader(jarLocalUrls.toArray, parentClassLoader)
|
||||
|
||||
val mainClass =
|
||||
try classLoader.loadClass(mainClass0)
|
||||
catch { case e: ClassNotFoundException =>
|
||||
exit(s"Error: class $mainClass0 not found")
|
||||
}
|
||||
|
||||
val mainMethod =
|
||||
try mainClass.getMethod("main", classOf[Array[String]])
|
||||
catch { case e: NoSuchMethodException =>
|
||||
exit(s"Error: main method not found in class $mainClass0")
|
||||
}
|
||||
|
||||
thread.setContextClassLoader(classLoader)
|
||||
try mainMethod.invoke(null, userArgs.toArray)
|
||||
finally {
|
||||
thread.setContextClassLoader(parentClassLoader)
|
||||
}
|
||||
|
||||
case _ =>
|
||||
exit("Usage: bootstrap main-class JAR-directory JAR-URLs...")
|
||||
}
|
||||
|
||||
}
|
||||
44
build.sbt
44
build.sbt
|
|
@ -34,6 +34,12 @@ lazy val publishingSettings = Seq(
|
|||
publishArtifactsAction := PgpKeys.publishSigned.value
|
||||
) ++ releaseSettings
|
||||
|
||||
lazy val noPublishSettings = Seq(
|
||||
publish := (),
|
||||
publishLocal := (),
|
||||
publishArtifact := false
|
||||
)
|
||||
|
||||
lazy val commonSettings = Seq(
|
||||
organization := "com.github.alexarchambault",
|
||||
scalaVersion := "2.11.7",
|
||||
|
|
@ -42,7 +48,13 @@ lazy val commonSettings = Seq(
|
|||
"Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases",
|
||||
Resolver.sonatypeRepo("releases"),
|
||||
Resolver.sonatypeRepo("snapshots")
|
||||
)
|
||||
),
|
||||
libraryDependencies ++= {
|
||||
if (scalaVersion.value startsWith "2.10.")
|
||||
Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full))
|
||||
else
|
||||
Seq()
|
||||
}
|
||||
)
|
||||
|
||||
lazy val core = crossProject
|
||||
|
|
@ -107,18 +119,17 @@ lazy val cli = project
|
|||
libraryDependencies ++= Seq(
|
||||
"com.github.alexarchambault" %% "case-app" % "1.0.0-SNAPSHOT",
|
||||
"ch.qos.logback" % "logback-classic" % "1.1.3"
|
||||
) ++ {
|
||||
if (scalaVersion.value startsWith "2.10.")
|
||||
Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full))
|
||||
else
|
||||
Seq()
|
||||
}
|
||||
),
|
||||
resourceGenerators in Compile += assembly.in(bootstrap).in(assembly).map { jar =>
|
||||
Seq(jar)
|
||||
}.taskValue
|
||||
)
|
||||
|
||||
lazy val web = project
|
||||
.enablePlugins(ScalaJSPlugin)
|
||||
.dependsOn(coreJs)
|
||||
.settings(commonSettings)
|
||||
.settings(noPublishSettings)
|
||||
.settings(
|
||||
libraryDependencies ++= {
|
||||
if (scalaVersion.value startsWith "2.10.")
|
||||
|
|
@ -134,8 +145,6 @@ lazy val web = project
|
|||
else
|
||||
dir
|
||||
},
|
||||
publish := (),
|
||||
publishLocal := (),
|
||||
test in Test := (),
|
||||
testOnly in Test := (),
|
||||
resolvers += "Webjars Bintray" at "https://dl.bintray.com/webjars/maven/",
|
||||
|
|
@ -147,12 +156,19 @@ lazy val web = project
|
|||
)
|
||||
)
|
||||
|
||||
lazy val `coursier` = project.in(file("."))
|
||||
.aggregate(coreJvm, coreJs, files, cli, web)
|
||||
lazy val bootstrap = project
|
||||
.settings(commonSettings)
|
||||
.settings(noPublishSettings)
|
||||
.settings(
|
||||
name := "coursier-bootstrap",
|
||||
assemblyJarName in assembly := s"bootstrap.jar"
|
||||
)
|
||||
|
||||
lazy val `coursier` = project.in(file("."))
|
||||
.aggregate(coreJvm, coreJs, files, cli, web, bootstrap)
|
||||
.settings(commonSettings)
|
||||
.settings(noPublishSettings)
|
||||
.settings(
|
||||
(unmanagedSourceDirectories in Compile) := Nil,
|
||||
(unmanagedSourceDirectories in Test) := Nil,
|
||||
publish := (),
|
||||
publishLocal := ()
|
||||
(unmanagedSourceDirectories in Test) := Nil
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package coursier
|
||||
package cli
|
||||
|
||||
import java.io.File
|
||||
import java.io.{ File, IOException }
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.{ Files => NIOFiles }
|
||||
|
||||
import caseapp._
|
||||
|
||||
|
|
@ -34,6 +35,8 @@ case class CommonOptions(
|
|||
val verbose0 = verbose.length + (if (quiet) 1 else 0)
|
||||
}
|
||||
|
||||
@AppName("Coursier")
|
||||
@ProgName("coursier")
|
||||
sealed trait CoursierCommand extends Command
|
||||
|
||||
case class Fetch(
|
||||
|
|
@ -60,7 +63,6 @@ case class Fetch(
|
|||
}
|
||||
|
||||
case class Launch(
|
||||
@HelpMessage("If -L or --launch is specified, the main class to launch")
|
||||
@ExtraName("M")
|
||||
@ExtraName("main")
|
||||
mainClass: String,
|
||||
|
|
@ -95,7 +97,10 @@ case class Launch(
|
|||
import scala.collection.JavaConverters._
|
||||
val cl = new URLClassLoader(
|
||||
files0.map(_.toURI.toURL).toArray,
|
||||
Thread.currentThread().getContextClassLoader // setting this to null provokes strange things (wrt terminal, ...)
|
||||
// setting this to null provokes strange things (wrt terminal, ...)
|
||||
// but this is far from perfect: this puts all our dependencies along with the user's,
|
||||
// and with a higher priority
|
||||
Thread.currentThread().getContextClassLoader
|
||||
)
|
||||
|
||||
val mainClass0 =
|
||||
|
|
@ -238,4 +243,79 @@ case class Repository(
|
|||
|
||||
}
|
||||
|
||||
case class Bootstrap(
|
||||
@ExtraName("M")
|
||||
@ExtraName("main")
|
||||
mainClass: String,
|
||||
@ExtraName("o")
|
||||
output: String,
|
||||
@ExtraName("D")
|
||||
downloadDir: String,
|
||||
@ExtraName("f")
|
||||
force: Boolean,
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
) extends CoursierCommand {
|
||||
|
||||
if (mainClass.isEmpty) {
|
||||
Console.err.println(s"Error: no main class specified. Specify one with -M or --main")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
if (downloadDir.isEmpty) {
|
||||
Console.err.println(s"Error: no download dir specified. Specify one with -D or --download-dir")
|
||||
Console.err.println("E.g. -D \"\\$HOME/.app-name/jars\"")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
val downloadDir0 =
|
||||
if (downloadDir.isEmpty)
|
||||
"$HOME/"
|
||||
else
|
||||
downloadDir
|
||||
|
||||
val bootstrapJar =
|
||||
Option(Thread.currentThread().getContextClassLoader.getResourceAsStream("bootstrap.jar")) match {
|
||||
case Some(is) => Files.readFullySync(is)
|
||||
case None =>
|
||||
Console.err.println(s"Error: bootstrap JAR not found")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
// scala-library version in the resulting JARs has to match the one in the bootstrap JAR
|
||||
// This should be enforced more strictly (possibly by having one bootstrap JAR per scala version).
|
||||
|
||||
val helper = new Helper(
|
||||
common,
|
||||
remainingArgs :+ s"org.scala-lang:scala-library:${scala.util.Properties.versionNumberString}"
|
||||
)
|
||||
|
||||
val artifacts = helper.res.artifacts
|
||||
|
||||
val urls = artifacts.map(_.url)
|
||||
|
||||
val unrecognized = urls.filter(s => !s.startsWith("http://") && !s.startsWith("https://"))
|
||||
if (unrecognized.nonEmpty)
|
||||
Console.err.println(s"Warning: non HTTP URLs:\n${unrecognized.mkString("\n")}")
|
||||
|
||||
val output0 = new File(output)
|
||||
if (!force && output0.exists()) {
|
||||
Console.err.println(s"Error: $output already exists, use -f option to force erasing it.")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
val shellPreamble = Seq(
|
||||
"#!/usr/bin/env sh",
|
||||
"exec java -jar \"$0\" \"" + mainClass + "\" \"" + downloadDir + "\" " + urls.map("\"" + _ + "\"").mkString(" ") + " -- \"$@\"",
|
||||
""
|
||||
).mkString("\n")
|
||||
|
||||
try NIOFiles.write(output0.toPath, shellPreamble.getBytes("UTF-8") ++ bootstrapJar)
|
||||
catch { case e: IOException =>
|
||||
Console.err.println(s"Error while writing $output0: ${e.getMessage}")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Coursier extends CommandAppOf[CoursierCommand]
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5")
|
|||
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
|
||||
addSbtPlugin("com.github.gseitz" % "sbt-release" % "0.8.5")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.1.0")
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.0")
|
||||
|
|
|
|||
Loading…
Reference in New Issue