Skip to content

Commit ce2b4d0

Browse files
committed
Don't serve stale JAR metadata from cache in classpath implementation
Adapted from scalac commit bcf47b165ccfd8e1827188f70aeb24e2cecfb80f by Jason Zaugg in scala/scala#6064: Use the last modified timestamp and the file inode to help detect when the file has been overwritten (as is common in SBT builds with `exportJars := true`, or when using snapshot dependencies). Fixes scala/bug#10295
1 parent 481ad48 commit ce2b4d0

File tree

2 files changed

+101
-10
lines changed

2 files changed

+101
-10
lines changed

compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ package dotty.tools.dotc.classpath
55

66
import java.io.File
77
import java.net.URL
8+
import java.nio.file.Files
9+
import java.nio.file.attribute.{BasicFileAttributes, FileTime}
10+
811
import scala.annotation.tailrec
912
import dotty.tools.io.{AbstractFile, ClassPath, ClassRepresentation, FileZipArchive, ManifestResources}
1013
import dotty.tools.dotc.core.Contexts.Context
@@ -19,23 +22,17 @@ import FileUtils._
1922
* when there are a lot of projects having a lot of common dependencies.
2023
*/
2124
sealed trait ZipAndJarFileLookupFactory {
22-
private val cache = collection.mutable.Map.empty[AbstractFile, ClassPath]
25+
private val cache = new FileBasedCache[ClassPath]
2326

2427
def create(zipFile: AbstractFile)(implicit ctx: Context): ClassPath = {
25-
if (ctx.settings.YdisableFlatCpCaching.value) createForZipFile(zipFile)
28+
if (ctx.settings.YdisableFlatCpCaching.value || zipFile.file == null) createForZipFile(zipFile)
2629
else createUsingCache(zipFile)
2730
}
2831

2932
protected def createForZipFile(zipFile: AbstractFile): ClassPath
3033

31-
private def createUsingCache(zipFile: AbstractFile)(implicit ctx: Context): ClassPath = cache.synchronized {
32-
def newClassPathInstance = {
33-
if (ctx.settings.verbose.value || ctx.settings.Ylogcp.value)
34-
println(s"$zipFile is not yet in the classpath cache")
35-
createForZipFile(zipFile)
36-
}
37-
cache.getOrElseUpdate(zipFile, newClassPathInstance)
38-
}
34+
private def createUsingCache(zipFile: AbstractFile): ClassPath =
35+
cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile))
3936
}
4037

4138
/**
@@ -179,3 +176,29 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
179176

180177
override protected def createForZipFile(zipFile: AbstractFile): ClassPath = ZipArchiveSourcePath(zipFile.file)
181178
}
179+
180+
final class FileBasedCache[T] {
181+
private case class Stamp(lastModified: FileTime, fileKey: Object)
182+
private val cache = collection.mutable.Map.empty[java.nio.file.Path, (Stamp, T)]
183+
184+
def getOrCreate(path: java.nio.file.Path, create: () => T): T = cache.synchronized {
185+
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
186+
val lastModified = attrs.lastModifiedTime()
187+
// only null on some platforms, but that's okay, we just use the last modified timestamp as our stamp
188+
val fileKey = attrs.fileKey()
189+
val stamp = Stamp(lastModified, fileKey)
190+
cache.get(path) match {
191+
case Some((cachedStamp, cached)) if cachedStamp == stamp => cached
192+
case _ =>
193+
val value = create()
194+
cache.put(path, (stamp, value))
195+
value
196+
}
197+
}
198+
199+
def clear(): Unit = cache.synchronized {
200+
// TODO support closing
201+
// cache.valuesIterator.foreach(_.close())
202+
cache.clear()
203+
}
204+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package dotty.tools.dotc.classpath
2+
3+
import org.junit.Test
4+
import java.nio.file._
5+
import java.nio.file.attribute.FileTime
6+
7+
import dotty.tools.dotc.core.Contexts.{Context, ContextBase}
8+
import dotty.tools.io.AbstractFile
9+
10+
11+
12+
class ZipAndJarFileLookupFactoryTest {
13+
@Test def cacheInvalidation(): Unit = {
14+
val f = Files.createTempFile("test-", ".jar")
15+
Files.delete(f)
16+
17+
implicit val ctx: Context = new ContextBase().initialCtx
18+
assert(!ctx.settings.YdisableFlatCpCaching.value) // we're testing with our JAR metadata caching enabled.
19+
20+
def createCp = ZipAndJarClassPathFactory.create(AbstractFile.getFile(f.toFile))
21+
try {
22+
createZip(f, Array(), "p1/C.class")
23+
createZip(f, Array(), "p2/X.class")
24+
createZip(f, Array(), "p3/Y.class")
25+
val cp1 = createCp
26+
assert(cp1.findClass("p1.C").isDefined)
27+
28+
// We expect get a cache hit as the underlying zip hasn't changed
29+
val cp2 = createCp
30+
assert(cp2 eq cp1)
31+
32+
// check things work after the cache hit
33+
cp1.findClassFile("p2.X").get.toByteArray
34+
35+
val lastMod1 = Files.getLastModifiedTime(f)
36+
// Create a new zip at the same path with different contents and last modified
37+
Files.delete(f)
38+
createZip(f, Array(), "p1/D.class")
39+
Files.setLastModifiedTime(f, FileTime.fromMillis(lastMod1.toMillis + 2000))
40+
41+
// Our classpath cache should create a new instance
42+
val cp3 = createCp
43+
assert(cp1 ne cp3, (System.identityHashCode(cp1), System.identityHashCode(cp3)))
44+
// And that instance should see D, not C, in package p1.
45+
assert(cp3.findClass("p1.C").isEmpty)
46+
assert(cp3.findClass("p1.D").isDefined)
47+
} finally Files.delete(f)
48+
}
49+
50+
def createZip(zipLocation: Path, content: Array[Byte], internalPath: String): Unit = {
51+
val env = new java.util.HashMap[String, String]()
52+
env.put("create", String.valueOf(Files.notExists(zipLocation)))
53+
val fileUri = zipLocation.toUri
54+
val zipUri = new java.net.URI("jar:" + fileUri.getScheme, fileUri.getPath, null)
55+
val zipfs = FileSystems.newFileSystem(zipUri, env)
56+
try {
57+
try {
58+
val internalTargetPath = zipfs.getPath(internalPath)
59+
Files.createDirectories(internalTargetPath.getParent)
60+
Files.write(internalTargetPath, content)
61+
} finally {
62+
if (zipfs != null) zipfs.close()
63+
}
64+
} finally {
65+
zipfs.close()
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)