Skip to content

Commit 42af574

Browse files
committed
sbt-dotty: Rework classpath and ScalaInstance handling
In particular, stop relying on zinc handling of the ScalaTool configuration (by setting managedScalaInstance to false) and instead fetch artifacts ourselves. Also document clearly what should end up on which classpath.
1 parent c98940a commit 42af574

File tree

2 files changed

+167
-70
lines changed

2 files changed

+167
-70
lines changed

project/Build.scala

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import xerial.sbt.pack.PackPlugin
1616
import xerial.sbt.pack.PackPlugin.autoImport._
1717

1818
import dotty.tools.sbtplugin.DottyPlugin.autoImport._
19+
import dotty.tools.sbtplugin.DottyPlugin.makeScalaInstance
1920
import dotty.tools.sbtplugin.DottyIDEPlugin.{ installCodeExtension, prepareCommand, runProcess }
2021
import dotty.tools.sbtplugin.DottyIDEPlugin.autoImport._
2122

@@ -223,20 +224,6 @@ object Build {
223224
// Use the same name as the non-bootstrapped projects for the artifacts
224225
moduleName ~= { _.stripSuffix("-bootstrapped") },
225226

226-
// Prevent sbt from setting the Scala bootclasspath, otherwise it will
227-
// contain `scalaInstance.value.libraryJar` which in our case is the
228-
// non-bootstrapped dotty-library that will then take priority over
229-
// the bootstrapped dotty-library on the classpath or sourcepath.
230-
classpathOptions ~= (_.withAutoBoot(false)),
231-
// ... but when running under Java 8, we still need a Scala bootclasspath that contains the JVM bootclasspath,
232-
// otherwise sbt incremental compilation breaks.
233-
scalacOptions ++= {
234-
if (isJavaAtLeast("9"))
235-
Seq()
236-
else
237-
Seq("-bootclasspath", sys.props("sun.boot.class.path"))
238-
},
239-
240227
// Enforce that the only Scala 2 classfiles we unpickle come from scala-library
241228
/*
242229
scalacOptions ++= {
@@ -257,35 +244,28 @@ object Build {
257244
// Compile using the non-bootstrapped and non-published dotty
258245
managedScalaInstance := false,
259246
scalaInstance := {
260-
import sbt.internal.inc.ScalaInstance
261-
import sbt.internal.inc.classpath.ClasspathUtilities
262-
263-
val updateReport = update.value
264-
val libraryJar = packageBin.in(`dotty-library`, Compile).value
265-
val compilerJar = packageBin.in(`dotty-compiler`, Compile).value
266-
267-
// All dotty-doc's and compiler's dependencies except the library.
268-
// (we get the compiler's dependencies because dottydoc depends on the compiler)
269-
val otherDependencies = {
270-
val excluded = Set("dotty-library", "dotty-compiler")
271-
fullClasspath.in(`dotty-doc`, Compile).value
272-
.filterNot(_.get(artifact.key).exists(a => excluded.contains(a.name)))
273-
.map(_.data)
274-
}
275-
276-
val allJars = libraryJar :: compilerJar :: otherDependencies.toList
277-
val classLoader = state.value.classLoaderCache(allJars)
278-
new ScalaInstance(
247+
// TODO: Here we use the output class directories directly, this might impact
248+
// performance when running the compiler (especially on Windows where file
249+
// IO is slow). We should benchmark whether using jars is actually faster
250+
// in practice (especially on our CI), this could be done using
251+
// `exportJars := true`.
252+
val all = fullClasspath.in(`dotty-doc`, Compile).value
253+
def getArtifact(name: String): File =
254+
all.find(_.get(artifact.key).exists(_.name == name))
255+
.getOrElse(throw new MessageOnlyException(s"Artifact for $name not found in $all"))
256+
.data
257+
258+
val scalaLibrary = getArtifact("scala-library")
259+
val dottyLibrary = getArtifact("dotty-library")
260+
val compiler = getArtifact("dotty-compiler")
261+
262+
makeScalaInstance(
263+
state.value,
279264
scalaVersion.value,
280-
classLoader,
281-
ClasspathUtilities.rootLoader, // FIXME: Should be a class loader which only includes the dotty-lib
282-
// See: https://github.com/sbt/zinc/commit/9397b6aaf94ac3cfab386e3abd11c0ef9c2ceaff#diff-ea135f2f26f43e40ff045089da221e1e
283-
// Should not matter, as it addresses an issue with `sbt run` that
284-
// only occur when `(fork in run) := false`
285-
libraryJar,
286-
compilerJar,
287-
allJars.toArray,
288-
None
265+
scalaLibrary,
266+
dottyLibrary,
267+
compiler,
268+
all.map(_.data)
289269
)
290270
}
291271
)

sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala

Lines changed: 145 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package dotty.tools.sbtplugin
22

33
import sbt._
44
import sbt.Keys._
5-
import sbt.librarymanagement.DependencyResolution
5+
import sbt.librarymanagement._
66
import sbt.internal.inc.ScalaInstance
77
import xsbti.compile._
88
import java.net.URLClassLoader
99
import java.util.Optional
10+
import scala.util.Properties.isJavaAtLeast
1011

1112
object DottyPlugin extends AutoPlugin {
1213
object autoImport {
@@ -165,7 +166,13 @@ object DottyPlugin extends AutoPlugin {
165166

166167
scalaCompilerBridgeBinaryJar := Def.taskDyn {
167168
if (isDotty.value) Def.task {
168-
val dottyBridgeArtifacts = fetchArtifactsOf("dotty-sbt-bridge", CrossVersion.disabled).value
169+
val dottyBridgeArtifacts = fetchArtifactsOf(
170+
dependencyResolution.value,
171+
scalaModuleInfo.value,
172+
updateConfiguration.value,
173+
(unresolvedWarningConfiguration in update).value,
174+
streams.value.log,
175+
scalaOrganization.value % "dotty-sbt-bridge" % scalaVersion.value).allFiles
169176
val jars = dottyBridgeArtifacts.filter(art => art.getName.startsWith("dotty-sbt-bridge") && art.getName.endsWith(".jar")).toArray
170177
if (jars.size == 0)
171178
throw new MessageOnlyException("No jar found for dotty-sbt-bridge")
@@ -187,19 +194,95 @@ object DottyPlugin extends AutoPlugin {
187194
scalaBinaryVersion.value
188195
},
189196

197+
// Ideally, we should have:
198+
//
199+
// 1. Nothing but the Java standard library on the _JVM_ bootclasspath
200+
// (starting with Java 9 we cannot inspect it so we don't have a choice)
201+
//
202+
// 2. scala-library, dotty-library, dotty-compiler, dotty-doc on the _JVM_
203+
// classpath, because we need all of those to actually run the compiler
204+
// and the doc tool.
205+
// NOTE: All of those should have the *same version* (equal to scalaVersion
206+
// for everything but scala-library).
207+
//
208+
// 3. scala-library, dotty-library on the _compiler_ bootclasspath because
209+
// user code should always have access to the symbols from these jars but
210+
// should not be able to shadow them (the compiler bootclasspath has
211+
// higher priority than the compiler classpath).
212+
// NOTE: the versions of {scala,dotty}-library used here do not necessarily
213+
// match the one used in 2. because a dependency of the current project might
214+
// require more recent versions, this is OK.
215+
//
216+
// 4. every other dependency of the user project on the _compiler_
217+
// classpath.
218+
//
219+
// Unfortunately, zinc assumes that the compiler bootclasspath is only
220+
// made of one jar (scala-library), so to make this work we'll need to
221+
// either change sbt's bootclasspath handling or wait until the
222+
// dotty-library jar and scala-library jars are merged into one jar.
223+
// Meanwhile, let's just put nothing at all on the compiler
224+
// bootclasspath, and instead put scala-library and dotty-library on the
225+
// compiler classpath. This means that user code could shadow symbols
226+
// from these jars but we can live with that for now.
227+
classpathOptions := {
228+
val old = classpathOptions.value
229+
if (isDotty.value)
230+
old
231+
.withAutoBoot(false) // we don't put the library on the compiler bootclasspath (as explained above)
232+
.withFilterLibrary(false) // ...instead, we put it on the compiler classpath
233+
else
234+
old
235+
},
236+
// ... but when running under Java 8, we still need a compiler bootclasspath
237+
// that contains the JVM bootclasspath, otherwise sbt incremental
238+
// compilation breaks.
239+
scalacOptions ++= {
240+
if (isDotty.value && !isJavaAtLeast("9"))
241+
Seq("-bootclasspath", sys.props("sun.boot.class.path"))
242+
else
243+
Seq()
244+
},
245+
managedScalaInstance := {
246+
val old = managedScalaInstance.value
247+
if (isDotty.value)
248+
false
249+
else
250+
old
251+
},
190252
scalaInstance := Def.taskDyn {
191-
val si = scalaInstance.value
192-
if (isDotty.value) {
193-
Def.task {
194-
val dottydocArtifacts = fetchArtifactsOf("dotty-doc", CrossVersion.binary).value
195-
val includeArtifact = (f: File) => f.getName.endsWith(".jar")
196-
val dottydocJars = dottydocArtifacts.filter(includeArtifact).toArray
197-
val allJars = (si.allJars ++ dottydocJars).distinct
198-
val loader = new URLClassLoader(Path.toURLs(dottydocJars), si.loader)
199-
new ScalaInstance(si.version, loader, si.loaderLibraryOnly, si.libraryJar, si.compilerJar, allJars, si.explicitActual)
200-
}
201-
} else {
202-
Def.task { si }
253+
if (isDotty.value) Def.task {
254+
val updateReport =
255+
fetchArtifactsOf(
256+
dependencyResolution.value,
257+
scalaModuleInfo.value,
258+
updateConfiguration.value,
259+
(unresolvedWarningConfiguration in update).value,
260+
streams.value.log,
261+
scalaOrganization.value %% "dotty-doc" % scalaVersion.value)
262+
val scalaLibraryJar = getJar(updateReport,
263+
"org.scala-lang", "scala-library", revision = AllPassFilter)
264+
val dottyLibraryJar = getJar(updateReport,
265+
scalaOrganization.value, s"dotty-library_${scalaBinaryVersion.value}", scalaVersion.value)
266+
val compilerJar = getJar(updateReport,
267+
scalaOrganization.value, s"dotty-compiler_${scalaBinaryVersion.value}", scalaVersion.value)
268+
val allJars =
269+
getJars(updateReport, AllPassFilter, AllPassFilter, AllPassFilter)
270+
271+
makeScalaInstance(
272+
state.value,
273+
scalaVersion.value,
274+
scalaLibraryJar,
275+
dottyLibraryJar,
276+
compilerJar,
277+
allJars
278+
)
279+
}
280+
else Def.task {
281+
// This should really be `old` with `val old = scalaInstance.value`
282+
// above, except that this would force the original definition of the
283+
// `scalaInstance` task to be computed when `isDotty` is true, which
284+
// would fail because `managedScalaInstance` is false.
285+
Defaults.scalaInstanceTask.value
203286
}
204287
}.value,
205288

@@ -220,7 +303,7 @@ object DottyPlugin extends AutoPlugin {
220303
// circular dependency found:
221304
// ch.epfl.lamp#scala-library;0.9.0-RC1->ch.epfl.lamp#dotty-library_0.9;0.9.0-RC1->...
222305
// (This should go away once we merge dotty-library and scala-library in one artefact)
223-
old.withCircularDependencyLevel(sbt.librarymanagement.ivy.CircularDependencyLevel.Ignore)
306+
old.withCircularDependencyLevel(ivy.CircularDependencyLevel.Ignore)
224307
} else old
225308
}
226309
) ++ inConfig(Compile)(docSettings) ++ inConfig(Test)(docSettings)
@@ -236,23 +319,57 @@ object DottyPlugin extends AutoPlugin {
236319
scalacOptions += "-from-tasty"
237320
))
238321

239-
/** Fetch artifacts for scalaOrganization.value %% moduleName % scalaVersion.value */
240-
private def fetchArtifactsOf(moduleName: String, crossVersion: CrossVersion) = Def.task {
241-
val dependencyResolution = Keys.dependencyResolution.value
242-
val log = streams.value.log
243-
val scalaInfo = scalaModuleInfo.value
244-
val updateConfiguration = Keys.updateConfiguration.value
245-
val warningConfiguration = (unresolvedWarningConfiguration in update).value
246-
247-
val moduleID = (scalaOrganization.value % moduleName % scalaVersion.value).cross(crossVersion)
248-
val descriptor = dependencyResolution.wrapDependencyInModule(moduleID, scalaInfo)
322+
/** Fetch artifacts for moduleID */
323+
def fetchArtifactsOf(
324+
dependencyRes: DependencyResolution,
325+
scalaInfo: Option[ScalaModuleInfo],
326+
updateConfig: UpdateConfiguration,
327+
warningConfig: UnresolvedWarningConfiguration,
328+
log: Logger,
329+
moduleID: ModuleID): UpdateReport = {
330+
val descriptor = dependencyRes.wrapDependencyInModule(moduleID, scalaInfo)
249331

250-
dependencyResolution.update(descriptor, updateConfiguration, warningConfiguration, log) match {
332+
dependencyRes.update(descriptor, updateConfig, warningConfig, log) match {
251333
case Right(report) =>
252-
report.allFiles
334+
report
253335
case _ =>
254336
throw new MessageOnlyException(
255-
s"Couldn't retrieve `${scalaOrganization.value} %% $moduleName %% ${scalaVersion.value}`.")
337+
s"Couldn't retrieve `$moduleID`.")
256338
}
257339
}
340+
341+
/** Get all jars in updateReport that match the given filter. */
342+
def getJars(updateReport: UpdateReport, organization: NameFilter, name: NameFilter, revision: NameFilter): Seq[File] = {
343+
updateReport.select(
344+
configurationFilter(Runtime.name),
345+
moduleFilter(organization, name, revision),
346+
artifactFilter(extension = "jar")
347+
)
348+
}
349+
350+
/** Get the single jar in updateReport that match the given filter.
351+
* If zero or more than one jar match, an exception will be thrown. */
352+
def getJar(updateReport: UpdateReport, organization: NameFilter, name: NameFilter, revision: NameFilter): File = {
353+
val jars = getJars(updateReport, organization, name, revision)
354+
assert(jars.size == 1, s"There should only be one $name jar but found: $jars")
355+
jars.head
356+
}
357+
358+
def makeScalaInstance(
359+
state: State, dottyVersion: String, scalaLibrary: File, dottyLibrary: File, compiler: File, all: Seq[File]
360+
): ScalaInstance = {
361+
val loader = state.classLoaderCache(all.toList)
362+
val loaderLibraryOnly = state.classLoaderCache(List(dottyLibrary, scalaLibrary))
363+
new ScalaInstance(
364+
dottyVersion,
365+
loader,
366+
loaderLibraryOnly,
367+
scalaLibrary, // Should be a Seq also containing dottyLibrary but zinc
368+
// doesn't support this, see comment above our redefinition
369+
// of `classpathOption`
370+
compiler,
371+
all.toArray,
372+
None)
373+
374+
}
258375
}

0 commit comments

Comments
 (0)