[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:
since-2017-hub 2026-02-26 21:58:33 -07:00 committed by GitHub
parent 33ac10c1ce
commit 2ee7c26d1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 234 additions and 4 deletions

View File

@ -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)
}
}
}

View File

@ -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