diff --git a/ivy/CustomPomParser.scala b/ivy/CustomPomParser.scala index cc1fb37d0..8f64fd950 100644 --- a/ivy/CustomPomParser.scala +++ b/ivy/CustomPomParser.scala @@ -1,14 +1,18 @@ package sbt - import org.apache.ivy.{core, plugins} + import org.apache.ivy.{core, plugins, util} import core.module.id.ModuleRevisionId import core.module.descriptor.{DefaultArtifact, DefaultExtendsDescriptor, DefaultModuleDescriptor, ModuleDescriptor} + import core.module.descriptor.{DefaultDependencyDescriptor, DependencyDescriptor} import plugins.parser.{m2, ModuleDescriptorParser, ModuleDescriptorParserRegistry, ParserSettings} import m2.{PomModuleDescriptorBuilder, PomModuleDescriptorParser} import plugins.repository.Resource + import plugins.namespace.NamespaceTransformer + import util.extendable.ExtendableItem import java.io.{File, InputStream} import java.net.URL + import java.util.regex.Pattern final class CustomPomParser(delegate: ModuleDescriptorParser, transform: (ModuleDescriptorParser, ModuleDescriptor) => ModuleDescriptor) extends ModuleDescriptorParser { @@ -28,6 +32,7 @@ object CustomPomParser { val SbtVersionKey = "sbtVersion" val ScalaVersionKey = "scalaVersion" + val ExtraAttributesKey = "extraDependencyAttributes" // packagings that should be jars, but that Ivy doesn't handle as jars val JarPackagings = Set("eclipse-plugin") @@ -38,16 +43,37 @@ object CustomPomParser def defaultTransform(parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor = { - import collection.JavaConverters._ + import collection.JavaConverters._ + // The element of the pom is used to store additional metadata for sbt plugins. + // This is done because the pom XSD does not appear to allow extra metadata anywhere else. + // The pom.xml does not need to be readable by maven because these are only enabled for sbt plugins. + // However, the pom.xml needs to be valid because other tools, like repository managers may read the pom.xml. val properties = PomModuleDescriptorBuilder.extractPomProperties(md.getExtraInfo).asInstanceOf[java.util.Map[String,String]].asScala.toMap + + // Extracts extra attributes (currently, sbt and Scala versions) stored in the element of the pom. + // These are attached to the module itself. val filtered = shouldBeUnqualified(properties) + + // Extracts extra attributes for the dependencies. + // Because the tag in pom.xml cannot include additional metadata, + // sbt includes extra attributes in a 'extraDependencyAttributes' property. + // This is read/written from/to a pure string (no element structure) because Ivy only + // parses the immediate text nodes of the property. + val extraDepAttributes = getDependencyExtra(filtered) + + // Fixes up the detected extension in some cases missed by Ivy. val convertArtifacts = artifactExtIncorrect(md) - if(filtered.isEmpty && !convertArtifacts) md else addExtra(filtered, parser, md) + + val unqualify = filtered - ExtraAttributesKey + if(unqualify.isEmpty && extraDepAttributes.isEmpty && !convertArtifacts) + md + else + addExtra(unqualify, extraDepAttributes, parser, md) } private[this] def artifactExtIncorrect(md: ModuleDescriptor): Boolean = 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 (k,_) => k == SbtVersionKey || k == ScalaVersionKey } + m.filter { case (SbtVersionKey | ScalaVersionKey | ExtraAttributesKey,_) => true; case _ => false } private[this] def condAddExtra(properties: Map[String, String], id: ModuleRevisionId): ModuleRevisionId = @@ -55,13 +81,74 @@ object CustomPomParser private[this] def addExtra(properties: Map[String, String], id: ModuleRevisionId): ModuleRevisionId = { import collection.JavaConverters._ - val oldExtra = id.getQualifiedExtraAttributes.asInstanceOf[java.util.Map[String,String]].asScala + val oldExtra = qualifiedExtra(id) val newExtra = (oldExtra ++ properties).asJava ModuleRevisionId.newInstance(id.getOrganisation, id.getName, id.getBranch, id.getRevision, newExtra) } + private[this] def getDependencyExtra(m: Map[String, String]): Map[ModuleRevisionId, Map[String,String]] = + (m get ExtraAttributesKey) match { + case None => Map.empty + case Some(str) => + def processDep(m: ModuleRevisionId) = (simplify(m), filterCustomExtra(m, include=true)) + readDependencyExtra(str).map(processDep).toMap + } + + def qualifiedExtra(item: ExtendableItem): Map[String,String] = + { + import collection.JavaConverters._ + item.getQualifiedExtraAttributes.asInstanceOf[java.util.Map[String,String]].asScala.toMap + } + def filterCustomExtra(item: ExtendableItem, include: Boolean): Map[String,String] = + (qualifiedExtra(item) filterKeys { k => qualifiedIsExtra(k) == include }) + + def writeDependencyExtra(s: Seq[DependencyDescriptor]): Seq[String] = + s.flatMap { dd => + val revId = dd.getDependencyRevisionId + if(filterCustomExtra(revId, include=true).isEmpty) + Nil + else + revId.encodeToString :: Nil + } + + // parses the sequence of dependencies with extra attribute information, with one dependency per line + def readDependencyExtra(s: String): Seq[ModuleRevisionId] = + LinesP.split(s).map(_.trim).filter(!_.isEmpty).map(ModuleRevisionId.decode) + + private[this] val LinesP = Pattern.compile("(?m)^") + + def qualifiedIsExtra(k: String): Boolean = k.endsWith(ScalaVersionKey) || k.endsWith(SbtVersionKey) + + // Reduces the id to exclude custom extra attributes + // This makes the id suitable as a key to associate a dependency parsed from a element + // with the extra attributes from the section + def simplify(id: ModuleRevisionId): ModuleRevisionId = + { + import collection.JavaConverters._ + ModuleRevisionId.newInstance(id.getOrganisation, id.getName, id.getBranch, id.getRevision, filterCustomExtra(id, include=false).asJava) + } + + private[this] def addExtra(dep: DependencyDescriptor, extra: Map[ModuleRevisionId, Map[String, String]]): DependencyDescriptor = + { + val extras = if(extra.isEmpty) None else extra get simplify(dep.getDependencyRevisionId) + extras match { + case None => dep + case Some(extraAttrs) => transform(dep, revId => addExtra(extraAttrs, revId)) + } + } + private[this] def transform(dep: DependencyDescriptor, f: ModuleRevisionId => ModuleRevisionId): DependencyDescriptor = + DefaultDependencyDescriptor.transformInstance(dep, namespaceTransformer(dep.getDependencyRevisionId, f), false) + private[this] def extraTransformer(txId: ModuleRevisionId, extra: Map[String, String]): NamespaceTransformer = + namespaceTransformer(txId, revId => addExtra(extra, revId) ) + + private[this] def namespaceTransformer(txId: ModuleRevisionId, f: ModuleRevisionId => ModuleRevisionId): NamespaceTransformer = + new NamespaceTransformer { + def transform(revId: ModuleRevisionId): ModuleRevisionId = if(revId == txId) f(revId) else revId + def isIdentity = false + } + import collection.JavaConverters._ - def addExtra(properties: Map[String, String], parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor = + def addExtra(properties: Map[String, String], dependencyExtra: Map[ModuleRevisionId, Map[String,String]], parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor = { val dmd = new DefaultModuleDescriptor(parser, md.getResource) @@ -81,7 +168,7 @@ object CustomPomParser for(l <- md.getLicenses) dmd.addLicense(l) 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) - for( dd <- md.getDependencies ) dmd.addDependency(dd) + for( dd <- md.getDependencies ) dmd.addDependency(addExtra(dd, dependencyExtra)) for( ed <- md.getInheritedDescriptors) dmd.addInheritedDescriptor( new DefaultExtendsDescriptor( mrid, resolvedMrid, ed.getLocation, ed.getExtendsTypes) ) for( conf <- md.getConfigurations) { diff --git a/ivy/MakePom.scala b/ivy/MakePom.scala index fc0a90c16..262a1ee35 100644 --- a/ivy/MakePom.scala +++ b/ivy/MakePom.scala @@ -46,8 +46,11 @@ class MakePom(val log: Logger) { makeStartYear(moduleInfo) } { makeOrganization(moduleInfo) } { extra } - { makeProperties(module) } - { makeDependencies(module, configurations) } + { + val deps = depsInConfs(module, configurations) + makeProperties(module, deps) ++ + makeDependencies(deps) + } { makeRepositories(ivy.getSettings, allRepositories, filterRepositories) } ) @@ -74,10 +77,12 @@ class MakePom(val log: Logger) { moduleInfo.organizationHomepage map { h => {h} } getOrElse NodeSeq.Empty } } - def makeProperties(module: ModuleDescriptor): NodeSeq = + def makeProperties(module: ModuleDescriptor, dependencies: Seq[DependencyDescriptor]): NodeSeq = { val extra = IvySbt.getExtraAttributes(module) - if(extra.isEmpty) NodeSeq.Empty else makeProperties(extra) + val depExtra = CustomPomParser.writeDependencyExtra(dependencies).mkString("\n") + val allExtra = if(depExtra.isEmpty) extra else extra.updated(CustomPomParser.ExtraAttributesKey, depExtra) + if(allExtra.isEmpty) NodeSeq.Empty else makeProperties(allExtra) } def makeProperties(extra: Map[String,String]): NodeSeq = { @@ -110,15 +115,13 @@ class MakePom(val log: Logger) } val IgnoreTypes: Set[String] = Set(Artifact.SourceType, Artifact.DocType, Artifact.PomType) - def makeDependencies(module: ModuleDescriptor, configurations: Option[Iterable[Configuration]]): NodeSeq = - { - val dependencies = depsInConfs(module, configurations) - if(dependencies.isEmpty) NodeSeq.Empty + def makeDependencies(dependencies: Seq[DependencyDescriptor]): NodeSeq = + if(dependencies.isEmpty) + NodeSeq.Empty else { dependencies.map(makeDependency) } - } def makeDependency(dependency: DependencyDescriptor): NodeSeq = { diff --git a/sbt/src/sbt-test/project/transitive-plugins/a/build.sbt b/sbt/src/sbt-test/project/transitive-plugins/a/build.sbt new file mode 100644 index 000000000..499e140e5 --- /dev/null +++ b/sbt/src/sbt-test/project/transitive-plugins/a/build.sbt @@ -0,0 +1,15 @@ +publishTo <<= baseDirectory(x => + Some(Resolver.file("test-publish", x / "../repo/")) +) + +resolvers <+= baseDirectory(x => + "test" at (x / "../repo/").asURL.toString +) + +name := "demo1" + +organization := "org.example" + +version := "0.1" + +sbtPlugin := true diff --git a/sbt/src/sbt-test/project/transitive-plugins/b/build.sbt b/sbt/src/sbt-test/project/transitive-plugins/b/build.sbt new file mode 100644 index 000000000..70aa6ffbf --- /dev/null +++ b/sbt/src/sbt-test/project/transitive-plugins/b/build.sbt @@ -0,0 +1,17 @@ +publishTo <<= baseDirectory(x => + Some(Resolver.file("test-publish", x / "../repo/")) +) + +resolvers <+= baseDirectory(x => + "test" at (x / "../repo/").asURL.toString +) + +name := "demo2" + +organization := "org.example" + +version := "0.2" + +sbtPlugin := true + +addSbtPlugin("org.example" % "demo1" % "0.1") diff --git a/sbt/src/sbt-test/project/transitive-plugins/c/build.sbt b/sbt/src/sbt-test/project/transitive-plugins/c/build.sbt new file mode 100644 index 000000000..c2e030dc5 --- /dev/null +++ b/sbt/src/sbt-test/project/transitive-plugins/c/build.sbt @@ -0,0 +1,19 @@ +publishTo <<= baseDirectory(x => + Some(Resolver.file("test-publish", x / "../repo/")) +) + +resolvers <+= baseDirectory(x => + "test" at (x / "../repo/").asURL.toString +) + +name := "demo3" + +organization := "org.example" + +version := "0.3" + +sbtPlugin := true + +//addSbtPlugin("org.example" % "demo1" % "0.1") + +addSbtPlugin("org.example" % "demo2" % "0.2") diff --git a/sbt/src/sbt-test/project/transitive-plugins/project/Build.scala b/sbt/src/sbt-test/project/transitive-plugins/project/Build.scala new file mode 100644 index 000000000..5b0cf40e4 --- /dev/null +++ b/sbt/src/sbt-test/project/transitive-plugins/project/Build.scala @@ -0,0 +1,10 @@ +import sbt._ +import Keys._ + +object Build extends Build +{ + lazy val root = Project("root", file(".")) aggregate(a,b,c) + lazy val a = Project("a", file("a")) + lazy val b = Project("b", file("b")) + lazy val c = Project("c", file("c")) +} \ No newline at end of file diff --git a/sbt/src/sbt-test/project/transitive-plugins/test b/sbt/src/sbt-test/project/transitive-plugins/test new file mode 100644 index 000000000..9cef436b6 --- /dev/null +++ b/sbt/src/sbt-test/project/transitive-plugins/test @@ -0,0 +1,10 @@ +-> b/update +-> c/update + +> a/publish +> b/update +-> c/update + +> b/publish +> c/update +> c/publish \ No newline at end of file