diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index facf6637a..d7a1f6a4b 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -679,6 +679,15 @@ object Defaults extends BuildCommon { else topLoader }, scalaInstance := scalaInstanceTask.value, + runtimeScalaInstance := Def.taskDyn { + scalaOutputVersion.?.value + .map { version => + scalaInstanceTask0(version = version, isRuntimeInstance = true) + } + .getOrElse { + scalaInstance + } + }.value, crossVersion := (if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled), pluginCrossBuild / sbtBinaryVersion := binarySbtVersion( (pluginCrossBuild / sbtVersion).value @@ -977,6 +986,49 @@ object Defaults extends BuildCommon { old ++ Seq("-Wconf:cat=unused-nowarn:s", "-Xsource:3") else old }, + scalacOptions := { + val old = scalacOptions.value + + // 3 possible sources of Scala output version + val fromCompiler = CrossVersion + .partialVersion(scalaVersion.value) + .map { case (major, minor) => s"${major}.${minor}" } + .getOrElse( + sys.error(s"Wrong value of `scalaVersion`: ${scalaVersion.value}") + ) + + val maybeFromSetting = scalaOutputVersion.?.value.map { version => + CrossVersion + .partialVersion(version) + .map { case (major, minor) => s"${major}.${minor}" } + .getOrElse( + sys.error(s"Wrong value of `scalaOutputVersion`: ${version}") + ) + } + + val maybeFromFlags = old.zipWithIndex.flatMap { + case (opt, idx) => + if (opt.startsWith("-scala-output-version:")) + Some(opt.stripPrefix("-scala-output-version:")) + else if (opt == "-scala-output-version" && idx + 1 < old.length) + Some(old(idx + 1)) + else None + }.lastOption + + // Add -scala-output-version flag when minor Scala versions are different + // unless the flag is already set properly + maybeFromSetting match { + case Some(fromSetting) if fromSetting != fromCompiler => + maybeFromFlags match { + case Some(fromFlags) if fromFlags == fromSetting => + old + case _ => + old ++ Seq("-scala-output-version", fromSetting) + } + case _ => + old + } + }, persistJarClasspath :== true, classpathEntryDefinesClassVF := { (if (persistJarClasspath.value) classpathDefinesClassCache.value @@ -1087,13 +1139,19 @@ object Defaults extends BuildCommon { } def scalaInstanceTask: Initialize[Task[ScalaInstance]] = Def.taskDyn { + scalaInstanceTask0(scalaVersion.value, false) + } + + private def scalaInstanceTask0( + version: String, + isRuntimeInstance: Boolean + ): 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 @@ -1111,7 +1169,7 @@ object Defaults extends BuildCommon { case _ => ScalaInstance(version, scalaProvider) } } else - scalaInstanceFromUpdate + scalaInstanceFromUpdate0(isRuntimeInstance) } } @@ -1132,22 +1190,29 @@ object Defaults extends BuildCommon { pre + post } - def scalaInstanceFromUpdate: Initialize[Task[ScalaInstance]] = Def.task { + def scalaInstanceFromUpdate: Initialize[Task[ScalaInstance]] = scalaInstanceFromUpdate0(false) + + private def scalaInstanceFromUpdate0( + isRuntimeInstance: Boolean + ): Initialize[Task[ScalaInstance]] = Def.task { val sv = scalaVersion.value val fullReport = update.value val toolReport = fullReport .configuration(Configurations.ScalaTool) .getOrElse(sys.error(noToolConfiguration(managedScalaInstance.value))) + lazy val runtimeReport = fullReport.configuration(Configurations.Runtime).get + val libraryReport = if (isRuntimeInstance) runtimeReport else toolReport - def file(id: String): File = { + def libraryFile(id: String): File = { val files = for { - m <- toolReport.modules if m.module.name.startsWith(id) + m <- libraryReport.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 libraryJars = ScalaArtifacts.libraryIds(sv).map(libraryFile) val allCompilerJars = toolReport.modules.flatMap(_.artifacts.map(_._2)) val allDocJars = fullReport @@ -1155,7 +1220,6 @@ object Defaults extends BuildCommon { .toSeq .flatMap(_.modules) .flatMap(_.artifacts.map(_._2)) - val libraryJars = ScalaArtifacts.libraryIds(sv).map(file) makeScalaInstance( sv, @@ -2034,7 +2098,7 @@ object Defaults extends BuildCommon { def runnerInit: Initialize[Task[ScalaRun]] = Def.task { val tmp = taskTemporaryDirectory.value val resolvedScope = resolvedScoped.value.scope - val si = scalaInstance.value + val si = runtimeScalaInstance.value val s = streams.value val opts = forkOptions.value val options = javaOptions.value @@ -3255,7 +3319,7 @@ object Classpaths { autoScalaLibrary.value && scalaHome.value.isEmpty && managedScalaInstance.value, sbtPlugin.value, scalaOrganization.value, - scalaVersion.value + scalaOutputVersion.?.value.getOrElse(scalaVersion.value) ), // Override the default to handle mixing in the sbtPlugin + scala dependencies. allDependencies := { diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 2270ee573..a4fee0b85 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -186,9 +186,11 @@ object Keys { val compileOptions = taskKey[CompileOptions]("Collects basic options to configure compilers").withRank(DTask) val compileInputs = taskKey[Inputs]("Collects all inputs needed for compilation.").withRank(DTask) val scalaHome = settingKey[Option[File]]("If Some, defines the local Scala installation to use for compilation, running, and testing.").withRank(ASetting) - val scalaInstance = taskKey[ScalaInstance]("Defines the Scala instance to use for compilation, running, and testing.").withRank(DTask) + val scalaInstance = taskKey[ScalaInstance]("Defines the Scala instance to use for compilation").withRank(DTask) + val runtimeScalaInstance = taskKey[ScalaInstance]("Defines the Scala instance used at runtime (also for tests).").withRank(DTask) val scalaOrganization = settingKey[String]("Organization/group ID of the Scala used in the project. Default value is 'org.scala-lang'. This is an advanced setting used for clones of the Scala Language. It should be disregarded in standard use cases.").withRank(CSetting) - val scalaVersion = settingKey[String]("The version of Scala used for building.").withRank(APlusSetting) + val scalaVersion = settingKey[String]("The version of Scala compiler used for building this project.").withRank(APlusSetting) + val scalaOutputVersion = settingKey[String]("The version of Scala standard library used for running this project and declared as its transitive dependency.").withRank(APlusSetting) val scalaBinaryVersion = settingKey[String]("The Scala version substring describing binary compatibility.").withRank(BPlusSetting) val crossScalaVersions = settingKey[Seq[String]]("The versions of Scala used when cross-building.").withRank(BPlusSetting) val crossVersion = settingKey[CrossVersion]("Configures handling of the Scala version when cross-building.").withRank(CSetting) diff --git a/main/src/main/scala/sbt/internal/ClassLoaders.scala b/main/src/main/scala/sbt/internal/ClassLoaders.scala index 1c5df8aa0..0275ae9dd 100644 --- a/main/src/main/scala/sbt/internal/ClassLoaders.scala +++ b/main/src/main/scala/sbt/internal/ClassLoaders.scala @@ -35,7 +35,7 @@ private[sbt] object ClassLoaders { * Get the class loader for a test task. The configuration could be IntegrationTest or Test. */ private[sbt] def testTask: Def.Initialize[Task[ClassLoader]] = Def.task { - val si = scalaInstance.value + val si = runtimeScalaInstance.value val cp = fullClasspath.value.map(_.data) val dependencyStamps = modifiedTimes((dependencyClasspathFiles / outputFileStamps).value).toMap def getLm(f: File): Long = dependencyStamps.getOrElse(f, IO.getModifiedTimeOrZero(f)) @@ -64,7 +64,7 @@ private[sbt] object ClassLoaders { private[sbt] def runner: Def.Initialize[Task[ScalaRun]] = Def.taskDyn { val resolvedScope = resolvedScoped.value.scope - val instance = scalaInstance.value + val instance = runtimeScalaInstance.value val s = streams.value val opts = forkOptions.value val options = javaOptions.value diff --git a/sbt-app/src/sbt-test/dependency-management/scala-output-version-redundant-flags/build.sbt b/sbt-app/src/sbt-test/dependency-management/scala-output-version-redundant-flags/build.sbt new file mode 100644 index 000000000..e1bc21ae3 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/scala-output-version-redundant-flags/build.sbt @@ -0,0 +1,30 @@ +val checkOptions = taskKey[Unit]("") + +lazy val p1 = project + .settings( + scalaVersion := "3.0.2", + checkOptions := { + assert((Compile / scalacOptions).value == Seq()) + assert((Test / scalacOptions).value == Seq()) + } + ) + +lazy val p2 = project + .settings( + scalaVersion := "3.0.2", + scalaOutputVersion := "3.0.2", + checkOptions := { + assert((Compile / scalacOptions).value == Seq()) + assert((Test / scalacOptions).value == Seq()) + } + ) + +lazy val p3 = project + .settings( + scalaVersion := "3.1.2-RC2", + scalaOutputVersion := "3.0.2", + checkOptions := { + assert((Compile / scalacOptions).value == Seq("-scala-output-version", "3.0")) + assert((Test / scalacOptions).value == Seq("-scala-output-version", "3.0")) + } + ) diff --git a/sbt-app/src/sbt-test/dependency-management/scala-output-version-redundant-flags/test b/sbt-app/src/sbt-test/dependency-management/scala-output-version-redundant-flags/test new file mode 100644 index 000000000..500721a31 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/scala-output-version-redundant-flags/test @@ -0,0 +1,3 @@ +> p1 / checkOptions +> p2 / checkOptions +> p3 / checkOptions \ No newline at end of file diff --git a/sbt-app/src/sbt-test/dependency-management/scala-output-version/Bar.scala b/sbt-app/src/sbt-test/dependency-management/scala-output-version/Bar.scala new file mode 100644 index 000000000..772478e95 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/scala-output-version/Bar.scala @@ -0,0 +1,5 @@ +object Bar { + def main(args: Array[String]) = { + assert(foo.main.Foo.numbers == Seq(1, 2, 3)) + } +} diff --git a/sbt-app/src/sbt-test/dependency-management/scala-output-version/build.sbt b/sbt-app/src/sbt-test/dependency-management/scala-output-version/build.sbt new file mode 100644 index 000000000..78ad544f4 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/scala-output-version/build.sbt @@ -0,0 +1,112 @@ +// cases 1, 2, 3: check for scala version in bar +// case a: check locally published Ivy dependency +// case b: check locally published Maven dependency +// case c: check unpublished sibling module dependency + +val org = "org.example" +val fooName = "sbt-test-scala-output-version-foo" +val revision = "0.0.1-SNAPSHOT" + +ThisBuild / organization := org +ThisBuild / version := revision + +lazy val foo = project.in(file("foo")) + .settings( + name := fooName, + scalaVersion := "3.1.2-RC2", + crossScalaVersions := List("2.13.8", "3.1.2-RC2"), + scalaOutputVersion := "3.0.2", + scalaOutputVersion := { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => "3.0.2" + case _ => scalaVersion.value + } + }, + libraryDependencies ++= Seq( + "org.scalameta" %% "munit" % "0.7.29" % Test + ), + TaskKey[Unit]("checkIvy") := { + val ivyFile = makeIvyXml.value + val ivyContent = IO.read(ivyFile) + val expectedContent = """""" + val hasCorrectStdlib = ivyContent.contains(expectedContent) + if (!hasCorrectStdlib) sys.error(s"The produced Ivy file is incorrect:\n\n${ivyContent}") + }, + TaskKey[Unit]("checkPom") := { + val pomFile = makePom.value + val pomContent = IO.read(pomFile) + val flatPom = pomContent.filterNot(_.isWhitespace) + val expectedContent = "org.scala-langscala3-library_33.0.2" + val hasCorrectStdlib = flatPom.contains(expectedContent) + if (!hasCorrectStdlib) sys.error(s"The produced POM file is incorrect:\n\n${pomContent}") + } + ) + +val scala3_1 = Seq(scalaVersion := "3.1.1") +val scala3_0 = Seq(scalaVersion := "3.0.2") +val scala2_13 = Seq(scalaVersion := "2.13.8") +val ivyFooDep = Seq( + libraryDependencies ++= Seq( + org %% fooName % revision + ), + resolvers := Seq(Resolver.defaultLocal) +) +val mavenFooDep = Seq( + libraryDependencies ++= Seq( + org %% fooName % revision + ), + resolvers := Seq(Resolver.mavenLocal) +) + +lazy val bar1a = project.in(file("bar1a")) + .settings( + scala3_1, + ivyFooDep + ) + +lazy val bar1b = project.in(file("bar1b")) + .settings( + scala3_1, + mavenFooDep + ) + +lazy val bar1c = project.in(file("bar1c")) + .settings( + scala3_1, + ).dependsOn(foo) + + +lazy val bar2a = project.in(file("bar2a")) + .settings( + scala3_0, + ivyFooDep + ) + +lazy val bar2b = project.in(file("bar2b")) + .settings( + scala3_0, + mavenFooDep + ) + +lazy val bar2c = project.in(file("bar2c")) + .settings( + scala3_0, + ).dependsOn(foo) + + +lazy val bar3a = project.in(file("bar3a")) + .settings( + scala2_13, + ivyFooDep + ) + +lazy val bar3b = project.in(file("bar3b")) + .settings( + scala2_13, + mavenFooDep + ) + +lazy val bar3c = project.in(file("bar3c")) + .settings( + scala2_13, + ).dependsOn(foo) diff --git a/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/main/scala-2.13/Foo.scala b/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/main/scala-2.13/Foo.scala new file mode 100644 index 000000000..6347cd083 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/main/scala-2.13/Foo.scala @@ -0,0 +1,11 @@ +package foo.main + +object Foo { + val numbers = Seq(1, 2, 3) +} + +object Run { + def main(args: Array[String]) = { + assert(Foo.numbers.length == 3) + } +} diff --git a/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/main/scala-3/Foo.scala b/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/main/scala-3/Foo.scala new file mode 100644 index 000000000..11ec280a3 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/main/scala-3/Foo.scala @@ -0,0 +1,17 @@ +package foo.main + +class MyException extends Exception("MyException") + +@annotation.experimental +object Exceptional: + import language.experimental.saferExceptions + def foo(): Unit throws MyException = // this requires at least 3.1.x to compile + throw new MyException + +object Foo: + val numbers = Seq(1, 2, 3) + +@main def run() = + val canEqualMethods = classOf[CanEqual.type].getMethods.toList + assert( canEqualMethods.exists(_.getName == "canEqualSeq")) // since 3.0.x + assert(!canEqualMethods.exists(_.getName == "canEqualSeqs")) // since 3.1.x diff --git a/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/test/scala-2.13/FooTest.scala b/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/test/scala-2.13/FooTest.scala new file mode 100644 index 000000000..4102861ae --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/test/scala-2.13/FooTest.scala @@ -0,0 +1,7 @@ +package foo.test + +class FooTest extends munit.FunSuite { + test("foo") { + assertEquals(foo.main.Foo.numbers, Seq(1, 2, 3)) + } +} diff --git a/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/test/scala-3/FooTest.scala b/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/test/scala-3/FooTest.scala new file mode 100644 index 000000000..cc0ee76b6 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/scala-output-version/foo/src/test/scala-3/FooTest.scala @@ -0,0 +1,19 @@ +package foo.test + +class MyException extends Exception("MyException") + +@annotation.experimental +object Exceptional: + import language.experimental.saferExceptions + def foo(): Unit throws MyException = // this requires at least 3.1.x to compile + throw new MyException + + +class FooTest extends munit.FunSuite: + test("foo") { + assertEquals(foo.main.Foo.numbers, Seq(1, 2, 3)) + + val canEqualMethods = classOf[CanEqual.type].getMethods.toList + assert( canEqualMethods.exists(_.getName == "canEqualSeq")) // since 3.0.x + assert(!canEqualMethods.exists(_.getName == "canEqualSeqs")) // since 3.1.x + } diff --git a/sbt-app/src/sbt-test/dependency-management/scala-output-version/test b/sbt-app/src/sbt-test/dependency-management/scala-output-version/test new file mode 100644 index 000000000..ee66638fb --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/scala-output-version/test @@ -0,0 +1,35 @@ +$ copy-file Bar.scala bar1a/src/main/scala/Bar.scala +$ copy-file Bar.scala bar1b/src/main/scala/Bar.scala +$ copy-file Bar.scala bar1c/src/main/scala/Bar.scala +$ copy-file Bar.scala bar2a/src/main/scala/Bar.scala +$ copy-file Bar.scala bar2b/src/main/scala/Bar.scala +$ copy-file Bar.scala bar2c/src/main/scala/Bar.scala +$ copy-file Bar.scala bar3a/src/main/scala/Bar.scala +$ copy-file Bar.scala bar3b/src/main/scala/Bar.scala +$ copy-file Bar.scala bar3c/src/main/scala/Bar.scala + +> ++3.1.2-RC2 +> foo / compile +> foo / run +> foo / test +> foo / publishLocal +> foo / checkIvy +> foo / publishM2 +> foo / checkPom + +> ++2.13.8 +> foo / compile +> foo / run +> foo / test +> foo / publishLocal +> foo / publishM2 + +> bar1a / run +> bar1b / run +> bar1c / run +> bar2a / run +> bar2b / run +> bar2c / run +> bar3a / run +> bar3b / run +> bar3c / run