From 152d51465d50bc8f90880a54f8b40de7a6cf627d Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Mon, 20 Aug 2012 15:55:50 -0400 Subject: [PATCH] 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. --- ivy/CustomPomParser.scala | 11 +++- ivy/Ivy.scala | 97 +++++++++++++++++++-------- ivy/IvyActions.scala | 2 +- ivy/IvyInterface.scala | 1 + ivy/MakePom.scala | 58 ++++++++++++---- ivy/MergeDescriptors.scala | 132 +++++++++++++++++++++++++++++++++++++ 6 files changed, 257 insertions(+), 44 deletions(-) create mode 100644 ivy/MergeDescriptors.scala 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 +}