Merge multiple dependency definitions for the same ID. Fixes #468, #285, #419, #480.

This is only a workaround.  Multiple dependency definitions should be avoided
in general.
This commit is contained in:
Mark Harrah 2012-08-20 15:55:50 -04:00
parent f0bdadc437
commit 152d51465d
6 changed files with 257 additions and 44 deletions

View File

@ -64,8 +64,11 @@ object CustomPomParser
// Fixes up the detected extension in some cases missed by Ivy.
val convertArtifacts = artifactExtIncorrect(md)
// Merges artifact sections for duplicate dependency definitions
val mergeDuplicates = IvySbt.hasDuplicateDependencies(md.getDependencies)
val unqualify = (filtered - ExtraAttributesKey) map { case (k,v) => ("e:" + k, v) }
if(unqualify.isEmpty && extraDepAttributes.isEmpty && !convertArtifacts)
if(unqualify.isEmpty && extraDepAttributes.isEmpty && !convertArtifacts && !mergeDuplicates)
md
else
addExtra(unqualify, extraDepAttributes, parser, md)
@ -74,7 +77,6 @@ object CustomPomParser
md.getConfigurations.exists(conf => md.getArtifacts(conf.getName).exists(art => JarPackagings(art.getExt)))
private[this] def shouldBeUnqualified(m: Map[String, String]): Map[String, String] =
m.filter { case (SbtVersionKey | ScalaVersionKey | ExtraAttributesKey,_) => true; case _ => false }
private[this] def condAddExtra(properties: Map[String, String], id: ModuleRevisionId): ModuleRevisionId =
if(properties.isEmpty) id else addExtra(properties, id)
@ -169,7 +171,10 @@ object CustomPomParser
for( (key,value) <- md.getExtraInfo.asInstanceOf[java.util.Map[String,String]].asScala ) dmd.addExtraInfo(key, value)
for( (key, value) <- md.getExtraAttributesNamespaces.asInstanceOf[java.util.Map[String,String]].asScala ) dmd.addExtraAttributeNamespace(key, value)
IvySbt.addExtraNamespace(dmd)
for( dd <- md.getDependencies ) dmd.addDependency(addExtra(dd, dependencyExtra))
val withExtra = md.getDependencies map { dd => addExtra(dd, dependencyExtra) }
val unique = IvySbt.mergeDuplicateDefinitions(withExtra)
unique foreach dmd.addDependency
for( ed <- md.getInheritedDescriptors) dmd.addInheritedDescriptor( new DefaultExtendsDescriptor( mrid, resolvedMrid, ed.getLocation, ed.getExtendsTypes) )
for( conf <- md.getConfigurations) {

View File

@ -146,7 +146,8 @@ final class IvySbt(val configuration: IvyConfiguration)
val parser = IvySbt.parseIvyXML(ivy.getSettings, IvySbt.wrapped(module, ivyXML), moduleID, defaultConf.name, validate)
IvySbt.addMainArtifact(moduleID)
IvySbt.addOverrides(moduleID, overrides, ivy.getSettings.getMatcher(PatternMatcher.EXACT))
IvySbt.addDependencies(moduleID, IvySbt.overrideDirect(dependencies, overrides), parser)
val transformedDeps = IvySbt.overrideDirect(dependencies, overrides)
IvySbt.addDependencies(moduleID, transformedDeps, parser)
(moduleID, parser.getDefaultConf)
}
private def newConfiguredModuleID(module: ModuleID, moduleInfo: ModuleInfo, configurations: Iterable[Configuration]) =
@ -423,36 +424,73 @@ private object IvySbt
}
/** This method is used to add inline dependencies to the provided module. */
def addDependencies(moduleID: DefaultModuleDescriptor, dependencies: Iterable[ModuleID], parser: CustomXmlParser.CustomParser)
def addDependencies(moduleID: DefaultModuleDescriptor, dependencies: Seq[ModuleID], parser: CustomXmlParser.CustomParser)
{
for(dependency <- dependencies)
{
val dependencyDescriptor = new DefaultDependencyDescriptor(moduleID, toID(dependency), dependency.isForce, dependency.isChanging, dependency.isTransitive)
dependency.configurations match
{
case None => // The configuration for this dependency was not explicitly specified, so use the default
parser.parseDepsConfs(parser.getDefaultConf, dependencyDescriptor)
case Some(confs) => // The configuration mapping (looks like: test->default) was specified for this dependency
parser.parseDepsConfs(confs, dependencyDescriptor)
}
for(artifact <- dependency.explicitArtifacts)
{
import artifact.{name, classifier, `type`, extension, url}
val extraMap = extra(artifact)
val ivyArtifact = new DefaultDependencyArtifactDescriptor(dependencyDescriptor, name, `type`, extension, url.getOrElse(null), extraMap)
for(conf <- dependencyDescriptor.getModuleConfigurations)
dependencyDescriptor.addDependencyArtifact(conf, ivyArtifact)
}
for(excls <- dependency.exclusions)
{
for(conf <- dependencyDescriptor.getModuleConfigurations)
{
dependencyDescriptor.addExcludeRule(conf, IvyScala.excludeRule(excls.organization, excls.name, excls.configurations, excls.artifact))
}
}
moduleID.addDependency(dependencyDescriptor)
}
val converted = dependencies map { dependency => convertDependency(moduleID, dependency, parser) }
val unique = if(hasDuplicateDependencies(converted)) mergeDuplicateDefinitions(converted) else converted
unique foreach moduleID.addDependency
}
/** Determines if there are multiple dependency definitions for the same dependency ID. */
def hasDuplicateDependencies(dependencies: Seq[DependencyDescriptor]): Boolean =
{
val ids = dependencies.map(_.getDependencyRevisionId)
ids.toSet.size != ids.size
}
/** Combines the artifacts, includes, and excludes of duplicate dependency definitions.
* This is somewhat fragile and is only intended to workaround Ivy (or sbt's use of Ivy) not handling this case properly.
* In particular, Ivy will create multiple dependency entries when converting a pom with a dependency on a classified artifact and a non-classified artifact:
* https://github.com/harrah/xsbt/issues/468
* It will also allow users to declare dependencies on classified modules in different configurations:
* https://groups.google.com/d/topic/simple-build-tool/H2MdAARz6e0/discussion
* as well as basic multi-classifier handling: #285, #419, #480.
* Multiple dependency definitions should otherwise be avoided as much as possible.
*/
def mergeDuplicateDefinitions(dependencies: Seq[DependencyDescriptor]): Seq[DependencyDescriptor] =
{
val deps = new java.util.LinkedHashMap[ModuleRevisionId, DependencyDescriptor]
for( dd <- dependencies )
{
val id = dd.getDependencyRevisionId
val updated = deps get id match {
case null => dd
case v => ivyint.MergeDescriptors(v, dd)
}
deps.put(id, updated)
}
import collection.JavaConverters._
deps.values.asScala.toSeq
}
/** Transforms an sbt ModuleID into an Ivy DefaultDependencyDescriptor.*/
def convertDependency(moduleID: DefaultModuleDescriptor, dependency: ModuleID, parser: CustomXmlParser.CustomParser): DefaultDependencyDescriptor =
{
val dependencyDescriptor = new DefaultDependencyDescriptor(moduleID, toID(dependency), dependency.isForce, dependency.isChanging, dependency.isTransitive)
dependency.configurations match
{
case None => // The configuration for this dependency was not explicitly specified, so use the default
parser.parseDepsConfs(parser.getDefaultConf, dependencyDescriptor)
case Some(confs) => // The configuration mapping (looks like: test->default) was specified for this dependency
parser.parseDepsConfs(confs, dependencyDescriptor)
}
for(artifact <- dependency.explicitArtifacts)
{
import artifact.{name, classifier, `type`, extension, url}
val extraMap = extra(artifact)
val ivyArtifact = new DefaultDependencyArtifactDescriptor(dependencyDescriptor, name, `type`, extension, url.getOrElse(null), extraMap)
for(conf <- dependencyDescriptor.getModuleConfigurations)
dependencyDescriptor.addDependencyArtifact(conf, ivyArtifact)
}
for(excls <- dependency.exclusions)
{
for(conf <- dependencyDescriptor.getModuleConfigurations)
{
dependencyDescriptor.addExcludeRule(conf, IvyScala.excludeRule(excls.organization, excls.name, excls.configurations, excls.artifact))
}
}
dependencyDescriptor
}
def addOverrides(moduleID: DefaultModuleDescriptor, overrides: Set[ModuleID], matcher: PatternMatcher): Unit =
overrides foreach addOverride(moduleID, matcher)
def addOverride(moduleID: DefaultModuleDescriptor, matcher: PatternMatcher)(overrideDef: ModuleID): Unit =
@ -500,6 +538,7 @@ private object IvySbt
}
}
/** This code converts the given ModuleDescriptor to a DefaultModuleDescriptor by casting or generating an error.
* Ivy 2.0.0 always produces a DefaultModuleDescriptor. */
private def toDefaultModuleDescriptor(md: ModuleDescriptor) =

View File

@ -20,7 +20,7 @@ final class PublishConfiguration(val ivyFile: Option[File], val resolverName: St
final class UpdateConfiguration(val retrieve: Option[RetrieveConfiguration], val missingOk: Boolean, val logging: UpdateLogging.Value)
final class RetrieveConfiguration(val retrieveDirectory: File, val outputPattern: String)
final case class MakePomConfiguration(file: File, moduleInfo: ModuleInfo, configurations: Option[Seq[Configuration]] = None, extra: NodeSeq = NodeSeq.Empty, process: XNode => XNode = n => n, filterRepositories: MavenRepository => Boolean = _ => true, allRepositories: Boolean, includeTypes: Set[String] = Set(Artifact.DefaultType))
final case class MakePomConfiguration(file: File, moduleInfo: ModuleInfo, configurations: Option[Seq[Configuration]] = None, extra: NodeSeq = NodeSeq.Empty, process: XNode => XNode = n => n, filterRepositories: MavenRepository => Boolean = _ => true, allRepositories: Boolean, includeTypes: Set[String] = Set(Artifact.DefaultType, Artifact.PomType))
// exclude is a map on a restricted ModuleID
final case class GetClassifiersConfiguration(module: GetClassifiersModule, exclude: Map[ModuleID, Set[String]], configuration: UpdateConfiguration, ivyScala: Option[IvyScala])
final case class GetClassifiersModule(id: ModuleID, modules: Seq[ModuleID], configurations: Seq[Configuration], classifiers: Seq[String])

View File

@ -37,6 +37,7 @@ final case class ModuleID(organization: String, name: String, revision: String,
def extra(attributes: (String,String)*) = copy(extraAttributes = this.extraAttributes ++ ModuleID.checkE(attributes))
def sources() = artifacts(Artifact.sources(name))
def javadoc() = artifacts(Artifact.javadoc(name))
def pomOnly() = artifacts(Artifact.pom(name))
def withSources() = jarIfEmpty.sources()
def withJavadoc() = jarIfEmpty.javadoc()
private def jarIfEmpty = if(explicitArtifacts.isEmpty) jar() else this

View File

@ -10,13 +10,12 @@ package sbt
import java.io.File
// Node needs to be renamed to XNode because the task subproject contains a Node type that will shadow
// scala.xml.Node when generating aggregated API documentation
import scala.xml.{Node => XNode, NodeSeq, PrettyPrinter}
import scala.xml.{Elem, Node => XNode, NodeSeq, PrettyPrinter}
import Configurations.Optional
import org.apache.ivy.{core, plugins, Ivy}
import core.settings.IvySettings
import core.module.descriptor
import descriptor.{DependencyDescriptor, License, ModuleDescriptor, ExcludeRule}
import core.module.descriptor.{DependencyArtifactDescriptor, DependencyDescriptor, License, ModuleDescriptor, ExcludeRule}
import plugins.resolver.{ChainResolver, DependencyResolver, IBiblioResolver}
class MakePom(val log: Logger)
@ -146,38 +145,75 @@ class MakePom(val log: Logger)
</dependencies>
def makeDependency(dependency: DependencyDescriptor, includeTypes: Set[String]): NodeSeq =
{
val artifacts = dependency.getAllDependencyArtifacts
val includeArtifacts = artifacts.filter(d => includeTypes(d.getType))
if(artifacts.isEmpty) {
val (scope, optional) = getScopeAndOptional(dependency.getModuleConfigurations)
makeDependencyElem(dependency, scope, optional, None, None)
}
else if(includeArtifacts.isEmpty)
NodeSeq.Empty
else
NodeSeq.fromSeq(artifacts.map( a => makeDependencyElem(dependency, a) ))
}
def makeDependencyElem(dependency: DependencyDescriptor, artifact: DependencyArtifactDescriptor): Elem =
{
val artifactConfigs = artifact.getConfigurations
val configs = if(artifactConfigs.isEmpty) dependency.getModuleConfigurations else artifactConfigs
val (scope, optional) = getScopeAndOptional(configs)
makeDependencyElem(dependency, scope, optional, artifactClassifier(artifact), artifactType(artifact))
}
def makeDependencyElem(dependency: DependencyDescriptor, scope: Option[String], optional: Boolean, classifier: Option[String], tpe: Option[String]): Elem =
{
val mrid = dependency.getDependencyRevisionId
<dependency>
<groupId>{mrid.getOrganisation}</groupId>
<artifactId>{mrid.getName}</artifactId>
<version>{mrid.getRevision}</version>
{ scopeAndOptional(dependency) }
{ classifier(dependency, includeTypes) }
{ scopeElem(scope) }
{ optionalElem(optional) }
{ classifierElem(classifier) }
{ typeElem(tpe) }
{ exclusions(dependency) }
</dependency>
}
@deprecated("No longer used and will be removed.", "0.12.1")
def classifier(dependency: DependencyDescriptor, includeTypes: Set[String]): NodeSeq =
{
val jarDep = dependency.getAllDependencyArtifacts.filter(d => includeTypes(d.getType)).headOption
jarDep match {
case Some(a) => {
val cl = a.getExtraAttribute("classifier")
if (cl != null) <classifier>{cl}</classifier> else NodeSeq.Empty
}
case _ => NodeSeq.Empty
case Some(a) => classifierElem(artifactClassifier(a))
case None => NodeSeq.Empty
}
}
def artifactType(artifact: DependencyArtifactDescriptor): Option[String] =
Option(artifact.getType).flatMap { tpe => if(tpe == "jar") None else Some(tpe) }
def typeElem(tpe: Option[String]): NodeSeq =
tpe match {
case Some(t) => <type>{t}</type>
case None => NodeSeq.Empty
}
def artifactClassifier(artifact: DependencyArtifactDescriptor): Option[String] =
Option(artifact.getExtraAttribute("classifier"))
def classifierElem(classifier: Option[String]): NodeSeq =
classifier match {
case Some(c) => <classifier>{c}</classifier>
case None => NodeSeq.Empty
}
@deprecated("No longer used and will be removed.", "0.12.1")
def scopeAndOptional(dependency: DependencyDescriptor): NodeSeq =
{
val (scope, opt) = getScopeAndOptional(dependency.getModuleConfigurations)
scopeElem(scope) ++ optionalElem(opt)
}
def scopeElem(scope: Option[String]): NodeSeq = scope match {
case Some(s) => <scope>{s}</scope>
case None => NodeSeq.Empty
case Some(s) => <scope>{s}</scope>
}
def optionalElem(opt: Boolean) = if(opt) <optional>true</optional> else NodeSeq.Empty
def moduleDescriptor(module: ModuleDescriptor) = module.getModuleRevisionId

132
ivy/MergeDescriptors.scala Normal file
View File

@ -0,0 +1,132 @@
package sbt
package ivyint
import java.io.File
import java.net.URI
import java.util.{Collection, Collections => CS}
import CS.singleton
import org.apache.ivy.{core, plugins, util, Ivy}
import core.module.descriptor.{DependencyArtifactDescriptor, DefaultDependencyArtifactDescriptor}
import core.module.descriptor.{DefaultDependencyDescriptor => DDD, DependencyDescriptor}
import core.module.id.{ArtifactId,ModuleId, ModuleRevisionId}
import plugins.namespace.Namespace
import util.extendable.ExtendableItem
private[sbt] object MergeDescriptors
{
def apply(a: DependencyDescriptor, b: DependencyDescriptor): DependencyDescriptor =
{
assert(a.isForce == b.isForce)
assert(a.isChanging == b.isChanging)
assert(a.isTransitive == b.isTransitive)
assert(a.getParentRevisionId == b.getParentRevisionId)
val amrid = a.getDependencyRevisionId
val bmrid = b.getDependencyRevisionId
assert(amrid == bmrid)
val adyn = a.getDynamicConstraintDependencyRevisionId
val bdyn = b.getDynamicConstraintDependencyRevisionId
assert(adyn == bdyn)
assert(a.getNamespace == b.getNamespace)
new MergedDescriptors(a,b)
}
}
// combines the artifacts, configurations, includes, and excludes for DependencyDescriptors `a` and `b`
// that otherwise have equal IDs
private final class MergedDescriptors(a: DependencyDescriptor, b: DependencyDescriptor) extends DependencyDescriptor
{
def getDependencyId = a.getDependencyId
def isForce = a.isForce
def isChanging = a.isChanging
def isTransitive = a.isTransitive
def getNamespace = a.getNamespace
def getParentRevisionId = a.getParentRevisionId
def getDependencyRevisionId = a.getDependencyRevisionId
def getDynamicConstraintDependencyRevisionId = a.getDynamicConstraintDependencyRevisionId
def getModuleConfigurations = concat(a.getModuleConfigurations, b.getModuleConfigurations)
def getDependencyConfigurations(moduleConfiguration: String, requestedConfiguration: String) =
concat(a.getDependencyConfigurations(moduleConfiguration, requestedConfiguration), b.getDependencyConfigurations(moduleConfiguration))
def getDependencyConfigurations(moduleConfiguration: String) =
concat(a.getDependencyConfigurations(moduleConfiguration), b.getDependencyConfigurations(moduleConfiguration))
def getDependencyConfigurations(moduleConfigurations: Array[String]) =
concat(a.getDependencyConfigurations(moduleConfigurations), b.getDependencyConfigurations(moduleConfigurations))
def getAllDependencyArtifacts = concatArtifacts(a, a.getAllDependencyArtifacts, b, b.getAllDependencyArtifacts)
def getDependencyArtifacts(moduleConfigurations: String) =
concatArtifacts(a, a.getDependencyArtifacts(moduleConfigurations), b, b.getDependencyArtifacts(moduleConfigurations))
def getDependencyArtifacts(moduleConfigurations: Array[String]) =
concatArtifacts(a, a.getDependencyArtifacts(moduleConfigurations), b, b.getDependencyArtifacts(moduleConfigurations))
def getAllIncludeRules = concat(a.getAllIncludeRules, b.getAllIncludeRules)
def getIncludeRules(moduleConfigurations: String) =
concat(a.getIncludeRules(moduleConfigurations), b.getIncludeRules(moduleConfigurations))
def getIncludeRules(moduleConfigurations: Array[String]) =
concat(a.getIncludeRules(moduleConfigurations), b.getIncludeRules(moduleConfigurations))
private[this] def concatArtifacts(a: DependencyDescriptor, as: Array[DependencyArtifactDescriptor], b: DependencyDescriptor, bs: Array[DependencyArtifactDescriptor]) =
{
if(as.isEmpty)
if(bs.isEmpty) as
else defaultArtifact(a) +: explicitConfigurations(b, bs)
else if(bs.isEmpty) explicitConfigurations(a, as) :+ defaultArtifact(b)
else concat(explicitConfigurations(a, as), explicitConfigurations(b, bs))
}
private[this] def explicitConfigurations(base: DependencyDescriptor, arts: Array[DependencyArtifactDescriptor]): Array[DependencyArtifactDescriptor] =
arts map { art => explicitConfigurations(base, art) }
private[this] def explicitConfigurations(base: DependencyDescriptor, art: DependencyArtifactDescriptor): DependencyArtifactDescriptor =
{
val aConfs = art.getConfigurations
if(aConfs == null || aConfs.isEmpty)
copyWithConfigurations(art, base.getModuleConfigurations)
else
art
}
private[this] def defaultArtifact(a: DependencyDescriptor): DependencyArtifactDescriptor =
{
val dd = new DefaultDependencyArtifactDescriptor(a, a.getDependencyRevisionId.getName, "jar", "jar", null, null)
addConfigurations(dd, a.getModuleConfigurations)
dd
}
private[this] def copyWithConfigurations(dd: DependencyArtifactDescriptor, confs: Seq[String]): DependencyArtifactDescriptor =
{
val dextra = dd.getQualifiedExtraAttributes
val newd = new DefaultDependencyArtifactDescriptor(dd.getDependencyDescriptor, dd.getName, dd.getType, dd.getExt, dd.getUrl, dextra)
addConfigurations(newd, confs)
newd
}
private[this] def addConfigurations(dd: DefaultDependencyArtifactDescriptor, confs: Seq[String]): Unit =
confs foreach dd.addConfiguration
private[this] def concat[T: ClassManifest](a: Array[T], b: Array[T]): Array[T] = (a ++ b).distinct.toArray
def getAllExcludeRules = concat(a.getAllExcludeRules, b.getAllExcludeRules)
def getExcludeRules(moduleConfigurations: String) = concat(a.getExcludeRules(moduleConfigurations), b.getExcludeRules(moduleConfigurations))
def getExcludeRules(moduleConfigurations: Array[String]) = concat(a.getExcludeRules(moduleConfigurations), b.getExcludeRules(moduleConfigurations))
def doesExclude(moduleConfigurations: Array[String], artifactId: ArtifactId) = a.doesExclude(moduleConfigurations, artifactId) || b.doesExclude(moduleConfigurations, artifactId)
def canExclude = a.canExclude || b.canExclude
def asSystem = this
def clone(revision: ModuleRevisionId) = new MergedDescriptors(a.clone(revision), b.clone(revision))
def getAttribute(name: String): String = a.getAttribute(name)
def getAttributes = a.getAttributes
def getExtraAttribute(name: String) = a.getExtraAttribute(name)
def getExtraAttributes = a.getExtraAttributes
def getQualifiedExtraAttributes = a.getQualifiedExtraAttributes
def getSourceModule = a.getSourceModule
}