Skip to content

include only scala-library into the classpath during run #505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 23, 2018

Conversation

eed3si9n
Copy link
Member

@eed3si9n eed3si9n commented Mar 2, 2018

Fixes sbt/sbt#3405
Ref scala/scala-xml#195

sbt's run is emulated using a classloader trick that includes ScalaInstance as the parent classloader under the classpath. The problem is the ScalaInstance classloader currently contains both compiler, library, and their transitive JARs:

res0: Array[java.io.File] = Array(scala-library.jar, scala-compiler.jar, jline.jar, scala-reflect.jar, scala-xml_2.12.jar)

This could have been causing various issues, but most recently it showed up as wrong version of scala-xml getting prioritized over what's passed by the user.

@eed3si9n eed3si9n requested a review from retronym March 2, 2018 07:36
/**
* Include only the scala-library.jar, if any, into the boot classpath.
* 1. Supposedly using boot classpath here is for performance reason:
* https://groups.google.com/d/msg/scala-internals/tT1pjH5GECE/dtgRj3w7ovIJ
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"boot classpath" isn't an accurate description of what's happening here. The performance trick is to cache and share libraryLoader in ScalaInstance, rather than forming new classpaths containing scala-library.jar.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createClasspathResources(cp, instance) does map things into BootClassPath.

lazy val libraryLoader: ClassLoader = {
classpath.ClasspathUtilities.toLibraryLoader(this)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be preferable to do this way in in ScalaProvider, ie change the way it creates loader, and then just accept libraryLoader and fullLoader here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your change will end up with scala-library loaded separates in loader and libraryLoader. That's a functional problem if you ever need interop between programs running in those loaders. It is also a small performance problem in space and time for the VM.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could sort of cheat without changing the interface of ScalaProvider and the intervening call chain.

The launcer could create the layered classloaders as I described, but still only pass the full one into ScalaInstance. Here, you could look at loader.getParent to get the libraryLoader (perhaps verifying that isIsntanceOf[LauncherLibraryClassloader] to know that the secret handshake is complete

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the secret handshake wasn't made (ie, we are being called by an old launcher), you could either do as you've done here, or just let the bug persist.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's in launcher land. I would rather not make ppl reinstall launchers. (Assuming bunch of ppl are using official sbt package, not sbt-extra)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could deliver both fixes: the launcher fix (that makes this part of the code trivial), and the existing fix you've got here (to fix the bug when using older launchers)

Copy link
Member

@retronym retronym Mar 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd need to deal with the xsbti.ExtendedScalaProvider interface reflectively in SBT to avoid a LinkageError with older launchers. This isn't much fun, but is a pretty standard way of evolving interfaces and implementations that require compatibility.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jvican

I think this is the way to go -- being able to recycle JVM optimizations that have already happened for the Scala library is important, and classloading the library again goes against that.

I agree. I think I am now starting to see the light of what Jason is saying.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x.getName.startsWith("scala-library")
}).getOrElse {
throw new InvalidScalaProvider(s"Couldn't find scala-library")
}
Copy link
Member

@retronym retronym Mar 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess to future proof this could find the transitive dependencies of scala-library, which could be computed in launcher, but would need some extra metadata (the scala build would need to add this to our distribution) when loading from scalaHome.

@eed3si9n eed3si9n changed the title include only scala-library into the boot classpath during run include only scala-library into the classpath during run Mar 7, 2018
val (loader, loaderLibraryOnly) =
(try {
provider match {
case p: ScalaProvider2 @unchecked => Option((provider.loader, p.loaderLibraryOnly))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to try the new loader first refectively, and fall back to manual creation.

@typesafe-tools
Copy link

The validator has checked the following projects against Scala 2.12,
tested using dbuild, projects built on top of each other.

Project Reference Commit
sbt 1.1.x sbt/sbt@27bbde8
zinc pull/505/head 733e629
io 1.1.x sbt/io@13eb241
librarymanagement 1.1.x sbt/librarymanagement@4c5e1b9
util 1.1.x sbt/util@226a543
website 1.1.x

✅ The result is: SUCCESS
(restart)

@eed3si9n
Copy link
Member Author

eed3si9n commented Mar 9, 2018

Addressed the review comments.
Could we get the ok on this, so we can include it in next sbt 1.1.x?

@jvican
Copy link
Member

jvican commented Mar 9, 2018

Cannot make the review today, but you'll have it by next Monday!

Copy link
Member

@jvican jvican left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just a couple of comments. Could you link to the PR that will integrate with this work over sbt/sbt?

new ScalaInstance(version, loader, libraryJar, compilerJar, jars, None)
// sbt launcher 1.0.3 will construct layered classloader. Use them if we find them.
// otherwise, construct layered loaders manually.
val (loader, loaderLibraryOnly) =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicks: can we wrap all this logic in curly braces and separate the computation of the default value (what's in getOrElse) from the rest of the logic?

val version = actualVersion(loader)(" (library jar " + library.getAbsolutePath + ")")
val compiler = compilerJar(scalaHome)
new ScalaInstance(version, loader, library, compiler, all.toArray, None)
new ScalaInstance(version, loader, loaderLibraryOnly, library, compiler, all.toArray, None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to break quite a bit of code. I would honestly consider cutting a major breaking release... xsbti.compile is quite core to the Zinc API.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to break quite a bit of code.

How so? Do you think there would be observable changes to sbt or other Zinc downstream users? (Besides being able to use the correct scala-xml version)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't mean behaviour-wise, but API-wise 😄. I think ScalaInstance is used pretty much everywhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. Maybe I can try to add another constructor that is bincompat and deprecate it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please do 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. Addressed the bincompat.

def makeLoader(classpath: Seq[File], instance: ScalaInstance): ClassLoader =
filterByClasspath(classpath, makeLoader(classpath, instance.loader, instance))
filterByClasspath(classpath, makeLoader(classpath, instance.loaderLibraryOnly, instance))
Copy link
Member

@retronym retronym Mar 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually should make even the addition of loaderLibraryOnly conditional on autoScalaLibrary := true. In the Scala build, we have that setting disabled (because the scala-library we want on the classpath is actually a regular sub-project in our build, not the one in our reference Scala version).

Here's an example of how we get the unwanted class on our classpath: scala/scala#6300 (comment). So far, we have worked around by forking tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. That sounds like a bug on sbt's Run or TestFramework. By the time someone called makeLoader(...) with instance I think it's ok to include loaderLibraryOnly. If autoScalaLibrary is false then we have to be careful not to create ScalaInstance based loader basically.

jvican
jvican previously approved these changes Mar 19, 2018
Copy link
Member

@jvican jvican left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have anything else to add, 👍

@eed3si9n
Copy link
Member Author

Cool. I'll squash the history and merge.

Fixes sbt/sbt#3405
Ref scala/scala-xml#195

sbt's `run` is emulated using a classloader trick that includes ScalaInstance as the parent classloader under the classpath. The problem is the ScalaInstance classloader currently contains both compiler, library, and their transitive JARs:

```scala
res0: Array[java.io.File] = Array(scala-library.jar, scala-compiler.jar, jline.jar, scala-reflect.jar, scala-xml_2.12.jar)
```

This could have been causing various issues, but most recently it showed up as wrong version of scala-xml getting prioritized over what's passed by the user.

1. new field loaderLibraryOnly is added to xsbti.ScalaInstance.
2. it is initialized to the library loader if the launcher creates it, otherwise create layered loader here.

This aims to isolate the library loader, and retain the perf.
@dwijnand dwijnand added this to the 1.1.2 milestone Mar 21, 2018
@eed3si9n eed3si9n merged commit a229439 into sbt:1.1.x Mar 23, 2018
@eed3si9n eed3si9n deleted the wip/run-fix branch March 23, 2018 12:29
eed3si9n added a commit that referenced this pull request Mar 23, 2018
I think I've made a mistake in #505 by using NoSuchMethodError. When trying to use Zinc 1.1.2, I ran into `java.lang.NoSuchMethodException`.
This is because `NoSuchMethodException` would be thrown for reflective calls, not `NoSuchMethodError`.
eed3si9n added a commit to eed3si9n/zinc that referenced this pull request Mar 24, 2018
I think I've made a mistake in sbt#505 by using NoSuchMethodError. When trying to use Zinc 1.1.2, I ran into `java.lang.NoSuchMethodException`.
This is because `NoSuchMethodException` would be thrown for reflective calls, not `NoSuchMethodError`.
@SethTisue
Copy link
Member

SethTisue commented Mar 27, 2018

thanks @eed3si9n , so glad you took this on 👏👏👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants