Merge pull request #8463 from MkDev11/fix/dependency-overrides-ivy-7951

[2.x] Fix #7951: Apply dependencyOverrides to delivered Ivy XML
This commit is contained in:
eugene yokota 2026-01-10 21:09:09 -05:00 committed by GitHub
commit 4bee8747e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 150 additions and 1 deletions

View File

@ -96,10 +96,85 @@ 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 = 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}")
}
case _ => // No overrides to apply
}
}
/**
* 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]]

View File

@ -0,0 +1,74 @@
package sbt.internal.librarymanagement
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class IvyActionsOverrideSpec extends AnyFlatSpec with Matchers {
"IvyActions.applyDependencyOverrides" should "replace rev attribute for matching dependencies" in {
val overrideMap = Map(("org.slf4j", "slf4j-api") -> "2.0.16")
val sampleXml =
<ivy-module version="2.0">
<info organisation="test" module="test" revision="1.0"/>
<dependencies>
<dependency org="org.slf4j" name="slf4j-api" rev="managed" conf="compile->default(compile)"/>
<dependency org="other.org" name="other-lib" rev="1.0.0" conf="compile->default(compile)"/>
</dependencies>
</ivy-module>
val updated = IvyActions.applyDependencyOverrides(sampleXml, overrideMap)
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 =
<ivy-module version="2.0">
<dependencies>
<dependency org="org.example" name="test-lib" rev="1.0.0" conf="compile->default" transitive="false" force="true"/>
</dependencies>
</ivy-module>
val updated = IvyActions.applyDependencyOverrides(sampleXml, overrideMap)
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 =
<ivy-module version="2.0">
<dependencies>
<dependency org="org.example" name="test-lib" rev="1.0.0" conf="compile->default"/>
</dependencies>
</ivy-module>
val updated = IvyActions.applyDependencyOverrides(sampleXml, overrideMap)
val dep = (updated \\ "dependency").head
(dep \ "@rev").text shouldBe "1.0.0" // Should remain unchanged
}
}