mirror of https://github.com/sbt/sbt.git
Simplify ClassLoader isolation in launch command
No more SBT-inspired ClasspathFilter. It now just tries to find the ClassLoader that loaded coursier, and use the parent as a base to load the launched application. Allows to remove the -b option of the bootstrap command, and remove BaseCommand (that was holding options common to all commands).
This commit is contained in:
parent
3e9a37edc3
commit
92e5917af6
|
|
@ -8,7 +8,6 @@ import java.lang.reflect.InvocationTargetException;
|
|||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.security.CodeSource;
|
||||
|
|
@ -170,8 +169,6 @@ public class Bootstrap {
|
|||
String mainClass0 = System.getProperty("bootstrap.mainClass");
|
||||
String jarDir0 = System.getProperty("bootstrap.jarDir");
|
||||
|
||||
boolean prependClasspath = Boolean.parseBoolean(System.getProperty("bootstrap.prependClasspath", "false"));
|
||||
|
||||
final File jarDir = new File(jarDir0);
|
||||
|
||||
if (jarDir.exists()) {
|
||||
|
|
@ -267,7 +264,7 @@ public class Bootstrap {
|
|||
parentClassLoader = new IsolatedClassLoader(contextURLs, parentClassLoader, new String[]{ isolationID });
|
||||
}
|
||||
|
||||
URLClassLoader classLoader = new URLClassLoader(localURLs.toArray(new URL[localURLs.size()]), parentClassLoader);
|
||||
ClassLoader classLoader = new BootstrapClassLoader(localURLs.toArray(new URL[localURLs.size()]), parentClassLoader);
|
||||
|
||||
Class<?> mainClass = null;
|
||||
Method mainMethod = null;
|
||||
|
|
@ -288,14 +285,6 @@ public class Bootstrap {
|
|||
|
||||
List<String> userArgs0 = new ArrayList<>();
|
||||
|
||||
if (prependClasspath) {
|
||||
for (URL url : localURLs) {
|
||||
assert url.getProtocol().equals("file");
|
||||
userArgs0.add("-B");
|
||||
userArgs0.add(url.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < args.length; i++)
|
||||
userArgs0.add(args[i]);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package coursier;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
public class BootstrapClassLoader extends URLClassLoader {
|
||||
|
||||
public BootstrapClassLoader(
|
||||
URL[] urls,
|
||||
ClassLoader parent
|
||||
) {
|
||||
super(urls, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be called by reflection by launched applications, to find the "main" `ClassLoader`
|
||||
* that loaded them, and possibly short-circuit it to load other things for example.
|
||||
*
|
||||
* The `launch` command of coursier does that.
|
||||
*/
|
||||
public boolean isBootstrapLoader() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9,7 +9,10 @@ import java.util.Properties
|
|||
import java.util.zip.{ ZipEntry, ZipOutputStream, ZipInputStream }
|
||||
|
||||
import caseapp.{ HelpMessage => Help, ValueDescription => Value, ExtraName => Short, _ }
|
||||
import coursier.util.{ Parse, ClasspathFilter }
|
||||
import coursier.util.Parse
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.language.reflectiveCalls
|
||||
|
||||
case class CommonOptions(
|
||||
@Help("Keep optional dependencies (Maven)")
|
||||
|
|
@ -161,6 +164,32 @@ case class IsolatedLoaderOptions(
|
|||
|
||||
}
|
||||
|
||||
object Launch {
|
||||
|
||||
@tailrec
|
||||
def mainClassLoader(cl: ClassLoader): Option[ClassLoader] =
|
||||
if (cl == null)
|
||||
None
|
||||
else {
|
||||
val isMainLoader = try {
|
||||
val cl0 = cl.asInstanceOf[Object {
|
||||
def isBootstrapLoader: Boolean
|
||||
}]
|
||||
|
||||
cl0.isBootstrapLoader
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
false
|
||||
}
|
||||
|
||||
if (isMainLoader)
|
||||
Some(cl)
|
||||
else
|
||||
mainClassLoader(cl.getParent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case class Launch(
|
||||
@Short("M")
|
||||
@Short("main")
|
||||
|
|
@ -188,12 +217,20 @@ case class Launch(
|
|||
|
||||
val files0 = helper.fetch(sources = false, javadoc = false)
|
||||
|
||||
val contextLoader = Thread.currentThread().getContextClassLoader
|
||||
|
||||
val parentLoader0: ClassLoader = new ClasspathFilter(
|
||||
Thread.currentThread().getContextClassLoader,
|
||||
Coursier.baseCp.map(new File(_)).toSet,
|
||||
exclude = true
|
||||
)
|
||||
val parentLoader0: ClassLoader = Launch.mainClassLoader(contextLoader)
|
||||
.flatMap(cl => Option(cl.getParent))
|
||||
.getOrElse {
|
||||
if (common.verbose0 >= 0)
|
||||
Console.err.println(
|
||||
"Warning: cannot find the main ClassLoader that launched coursier. " +
|
||||
"Was coursier launched by its main launcher? " +
|
||||
"The ClassLoader of the application that is about to be launched will be intertwined " +
|
||||
"with the one of coursier, which may be a problem if their dependencies conflict."
|
||||
)
|
||||
contextLoader
|
||||
}
|
||||
|
||||
val (parentLoader, filteredFiles) =
|
||||
if (isolated.isolated.isEmpty)
|
||||
|
|
@ -307,9 +344,6 @@ case class Bootstrap(
|
|||
downloadDir: String,
|
||||
@Short("f")
|
||||
force: Boolean,
|
||||
@Help(s"Internal use - prepend base classpath options to arguments (like -B jar1 -B jar2 etc.)")
|
||||
@Short("b")
|
||||
prependClasspath: Boolean,
|
||||
@Help("Set environment variables in the generated launcher. No escaping is done. Value is simply put between quotes in the launcher preamble.")
|
||||
@Value("key=value")
|
||||
@Short("P")
|
||||
|
|
@ -440,7 +474,6 @@ case class Bootstrap(
|
|||
val properties = new Properties()
|
||||
properties.setProperty("bootstrap.mainClass", mainClass)
|
||||
properties.setProperty("bootstrap.jarDir", downloadDir)
|
||||
properties.setProperty("bootstrap.prependClasspath", prependClasspath.toString)
|
||||
|
||||
outputZip.putNextEntry(propsEntry)
|
||||
properties.store(outputZip, "")
|
||||
|
|
@ -487,27 +520,7 @@ case class Bootstrap(
|
|||
|
||||
}
|
||||
|
||||
case class BaseCommand(
|
||||
@Hidden
|
||||
@Short("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] {
|
||||
object Coursier extends CommandAppOf[CoursierCommand] {
|
||||
override def appName = "Coursier"
|
||||
override def progName = "coursier"
|
||||
|
||||
private[coursier] var baseCp = Seq.empty[String]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
package coursier
|
||||
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
|
||||
import coursier.util.ClasspathFilter
|
||||
|
||||
class ResolutionClassLoader(
|
||||
val resolution: Resolution,
|
||||
val artifacts: Seq[(Dependency, Artifact, File)],
|
||||
parent: ClassLoader
|
||||
) extends URLClassLoader(
|
||||
artifacts.map { case (_, _, f) => f.toURI.toURL }.toArray,
|
||||
parent
|
||||
) {
|
||||
|
||||
/**
|
||||
* Filtered version of this `ClassLoader`, exposing only `dependencies` and their
|
||||
* their transitive dependencies, and filtering out the other dependencies from
|
||||
* `resolution` - for `ClassLoader` isolation.
|
||||
*
|
||||
* An application launched by `coursier launch -C` has `ResolutionClassLoader` set as its
|
||||
* context `ClassLoader` (can be obtain with `Thread.currentThread().getContextClassLoader`).
|
||||
* If it aims at doing `ClassLoader` isolation, exposing only a dependency `dep` to the isolated
|
||||
* things, `filter(dep)` provides a `ClassLoader` that loaded `dep` and all its transitive
|
||||
* dependencies through the same loader as the contextual one, but that "exposes" only
|
||||
* `dep` and its transitive dependencies, nothing more.
|
||||
*/
|
||||
def filter(dependencies: Set[Dependency]): ClassLoader = {
|
||||
val subRes = resolution.subset(dependencies)
|
||||
val subArtifacts = subRes.dependencyArtifacts.map { case (_, a) => a }.toSet
|
||||
val subFiles = artifacts.collect { case (_, a, f) if subArtifacts(a) => f }
|
||||
|
||||
new ClasspathFilter(this, subFiles.toSet, exclude = false)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
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 =
|
||||
try super.loadClass(className, resolve)
|
||||
catch {
|
||||
case e: LinkageError =>
|
||||
// Happens when trying to derive a shapeless.Generic
|
||||
// from an Ammonite session launched like
|
||||
// ./coursier launch com.lihaoyi:ammonite-repl_2.11.7:0.5.2
|
||||
// For className == "shapeless.GenericMacros",
|
||||
// the super.loadClass above - which would be filtered out below anyway,
|
||||
// raises a NoClassDefFoundError.
|
||||
null
|
||||
}
|
||||
|
||||
if (c != null && 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
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ CACHE_VERSION=v1
|
|||
--no-default \
|
||||
-r central \
|
||||
-D "\${user.home}/.coursier/bootstrap/$VERSION" \
|
||||
-b \
|
||||
-f -o coursier \
|
||||
-M coursier.cli.Coursier \
|
||||
-P coursier.cache="\${user.home}/.coursier/cache/$CACHE_VERSION"
|
||||
|
|
|
|||
Loading…
Reference in New Issue