Skip to content

Commit 0162bf4

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 db7ced4 commit 0162bf4

File tree

2 files changed

+199
-84
lines changed

2 files changed

+199
-84
lines changed

project/Build.scala

Lines changed: 22 additions & 41 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

@@ -209,20 +210,6 @@ object Build {
209210
// Use the same name as the non-bootstrapped projects for the artifacts
210211
moduleName ~= { _.stripSuffix("-bootstrapped") },
211212

212-
// Prevent sbt from setting the Scala bootclasspath, otherwise it will
213-
// contain `scalaInstance.value.libraryJar` which in our case is the
214-
// non-bootstrapped dotty-library that will then take priority over
215-
// the bootstrapped dotty-library on the classpath or sourcepath.
216-
classpathOptions ~= (_.withAutoBoot(false)),
217-
// ... but when running under Java 8, we still need a Scala bootclasspath that contains the JVM bootclasspath,
218-
// otherwise sbt incremental compilation breaks.
219-
scalacOptions ++= {
220-
if (isJavaAtLeast("9"))
221-
Seq()
222-
else
223-
Seq("-bootclasspath", sys.props("sun.boot.class.path"))
224-
},
225-
226213
// Enforce that the only Scala 2 classfiles we unpickle come from scala-library
227214
/*
228215
scalacOptions ++= {
@@ -243,34 +230,28 @@ object Build {
243230
// Compile using the non-bootstrapped and non-published dotty
244231
managedScalaInstance := false,
245232
scalaInstance := {
246-
import sbt.internal.inc.ScalaInstance
247-
import sbt.internal.inc.classpath.ClasspathUtilities
248-
249-
val libraryJar = packageBin.in(`dotty-library`, Compile).value
250-
val compilerJar = packageBin.in(`dotty-compiler`, Compile).value
251-
252-
// All dotty-doc's and compiler's dependencies except the library.
253-
// (we get the compiler's dependencies because dottydoc depends on the compiler)
254-
val otherDependencies = {
255-
val excluded = Set("dotty-library", "dotty-compiler")
256-
fullClasspath.in(`dotty-doc`, Compile).value
257-
.filterNot(_.get(artifact.key).exists(a => excluded.contains(a.name)))
258-
.map(_.data)
259-
}
260-
261-
val allJars = libraryJar :: compilerJar :: otherDependencies.toList
262-
val classLoader = state.value.classLoaderCache(allJars)
263-
new ScalaInstance(
233+
// TODO: Here we use the output class directories directly, this might impact
234+
// performance when running the compiler (especially on Windows where file
235+
// IO is slow). We should benchmark whether using jars is actually faster
236+
// in practice (especially on our CI), this could be done using
237+
// `exportJars := true`.
238+
val all = fullClasspath.in(`dotty-doc`, Compile).value
239+
def getArtifact(name: String): File =
240+
all.find(_.get(artifact.key).exists(_.name == name))
241+
.getOrElse(throw new MessageOnlyException(s"Artifact for $name not found in $all"))
242+
.data
243+
244+
val scalaLibrary = getArtifact("scala-library")
245+
val dottyLibrary = getArtifact("dotty-library")
246+
val compiler = getArtifact("dotty-compiler")
247+
248+
makeScalaInstance(
249+
state.value,
264250
scalaVersion.value,
265-
classLoader,
266-
ClasspathUtilities.rootLoader, // FIXME: Should be a class loader which only includes the dotty-lib
267-
// See: https://github.com/sbt/zinc/commit/9397b6aaf94ac3cfab386e3abd11c0ef9c2ceaff#diff-ea135f2f26f43e40ff045089da221e1e
268-
// Should not matter, as it addresses an issue with `sbt run` that
269-
// only occur when `(fork in run) := false`
270-
libraryJar,
271-
compilerJar,
272-
allJars.toArray,
273-
None
251+
scalaLibrary,
252+
dottyLibrary,
253+
compiler,
254+
all.map(_.data)
274255
)
275256
}
276257
)

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

Lines changed: 177 additions & 43 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.settingDyn {
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,41 +194,134 @@ 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+
// Furthermore, zinc will put on the compiler bootclasspath the
224+
// scala-library used on the JVM classpath, even if the current project
225+
// transitively depends on a newer scala-library (this works because Scala
226+
// 2 guarantees forward- and backward- binary compatibility, but we don't
227+
// necessarily want to keep doing that in Scala 3).
228+
// So for the moment, let's just put nothing at all on the compiler
229+
// bootclasspath, and instead have scala-library and dotty-library on the
230+
// compiler classpath. This means that user code could shadow symbols
231+
// from these jars but we can live with that for now.
232+
classpathOptions := {
233+
val old = classpathOptions.value
234+
if (isDotty.value)
235+
old
236+
.withAutoBoot(false) // we don't put the library on the compiler bootclasspath (as explained above)
237+
.withFilterLibrary(false) // ...instead, we put it on the compiler classpath
238+
else
239+
old
240+
},
241+
// ... but when running under Java 8, we still need a compiler bootclasspath
242+
// that contains the JVM bootclasspath, otherwise sbt incremental
243+
// compilation breaks.
244+
scalacOptions ++= {
245+
if (isDotty.value && !isJavaAtLeast("9"))
246+
Seq("-bootclasspath", sys.props("sun.boot.class.path"))
247+
else
248+
Seq()
249+
},
250+
// If the current scalaVersion is N and we transitively depend on
251+
// {scala, dotty}-{library, compiler, ...} M where M > N, we want the
252+
// newest version on our compiler classpath, but sbt by default will
253+
// instead rewrite all our dependencies to version N, the following line
254+
// prevents this behavior.
255+
scalaModuleInfo := {
256+
val old = scalaModuleInfo.value
257+
if (isDotty.value)
258+
old.map(_.withOverrideScalaVersion(false))
259+
else
260+
old
261+
},
262+
// Prevent sbt from creating a ScalaTool configuration
263+
managedScalaInstance := {
264+
val old = managedScalaInstance.value
265+
if (isDotty.value)
266+
false
267+
else
268+
old
269+
},
270+
// ... instead, we'll fetch the compiler and its dependencies ourselves.
190271
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 }
272+
if (isDotty.value) Def.task {
273+
val updateReport =
274+
fetchArtifactsOf(
275+
dependencyResolution.value,
276+
scalaModuleInfo.value,
277+
updateConfiguration.value,
278+
(unresolvedWarningConfiguration in update).value,
279+
streams.value.log,
280+
scalaOrganization.value %% "dotty-doc" % scalaVersion.value)
281+
val scalaLibraryJar = getJar(updateReport,
282+
"org.scala-lang", "scala-library", revision = AllPassFilter)
283+
val dottyLibraryJar = getJar(updateReport,
284+
scalaOrganization.value, s"dotty-library_${scalaBinaryVersion.value}", scalaVersion.value)
285+
val compilerJar = getJar(updateReport,
286+
scalaOrganization.value, s"dotty-compiler_${scalaBinaryVersion.value}", scalaVersion.value)
287+
val allJars =
288+
getJars(updateReport, AllPassFilter, AllPassFilter, AllPassFilter)
289+
290+
makeScalaInstance(
291+
state.value,
292+
scalaVersion.value,
293+
scalaLibraryJar,
294+
dottyLibraryJar,
295+
compilerJar,
296+
allJars
297+
)
298+
}
299+
else Def.task {
300+
// This should really be `old` with `val old = scalaInstance.value`
301+
// above, except that this would force the original definition of the
302+
// `scalaInstance` task to be computed when `isDotty` is true, which
303+
// would fail because `managedScalaInstance` is false.
304+
Defaults.scalaInstanceTask.value
203305
}
204306
}.value,
205307

308+
// Because managedScalaInstance is false, sbt won't add the standard library to our dependencies for us
309+
libraryDependencies ++= {
310+
if (isDotty.value && autoScalaLibrary.value)
311+
Seq(scalaOrganization.value %% "dotty-library" % scalaVersion.value)
312+
else
313+
Seq()
314+
},
315+
316+
// Turns off the warning:
317+
// [warn] Binary version (0.9.0-RC1) for dependency ...;0.9.0-RC1
318+
// [warn] in ... differs from Scala binary version in project (0.9).
206319
scalaModuleInfo := {
207320
val old = scalaModuleInfo.value
208-
if (isDotty.value) {
209-
// Turns off the warning:
210-
// [warn] Binary version (0.9.0-RC1) for dependency ...;0.9.0-RC1
211-
// [warn] in ... differs from Scala binary version in project (0.9).
321+
if (isDotty.value)
212322
old.map(_.withCheckExplicit(false))
213-
} else old
214-
},
215-
216-
updateOptions := {
217-
val old = updateOptions.value
218-
if (isDotty.value) {
219-
// Turn off the warning:
220-
// circular dependency found:
221-
// ch.epfl.lamp#scala-library;0.9.0-RC1->ch.epfl.lamp#dotty-library_0.9;0.9.0-RC1->...
222-
// (This should go away once we merge dotty-library and scala-library in one artefact)
223-
old.withCircularDependencyLevel(sbt.librarymanagement.ivy.CircularDependencyLevel.Ignore)
224-
} else old
323+
else
324+
old
225325
}
226326
) ++ inConfig(Compile)(docSettings) ++ inConfig(Test)(docSettings)
227327
}
@@ -236,23 +336,57 @@ object DottyPlugin extends AutoPlugin {
236336
scalacOptions += "-from-tasty"
237337
))
238338

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
339+
/** Fetch artifacts for moduleID */
340+
def fetchArtifactsOf(
341+
dependencyRes: DependencyResolution,
342+
scalaInfo: Option[ScalaModuleInfo],
343+
updateConfig: UpdateConfiguration,
344+
warningConfig: UnresolvedWarningConfiguration,
345+
log: Logger,
346+
moduleID: ModuleID): UpdateReport = {
347+
val descriptor = dependencyRes.wrapDependencyInModule(moduleID, scalaInfo)
246348

247-
val moduleID = (scalaOrganization.value % moduleName % scalaVersion.value).cross(crossVersion)
248-
val descriptor = dependencyResolution.wrapDependencyInModule(moduleID, scalaInfo)
249-
250-
dependencyResolution.update(descriptor, updateConfiguration, warningConfiguration, log) match {
349+
dependencyRes.update(descriptor, updateConfig, warningConfig, log) match {
251350
case Right(report) =>
252-
report.allFiles
351+
report
253352
case _ =>
254353
throw new MessageOnlyException(
255-
s"Couldn't retrieve `${scalaOrganization.value} %% $moduleName %% ${scalaVersion.value}`.")
354+
s"Couldn't retrieve `$moduleID`.")
256355
}
257356
}
357+
358+
/** Get all jars in updateReport that match the given filter. */
359+
def getJars(updateReport: UpdateReport, organization: NameFilter, name: NameFilter, revision: NameFilter): Seq[File] = {
360+
updateReport.select(
361+
configurationFilter(Runtime.name),
362+
moduleFilter(organization, name, revision),
363+
artifactFilter(extension = "jar")
364+
)
365+
}
366+
367+
/** Get the single jar in updateReport that match the given filter.
368+
* If zero or more than one jar match, an exception will be thrown. */
369+
def getJar(updateReport: UpdateReport, organization: NameFilter, name: NameFilter, revision: NameFilter): File = {
370+
val jars = getJars(updateReport, organization, name, revision)
371+
assert(jars.size == 1, s"There should only be one $name jar but found: $jars")
372+
jars.head
373+
}
374+
375+
def makeScalaInstance(
376+
state: State, dottyVersion: String, scalaLibrary: File, dottyLibrary: File, compiler: File, all: Seq[File]
377+
): ScalaInstance = {
378+
val loader = state.classLoaderCache(all.toList)
379+
val loaderLibraryOnly = state.classLoaderCache(List(dottyLibrary, scalaLibrary))
380+
new ScalaInstance(
381+
dottyVersion,
382+
loader,
383+
loaderLibraryOnly,
384+
scalaLibrary, // Should be a Seq also containing dottyLibrary but zinc
385+
// doesn't support this, see comment above our redefinition
386+
// of `classpathOption`
387+
compiler,
388+
all.toArray,
389+
None)
390+
391+
}
258392
}

0 commit comments

Comments
 (0)