diff --git a/ivy/src/main/java/sbt/mavenint/SbtPomExtraProperties.java b/ivy/src/main/java/sbt/mavenint/SbtPomExtraProperties.java new file mode 100644 index 000000000..fde6af15b --- /dev/null +++ b/ivy/src/main/java/sbt/mavenint/SbtPomExtraProperties.java @@ -0,0 +1,25 @@ +package sbt.mavenint; + +/** + * Extra properties we dump from Aether into the properties list. + */ +public class SbtPomExtraProperties { + + public static final String MAVEN_PACKAGING_KEY = "sbt.pom.packaging"; + public static final String SCALA_VERSION_KEY = "sbt.pom.scalaversion"; + public static final String SBT_VERSION_KEY = "sbt.pom.sbtversion"; + + public static final String POM_INFO_KEY_PREFIX = "info."; + public static final String POM_SCALA_VERSION = "scalaVersion"; + public static final String POM_SBT_VERSION = "sbtVersion"; + public static final String POM_API_KEY = "info.apiURL"; + + public static final String LICENSE_COUNT_KEY = "license.count"; + + public static String makeLicenseName(int i) { + return "license." + i + ".name"; + } + public static String makeLicenseUrl(int i) { + return "license." + i + ".url"; + } +} diff --git a/ivy/src/main/scala/org/apache/ivy/plugins/parser/m2/ReplaceMavenConfigurationMappings.scala b/ivy/src/main/scala/org/apache/ivy/plugins/parser/m2/ReplaceMavenConfigurationMappings.scala index 68a9dad40..0e85d5b6b 100644 --- a/ivy/src/main/scala/org/apache/ivy/plugins/parser/m2/ReplaceMavenConfigurationMappings.scala +++ b/ivy/src/main/scala/org/apache/ivy/plugins/parser/m2/ReplaceMavenConfigurationMappings.scala @@ -26,6 +26,11 @@ import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor; */ object ReplaceMavenConfigurationMappings { + def addMappings(dd: DefaultDependencyDescriptor, scope: String, isOptional: Boolean) = { + val mapping = ReplaceMavenConfigurationMappings.REPLACEMENT_MAVEN_MAPPINGS.get(scope) + mapping.addMappingConfs(dd, isOptional) + } + val REPLACEMENT_MAVEN_MAPPINGS = { // Here we copy paste from Ivy val REPLACEMENT_MAPPINGS = new java.util.HashMap[String, PomModuleDescriptorBuilder.ConfMapper] diff --git a/ivy/src/main/scala/sbt/ConvertResolver.scala b/ivy/src/main/scala/sbt/ConvertResolver.scala index 35cfb669f..26abec09c 100644 --- a/ivy/src/main/scala/sbt/ConvertResolver.scala +++ b/ivy/src/main/scala/sbt/ConvertResolver.scala @@ -19,6 +19,8 @@ import org.apache.ivy.util.{ FileUtil, ChecksumHelper } import org.apache.ivy.core.module.descriptor.{ Artifact => IArtifact } private[sbt] object ConvertResolver { + import UpdateOptions.ResolverConverter + /** * This class contains all the reflective lookups used in the * checksum-friendly URL publishing shim. @@ -94,15 +96,25 @@ private[sbt] object ConvertResolver { } } - /** Converts the given sbt resolver into an Ivy resolver..*/ - def apply(r: Resolver, settings: IvySettings, log: Logger) = - { + /** Converts the given sbt resolver into an Ivy resolver. */ + @deprecated("0.13.8", "Use the variant with updateOptions") + def apply(r: Resolver, settings: IvySettings, log: Logger): DependencyResolver = + apply(r, settings, UpdateOptions(), log) + + /** Converts the given sbt resolver into an Ivy resolver. */ + def apply(r: Resolver, settings: IvySettings, updateOptions: UpdateOptions, log: Logger): DependencyResolver = + (updateOptions.resolverConverter orElse defaultConvert)((r, settings, log)) + + /** The default implementation of converter. */ + lazy val defaultConvert: ResolverConverter = { + case (r, settings, log) => r match { case repo: MavenRepository => { val pattern = Collections.singletonList(Resolver.resolvePattern(repo.root, Resolver.mavenStyleBasePattern)) final class PluginCapableResolver extends IBiblioResolver with ChecksumFriendlyURLResolver with DescriptorRequired { - def setPatterns() { // done this way for access to protected methods. + def setPatterns() { + // done this way for access to protected methods. setArtifactPatterns(pattern) setIvyPatterns(pattern) } @@ -163,7 +175,7 @@ private[sbt] object ConvertResolver { case repo: ChainedResolver => IvySbt.resolverChain(repo.name, repo.resolvers, false, settings, log) case repo: RawRepository => repo.resolver } - } + } private sealed trait DescriptorRequired extends BasicResolver { override def getDependency(dd: DependencyDescriptor, data: ResolveData) = diff --git a/ivy/src/main/scala/sbt/CustomPomParser.scala b/ivy/src/main/scala/sbt/CustomPomParser.scala index 202216700..d9ceff32a 100644 --- a/ivy/src/main/scala/sbt/CustomPomParser.scala +++ b/ivy/src/main/scala/sbt/CustomPomParser.scala @@ -12,7 +12,9 @@ import org.apache.ivy.util.extendable.ExtendableItem import java.io.{ File, InputStream } import java.net.URL import java.util.regex.Pattern +import sbt.mavenint.{ PomExtraDependencyAttributes, SbtPomExtraProperties } +@deprecated("0.13.8", "We now use an Aether-based pom parser.") final class CustomPomParser(delegate: ModuleDescriptorParser, transform: (ModuleDescriptorParser, ModuleDescriptor) => ModuleDescriptor) extends ModuleDescriptorParser { override def parseDescriptor(ivySettings: ParserSettings, descriptorURL: URL, validate: Boolean) = transform(this, delegate.parseDescriptor(ivySettings, descriptorURL, validate)) @@ -26,21 +28,23 @@ final class CustomPomParser(delegate: ModuleDescriptorParser, transform: (Module override def getType() = delegate.getType() override def getMetadataArtifact(mrid: ModuleRevisionId, res: Resource) = delegate.getMetadataArtifact(mrid, res) } +@deprecated("0.13.8", "We now use an Aether-based pom parser.") object CustomPomParser { // Evil hackery to override the default maven pom mappings. ReplaceMavenConfigurationMappings.init() /** The key prefix that indicates that this is used only to store extra information and is not intended for dependency resolution.*/ - val InfoKeyPrefix = "info." - val ApiURLKey = "info.apiURL" + val InfoKeyPrefix = SbtPomExtraProperties.POM_INFO_KEY_PREFIX + val ApiURLKey = SbtPomExtraProperties.POM_API_KEY - val SbtVersionKey = "sbtVersion" - val ScalaVersionKey = "scalaVersion" - val ExtraAttributesKey = "extraDependencyAttributes" + val SbtVersionKey = PomExtraDependencyAttributes.SbtVersionKey + val ScalaVersionKey = PomExtraDependencyAttributes.ScalaVersionKey + val ExtraAttributesKey = PomExtraDependencyAttributes.ExtraAttributesKey private[this] val unqualifiedKeys = Set(SbtVersionKey, ScalaVersionKey, ExtraAttributesKey, ApiURLKey) // packagings that should be jars, but that Ivy doesn't handle as jars + // TODO - move this elsewhere. val JarPackagings = Set("eclipse-plugin", "hk2-jar", "orbit", "scala-jar") val default = new CustomPomParser(PomModuleDescriptorParser.getInstance, defaultTransform) @@ -123,46 +127,24 @@ object CustomPomParser { } 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 - } + PomExtraDependencyAttributes.getDependencyExtra(m) - def qualifiedExtra(item: ExtendableItem): Map[String, String] = - { - import collection.JavaConverters._ - item.getQualifiedExtraAttributes.asInstanceOf[java.util.Map[String, String]].asScala.toMap - } + def qualifiedExtra(item: ExtendableItem): Map[String, String] = PomExtraDependencyAttributes.qualifiedExtra(item) 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 - } + PomExtraDependencyAttributes.writeDependencyExtra(s) // 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) + def readDependencyExtra(s: String): Seq[ModuleRevisionId] = PomExtraDependencyAttributes.readDependencyExtra(s) - private[this] val LinesP = Pattern.compile("(?m)^") - - def qualifiedIsExtra(k: String): Boolean = k.endsWith(ScalaVersionKey) || k.endsWith(SbtVersionKey) + def qualifiedIsExtra(k: String): Boolean = PomExtraDependencyAttributes.qualifiedIsExtra(k) // 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) - } + def simplify(id: ModuleRevisionId): ModuleRevisionId = PomExtraDependencyAttributes.simplify(id) private[this] def addExtra(dep: DependencyDescriptor, extra: Map[ModuleRevisionId, Map[String, String]]): DependencyDescriptor = { diff --git a/ivy/src/main/scala/sbt/Ivy.scala b/ivy/src/main/scala/sbt/Ivy.scala index 32f2f6440..be86d1131 100644 --- a/ivy/src/main/scala/sbt/Ivy.scala +++ b/ivy/src/main/scala/sbt/Ivy.scala @@ -14,19 +14,20 @@ import java.util.{ Collection, Collections => CS, Date } import CS.singleton import org.apache.ivy.Ivy +import org.apache.ivy.core.report.ResolveReport import org.apache.ivy.core.{ IvyPatternHelper, LogOptions, IvyContext } -import org.apache.ivy.core.cache.{ CacheMetadataOptions, DefaultRepositoryCacheManager, ModuleDescriptorWriter } +import org.apache.ivy.core.cache.{ ResolutionCacheManager, CacheMetadataOptions, DefaultRepositoryCacheManager, ModuleDescriptorWriter } import org.apache.ivy.core.event.EventManager import org.apache.ivy.core.module.descriptor.{ Artifact => IArtifact, DefaultArtifact, DefaultDependencyArtifactDescriptor, MDArtifact } import org.apache.ivy.core.module.descriptor.{ DefaultDependencyDescriptor, DefaultModuleDescriptor, DependencyDescriptor, ModuleDescriptor, License } import org.apache.ivy.core.module.descriptor.{ OverrideDependencyDescriptorMediator } import org.apache.ivy.core.module.id.{ ArtifactId, ModuleId, ModuleRevisionId } -import org.apache.ivy.core.resolve.{ IvyNode, ResolveData, ResolvedModuleRevision, ResolveEngine } +import org.apache.ivy.core.resolve._ import org.apache.ivy.core.settings.IvySettings import org.apache.ivy.core.sort.SortEngine import org.apache.ivy.plugins.latest.{ LatestStrategy, LatestRevisionStrategy, ArtifactInfo } import org.apache.ivy.plugins.matcher.PatternMatcher -import org.apache.ivy.plugins.parser.m2.PomModuleDescriptorParser +import org.apache.ivy.plugins.parser.m2.{ PomModuleDescriptorParser } import org.apache.ivy.plugins.resolver.{ ChainResolver, DependencyResolver, BasicResolver } import org.apache.ivy.plugins.resolver.util.{ HasLatestStrategy, ResolvedResource } import org.apache.ivy.plugins.version.ExactVersionMatcher @@ -68,9 +69,11 @@ final class IvySbt(val configuration: IvyConfiguration) { private lazy val settings: IvySettings = { val is = new IvySettings + is.setBaseDir(baseDirectory) is.setCircularDependencyStrategy(configuration.updateOptions.circularDependencyLevel.ivyStrategy) CustomPomParser.registerDefault + configuration match { case e: ExternalIvyConfiguration => IvySbt.addResolvers(e.extraResolvers, is, configuration.log) @@ -104,6 +107,7 @@ final class IvySbt(val configuration: IvyConfiguration) { super.bind() } } + i.setSettings(settings) i.bind() i.getLoggerEngine.pushLogger(new IvyLoggerInterface(configuration.log)) @@ -284,7 +288,7 @@ private[sbt] object IvySbt { def resolverChain(name: String, resolvers: Seq[Resolver], localOnly: Boolean, settings: IvySettings, log: Logger): DependencyResolver = resolverChain(name, resolvers, localOnly, settings, UpdateOptions(), log) def resolverChain(name: String, resolvers: Seq[Resolver], localOnly: Boolean, settings: IvySettings, updateOptions: UpdateOptions, log: Logger): DependencyResolver = { - def mapResolvers(rs: Seq[Resolver]) = rs.map(r => ConvertResolver(r, settings, log)) + def mapResolvers(rs: Seq[Resolver]) = rs.map(r => ConvertResolver(r, settings, updateOptions, log)) val (projectResolvers, rest) = resolvers.partition(_.name == "inter-project") if (projectResolvers.isEmpty) new ivyint.SbtChainResolver(name, mapResolvers(rest), settings, updateOptions, log) else { diff --git a/ivy/src/main/scala/sbt/MakePom.scala b/ivy/src/main/scala/sbt/MakePom.scala index eba0cd94d..2cb7331e6 100644 --- a/ivy/src/main/scala/sbt/MakePom.scala +++ b/ivy/src/main/scala/sbt/MakePom.scala @@ -8,6 +8,9 @@ package sbt import java.io.File + +import sbt.mavenint.PomExtraDependencyAttributes + // 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.{ Elem, Node => XNode, NodeSeq, PrettyPrinter, PrefixedAttribute } @@ -17,6 +20,7 @@ import org.apache.ivy.Ivy import org.apache.ivy.core.settings.IvySettings import org.apache.ivy.core.module.descriptor.{ DependencyArtifactDescriptor, DependencyDescriptor, License, ModuleDescriptor, ExcludeRule } import org.apache.ivy.plugins.resolver.{ ChainResolver, DependencyResolver, IBiblioResolver } +import ivyint.CustomRemoteMavenResolver class MakePom(val log: Logger) { @deprecated("Use `write(Ivy, ModuleDescriptor, ModuleInfo, Option[Iterable[Configuration]], Set[String], NodeSeq, XNode => XNode, MavenRepository => Boolean, Boolean, File)` instead", "0.11.2") @@ -119,12 +123,12 @@ class MakePom(val log: Logger) { def makeProperties(module: ModuleDescriptor, dependencies: Seq[DependencyDescriptor]): NodeSeq = { val extra = IvySbt.getExtraAttributes(module) - val depExtra = CustomPomParser.writeDependencyExtra(dependencies).mkString("\n") - val allExtra = if (depExtra.isEmpty) extra else extra.updated(CustomPomParser.ExtraAttributesKey, depExtra) + val depExtra = PomExtraDependencyAttributes.writeDependencyExtra(dependencies).mkString("\n") + val allExtra = if (depExtra.isEmpty) extra else extra.updated(PomExtraDependencyAttributes.ExtraAttributesKey, depExtra) if (allExtra.isEmpty) NodeSeq.Empty else makeProperties(allExtra) } def makeProperties(extra: Map[String, String]): NodeSeq = { - def _extraAttributes(k: String) = if (k == CustomPomParser.ExtraAttributesKey) xmlSpacePreserve else scala.xml.Null + def _extraAttributes(k: String) = if (k == PomExtraDependencyAttributes.ExtraAttributesKey) xmlSpacePreserve else scala.xml.Null { for ((key, value) <- extra) yield ({ value }).copy(label = key, attributes = _extraAttributes(key)) } @@ -330,6 +334,8 @@ class MakePom(val log: Logger) { val repositories = if (includeAll) allResolvers(settings) else resolvers(settings.getDefaultResolver) val mavenRepositories = repositories.flatMap { + case m: CustomRemoteMavenResolver if m.repo.root != DefaultMavenRepository.root => + MavenRepository(m.repo.name, m.repo.root) :: Nil case m: IBiblioResolver if m.isM2compatible && m.getRoot != DefaultMavenRepository.root => MavenRepository(m.getName, m.getRoot) :: Nil case _ => Nil diff --git a/ivy/src/main/scala/sbt/ModuleID.scala b/ivy/src/main/scala/sbt/ModuleID.scala index 178940666..c0e8670d5 100644 --- a/ivy/src/main/scala/sbt/ModuleID.scala +++ b/ivy/src/main/scala/sbt/ModuleID.scala @@ -5,6 +5,8 @@ package sbt import java.net.URL +import sbt.mavenint.SbtPomExtraProperties + final case class ModuleID(organization: String, name: String, revision: String, configurations: Option[String] = None, isChanging: Boolean = false, isTransitive: Boolean = true, isForce: Boolean = false, explicitArtifacts: Seq[Artifact] = Nil, exclusions: Seq[ExclusionRule] = Nil, extraAttributes: Map[String, String] = Map.empty, crossVersion: CrossVersion = CrossVersion.Disabled) { override def toString: String = organization + ":" + name + ":" + revision + @@ -15,7 +17,7 @@ final case class ModuleID(organization: String, name: String, revision: String, def extraString: String = extraDependencyAttributes.map { case (k, v) => k + "=" + v } mkString ("(", ", ", ")") /** Returns the extra attributes except for ones marked as information only (ones that typically would not be used for dependency resolution). */ - def extraDependencyAttributes: Map[String, String] = extraAttributes.filterKeys(!_.startsWith(CustomPomParser.InfoKeyPrefix)) + def extraDependencyAttributes: Map[String, String] = extraAttributes.filterKeys(!_.startsWith(SbtPomExtraProperties.POM_INFO_KEY_PREFIX)) @deprecated("Use `cross(CrossVersion)`, the variant accepting a CrossVersion value constructed by a member of the CrossVersion object instead.", "0.12.0") def cross(v: Boolean): ModuleID = cross(if (v) CrossVersion.binary else CrossVersion.Disabled) diff --git a/ivy/src/main/scala/sbt/Resolver.scala b/ivy/src/main/scala/sbt/Resolver.scala index 406c27e78..ce3a2830f 100644 --- a/ivy/src/main/scala/sbt/Resolver.scala +++ b/ivy/src/main/scala/sbt/Resolver.scala @@ -30,8 +30,23 @@ final class RawRepository(val resolver: DependencyResolver) extends Resolver { } } sealed case class ChainedResolver(name: String, resolvers: Seq[Resolver]) extends Resolver + +/** An instance of a remote maven repository. Note: This will use Aether/Maven to resolve artifacts. */ sealed case class MavenRepository(name: String, root: String) extends Resolver { override def toString = name + ": " + root + def isCache: Boolean = false +} + +/** + * An instance of maven CACHE directory. You cannot treat a cache directory the same as a a remote repository because + * the metadata is different (see Aether ML discussion). + */ +final class MavenCache(name: String, val rootFile: File) extends MavenRepository(name, rootFile.toURI.toURL.toString) { + override val toString = "cache:" + name + ": " + rootFile.getAbsolutePath + override def isCache: Boolean = true +} +object MavenCache { + def apply(name: String, rootFile: File): MavenCache = new MavenCache(name, rootFile) } final class Patterns(val ivyPatterns: Seq[String], val artifactPatterns: Seq[String], val isMavenCompatible: Boolean, val descriptorOptional: Boolean, val skipConsistencyCheck: Boolean) { @@ -334,8 +349,9 @@ object Resolver { loadHomeFromSettings(() => new File(new File(System.getenv("M2_HOME")), "conf/settings.xml")) getOrElse new File(Path.userHome, ".m2/repository") } - def publishMavenLocal = Resolver.file("publish-m2-local", mavenLocalDir) - def mavenLocal = MavenRepository("Maven2 Local", mavenLocalDir.toURI.toString) + // TODO - should this just be the *exact* same as mavenLocal? probably... + def publishMavenLocal: MavenCache = new MavenCache("publish-m2-local", mavenLocalDir) + def mavenLocal: MavenRepository = new MavenCache("Maven2 Local", mavenLocalDir) def defaultLocal = defaultUserFileRepository("local") def defaultShared = defaultUserFileRepository("shared") def defaultUserFileRepository(id: String) = diff --git a/ivy/src/main/scala/sbt/UpdateOptions.scala b/ivy/src/main/scala/sbt/UpdateOptions.scala index 1d4688481..3a710026c 100644 --- a/ivy/src/main/scala/sbt/UpdateOptions.scala +++ b/ivy/src/main/scala/sbt/UpdateOptions.scala @@ -1,6 +1,8 @@ package sbt import java.io.File +import org.apache.ivy.plugins.resolver.DependencyResolver +import org.apache.ivy.core.settings.IvySettings /** * Represents configurable options for update task. @@ -17,8 +19,9 @@ final class UpdateOptions private[sbt] ( /** If set to true, use consolidated resolution. */ val consolidatedResolution: Boolean, /** If set to true, use cached resolution. */ - val cachedResolution: Boolean) { - + val cachedResolution: Boolean, + /** Extention point for an alternative resolver converter. */ + val resolverConverter: UpdateOptions.ResolverConverter) { def withCircularDependencyLevel(circularDependencyLevel: CircularDependencyLevel): UpdateOptions = copy(circularDependencyLevel = circularDependencyLevel) def withLatestSnapshots(latestSnapshots: Boolean): UpdateOptions = @@ -30,22 +33,28 @@ final class UpdateOptions private[sbt] ( def withCachedResolution(cachedResoluton: Boolean): UpdateOptions = copy(cachedResolution = cachedResoluton, consolidatedResolution = cachedResolution) + /** Extention point for an alternative resolver converter. */ + def withResolverConverter(resolverConverter: UpdateOptions.ResolverConverter): UpdateOptions = + copy(resolverConverter = resolverConverter) private[sbt] def copy( circularDependencyLevel: CircularDependencyLevel = this.circularDependencyLevel, latestSnapshots: Boolean = this.latestSnapshots, consolidatedResolution: Boolean = this.consolidatedResolution, - cachedResolution: Boolean = this.cachedResolution): UpdateOptions = + cachedResolution: Boolean = this.cachedResolution, + resolverConverter: UpdateOptions.ResolverConverter = this.resolverConverter): UpdateOptions = new UpdateOptions(circularDependencyLevel, latestSnapshots, consolidatedResolution, - cachedResolution) + cachedResolution, + resolverConverter) override def equals(o: Any): Boolean = o match { case o: UpdateOptions => this.circularDependencyLevel == o.circularDependencyLevel && this.latestSnapshots == o.latestSnapshots && - this.cachedResolution == o.cachedResolution + this.cachedResolution == o.cachedResolution && + this.resolverConverter == o.resolverConverter case _ => false } @@ -55,15 +64,19 @@ final class UpdateOptions private[sbt] ( hash = hash * 31 + this.circularDependencyLevel.## hash = hash * 31 + this.latestSnapshots.## hash = hash * 31 + this.cachedResolution.## + hash = hash * 31 + this.resolverConverter.## hash } } object UpdateOptions { + type ResolverConverter = PartialFunction[(Resolver, IvySettings, Logger), DependencyResolver] + def apply(): UpdateOptions = new UpdateOptions( circularDependencyLevel = CircularDependencyLevel.Warn, latestSnapshots = false, consolidatedResolution = false, - cachedResolution = false) + cachedResolution = false, + resolverConverter = PartialFunction.empty) } diff --git a/ivy/src/main/scala/sbt/ivyint/CustomMavenResolver.scala b/ivy/src/main/scala/sbt/ivyint/CustomMavenResolver.scala new file mode 100644 index 000000000..e3e41148e --- /dev/null +++ b/ivy/src/main/scala/sbt/ivyint/CustomMavenResolver.scala @@ -0,0 +1,11 @@ +package sbt +package ivyint + +import org.apache.ivy.plugins.resolver.DependencyResolver + +// These are placeholder traits for sbt-aether-resolver +trait CustomMavenResolver extends DependencyResolver { +} +trait CustomRemoteMavenResolver extends CustomMavenResolver { + def repo: MavenRepository +} diff --git a/ivy/src/main/scala/sbt/ivyint/SbtChainResolver.scala b/ivy/src/main/scala/sbt/ivyint/SbtChainResolver.scala index a4d60c997..08c8f00e7 100644 --- a/ivy/src/main/scala/sbt/ivyint/SbtChainResolver.scala +++ b/ivy/src/main/scala/sbt/ivyint/SbtChainResolver.scala @@ -12,7 +12,7 @@ import org.apache.ivy.core.resolve.{ ResolvedModuleRevision, ResolveData } import org.apache.ivy.plugins.latest.LatestStrategy import org.apache.ivy.plugins.repository.file.{ FileRepository => IFileRepository, FileResource } import org.apache.ivy.plugins.repository.url.URLResource -import org.apache.ivy.plugins.resolver.{ ChainResolver, BasicResolver, DependencyResolver } +import org.apache.ivy.plugins.resolver._ import org.apache.ivy.plugins.resolver.util.{ HasLatestStrategy, ResolvedResource } import org.apache.ivy.util.{ Message, MessageLogger, StringUtils => IvyStringUtils } @@ -155,34 +155,39 @@ private[sbt] case class SbtChainResolver( val sorted = if (useLatest) (foundRevisions.sortBy { case (rmr, resolver) => + Message.warn(s"Sorrting results from $rmr, using ${rmr.getPublicationDate} and ${rmr.getDescriptor.getPublicationDate}") // Just issue warning about issues with publication date, and fake one on it for now. - rmr.getDescriptor.getPublicationDate match { - case null => + Option(rmr.getPublicationDate) orElse Option(rmr.getDescriptor.getPublicationDate) match { + case None => (resolver.findIvyFileRef(dd, data), rmr.getDescriptor) match { case (null, _) => // In this instance, the dependency is specified by a direct URL or some other sort of "non-ivy" file if (dd.isChanging) Message.warn(s"Resolving a changing dependency (${rmr.getId}) with no ivy/pom file!, resolution order is undefined!") + 0L case (ivf, dmd: DefaultModuleDescriptor) => val lmd = new java.util.Date(ivf.getLastModified) Message.debug(s"Getting no publication date from resolver: ${resolver} for ${rmr.getId}, setting to: ${lmd}") dmd.setPublicationDate(lmd) + ivf.getLastModified case _ => Message.warn(s"Getting null publication date from resolver: ${resolver} for ${rmr.getId}, resolution order is undefined!") + 0L } - case _ => // All other cases ok - } - rmr.getDescriptor.getPublicationDate match { - case null => 0L - case d => d.getTime + case Some(date) => // All other cases ok + date.getTime } }).reverse.headOption map { case (rmr, resolver) => + Message.warn(s"Choosing ${resolver} for ${rmr.getId}") // Now that we know the real latest revision, let's force Ivy to use it val artifactOpt = findFirstArtifactRef(rmr.getDescriptor, dd, data, resolver) artifactOpt match { case None if resolver.getName == "inter-project" => // do nothing - case None => throw new RuntimeException(s"\t${resolver.getName}: no ivy file nor artifact found for $rmr") + case None if resolver.isInstanceOf[CustomMavenResolver] => + // do nothing for now.... + // We want to see if the maven caching is sufficient and we do not need to duplicate within the ivy cache... + case None => throw new RuntimeException(s"\t${resolver.getName}: no ivy file nor artifact found for $rmr") case Some(artifactRef) => val systemMd = toSystem(rmr.getDescriptor) getRepositoryCacheManager.cacheModuleDescriptor(resolver, artifactRef, @@ -210,6 +215,9 @@ private[sbt] case class SbtChainResolver( } // Ivy seem to not want to use the module descriptor found at the latest resolver private[this] def reparseModuleDescriptor(dd: DependencyDescriptor, data: ResolveData, resolver: DependencyResolver, rmr: ResolvedModuleRevision): ResolvedModuleRevision = + // TODO - Redownloading/parsing the ivy file is not really the best way to make this correct. + // We should figure out a better alternative, or directly attack the resolvers Ivy uses to + // give them correct behavior around -SNAPSHOT. Option(resolver.findIvyFileRef(dd, data)) flatMap { ivyFile => ivyFile.getResource match { case r: FileResource => @@ -222,7 +230,10 @@ private[sbt] case class SbtChainResolver( } case _ => None } - } getOrElse rmr + } getOrElse { + Message.warn(s"Unable to reparse ${dd.getDependencyRevisionId} from $resolver, using ${rmr.getPublicationDate}") + rmr + } /** Ported from BasicResolver#findFirstAirfactRef. */ private[this] def findFirstArtifactRef(md: ModuleDescriptor, dd: DependencyDescriptor, data: ResolveData, resolver: DependencyResolver): Option[ResolvedResource] = { diff --git a/ivy/src/main/scala/sbt/mavenint/PomExtraDependencyAttributes.scala b/ivy/src/main/scala/sbt/mavenint/PomExtraDependencyAttributes.scala new file mode 100644 index 000000000..ab87cae75 --- /dev/null +++ b/ivy/src/main/scala/sbt/mavenint/PomExtraDependencyAttributes.scala @@ -0,0 +1,111 @@ +package sbt.mavenint + +import java.util.Properties +import java.util.regex.Pattern + +import org.apache.ivy.core.module.descriptor.DependencyDescriptor +import org.apache.ivy.core.module.id.ModuleRevisionId +import org.apache.ivy.util.extendable.ExtendableItem + +/** + * This class contains all the logic for dealing with the extra attributes in pom files relating to extra attributes + * on dependency declarations. + * + * Specifically, if we have a dependency on an sbt plugin, there are two properties that need to propogate: + * - `sbtVersion` + * - `scalaVersion` + * + * These need to exist on the *dependency declaration*. Maven/Aether has no way to inject these into + * the section of pom files, so we use Ivy's Extra attribute hackery to inject a lookup table + * of extra attributes by dependency id into POM files and later we read these back. + */ +object PomExtraDependencyAttributes { + + val ExtraAttributesKey = "extraDependencyAttributes" + val SbtVersionKey = "sbtVersion" + val ScalaVersionKey = "scalaVersion" + /** + * Reads the extra dependency attributes out of a maven property. + * @param props The properties from an Aether resolution. + * @return + * A map of module id to extra dependency attributes associated with dependencies on that module. + */ + def readFromAether(props: java.util.Map[String, AnyRef]): Map[ModuleRevisionId, Map[String, String]] = { + import scala.collection.JavaConverters._ + (props.asScala get ExtraAttributesKey) match { + case None => Map.empty + case Some(str) => + def processDep(m: ModuleRevisionId) = (simplify(m), filterCustomExtra(m, include = true)) + (for { + (id, props) <- readDependencyExtra(str.toString).map(processDep) + } yield id -> props).toMap + } + } + + /** + * Mutates the to collection with the extra depdendency attributes from the incoming pom properties list. + * + * @param from The properties directly off a maven POM file + * @param to The aaether properties where we can write whatever we want. + * + * TODO - maybe we can just parse this directly here. Note the `readFromAether` method uses + * whatever we set here. + */ + def transferDependencyExtraAttributes(from: Properties, to: java.util.Map[String, AnyRef]): Unit = { + Option(from.getProperty(ExtraAttributesKey, null)) match { + case Some(str) => to.put(ExtraAttributesKey, str) + case None => + } + } + + /** + * Reads the extra dependency information out of Ivy's notion of POM properties and returns + * the map of ID -> Extra Properties. + */ + 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 scala.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 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 scala.collection.JavaConverters._ + ModuleRevisionId.newInstance(id.getOrganisation, id.getName, id.getBranch, id.getRevision, filterCustomExtra(id, include = false).asJava) + } + + /** 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)^") + + /** + * Creates the "extra" property values for DependencyDescriptors that can be written into a maven pom + * so we don't loose the information. + * @param s + * @return + */ + 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 + } + +} diff --git a/ivy/src/test/scala/BaseIvySpecification.scala b/ivy/src/test/scala/BaseIvySpecification.scala index 09d7da7a2..1be01ab25 100644 --- a/ivy/src/test/scala/BaseIvySpecification.scala +++ b/ivy/src/test/scala/BaseIvySpecification.scala @@ -4,6 +4,8 @@ import Path._, Configurations._ import java.io.File import org.specs2._ import cross.CrossVersionUtil +import sbt.PublishConfiguration +import sbt.ivyint.SbtChainResolver trait BaseIvySpecification extends Specification { def currentBase: File = new File(".") @@ -12,6 +14,8 @@ trait BaseIvySpecification extends Specification { def currentDependency: File = currentBase / "target" / "dependency" def defaultModuleId: ModuleID = ModuleID("com.example", "foo", "0.1.0", Some("compile")) lazy val log = ConsoleLogger() + + def configurations = Seq(Compile, Test, Runtime) def module(moduleId: ModuleID, deps: Seq[ModuleID], scalaFullVersion: Option[String], uo: UpdateOptions = UpdateOptions()): IvySbt#Module = { val ivyScala = scalaFullVersion map { fv => @@ -28,21 +32,24 @@ trait BaseIvySpecification extends Specification { module = moduleId, moduleInfo = ModuleInfo("foo"), dependencies = deps, - configurations = Seq(Compile, Test, Runtime), + configurations = configurations, ivyScala = ivyScala) val ivySbt = new IvySbt(mkIvyConfiguration(uo)) new ivySbt.Module(moduleSetting) } + def resolvers: Seq[Resolver] = Seq(DefaultMavenRepository) + + def chainResolver = ChainedResolver("sbt-chain", resolvers) + def mkIvyConfiguration(uo: UpdateOptions): IvyConfiguration = { val paths = new IvyPaths(currentBase, Some(currentTarget)) - val rs = Seq(DefaultMavenRepository) val other = Nil - val moduleConfs = Seq(ModuleConfiguration("*", DefaultMavenRepository)) + val moduleConfs = Seq(ModuleConfiguration("*", chainResolver)) val off = false val check = Nil val resCacheDir = currentTarget / "resolution-cache" - new InlineIvyConfiguration(paths, rs, other, moduleConfs, off, None, check, Some(resCacheDir), uo, log) + new InlineIvyConfiguration(paths, resolvers, other, moduleConfs, off, None, check, Some(resCacheDir), uo, log) } def ivyUpdateEither(module: IvySbt#Module): Either[UnresolvedWarning, UpdateReport] = { @@ -58,4 +65,18 @@ trait BaseIvySpecification extends Specification { case Left(w) => throw w.resolveException } + + def mkPublishConfiguration(resolver: Resolver, artifacts: Map[Artifact, File]): PublishConfiguration = { + new PublishConfiguration( + ivyFile = None, + resolverName = resolver.name, + artifacts = artifacts, + checksums = Seq(), + logging = UpdateLogging.Full, + overwrite = true) + } + + def ivyPublish(module: IvySbt#Module, config: PublishConfiguration) = { + IvyActions.publish(module, config, log) + } }