diff --git a/ivy/CustomPomParser.scala b/ivy/CustomPomParser.scala
index aced1d48a..668aa5cff 100644
--- a/ivy/CustomPomParser.scala
+++ b/ivy/CustomPomParser.scala
@@ -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) {
diff --git a/ivy/Ivy.scala b/ivy/Ivy.scala
index 2866c8484..93ab5e94b 100644
--- a/ivy/Ivy.scala
+++ b/ivy/Ivy.scala
@@ -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) =
diff --git a/ivy/IvyActions.scala b/ivy/IvyActions.scala
index 902b84d95..a2d040a97 100644
--- a/ivy/IvyActions.scala
+++ b/ivy/IvyActions.scala
@@ -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])
diff --git a/ivy/IvyInterface.scala b/ivy/IvyInterface.scala
index 9a1708059..0f37aa241 100644
--- a/ivy/IvyInterface.scala
+++ b/ivy/IvyInterface.scala
@@ -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
diff --git a/ivy/MakePom.scala b/ivy/MakePom.scala
index 31a83a705..5dc39f069 100644
--- a/ivy/MakePom.scala
+++ b/ivy/MakePom.scala
@@ -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)
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
{mrid.getOrganisation}
{mrid.getName}
{mrid.getRevision}
- { scopeAndOptional(dependency) }
- { classifier(dependency, includeTypes) }
+ { scopeElem(scope) }
+ { optionalElem(optional) }
+ { classifierElem(classifier) }
+ { typeElem(tpe) }
{ exclusions(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) {cl} 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) => {t}
+ 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) => {c}
+ 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) => {s}
case None => NodeSeq.Empty
+ case Some(s) => {s}
}
def optionalElem(opt: Boolean) = if(opt) true else NodeSeq.Empty
def moduleDescriptor(module: ModuleDescriptor) = module.getModuleRevisionId
diff --git a/ivy/MergeDescriptors.scala b/ivy/MergeDescriptors.scala
new file mode 100644
index 000000000..f46f90501
--- /dev/null
+++ b/ivy/MergeDescriptors.scala
@@ -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
+}