From ed7da85ef012b71919c8ce35b5e46c760db7766a Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 1 Nov 2025 14:53:58 -0400 Subject: [PATCH] Backport Compiler Backport scala.internal.Compiler from sbt 2.x so we can merge easier. --- main/src/main/scala/sbt/Defaults.scala | 178 +------------ .../main/scala/sbt/internal/Compiler.scala | 234 ++++++++++++++++++ 2 files changed, 243 insertions(+), 169 deletions(-) create mode 100644 main/src/main/scala/sbt/internal/Compiler.scala diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index fd49fdcc3..a6c2c5a56 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -689,7 +689,7 @@ object Defaults extends BuildCommon { } else topLoader }, - scalaInstance := scalaInstanceTask.value, + scalaInstance := Compiler.scalaInstanceTask.value, crossVersion := (if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled), pluginCrossBuild / sbtBinaryVersion := binarySbtVersion( (pluginCrossBuild / sbtVersion).value @@ -1139,34 +1139,8 @@ object Defaults extends BuildCommon { } } - def scalaInstanceTask: Initialize[Task[ScalaInstance]] = Def.taskDyn { - // if this logic changes, ensure that `unmanagedScalaInstanceOnly` and `update` are changed - // appropriately to avoid cycles - scalaHome.value match { - case Some(h) => scalaInstanceFromHome(h) - case None => - val scalaProvider = appConfiguration.value.provider.scalaProvider - val version = scalaVersion.value - if (version == scalaProvider.version) // use the same class loader as the Scala classes used by sbt - Def.task { - val allJars = scalaProvider.jars - val libraryJars = allJars.filter(_.getName == "scala-library.jar") - allJars.filter(_.getName == "scala-compiler.jar") match { - case Array(compilerJar) if libraryJars.nonEmpty => - makeScalaInstance( - version, - libraryJars, - allJars, - Seq.empty, - state.value, - scalaInstanceTopLoader.value - ) - case _ => ScalaInstance(version, scalaProvider) - } - } else - scalaInstanceFromUpdate - } - } + @deprecated("Use Compiler.scalaInstanceTask", "1.12.0") + def scalaInstanceTask: Initialize[Task[ScalaInstance]] = Compiler.scalaInstanceTask // Returns the ScalaInstance only if it was not constructed via `update` // This is necessary to prevent cycles between `update` and `scalaInstance` @@ -1175,108 +1149,9 @@ object Defaults extends BuildCommon { if (scalaHome.value.isDefined) Def.task(Some(scalaInstance.value)) else Def.task(None) } - private[this] def noToolConfiguration(autoInstance: Boolean): String = { - val pre = "Missing Scala tool configuration from the 'update' report. " - val post = - if (autoInstance) - "'scala-tool' is normally added automatically, so this may indicate a bug in sbt or you may be removing it from ivyConfigurations, for example." - else - "Explicitly define scalaInstance or scalaHome or include Scala dependencies in the 'scala-tool' configuration." - pre + post - } + def scalaInstanceFromUpdate: Initialize[Task[ScalaInstance]] = + Compiler.scalaInstanceFromUpdate - def scalaInstanceFromUpdate: Initialize[Task[ScalaInstance]] = Def.task { - val sv = scalaVersion.value - val fullReport = update.value - val s = streams.value - - // For Scala 3, update scala-library.jar in `scala-tool` and `scala-doc-tool` in case a newer version - // is present in the `compile` configuration. This is needed once forwards binary compatibility is dropped - // to avoid NoSuchMethod exceptions when expanding macros. - def updateLibraryToCompileConfiguration(report: ConfigurationReport) = - if (!ScalaArtifacts.isScala3(sv)) report - else - (for { - compileConf <- fullReport.configuration(Configurations.Compile) - compileLibMod <- compileConf.modules.find(_.module.name == ScalaArtifacts.LibraryID) - reportLibMod <- report.modules.find(_.module.name == ScalaArtifacts.LibraryID) - if VersionNumber(reportLibMod.module.revision) - .matchesSemVer(SemanticSelector(s"<${compileLibMod.module.revision}")) - } yield { - val newMods = report.modules - .filterNot(_.module.name == ScalaArtifacts.LibraryID) :+ compileLibMod - report.withModules(newMods) - }).getOrElse(report) - - val toolReport = updateLibraryToCompileConfiguration( - fullReport - .configuration(Configurations.ScalaTool) - .getOrElse(sys.error(noToolConfiguration(managedScalaInstance.value))) - ) - - if (Classpaths.isScala213(sv)) { - val scalaDeps = for { - compileReport <- fullReport.configuration(Configurations.Compile).iterator - libName <- ScalaArtifacts.Artifacts.iterator - lib <- compileReport.modules.find(_.module.name == libName) - } yield lib - for (lib <- scalaDeps.take(1)) { - val libVer = lib.module.revision - val libName = lib.module.name - val proj = Def.displayBuildRelative(thisProjectRef.value.build, thisProjectRef.value) - if (VersionNumber(sv).matchesSemVer(SemanticSelector(s"<$libVer"))) { - val err = !allowUnsafeScalaLibUpgrade.value - val fix = - if (err) - """Upgrade the `scalaVersion` to fix the build. If upgrading the Scala compiler version is - |not possible (for example due to a regression in the compiler or a missing dependency), - |this error can be demoted by setting `allowUnsafeScalaLibUpgrade := true`.""".stripMargin - else - s"""Note that the dependency classpath and the runtime classpath of your project - |contain the newer $libName $libVer, even if the scalaVersion is $sv. - |Compilation (macro expansion) or using the Scala REPL in sbt may fail with a LinkageError.""".stripMargin - - val msg = - s"""Expected `$proj scalaVersion` to be $libVer or later, but found $sv. - |To support backwards-only binary compatibility (SIP-51), the Scala 2.13 compiler - |should not be older than $libName on the dependency classpath. - | - |$fix - | - |See `$proj evicted` to know why $libName $libVer is getting pulled in. - |""".stripMargin - if (err) sys.error(msg) - else s.log.warn(msg) - } - } - } - def file(id: String): File = { - val files = for { - m <- toolReport.modules if m.module.name.startsWith(id) - (art, file) <- m.artifacts if art.`type` == Artifact.DefaultType - } yield file - files.headOption getOrElse sys.error(s"Missing $id jar file") - } - - val allCompilerJars = toolReport.modules.flatMap(_.artifacts.map(_._2)) - val allDocJars = - fullReport - .configuration(Configurations.ScalaDocTool) - .map(updateLibraryToCompileConfiguration) - .toSeq - .flatMap(_.modules) - .flatMap(_.artifacts.map(_._2)) - val libraryJars = ScalaArtifacts.libraryIds(sv).map(file) - - makeScalaInstance( - sv, - libraryJars, - allCompilerJars, - allDocJars, - state.value, - scalaInstanceTopLoader.value, - ) - } def makeScalaInstance( version: String, libraryJars: Array[File], @@ -1284,46 +1159,11 @@ object Defaults extends BuildCommon { allDocJars: Seq[File], state: State, topLoader: ClassLoader, - ): ScalaInstance = { - val classLoaderCache = state.extendedClassLoaderCache - val compilerJars = allCompilerJars.filterNot(libraryJars.contains).distinct.toArray - val docJars = allDocJars - .filterNot(jar => libraryJars.contains(jar) || compilerJars.contains(jar)) - .distinct - .toArray - val allJars = libraryJars ++ compilerJars ++ docJars + ): ScalaInstance = + Compiler.makeScalaInstance(version, libraryJars, allCompilerJars, allDocJars, state, topLoader) - val libraryLoader = classLoaderCache(libraryJars.toList, topLoader) - val compilerLoader = classLoaderCache(compilerJars.toList, libraryLoader) - val fullLoader = - if (docJars.isEmpty) compilerLoader - else classLoaderCache(docJars.distinct.toList, compilerLoader) - new ScalaInstance( - version = version, - loader = fullLoader, - loaderCompilerOnly = compilerLoader, - loaderLibraryOnly = libraryLoader, - libraryJars = libraryJars, - compilerJars = compilerJars, - allJars = allJars, - explicitActual = Some(version) - ) - } - def scalaInstanceFromHome(dir: File): Initialize[Task[ScalaInstance]] = Def.task { - val dummy = ScalaInstance(dir)(state.value.classLoaderCache.apply) - Seq(dummy.loader, dummy.loaderLibraryOnly).foreach { - case a: AutoCloseable => a.close() - case _ => - } - makeScalaInstance( - dummy.version, - dummy.libraryJars, - dummy.compilerJars, - dummy.allJars, - state.value, - scalaInstanceTopLoader.value, - ) - } + def scalaInstanceFromHome(dir: File): Initialize[Task[ScalaInstance]] = + Compiler.scalaInstanceFromHome(dir) private[this] def testDefaults = Defaults.globalDefaults( diff --git a/main/src/main/scala/sbt/internal/Compiler.scala b/main/src/main/scala/sbt/internal/Compiler.scala new file mode 100644 index 000000000..d52b33db5 --- /dev/null +++ b/main/src/main/scala/sbt/internal/Compiler.scala @@ -0,0 +1,234 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal + +import java.io.File +import sbt.internal.inc.ScalaInstance +import sbt.librarymanagement.{ + Artifact, + Configurations, + ConfigurationReport, + ScalaArtifacts, + SemanticSelector, + VersionNumber +} +import xsbti.ScalaProvider + +private[sbt] object Compiler { + def scalaInstanceTask: Def.Initialize[Task[ScalaInstance]] = + Def.taskDyn { + val sh = Keys.scalaHome.value + val app = Keys.appConfiguration.value + val managed = Keys.managedScalaInstance.value + val sv = Keys.scalaVersion.value + // if this logic changes, ensure that `unmanagedScalaInstanceOnly` and `update` are changed + // appropriately to avoid cycles + sh match { + case Some(h) => scalaInstanceFromHome(h) + case _ => + val scalaProvider = app.provider.scalaProvider + if (!managed) emptyScalaInstance + else if (sv == scalaProvider.version) optimizedScalaInstance(sv, scalaProvider) + else scalaInstanceFromUpdate + } + } + + /** + * A dummy ScalaInstance for Java-only projects. + */ + def emptyScalaInstance: Def.Initialize[Task[ScalaInstance]] = Def.task { + makeScalaInstance( + "0.0.0", + Array.empty, + Seq.empty, + Seq.empty, + Keys.state.value, + Keys.scalaInstanceTopLoader.value, + ) + } + + // Use the same class loader as the Scala classes used by sbt + def optimizedScalaInstance( + sv: String, + scalaProvider: ScalaProvider + ): Def.Initialize[Task[ScalaInstance]] = Def.task { + val allJars = scalaProvider.jars + val libraryJars = allJars.filter { jar => + jar.getName == "scala-library.jar" || jar.getName.startsWith("scala3-library_3") + } + val compilerJar = allJars.filter { jar => + jar.getName == "scala-compiler.jar" || jar.getName.startsWith("scala3-compiler_3") + } + compilerJar match { + case Array(compilerJar) if libraryJars.nonEmpty => + makeScalaInstance( + sv, + libraryJars, + allJars.toSeq, + Seq.empty, + Keys.state.value, + Keys.scalaInstanceTopLoader.value, + ) + case _ => ScalaInstance(sv, scalaProvider) + } + } + + def scalaInstanceFromHome(dir: File): Def.Initialize[Task[ScalaInstance]] = Def.task { + val dummy = ScalaInstance(dir)(Keys.state.value.classLoaderCache.apply) + Seq(dummy.loader, dummy.loaderLibraryOnly).foreach { + case a: AutoCloseable => a.close() + case _ => + } + makeScalaInstance( + dummy.version, + dummy.libraryJars, + dummy.compilerJars.toSeq, + dummy.allJars.toSeq, + Keys.state.value, + Keys.scalaInstanceTopLoader.value, + ) + } + + def scalaInstanceFromUpdate: Def.Initialize[Task[ScalaInstance]] = Def.task { + val sv = Keys.scalaVersion.value + val fullReport = Keys.update.value + val s = Keys.streams.value + + // For Scala 3, update scala-library.jar in `scala-tool` and `scala-doc-tool` in case a newer version + // is present in the `compile` configuration. This is needed once forwards binary compatibility is dropped + // to avoid NoSuchMethod exceptions when expanding macros. + def updateLibraryToCompileConfiguration(report: ConfigurationReport) = + if (!ScalaArtifacts.isScala3(sv)) report + else + (for { + compileConf <- fullReport.configuration(Configurations.Compile) + compileLibMod <- compileConf.modules.find(_.module.name == ScalaArtifacts.LibraryID) + reportLibMod <- report.modules.find(_.module.name == ScalaArtifacts.LibraryID) + if VersionNumber(reportLibMod.module.revision) + .matchesSemVer(SemanticSelector(s"<${compileLibMod.module.revision}")) + } yield { + val newMods = report.modules + .filterNot(_.module.name == ScalaArtifacts.LibraryID) :+ compileLibMod + report.withModules(newMods) + }).getOrElse(report) + + val toolReport = updateLibraryToCompileConfiguration( + fullReport + .configuration(Configurations.ScalaTool) + .getOrElse(sys.error(noToolConfiguration(Keys.managedScalaInstance.value))) + ) + + if (Classpaths.isScala213(sv)) { + val scalaDeps = for { + compileReport <- fullReport.configuration(Configurations.Compile).iterator + libName <- ScalaArtifacts.Artifacts.iterator + lib <- compileReport.modules.find(_.module.name == libName) + } yield lib + for (lib <- scalaDeps.take(1)) { + val libVer = lib.module.revision + val libName = lib.module.name + val proj = + Def.displayBuildRelative(Keys.thisProjectRef.value.build, Keys.thisProjectRef.value) + if (VersionNumber(sv).matchesSemVer(SemanticSelector(s"<$libVer"))) { + val err = !Keys.allowUnsafeScalaLibUpgrade.value + val fix = + if (err) + """Upgrade the `scalaVersion` to fix the build. If upgrading the Scala compiler version is + |not possible (for example due to a regression in the compiler or a missing dependency), + |this error can be demoted by setting `allowUnsafeScalaLibUpgrade := true`.""".stripMargin + else + s"""Note that the dependency classpath and the runtime classpath of your project + |contain the newer $libName $libVer, even if the scalaVersion is $sv. + |Compilation (macro expansion) or using the Scala REPL in sbt may fail with a LinkageError.""".stripMargin + + val msg = + s"""Expected `$proj scalaVersion` to be $libVer or later, but found $sv. + |To support backwards-only binary compatibility (SIP-51), the Scala 2.13 compiler + |should not be older than $libName on the dependency classpath. + | + |$fix + | + |See `$proj evicted` to know why $libName $libVer is getting pulled in. + |""".stripMargin + if (err) sys.error(msg) + else s.log.warn(msg) + } + } + } + def file(id: String): File = { + val files = for { + m <- toolReport.modules if m.module.name.startsWith(id) + (art, file) <- m.artifacts if art.`type` == Artifact.DefaultType + } yield file + files.headOption getOrElse sys.error(s"Missing $id jar file") + } + + val allCompilerJars = toolReport.modules.flatMap(_.artifacts.map(_._2)) + val allDocJars = + fullReport + .configuration(Configurations.ScalaDocTool) + .map(updateLibraryToCompileConfiguration) + .toSeq + .flatMap(_.modules) + .flatMap(_.artifacts.map(_._2)) + val libraryJars = ScalaArtifacts.libraryIds(sv).map(file) + + makeScalaInstance( + sv, + libraryJars, + allCompilerJars, + allDocJars, + Keys.state.value, + Keys.scalaInstanceTopLoader.value, + ) + } + + def makeScalaInstance( + version: String, + libraryJars: Array[File], + allCompilerJars: Seq[File], + allDocJars: Seq[File], + state: State, + topLoader: ClassLoader, + ): ScalaInstance = { + val classLoaderCache = state.extendedClassLoaderCache + val compilerJars = allCompilerJars.filterNot(libraryJars.contains).distinct.toArray + val docJars = allDocJars + .filterNot(jar => libraryJars.contains(jar) || compilerJars.contains(jar)) + .distinct + .toArray + val allJars = libraryJars ++ compilerJars ++ docJars + + val libraryLoader = classLoaderCache(libraryJars.toList, topLoader) + val compilerLoader = classLoaderCache(compilerJars.toList, libraryLoader) + val fullLoader = + if (docJars.isEmpty) compilerLoader + else classLoaderCache(docJars.distinct.toList, compilerLoader) + new ScalaInstance( + version = version, + loader = fullLoader, + loaderCompilerOnly = compilerLoader, + loaderLibraryOnly = libraryLoader, + libraryJars = libraryJars, + compilerJars = compilerJars, + allJars = allJars, + explicitActual = Some(version) + ) + } + + private def noToolConfiguration(autoInstance: Boolean): String = { + val pre = "Missing Scala tool configuration from the 'update' report. " + val post = + if (autoInstance) + "'scala-tool' is normally added automatically, so this may indicate a bug in sbt or you may be removing it from ivyConfigurations, for example." + else + "Explicitly define scalaInstance or scalaHome or include Scala dependencies in the 'scala-tool' configuration." + pre + post + } +}