mirror of https://github.com/sbt/sbt.git
[2.x] Fix duplicate compiler bridge jar in ZincComponentManager (#8800)
* Fix duplicate compiler bridge jar in ZincComponentManager * Fix update method to clean stale stamped jars in ZincComponentManager * Fix update method to prevent duplicate jars from secondary cache lookups in ZincComponentManagerSpec Generated-by: GitHub Copilot
This commit is contained in:
parent
33ac10c1ce
commit
2ee7c26d1e
|
|
@ -74,6 +74,14 @@ class ZincComponentManager(
|
|||
def file(id: String)(ifMissing: IfMissing): File = {
|
||||
files(id)(ifMissing).toList match {
|
||||
case x :: Nil => x
|
||||
case xs if xs.size > 1 =>
|
||||
val canonical = xs.find(_.getName == s"$id.jar").getOrElse(xs.head)
|
||||
val toRemove = xs.filterNot(_ == canonical)
|
||||
toRemove.foreach(f => IO.delete(f))
|
||||
log.warn(
|
||||
s"Multiple files found for component '$id', removing extras: ${toRemove.mkString(", ")}"
|
||||
)
|
||||
canonical
|
||||
case xs => invalid(s"Expected single file for component '$id', found: ${xs.mkString(", ")}")
|
||||
}
|
||||
}
|
||||
|
|
@ -97,12 +105,28 @@ class ZincComponentManager(
|
|||
|
||||
private def invalid(msg: String) = throw new InvalidComponent(msg)
|
||||
|
||||
/** Retrieve the file for component 'id' from the secondary cache. */
|
||||
/**
|
||||
* Retrieve the file for component 'id' from the secondary cache.
|
||||
*
|
||||
* The secondary cache stores jars with a stamped version in the file name
|
||||
* (e.g. `id-1.10.5_20241130T035052.jar`). When `defineComponent` copies this file
|
||||
* into the local component directory, it preserves the original file name.
|
||||
* If the stamped version later changes, a new file is created alongside the old one,
|
||||
* causing `file()` to find multiple jars for the same component.
|
||||
*
|
||||
* To prevent this, we first remove any existing files in the component directory
|
||||
* before defining the component with the canonical name (`id.jar`).
|
||||
*/
|
||||
private def update(id: String): Unit = {
|
||||
secondaryCacheDir.foreach { dir =>
|
||||
val file = secondaryCacheFile(id, dir)
|
||||
if (file.exists) {
|
||||
define(id, Seq(file))
|
||||
val secondary = secondaryCacheFile(id, dir)
|
||||
if (secondary.exists) {
|
||||
val componentDir = provider.componentLocation(id)
|
||||
if (componentDir.isDirectory) {
|
||||
IO.listFiles(componentDir).foreach(IO.delete)
|
||||
}
|
||||
val canonicalJar = new File(componentDir, s"$id.jar")
|
||||
IO.copyFile(secondary, canonicalJar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2023, Scala center
|
||||
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package inc
|
||||
|
||||
import java.io.File
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
import hedgehog.*
|
||||
import hedgehog.runner.*
|
||||
import hedgehog.core.Result
|
||||
import _root_.sbt.io.IO
|
||||
import _root_.sbt.io.syntax.*
|
||||
import xsbti.{ ComponentProvider, GlobalLock, Logger }
|
||||
|
||||
object ZincComponentManagerTest extends Properties:
|
||||
override def tests: List[Test] = List(
|
||||
example("files should return defined component files", testFilesReturnsDefined),
|
||||
example(
|
||||
"files should throw InvalidComponent when component is missing and IfMissing.Fail",
|
||||
testFilesMissingFail,
|
||||
),
|
||||
example("file should return single file for a component", testFileSingle),
|
||||
example("file should throw when multiple files exist for a component", testFileMultiple),
|
||||
example("define should register component files", testDefine),
|
||||
example("files should call IfMissing.Define when component is missing", testFilesMissingDefine),
|
||||
example(
|
||||
"files should cache to secondary cache when IfMissing.Define with useSecondaryCache",
|
||||
testSecondaryCacheWrite,
|
||||
),
|
||||
example(
|
||||
"files should retrieve from secondary cache when component is missing locally",
|
||||
testSecondaryCacheRead,
|
||||
),
|
||||
)
|
||||
|
||||
private val noOpLock: GlobalLock = new GlobalLock:
|
||||
override def apply[T](file: File, callable: Callable[T]): T = callable.call()
|
||||
|
||||
private val silentLogger: Logger = new Logger:
|
||||
override def error(msg: java.util.function.Supplier[String]): Unit = ()
|
||||
override def warn(msg: java.util.function.Supplier[String]): Unit = ()
|
||||
override def info(msg: java.util.function.Supplier[String]): Unit = ()
|
||||
override def debug(msg: java.util.function.Supplier[String]): Unit = ()
|
||||
override def trace(exception: java.util.function.Supplier[Throwable]): Unit = ()
|
||||
|
||||
private def withTempDir[A](f: File => A): A =
|
||||
IO.withTemporaryDirectory(f)
|
||||
|
||||
private def fileProvider(baseDir: File): ComponentProvider = new ComponentProvider:
|
||||
private def componentDir(id: String): File =
|
||||
val dir = baseDir / id
|
||||
IO.createDirectory(dir)
|
||||
dir
|
||||
|
||||
override def componentLocation(id: String): File = componentDir(id)
|
||||
|
||||
override def component(componentID: String): Array[File] =
|
||||
val dir = componentDir(componentID)
|
||||
if dir.exists() then dir.listFiles().filter(_.isFile)
|
||||
else Array.empty
|
||||
|
||||
override def defineComponent(componentID: String, files: Array[File]): Unit =
|
||||
val dir = componentDir(componentID)
|
||||
files.foreach: f =>
|
||||
IO.copyFile(f, dir / f.getName)
|
||||
|
||||
override def addToComponent(componentID: String, files: Array[File]): Boolean =
|
||||
defineComponent(componentID, files)
|
||||
true
|
||||
|
||||
override def lockFile(): File = baseDir / ".lock"
|
||||
|
||||
private def createTempJar(dir: File, name: String): File =
|
||||
val f = dir / name
|
||||
IO.write(f, "fake-jar-content")
|
||||
f
|
||||
|
||||
def testFilesReturnsDefined: Result = withTempDir: tmpDir =>
|
||||
val provider = fileProvider(tmpDir / "components")
|
||||
val manager = new ZincComponentManager(noOpLock, provider, None, silentLogger)
|
||||
val sourceDir = tmpDir / "source"
|
||||
IO.createDirectory(sourceDir)
|
||||
val jar = createTempJar(sourceDir, "bridge.jar")
|
||||
manager.define("test-component", Seq(jar))
|
||||
val result = manager.files("test-component")(IfMissing.fail)
|
||||
Result
|
||||
.assert(result.nonEmpty)
|
||||
.log(s"expected non-empty files, got: ${result.toList}")
|
||||
|
||||
def testFilesMissingFail: Result = withTempDir: tmpDir =>
|
||||
val provider = fileProvider(tmpDir / "components")
|
||||
val manager = new ZincComponentManager(noOpLock, provider, None, silentLogger)
|
||||
try
|
||||
manager.files("nonexistent")(IfMissing.fail)
|
||||
Result.failure.log("expected InvalidComponent to be thrown")
|
||||
catch
|
||||
case _: InvalidComponent => Result.success
|
||||
case e: Throwable =>
|
||||
Result.failure.log(s"unexpected exception: ${e.getClass.getName}: ${e.getMessage}")
|
||||
|
||||
def testFileSingle: Result = withTempDir: tmpDir =>
|
||||
val provider = fileProvider(tmpDir / "components")
|
||||
val manager = new ZincComponentManager(noOpLock, provider, None, silentLogger)
|
||||
val sourceDir = tmpDir / "source"
|
||||
IO.createDirectory(sourceDir)
|
||||
val jar = createTempJar(sourceDir, "single.jar")
|
||||
manager.define("single-component", Seq(jar))
|
||||
val result = manager.file("single-component")(IfMissing.fail)
|
||||
Result
|
||||
.assert(result.getName == "single.jar")
|
||||
.log(s"expected single.jar, got: ${result.getName}")
|
||||
|
||||
def testFileMultiple: Result = withTempDir: tmpDir =>
|
||||
val provider = fileProvider(tmpDir / "components")
|
||||
val manager = new ZincComponentManager(noOpLock, provider, None, silentLogger)
|
||||
val sourceDir = tmpDir / "source"
|
||||
IO.createDirectory(sourceDir)
|
||||
val jar1 = createTempJar(sourceDir, "multi-component.jar")
|
||||
val jar2 = createTempJar(sourceDir, "b.jar")
|
||||
manager.define("multi-component", Seq(jar1, jar2))
|
||||
val result = manager.file("multi-component")(IfMissing.fail)
|
||||
val remaining = provider.component("multi-component").filter(_.isFile)
|
||||
Result
|
||||
.assert(result.getName == "multi-component.jar")
|
||||
.and(Result.assert(remaining.length == 1))
|
||||
.log(s"got: ${result.getName}, remaining: ${remaining.toList.map(_.getName)}")
|
||||
|
||||
def testDefine: Result = withTempDir: tmpDir =>
|
||||
val provider = fileProvider(tmpDir / "components")
|
||||
val manager = new ZincComponentManager(noOpLock, provider, None, silentLogger)
|
||||
val sourceDir = tmpDir / "source"
|
||||
IO.createDirectory(sourceDir)
|
||||
val jar = createTempJar(sourceDir, "defined.jar")
|
||||
manager.define("def-component", Seq(jar))
|
||||
val files = provider.component("def-component")
|
||||
Result
|
||||
.assert(files.length == 1)
|
||||
.and(Result.assert(files.head.getName == "defined.jar"))
|
||||
.log(s"files: ${files.toList.map(_.getName)}")
|
||||
|
||||
def testFilesMissingDefine: Result = withTempDir: tmpDir =>
|
||||
val secondaryDir = tmpDir / "secondary"
|
||||
IO.createDirectory(secondaryDir)
|
||||
val provider = fileProvider(tmpDir / "components")
|
||||
val manager = new ZincComponentManager(noOpLock, provider, Some(secondaryDir), silentLogger)
|
||||
val sourceDir = tmpDir / "source"
|
||||
IO.createDirectory(sourceDir)
|
||||
val jar = createTempJar(sourceDir, "created.jar")
|
||||
val ifMissing = IfMissing.define(
|
||||
false,
|
||||
manager.define("lazy-component", Seq(jar)),
|
||||
)
|
||||
val result = manager.files("lazy-component")(ifMissing)
|
||||
Result
|
||||
.assert(result.nonEmpty)
|
||||
.log(s"expected non-empty files after define, got: ${result.toList}")
|
||||
|
||||
def testSecondaryCacheWrite: Result = withTempDir: tmpDir =>
|
||||
val secondaryDir = tmpDir / "secondary"
|
||||
IO.createDirectory(secondaryDir)
|
||||
val provider = fileProvider(tmpDir / "components")
|
||||
val manager = new ZincComponentManager(noOpLock, provider, Some(secondaryDir), silentLogger)
|
||||
val sourceDir = tmpDir / "source"
|
||||
IO.createDirectory(sourceDir)
|
||||
val jar = createTempJar(sourceDir, "cached.jar")
|
||||
val ifMissing = IfMissing.define(
|
||||
true,
|
||||
manager.define("cache-component", Seq(jar)),
|
||||
)
|
||||
manager.files("cache-component")(ifMissing)
|
||||
val sbtOrgDir = secondaryDir / "org.scala-sbt"
|
||||
val cachedFiles = if sbtOrgDir.exists() then sbtOrgDir.listFiles().toList else Nil
|
||||
Result
|
||||
.assert(cachedFiles.nonEmpty)
|
||||
.log(s"expected files in secondary cache dir, got: $cachedFiles")
|
||||
|
||||
def testSecondaryCacheRead: Result = withTempDir: tmpDir =>
|
||||
val secondaryDir = tmpDir / "secondary"
|
||||
IO.createDirectory(secondaryDir)
|
||||
val provider1 = fileProvider(tmpDir / "components1")
|
||||
val manager1 = new ZincComponentManager(noOpLock, provider1, Some(secondaryDir), silentLogger)
|
||||
val sourceDir = tmpDir / "source"
|
||||
IO.createDirectory(sourceDir)
|
||||
val jar = createTempJar(sourceDir, "shared.jar")
|
||||
val ifMissing1 = IfMissing.define(
|
||||
true,
|
||||
manager1.define("shared-component", Seq(jar)),
|
||||
)
|
||||
manager1.files("shared-component")(ifMissing1)
|
||||
|
||||
val provider2 = fileProvider(tmpDir / "components2")
|
||||
val manager2 = new ZincComponentManager(noOpLock, provider2, Some(secondaryDir), silentLogger)
|
||||
val result = manager2.files("shared-component")(IfMissing.fail)
|
||||
Result
|
||||
.assert(result.nonEmpty)
|
||||
.log(s"expected to retrieve component from secondary cache, got: ${result.toList}")
|
||||
|
||||
end ZincComponentManagerTest
|
||||
Loading…
Reference in New Issue