From a9ffa59bafb98ab1b81bd55925f8f48a8430dbf1 Mon Sep 17 00:00:00 2001 From: mkdev11 Date: Sat, 10 Jan 2026 22:38:32 +0200 Subject: [PATCH 1/2] Fix #7951: Apply dependencyOverrides to delivered Ivy XML Post-process the delivered Ivy XML file to apply dependency overrides. This is necessary because Ivy's deliver() method doesn't automatically apply DependencyDescriptorMediators when writing the XML. The fix: - Adds applyOverridesToDeliveredIvy() method in IvyActions - Reads the delivered Ivy XML and transforms dependency elements - Updates rev attributes for dependencies that have overrides - Ensures consistency between Maven POM and Ivy XML publishing Added unit tests to verify: - Overrides are correctly applied to matching dependencies - All other attributes are preserved - Non-matching dependencies remain unchanged --- .../librarymanagement/IvyActions.scala | 65 ++++++- .../IvyActionsOverrideSpec.scala | 178 ++++++++++++++++++ .../main/scala/sbt/internal/GCMonitor.scala | 6 +- 3 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 lm-ivy/src/test/scala/sbt/internal/librarymanagement/IvyActionsOverrideSpec.scala diff --git a/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala index b0f6ebc2f..9581449ad 100644 --- a/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala +++ b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala @@ -96,7 +96,70 @@ object IvyActions { val options = DeliverOptions.newInstance(ivy.getSettings).setStatus(status) options.setConfs(getConfigurations(md, configuration.configurations)) ivy.deliver(revID, revID.getRevision, deliverIvyPattern, options) - deliveredFile(ivy, deliverIvyPattern, md) + val file = deliveredFile(ivy, deliverIvyPattern, md) + + // Apply dependency overrides to the delivered Ivy XML + applyOverridesToDeliveredIvy(file, module.moduleSettings, log) + + file + } + } + + /** + * Post-processes the delivered Ivy XML file to apply dependency overrides. + * This is necessary because Ivy's deliver() method doesn't automatically apply + * DependencyDescriptorMediators when writing the XML. + */ + private def applyOverridesToDeliveredIvy( + ivyFile: File, + moduleSettings: ModuleSettings, + log: Logger + ): Unit = { + moduleSettings match { + case ic: InlineConfiguration if ic.overrides.nonEmpty => + val overrideMap = ic.overrides.map(m => (m.organization, m.name) -> m.revision).toMap + + if (ivyFile.exists()) { + val xml = scala.xml.XML.loadFile(ivyFile) + val updated = + new scala.xml.transform.RuleTransformer(new scala.xml.transform.RewriteRule { + override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { + case e @ scala.xml.Elem(prefix, "dependency", attrs, scope, children*) => + val org = attrs.get("org").map(_.text).getOrElse("") + val name = attrs.get("name").map(_.text).getOrElse("") + overrideMap.get((org, name)) match { + case Some(overrideRev) => + // Build new attributes by replacing 'rev' attribute value + def updateAttrs(metadata: scala.xml.MetaData): scala.xml.MetaData = { + metadata match { + case scala.xml.Null => scala.xml.Null + case attr if attr.key == "rev" => + new scala.xml.UnprefixedAttribute( + "rev", + overrideRev, + updateAttrs(attr.next) + ) + case attr => + attr.copy(next = updateAttrs(attr.next)) + } + } + scala.xml.Elem( + prefix, + "dependency", + updateAttrs(attrs), + scope, + minimizeEmpty = true, + children* + ) + case None => e + } + case other => other + } + }).transform(xml).head + scala.xml.XML.save(ivyFile.getAbsolutePath, updated, "UTF-8", xmlDecl = true, null) + log.debug(s"Applied ${overrideMap.size} dependency override(s) to ${ivyFile.getName}") + } + case _ => // No overrides to apply } } diff --git a/lm-ivy/src/test/scala/sbt/internal/librarymanagement/IvyActionsOverrideSpec.scala b/lm-ivy/src/test/scala/sbt/internal/librarymanagement/IvyActionsOverrideSpec.scala new file mode 100644 index 000000000..5dc5e7d57 --- /dev/null +++ b/lm-ivy/src/test/scala/sbt/internal/librarymanagement/IvyActionsOverrideSpec.scala @@ -0,0 +1,178 @@ +package sbt.internal.librarymanagement + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class IvyActionsOverrideSpec extends AnyFlatSpec with Matchers { + + "applyOverridesToDeliveredIvy XML transformation" should "replace rev attribute for matching dependencies" in { + // Simulate the override map + val overrideMap = Map(("org.slf4j", "slf4j-api") -> "2.0.16") + + // Sample Ivy XML with "managed" version (as reported in issue #7951) + val sampleXml = + + + + + + + + + // Apply the same transformation logic as in IvyActions + val updated = + new scala.xml.transform.RuleTransformer(new scala.xml.transform.RewriteRule { + override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { + case e @ scala.xml.Elem(prefix, "dependency", attrs, scope, children*) => + val org = attrs.get("org").map(_.text).getOrElse("") + val name = attrs.get("name").map(_.text).getOrElse("") + overrideMap.get((org, name)) match { + case Some(overrideRev) => + // Build new attributes by replacing 'rev' attribute value + def updateAttrs(metadata: scala.xml.MetaData): scala.xml.MetaData = { + metadata match { + case scala.xml.Null => scala.xml.Null + case attr if attr.key == "rev" => + new scala.xml.UnprefixedAttribute( + "rev", + overrideRev, + updateAttrs(attr.next) + ) + case attr => + attr.copy(next = updateAttrs(attr.next)) + } + } + scala.xml.Elem( + prefix, + "dependency", + updateAttrs(attrs), + scope, + minimizeEmpty = true, + children* + ) + case None => e + } + case other => other + } + }).transform(sampleXml).head + + // Verify the transformation + val dependencies = (updated \\ "dependency") + + // Check slf4j-api has overridden version + val slf4jDep = dependencies.find(d => (d \ "@org").text == "org.slf4j") + slf4jDep shouldBe defined + (slf4jDep.get \ "@rev").text shouldBe "2.0.16" + (slf4jDep.get \ "@name").text shouldBe "slf4j-api" + (slf4jDep.get \ "@conf").text shouldBe "compile->default(compile)" + + // Check other-lib is unchanged + val otherDep = dependencies.find(d => (d \ "@org").text == "other.org") + otherDep shouldBe defined + (otherDep.get \ "@rev").text shouldBe "1.0.0" + (otherDep.get \ "@name").text shouldBe "other-lib" + } + + it should "preserve all attributes when replacing rev" in { + val overrideMap = Map(("org.example", "test-lib") -> "3.0.0") + + val sampleXml = + + + + + + + val updated = + new scala.xml.transform.RuleTransformer(new scala.xml.transform.RewriteRule { + override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { + case e @ scala.xml.Elem(prefix, "dependency", attrs, scope, children*) => + val org = attrs.get("org").map(_.text).getOrElse("") + val name = attrs.get("name").map(_.text).getOrElse("") + overrideMap.get((org, name)) match { + case Some(overrideRev) => + def updateAttrs(metadata: scala.xml.MetaData): scala.xml.MetaData = { + metadata match { + case scala.xml.Null => scala.xml.Null + case attr if attr.key == "rev" => + new scala.xml.UnprefixedAttribute( + "rev", + overrideRev, + updateAttrs(attr.next) + ) + case attr => + attr.copy(next = updateAttrs(attr.next)) + } + } + scala.xml.Elem( + prefix, + "dependency", + updateAttrs(attrs), + scope, + minimizeEmpty = true, + children* + ) + case None => e + } + case other => other + } + }).transform(sampleXml).head + + val dep = (updated \\ "dependency").head + (dep \ "@org").text shouldBe "org.example" + (dep \ "@name").text shouldBe "test-lib" + (dep \ "@rev").text shouldBe "3.0.0" + (dep \ "@conf").text shouldBe "compile->default" + (dep \ "@transitive").text shouldBe "false" + (dep \ "@force").text shouldBe "true" + } + + it should "not modify dependencies without overrides" in { + val overrideMap = Map(("org.other", "other-lib") -> "2.0.0") + + val sampleXml = + + + + + + + val updated = + new scala.xml.transform.RuleTransformer(new scala.xml.transform.RewriteRule { + override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { + case e @ scala.xml.Elem(prefix, "dependency", attrs, scope, children*) => + val org = attrs.get("org").map(_.text).getOrElse("") + val name = attrs.get("name").map(_.text).getOrElse("") + overrideMap.get((org, name)) match { + case Some(overrideRev) => + def updateAttrs(metadata: scala.xml.MetaData): scala.xml.MetaData = { + metadata match { + case scala.xml.Null => scala.xml.Null + case attr if attr.key == "rev" => + new scala.xml.UnprefixedAttribute( + "rev", + overrideRev, + updateAttrs(attr.next) + ) + case attr => + attr.copy(next = updateAttrs(attr.next)) + } + } + scala.xml.Elem( + prefix, + "dependency", + updateAttrs(attrs), + scope, + minimizeEmpty = true, + children* + ) + case None => e + } + case other => other + } + }).transform(sampleXml).head + + val dep = (updated \\ "dependency").head + (dep \ "@rev").text shouldBe "1.0.0" // Should remain unchanged + } +} diff --git a/main/src/main/scala/sbt/internal/GCMonitor.scala b/main/src/main/scala/sbt/internal/GCMonitor.scala index 79b49af02..f39b8a4ff 100644 --- a/main/src/main/scala/sbt/internal/GCMonitor.scala +++ b/main/src/main/scala/sbt/internal/GCMonitor.scala @@ -62,8 +62,10 @@ class GCMonitor(logger: Logger) extends GCMonitorBase with AutoCloseable { override protected def emitWarning(total: Long, over: Option[Long]): Unit = { val totalSeconds = total / 1000.0 - val amountMsg = over.fold(s"$totalSeconds seconds") { d => - "In the last " + (d / 1000.0).ceil.toInt + f" seconds, $totalSeconds (${total.toDouble / d * 100}%.1f%%)" + val amountMsg = over.fold(f"$totalSeconds%.3f seconds") { d => + val dSeconds = (d / 1000.0).ceil.toInt + val percentage = total.toDouble / d * 100 + f"In the last $dSeconds seconds, $totalSeconds%.3f seconds ($percentage%.1f%%) of GC pause" } val msg = s"$amountMsg were spent in GC. " + s"[Heap: ${gbString(runtime.freeMemory())} free " + From e63a4b8f8c1b1a7e25dfe127a7741b70b2b24354 Mon Sep 17 00:00:00 2001 From: mkdev11 Date: Sun, 11 Jan 2026 01:40:51 +0200 Subject: [PATCH 2/2] Refactor applyDependencyOverrides into testable function --- .../librarymanagement/IvyActions.scala | 84 +++++++------ .../IvyActionsOverrideSpec.scala | 112 +----------------- 2 files changed, 52 insertions(+), 144 deletions(-) diff --git a/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala index 9581449ad..9c077ca9d 100644 --- a/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala +++ b/lm-ivy/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala @@ -118,44 +118,9 @@ object IvyActions { moduleSettings match { case ic: InlineConfiguration if ic.overrides.nonEmpty => val overrideMap = ic.overrides.map(m => (m.organization, m.name) -> m.revision).toMap - if (ivyFile.exists()) { val xml = scala.xml.XML.loadFile(ivyFile) - val updated = - new scala.xml.transform.RuleTransformer(new scala.xml.transform.RewriteRule { - override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { - case e @ scala.xml.Elem(prefix, "dependency", attrs, scope, children*) => - val org = attrs.get("org").map(_.text).getOrElse("") - val name = attrs.get("name").map(_.text).getOrElse("") - overrideMap.get((org, name)) match { - case Some(overrideRev) => - // Build new attributes by replacing 'rev' attribute value - def updateAttrs(metadata: scala.xml.MetaData): scala.xml.MetaData = { - metadata match { - case scala.xml.Null => scala.xml.Null - case attr if attr.key == "rev" => - new scala.xml.UnprefixedAttribute( - "rev", - overrideRev, - updateAttrs(attr.next) - ) - case attr => - attr.copy(next = updateAttrs(attr.next)) - } - } - scala.xml.Elem( - prefix, - "dependency", - updateAttrs(attrs), - scope, - minimizeEmpty = true, - children* - ) - case None => e - } - case other => other - } - }).transform(xml).head + val updated = applyDependencyOverrides(xml, overrideMap) scala.xml.XML.save(ivyFile.getAbsolutePath, updated, "UTF-8", xmlDecl = true, null) log.debug(s"Applied ${overrideMap.size} dependency override(s) to ${ivyFile.getName}") } @@ -163,6 +128,53 @@ object IvyActions { } } + /** + * Applies dependency overrides to an Ivy XML node by updating the rev attribute + * of dependency elements that match entries in the override map. + * + * @param xml The Ivy XML root node to transform + * @param overrideMap Map from (organization, name) to the overridden revision + * @return The transformed XML node with updated dependency revisions + */ + def applyDependencyOverrides( + xml: scala.xml.Node, + overrideMap: Map[(String, String), String] + ): scala.xml.Node = { + new scala.xml.transform.RuleTransformer(new scala.xml.transform.RewriteRule { + override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { + case e @ scala.xml.Elem(prefix, "dependency", attrs, scope, children*) => + val org = attrs.get("org").map(_.text).getOrElse("") + val name = attrs.get("name").map(_.text).getOrElse("") + overrideMap.get((org, name)) match { + case Some(overrideRev) => + def updateAttrs(metadata: scala.xml.MetaData): scala.xml.MetaData = { + metadata match { + case scala.xml.Null => scala.xml.Null + case attr if attr.key == "rev" => + new scala.xml.UnprefixedAttribute( + "rev", + overrideRev, + updateAttrs(attr.next) + ) + case attr => + attr.copy(next = updateAttrs(attr.next)) + } + } + scala.xml.Elem( + prefix, + "dependency", + updateAttrs(attrs), + scope, + minimizeEmpty = true, + children* + ) + case None => e + } + case other => other + } + }).transform(xml).head + } + def getConfigurations( module: ModuleDescriptor, configurations: Option[Vector[ConfigRef]] diff --git a/lm-ivy/src/test/scala/sbt/internal/librarymanagement/IvyActionsOverrideSpec.scala b/lm-ivy/src/test/scala/sbt/internal/librarymanagement/IvyActionsOverrideSpec.scala index 5dc5e7d57..454837f53 100644 --- a/lm-ivy/src/test/scala/sbt/internal/librarymanagement/IvyActionsOverrideSpec.scala +++ b/lm-ivy/src/test/scala/sbt/internal/librarymanagement/IvyActionsOverrideSpec.scala @@ -5,11 +5,9 @@ import org.scalatest.matchers.should.Matchers class IvyActionsOverrideSpec extends AnyFlatSpec with Matchers { - "applyOverridesToDeliveredIvy XML transformation" should "replace rev attribute for matching dependencies" in { - // Simulate the override map + "IvyActions.applyDependencyOverrides" should "replace rev attribute for matching dependencies" in { val overrideMap = Map(("org.slf4j", "slf4j-api") -> "2.0.16") - // Sample Ivy XML with "managed" version (as reported in issue #7951) val sampleXml = @@ -19,44 +17,8 @@ class IvyActionsOverrideSpec extends AnyFlatSpec with Matchers { - // Apply the same transformation logic as in IvyActions - val updated = - new scala.xml.transform.RuleTransformer(new scala.xml.transform.RewriteRule { - override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { - case e @ scala.xml.Elem(prefix, "dependency", attrs, scope, children*) => - val org = attrs.get("org").map(_.text).getOrElse("") - val name = attrs.get("name").map(_.text).getOrElse("") - overrideMap.get((org, name)) match { - case Some(overrideRev) => - // Build new attributes by replacing 'rev' attribute value - def updateAttrs(metadata: scala.xml.MetaData): scala.xml.MetaData = { - metadata match { - case scala.xml.Null => scala.xml.Null - case attr if attr.key == "rev" => - new scala.xml.UnprefixedAttribute( - "rev", - overrideRev, - updateAttrs(attr.next) - ) - case attr => - attr.copy(next = updateAttrs(attr.next)) - } - } - scala.xml.Elem( - prefix, - "dependency", - updateAttrs(attrs), - scope, - minimizeEmpty = true, - children* - ) - case None => e - } - case other => other - } - }).transform(sampleXml).head + val updated = IvyActions.applyDependencyOverrides(sampleXml, overrideMap) - // Verify the transformation val dependencies = (updated \\ "dependency") // Check slf4j-api has overridden version @@ -83,40 +45,7 @@ class IvyActionsOverrideSpec extends AnyFlatSpec with Matchers { - val updated = - new scala.xml.transform.RuleTransformer(new scala.xml.transform.RewriteRule { - override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { - case e @ scala.xml.Elem(prefix, "dependency", attrs, scope, children*) => - val org = attrs.get("org").map(_.text).getOrElse("") - val name = attrs.get("name").map(_.text).getOrElse("") - overrideMap.get((org, name)) match { - case Some(overrideRev) => - def updateAttrs(metadata: scala.xml.MetaData): scala.xml.MetaData = { - metadata match { - case scala.xml.Null => scala.xml.Null - case attr if attr.key == "rev" => - new scala.xml.UnprefixedAttribute( - "rev", - overrideRev, - updateAttrs(attr.next) - ) - case attr => - attr.copy(next = updateAttrs(attr.next)) - } - } - scala.xml.Elem( - prefix, - "dependency", - updateAttrs(attrs), - scope, - minimizeEmpty = true, - children* - ) - case None => e - } - case other => other - } - }).transform(sampleXml).head + val updated = IvyActions.applyDependencyOverrides(sampleXml, overrideMap) val dep = (updated \\ "dependency").head (dep \ "@org").text shouldBe "org.example" @@ -137,40 +66,7 @@ class IvyActionsOverrideSpec extends AnyFlatSpec with Matchers { - val updated = - new scala.xml.transform.RuleTransformer(new scala.xml.transform.RewriteRule { - override def transform(n: scala.xml.Node): Seq[scala.xml.Node] = n match { - case e @ scala.xml.Elem(prefix, "dependency", attrs, scope, children*) => - val org = attrs.get("org").map(_.text).getOrElse("") - val name = attrs.get("name").map(_.text).getOrElse("") - overrideMap.get((org, name)) match { - case Some(overrideRev) => - def updateAttrs(metadata: scala.xml.MetaData): scala.xml.MetaData = { - metadata match { - case scala.xml.Null => scala.xml.Null - case attr if attr.key == "rev" => - new scala.xml.UnprefixedAttribute( - "rev", - overrideRev, - updateAttrs(attr.next) - ) - case attr => - attr.copy(next = updateAttrs(attr.next)) - } - } - scala.xml.Elem( - prefix, - "dependency", - updateAttrs(attrs), - scope, - minimizeEmpty = true, - children* - ) - case None => e - } - case other => other - } - }).transform(sampleXml).head + val updated = IvyActions.applyDependencyOverrides(sampleXml, overrideMap) val dep = (updated \\ "dependency").head (dep \ "@rev").text shouldBe "1.0.0" // Should remain unchanged