launch command should be fine

This commit is contained in:
Alexandre Archambault 2015-11-22 23:50:33 +01:00
parent a89c0d92e3
commit 2f5e731378
5 changed files with 312 additions and 121 deletions

View File

@ -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)
}
}

View File

@ -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("."))

View 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]
}

View File

@ -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
}

View File

@ -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
}
}