diff --git a/main/src/main/scala/sbt/internal/ClassLoaders.scala b/main/src/main/scala/sbt/internal/ClassLoaders.scala index 047cd27e9..fcd315834 100644 --- a/main/src/main/scala/sbt/internal/ClassLoaders.scala +++ b/main/src/main/scala/sbt/internal/ClassLoaders.scala @@ -9,7 +9,7 @@ package sbt package internal -import java.io.File +import java.io.{ IOException, File } import java.net.URL import java.nio.file.Path import sbt.ClassLoaderLayeringStrategy.* @@ -29,8 +29,22 @@ import xsbti.ArtifactInfo import xsbti.HashedVirtualFileRef private[sbt] object ClassLoaders { + + /** + * https://github.com/sbt/sbt/issues/9468 + * Resolves symlinks before converting to a URL. Classpath entries backed by a + * CAS cache would otherwise all share one URL across builds; + * the JVM's jar/URL connection caching is keyed by path, so once a jar: URL for that + * stable path has been opened, later reads can keep returning the first content seen\ + * even after the symlink target (and the underlying file content) changes. + */ + private[sbt] def toRealURL(f: File): URL = + val real = try f.toPath.toRealPath().toFile + catch case _: IOException => f + real.toURI.toURL + extension (files: Seq[File]) { - def urls: Array[URL] = files.toArray.map(_.toURI.toURL) + def urls: Array[URL] = files.toArray.map(toRealURL) } private val interfaceLoader = classOf[sbt.testing.Framework].getClassLoader /* diff --git a/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala b/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala index 4d60b1b95..ee208090e 100644 --- a/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala +++ b/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala @@ -45,10 +45,10 @@ private[internal] final class ReverseLookupClassLoaderHolder( val closeThis: Boolean, val allowZombies: Boolean, val logger: Logger -) extends URLClassLoader(Array.empty, null) { +) extends URLClassLoader(Array.empty, null): private val cached: AtomicReference[ReverseLookupClassLoader] = new AtomicReference private val closed = new AtomicBoolean(false) - private val urls = classpath.map(_.toURI.toURL).toArray + private val urls = classpath.map(ClassLoaders.toRealURL).toArray /** * Get a classloader. If there is a loader available in the cache, it will use that loader, @@ -69,7 +69,7 @@ private[internal] final class ReverseLookupClassLoaderHolder( reverseLookupClassLoader.setup(tempDir) new BottomClassLoader( ReverseLookupClassLoaderHolder.this, - fullClasspath.map(_.toURI.toURL).toArray, + fullClasspath.map(ClassLoaders.toRealURL).toArray, reverseLookupClassLoader, tempDir, closeThis, @@ -97,7 +97,7 @@ private[internal] final class ReverseLookupClassLoaderHolder( case c => c.close() } } -} +end ReverseLookupClassLoaderHolder /** * This is more or less copied from the NativeCopyLoader in zinc. It differs from the zinc diff --git a/sbt-app/src/sbt-test/compiler-project/run-test/build.sbt b/sbt-app/src/sbt-test/compiler-project/run-test/build.sbt index 0dbb960ab..268c5bc77 100644 --- a/sbt-app/src/sbt-test/compiler-project/run-test/build.sbt +++ b/sbt-app/src/sbt-test/compiler-project/run-test/build.sbt @@ -6,3 +6,5 @@ libraryDependencies ++= Seq( "commons-io" % "commons-io" % "2.5" % Runtime, ) libraryDependencies += scalaVersion("org.scala-lang" % "scala-compiler" % _ ).value + +Test / fork := true diff --git a/sbt-app/src/sbt-test/tests/test-resource/build.sbt b/sbt-app/src/sbt-test/tests/test-resource/build.sbt new file mode 100644 index 000000000..c22c55b5d --- /dev/null +++ b/sbt-app/src/sbt-test/tests/test-resource/build.sbt @@ -0,0 +1,7 @@ +val munit = "org.scalameta" %% "munit" % "1.0.4" +scalaVersion := "3.8.4" + +lazy val root = rootProject + .settings( + libraryDependencies += munit % Test + ) diff --git a/sbt-app/src/sbt-test/tests/test-resource/changes/bad.txt b/sbt-app/src/sbt-test/tests/test-resource/changes/bad.txt new file mode 100644 index 000000000..67be85f12 --- /dev/null +++ b/sbt-app/src/sbt-test/tests/test-resource/changes/bad.txt @@ -0,0 +1 @@ +bad diff --git a/sbt-app/src/sbt-test/tests/test-resource/src/test/resources/hello.txt b/sbt-app/src/sbt-test/tests/test-resource/src/test/resources/hello.txt new file mode 100644 index 000000000..ce0136250 --- /dev/null +++ b/sbt-app/src/sbt-test/tests/test-resource/src/test/resources/hello.txt @@ -0,0 +1 @@ +hello diff --git a/sbt-app/src/sbt-test/tests/test-resource/src/test/scala/ATest.scala b/sbt-app/src/sbt-test/tests/test-resource/src/test/scala/ATest.scala new file mode 100644 index 000000000..297752714 --- /dev/null +++ b/sbt-app/src/sbt-test/tests/test-resource/src/test/scala/ATest.scala @@ -0,0 +1,13 @@ +package testpkg + +import munit.* +import scala.io.Source +import scala.util.Using + +class ATest extends FunSuite: + test("hello.txt should contain hello") { + Using.resource(Source.fromResource("hello.txt")): r => + val actual = r.mkString.trim + assert(actual == "hello", s"actual: $actual") + } +end ATest diff --git a/sbt-app/src/sbt-test/tests/test-resource/test b/sbt-app/src/sbt-test/tests/test-resource/test new file mode 100644 index 000000000..634630393 --- /dev/null +++ b/sbt-app/src/sbt-test/tests/test-resource/test @@ -0,0 +1,5 @@ +> test + +$ copy-file changes/bad.txt src/test/resources/hello.txt + +-> test