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.lang.reflect.Method;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.security.CodeSource;
|
import java.security.CodeSource;
|
||||||
|
|
@ -170,8 +169,6 @@ public class Bootstrap {
|
||||||
String mainClass0 = System.getProperty("bootstrap.mainClass");
|
String mainClass0 = System.getProperty("bootstrap.mainClass");
|
||||||
String jarDir0 = System.getProperty("bootstrap.jarDir");
|
String jarDir0 = System.getProperty("bootstrap.jarDir");
|
||||||
|
|
||||||
boolean prependClasspath = Boolean.parseBoolean(System.getProperty("bootstrap.prependClasspath", "false"));
|
|
||||||
|
|
||||||
final File jarDir = new File(jarDir0);
|
final File jarDir = new File(jarDir0);
|
||||||
|
|
||||||
if (jarDir.exists()) {
|
if (jarDir.exists()) {
|
||||||
|
|
@ -267,7 +264,7 @@ public class Bootstrap {
|
||||||
parentClassLoader = new IsolatedClassLoader(contextURLs, parentClassLoader, new String[]{ isolationID });
|
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;
|
Class<?> mainClass = null;
|
||||||
Method mainMethod = null;
|
Method mainMethod = null;
|
||||||
|
|
@ -288,14 +285,6 @@ public class Bootstrap {
|
||||||
|
|
||||||
List<String> userArgs0 = new ArrayList<>();
|
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++)
|
for (int i = 0; i < args.length; i++)
|
||||||
userArgs0.add(args[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 java.util.zip.{ ZipEntry, ZipOutputStream, ZipInputStream }
|
||||||
|
|
||||||
import caseapp.{ HelpMessage => Help, ValueDescription => Value, ExtraName => Short, _ }
|
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(
|
case class CommonOptions(
|
||||||
@Help("Keep optional dependencies (Maven)")
|
@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(
|
case class Launch(
|
||||||
@Short("M")
|
@Short("M")
|
||||||
@Short("main")
|
@Short("main")
|
||||||
|
|
@ -188,12 +217,20 @@ case class Launch(
|
||||||
|
|
||||||
val files0 = helper.fetch(sources = false, javadoc = false)
|
val files0 = helper.fetch(sources = false, javadoc = false)
|
||||||
|
|
||||||
|
val contextLoader = Thread.currentThread().getContextClassLoader
|
||||||
|
|
||||||
val parentLoader0: ClassLoader = new ClasspathFilter(
|
val parentLoader0: ClassLoader = Launch.mainClassLoader(contextLoader)
|
||||||
Thread.currentThread().getContextClassLoader,
|
.flatMap(cl => Option(cl.getParent))
|
||||||
Coursier.baseCp.map(new File(_)).toSet,
|
.getOrElse {
|
||||||
exclude = true
|
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) =
|
val (parentLoader, filteredFiles) =
|
||||||
if (isolated.isolated.isEmpty)
|
if (isolated.isolated.isEmpty)
|
||||||
|
|
@ -307,9 +344,6 @@ case class Bootstrap(
|
||||||
downloadDir: String,
|
downloadDir: String,
|
||||||
@Short("f")
|
@Short("f")
|
||||||
force: Boolean,
|
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.")
|
@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")
|
@Value("key=value")
|
||||||
@Short("P")
|
@Short("P")
|
||||||
|
|
@ -440,7 +474,6 @@ case class Bootstrap(
|
||||||
val properties = new Properties()
|
val properties = new Properties()
|
||||||
properties.setProperty("bootstrap.mainClass", mainClass)
|
properties.setProperty("bootstrap.mainClass", mainClass)
|
||||||
properties.setProperty("bootstrap.jarDir", downloadDir)
|
properties.setProperty("bootstrap.jarDir", downloadDir)
|
||||||
properties.setProperty("bootstrap.prependClasspath", prependClasspath.toString)
|
|
||||||
|
|
||||||
outputZip.putNextEntry(propsEntry)
|
outputZip.putNextEntry(propsEntry)
|
||||||
properties.store(outputZip, "")
|
properties.store(outputZip, "")
|
||||||
|
|
@ -487,27 +520,7 @@ case class Bootstrap(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case class BaseCommand(
|
object Coursier extends CommandAppOf[CoursierCommand] {
|
||||||
@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] {
|
|
||||||
override def appName = "Coursier"
|
override def appName = "Coursier"
|
||||||
override def progName = "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 \
|
--no-default \
|
||||||
-r central \
|
-r central \
|
||||||
-D "\${user.home}/.coursier/bootstrap/$VERSION" \
|
-D "\${user.home}/.coursier/bootstrap/$VERSION" \
|
||||||
-b \
|
|
||||||
-f -o coursier \
|
-f -o coursier \
|
||||||
-M coursier.cli.Coursier \
|
-M coursier.cli.Coursier \
|
||||||
-P coursier.cache="\${user.home}/.coursier/cache/$CACHE_VERSION"
|
-P coursier.cache="\${user.home}/.coursier/cache/$CACHE_VERSION"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue