mirror of https://github.com/sbt/sbt.git
launch command should be fine
This commit is contained in:
parent
a89c0d92e3
commit
2f5e731378
|
|
@ -48,96 +48,108 @@ object Bootstrap extends App {
|
|||
sys.exit(255)
|
||||
}
|
||||
|
||||
args match {
|
||||
val (prependClasspath, mainClass0, jarDir0, remainingArgs) = args match {
|
||||
case Array("-B", mainClass0, jarDir0, remainingArgs @ _*) =>
|
||||
(true, mainClass0, jarDir0, remainingArgs)
|
||||
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)
|
||||
}
|
||||
|
||||
(false, mainClass0, jarDir0, remainingArgs)
|
||||
case _ =>
|
||||
exit("Usage: bootstrap main-class JAR-directory JAR-URLs...")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
val userArgs0 =
|
||||
if (prependClasspath)
|
||||
jarLocalUrls.flatMap { url =>
|
||||
assert(url.getProtocol == "file")
|
||||
Seq("-B", url.getPath)
|
||||
} ++ userArgs
|
||||
else
|
||||
userArgs
|
||||
|
||||
thread.setContextClassLoader(classLoader)
|
||||
try mainMethod.invoke(null, userArgs0.toArray)
|
||||
finally {
|
||||
thread.setContextClassLoader(parentClassLoader)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -161,7 +161,10 @@ lazy val bootstrap = project
|
|||
.settings(noPublishSettings)
|
||||
.settings(
|
||||
name := "coursier-bootstrap",
|
||||
assemblyJarName in assembly := s"bootstrap.jar"
|
||||
assemblyJarName in assembly := s"bootstrap.jar",
|
||||
assemblyShadeRules in assembly := Seq(
|
||||
ShadeRule.rename("scala.**" -> "shadedscala.@1").inAll
|
||||
)
|
||||
)
|
||||
|
||||
lazy val `coursier` = project.in(file("."))
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ package cli
|
|||
import java.io.{ File, IOException }
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.{ Files => NIOFiles }
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
|
||||
import caseapp._
|
||||
import coursier.util.ClasspathFilter
|
||||
|
||||
case class CommonOptions(
|
||||
@HelpMessage("Keep optional dependencies (Maven)")
|
||||
|
|
@ -83,44 +85,49 @@ case class Launch(
|
|||
|
||||
val files0 = helper.fetch(main = true, sources = false, javadoc = false)
|
||||
|
||||
|
||||
def printParents(cl: ClassLoader): Unit =
|
||||
Option(cl.getParent) match {
|
||||
case None =>
|
||||
case Some(cl0) =>
|
||||
println(cl0.toString)
|
||||
printParents(cl0)
|
||||
}
|
||||
|
||||
printParents(Thread.currentThread().getContextClassLoader)
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
val cl = new URLClassLoader(
|
||||
files0.map(_.toURI.toURL).toArray,
|
||||
// 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
|
||||
new ClasspathFilter(
|
||||
Thread.currentThread().getContextClassLoader,
|
||||
Coursier.baseCp.map(new File(_)).toSet,
|
||||
exclude = true
|
||||
)
|
||||
)
|
||||
|
||||
val mainClass0 =
|
||||
if (mainClass.nonEmpty)
|
||||
mainClass
|
||||
if (mainClass.nonEmpty) mainClass
|
||||
else {
|
||||
val metaInfs = cl.findResources("META-INF/MANIFEST.MF").asScala.toVector
|
||||
val mainClasses = metaInfs.flatMap { url =>
|
||||
Option(new java.util.jar.Manifest(url.openStream()).getMainAttributes.getValue("Main-Class"))
|
||||
}
|
||||
val mainClasses = Helper.mainClasses(cl)
|
||||
|
||||
if (mainClasses.isEmpty) {
|
||||
println(s"No main class found. Specify one with -M or --main.")
|
||||
sys.exit(255)
|
||||
}
|
||||
val mainClass =
|
||||
if (mainClasses.isEmpty) {
|
||||
Console.err.println(s"No main class found. Specify one with -M or --main.")
|
||||
sys.exit(255)
|
||||
} else if (mainClasses.size == 1) {
|
||||
val (_, mainClass) = mainClasses.head
|
||||
mainClass
|
||||
} else {
|
||||
// Trying to get the main class of the first artifact
|
||||
val mainClassOpt = for {
|
||||
(module, _) <- helper.moduleVersions.headOption
|
||||
mainClass <- mainClasses.collectFirst {
|
||||
case ((org, name), mainClass)
|
||||
if org == module.organization && (
|
||||
module.name == name ||
|
||||
module.name.startsWith(name + "_") // Ignore cross version suffix
|
||||
) =>
|
||||
mainClass
|
||||
}
|
||||
} yield mainClass
|
||||
|
||||
if (common.verbose0 >= 0)
|
||||
println(s"Found ${mainClasses.length} main class(es):\n${mainClasses.map(" " + _).mkString("\n")}")
|
||||
mainClassOpt.getOrElse {
|
||||
println(mainClasses)
|
||||
Console.err.println(s"Cannot find default main class. Specify one with -M or --main.")
|
||||
sys.exit(255)
|
||||
}
|
||||
}
|
||||
|
||||
mainClasses.head
|
||||
mainClass
|
||||
}
|
||||
|
||||
val cls =
|
||||
|
|
@ -248,15 +255,20 @@ case class Bootstrap(
|
|||
@ExtraName("main")
|
||||
mainClass: String,
|
||||
@ExtraName("o")
|
||||
output: String,
|
||||
output: String = "bootstrap",
|
||||
@ExtraName("D")
|
||||
downloadDir: String,
|
||||
@ExtraName("f")
|
||||
force: Boolean,
|
||||
@HelpMessage(s"Internal use - prepend base classpath options to arguments")
|
||||
@ExtraName("b")
|
||||
prependClasspath: Boolean,
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
) extends CoursierCommand {
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
if (mainClass.isEmpty) {
|
||||
Console.err.println(s"Error: no main class specified. Specify one with -M or --main")
|
||||
sys.exit(255)
|
||||
|
|
@ -306,7 +318,7 @@ case class Bootstrap(
|
|||
|
||||
val shellPreamble = Seq(
|
||||
"#!/usr/bin/env sh",
|
||||
"exec java -jar \"$0\" \"" + mainClass + "\" \"" + downloadDir + "\" " + urls.map("\"" + _ + "\"").mkString(" ") + " -- \"$@\"",
|
||||
"exec java -jar \"$0\" " + (if (prependClasspath) "-B " else "") + "\"" + mainClass + "\" \"" + downloadDir + "\" " + urls.map("\"" + _ + "\"").mkString(" ") + " -- \"$@\"",
|
||||
""
|
||||
).mkString("\n")
|
||||
|
||||
|
|
@ -316,6 +328,51 @@ case class Bootstrap(
|
|||
sys.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
val perms = NIOFiles.getPosixFilePermissions(output0.toPath).asScala.toSet
|
||||
|
||||
var newPerms = perms
|
||||
if (perms(PosixFilePermission.OWNER_READ))
|
||||
newPerms += PosixFilePermission.OWNER_EXECUTE
|
||||
if (perms(PosixFilePermission.GROUP_READ))
|
||||
newPerms += PosixFilePermission.GROUP_EXECUTE
|
||||
if (perms(PosixFilePermission.OTHERS_READ))
|
||||
newPerms += PosixFilePermission.OTHERS_EXECUTE
|
||||
|
||||
if (newPerms != perms)
|
||||
NIOFiles.setPosixFilePermissions(
|
||||
output0.toPath,
|
||||
newPerms.asJava
|
||||
)
|
||||
} catch {
|
||||
case e: UnsupportedOperationException =>
|
||||
// Ignored
|
||||
case e: IOException =>
|
||||
Console.err.println(s"Error while making $output0 executable: ${e.getMessage}")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Coursier extends CommandAppOf[CoursierCommand]
|
||||
case class BaseCommand(
|
||||
// FIXME Need a @NoHelp annotation in case-app to hide an option from help message
|
||||
@HelpMessage("For internal use only - class path used to launch coursier")
|
||||
@ExtraName("B")
|
||||
baseCp: List[String]
|
||||
) extends Command {
|
||||
Coursier.baseCp = baseCp
|
||||
|
||||
// FIXME Should be in a trait in case-app
|
||||
override def setCommand(cmd: Option[Either[String, String]]): Unit = {
|
||||
if (cmd.isEmpty) {
|
||||
// FIXME Print available commands too?
|
||||
Console.err.println("Error: no command specified")
|
||||
sys.exit(255)
|
||||
}
|
||||
super.setCommand(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
object Coursier extends CommandAppOfWithBase[BaseCommand, CoursierCommand] {
|
||||
private[coursier] var baseCp = Seq.empty[String]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,23 @@ object Helper {
|
|||
)
|
||||
}
|
||||
|
||||
def mainClasses(cl: ClassLoader): Map[(String, String), String] = {
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
val metaInfs = cl.getResources("META-INF/MANIFEST.MF").asScala.toVector
|
||||
|
||||
val mainClasses = metaInfs.flatMap { url =>
|
||||
val attributes = new java.util.jar.Manifest(url.openStream()).getMainAttributes
|
||||
|
||||
val vendor = Option(attributes.getValue("Specification-Vendor")).getOrElse("")
|
||||
val title = Option(attributes.getValue("Specification-Title")).getOrElse("")
|
||||
val mainClass = Option(attributes.getValue("Main-Class"))
|
||||
|
||||
mainClass.map((vendor, title) -> _)
|
||||
}
|
||||
|
||||
mainClasses.toMap
|
||||
}
|
||||
}
|
||||
|
||||
class Helper(
|
||||
|
|
@ -151,7 +168,7 @@ class Helper(
|
|||
.partition(_.length == 3)
|
||||
|
||||
if (splitDependencies.isEmpty) {
|
||||
???
|
||||
Console.err.println(s"Error: no dependencies specified.")
|
||||
// CaseApp.printUsage[Coursier]()
|
||||
sys exit 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
package coursier.util
|
||||
|
||||
// Extracted and adapted from SBT
|
||||
|
||||
import java.io.File
|
||||
import java.net._
|
||||
|
||||
/**
|
||||
* Doesn't load any classes itself, but instead verifies that all classes loaded through `parent`
|
||||
* come from `classpath`.
|
||||
*
|
||||
* If `exclude` is `true`, does the opposite - exclude classes from `classpath`.
|
||||
*/
|
||||
class ClasspathFilter(parent: ClassLoader, classpath: Set[File], exclude: Boolean) extends ClassLoader(parent) {
|
||||
override def toString = s"ClasspathFilter(parent = $parent, cp = $classpath)"
|
||||
|
||||
private val directories = classpath.toSeq.filter(_.isDirectory)
|
||||
|
||||
|
||||
private def toFile(url: URL) =
|
||||
try { new File(url.toURI) }
|
||||
catch { case _: URISyntaxException => new File(url.getPath) }
|
||||
|
||||
private def uriToFile(uriString: String) = {
|
||||
val uri = new URI(uriString)
|
||||
assert(uri.getScheme == "file", s"Expected protocol to be 'file' in URI $uri")
|
||||
if (uri.getAuthority == null)
|
||||
new File(uri)
|
||||
else {
|
||||
/* https://github.com/sbt/sbt/issues/564
|
||||
* http://blogs.msdn.com/b/ie/archive/2006/12/06/file-uris-in-windows.aspx
|
||||
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5086147
|
||||
* The specific problem here is that `uri` will have a defined authority component for UNC names like //foo/bar/some/path.jar
|
||||
* but the File constructor requires URIs with an undefined authority component.
|
||||
*/
|
||||
new File(uri.getSchemeSpecificPart)
|
||||
}
|
||||
}
|
||||
|
||||
private def urlAsFile(url: URL) =
|
||||
url.getProtocol match {
|
||||
case "file" => Some(toFile(url))
|
||||
case "jar" =>
|
||||
val path = url.getPath
|
||||
val end = path.indexOf('!')
|
||||
Some(uriToFile(if (end < 0) path else path.substring(0, end)))
|
||||
case _ => None
|
||||
}
|
||||
|
||||
private def baseFileString(baseFile: File) =
|
||||
Some(baseFile).filter(_.isDirectory).map { d =>
|
||||
val cp = d.getAbsolutePath.ensuring(_.nonEmpty)
|
||||
if (cp.last == File.separatorChar) cp else cp + File.separatorChar
|
||||
}
|
||||
|
||||
private def relativize(base: File, file: File) =
|
||||
baseFileString(base) .flatMap { baseString =>
|
||||
Some(file.getAbsolutePath).filter(_ startsWith baseString).map(_ substring baseString.length)
|
||||
}
|
||||
|
||||
private def fromClasspath(c: Class[_]): Boolean = {
|
||||
val codeSource = c.getProtectionDomain.getCodeSource
|
||||
(codeSource eq null) ||
|
||||
onClasspath(codeSource.getLocation) ||
|
||||
// workaround SBT classloader returning the target directory as sourcecode
|
||||
// make us keep more class than expected
|
||||
urlAsFile(codeSource.getLocation).exists(_.isDirectory)
|
||||
}
|
||||
|
||||
private val onClasspath: URL => Boolean = {
|
||||
if (exclude)
|
||||
src =>
|
||||
src == null || urlAsFile(src).forall(f =>
|
||||
!classpath(f) &&
|
||||
directories.forall(relativize(_, f).isEmpty)
|
||||
)
|
||||
else
|
||||
src =>
|
||||
src == null || urlAsFile(src).exists(f =>
|
||||
classpath(f) ||
|
||||
directories.exists(relativize(_, f).isDefined)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override def loadClass(className: String, resolve: Boolean): Class[_] = {
|
||||
val c = super.loadClass(className, resolve)
|
||||
if (fromClasspath(c)) c
|
||||
else throw new ClassNotFoundException(className)
|
||||
}
|
||||
|
||||
override def getResource(name: String): URL = {
|
||||
val res = super.getResource(name)
|
||||
if (onClasspath(res)) res else null
|
||||
}
|
||||
|
||||
override def getResources(name: String): java.util.Enumeration[URL] = {
|
||||
import collection.JavaConverters._
|
||||
val res = super.getResources(name)
|
||||
if (res == null) null else res.asScala.filter(onClasspath).asJavaEnumeration
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue