diff --git a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/Ivy.scala b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/Ivy.scala index 21c6cf6e2..400710b67 100644 --- a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/Ivy.scala +++ b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/Ivy.scala @@ -140,6 +140,7 @@ final class IvySbt(val configuration: IvyConfiguration) { /** * Cleans cached resolution cache. + * * @param md - module descriptor of the original Ivy graph. */ private[sbt] def cleanCachedResolutionCache(md: ModuleDescriptor, log: Logger): Unit = @@ -621,6 +622,12 @@ private[sbt] object IvySbt { dependencyDescriptor.addExcludeRule(conf, IvyScala.excludeRule(excls.organization, excls.name, excls.configurations, excls.artifact)) } } + for (incls <- dependency.inclusions) { + for (conf <- dependencyDescriptor.getModuleConfigurations) { + dependencyDescriptor.addIncludeRule(conf, IvyScala.includeRule(incls.organization, incls.name, incls.configurations, incls.artifact)) + } + } + dependencyDescriptor } def copyConfigurations(artifact: Artifact, addConfiguration: String => Unit): Unit = diff --git a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala index 6f131f049..b197b75e0 100644 --- a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala +++ b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/IvyActions.scala @@ -29,21 +29,27 @@ final class PublishConfiguration(val ivyFile: Option[File], val resolverName: St this(ivyFile, resolverName, artifacts, checksums, logging, false) } -final class UpdateConfiguration(val retrieve: Option[RetrieveConfiguration], val missingOk: Boolean, val logging: UpdateLogging.Value) { +final class UpdateConfiguration(val retrieve: Option[RetrieveConfiguration], val missingOk: Boolean, val logging: UpdateLogging.Value, val artifactFilter: ArtifactTypeFilter) { + @deprecated("You should use the constructor that provides an artifactFilter", "1.0.x") + def this(retrieve: Option[RetrieveConfiguration], missingOk: Boolean, logging: UpdateLogging.Value) { + this(retrieve, missingOk, logging, ArtifactTypeFilter.forbid(Set("src", "doc"))) // allow everything but "src", "doc" by default + } + private[sbt] def copy( retrieve: Option[RetrieveConfiguration] = this.retrieve, missingOk: Boolean = this.missingOk, - logging: UpdateLogging.Value = this.logging + logging: UpdateLogging.Value = this.logging, + artifactFilter: ArtifactTypeFilter = this.artifactFilter ): UpdateConfiguration = - new UpdateConfiguration(retrieve, missingOk, logging) + new UpdateConfiguration(retrieve, missingOk, logging, artifactFilter) } final class RetrieveConfiguration(val retrieveDirectory: File, val outputPattern: String, val sync: Boolean, val configurationsToRetrieve: Option[Set[Configuration]]) { def this(retrieveDirectory: File, outputPattern: String) = this(retrieveDirectory, outputPattern, false, None) def this(retrieveDirectory: File, outputPattern: String, sync: Boolean) = this(retrieveDirectory, outputPattern, sync, None) } 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]) +/** @param exclude is a map from ModuleID to classifiers that were previously tried and failed, so should now be excluded */ +final case class GetClassifiersConfiguration(module: GetClassifiersModule, exclude: Map[ModuleID, Set[String]], configuration: UpdateConfiguration, ivyScala: Option[IvyScala], sourceArtifactTypes: Set[String], docArtifactTypes: Set[String]) final case class GetClassifiersModule(id: ModuleID, modules: Seq[ModuleID], configurations: Seq[Configuration], classifiers: Seq[String]) final class UnresolvedWarningConfiguration private[sbt] ( @@ -180,6 +186,7 @@ object IvyActions { val resolveOptions = new ResolveOptions val resolveId = ResolveOptions.getDefaultResolveId(md) resolveOptions.setResolveId(resolveId) + resolveOptions.setArtifactFilter(configuration.artifactFilter) resolveOptions.setLog(ivyLogLevel(configuration.logging)) x.customResolve(md, configuration.missingOk, logicalClock, resolveOptions, depDir getOrElse { sys.error("dependency base directory is not specified") }, log) match { case Left(x) => @@ -194,7 +201,7 @@ object IvyActions { case (ivy, md, default) => val iw = IvySbt.inconsistentDuplicateWarning(md) iw foreach { log.warn(_) } - val (report, err) = resolve(configuration.logging)(ivy, md, default) + val (report, err) = resolve(configuration.logging)(ivy, md, default, configuration.artifactFilter) err match { case Some(x) if !configuration.missingOk => Left(UnresolvedWarning(x, uwconfig)) @@ -243,7 +250,15 @@ object IvyActions { def updateClassifiers(ivySbt: IvySbt, config: GetClassifiersConfiguration, log: Logger): UpdateReport = updateClassifiers(ivySbt, config, UnresolvedWarningConfiguration(), LogicalClock.unknown, None, Vector(), log) - // artifacts can be obtained from calling toSeq on UpdateReport + /** + * Creates explicit artifacts for each classifier in `config.module`, and then attempts to resolve them directly. This + * is for Maven compatibility, where these artifacts are not "published" in the POM, so they don't end up in the Ivy + * that sbt generates for them either.
+ * Artifacts can be obtained from calling toSeq on UpdateReport.
+ * In addition, retrieves specific Ivy artifacts if they have one of the requested `config.configuration.types`. + * @param config important to set `config.configuration.types` to only allow artifact types that can correspond to + * "classified" artifacts (sources and javadocs). + */ private[sbt] def updateClassifiers(ivySbt: IvySbt, config: GetClassifiersConfiguration, uwconfig: UnresolvedWarningConfiguration, logicalClock: LogicalClock, depDir: Option[File], artifacts: Vector[(String, ModuleID, Artifact, File)], @@ -252,14 +267,28 @@ object IvyActions { import config.{ configuration => c, module => mod, _ } import mod.{ configurations => confs, _ } assert(classifiers.nonEmpty, "classifiers cannot be empty") + assert(c.artifactFilter.types.nonEmpty, "UpdateConfiguration must filter on some types") val baseModules = modules map { m => restrictedCopy(m, true) } // Adding list of explicit artifacts here. val deps = baseModules.distinct flatMap classifiedArtifacts(classifiers, exclude, artifacts) val base = restrictedCopy(id, true).copy(name = id.name + classifiers.mkString("$", "_", "")) val module = new ivySbt.Module(InlineConfigurationWithExcludes(base, ModuleInfo(base.name), deps).copy(ivyScala = ivyScala, configurations = confs)) - val upConf = new UpdateConfiguration(c.retrieve, true, c.logging) + // c.copy ensures c.types is preserved too + val upConf = c.copy(missingOk = true) updateEither(module, upConf, uwconfig, logicalClock, depDir, log) match { - case Right(r) => r + case Right(r) => + // The artifacts that came from Ivy don't have their classifier set, let's set it according to + // FIXME: this is only done because IDE plugins depend on `classifier` to determine type. They + val typeClassifierMap: Map[String, String] = + ((sourceArtifactTypes.toIterable map (_ -> Artifact.SourceClassifier)) + :: (docArtifactTypes.toIterable map (_ -> Artifact.DocClassifier)) :: Nil).flatten.toMap + r.substitute { (conf, mid, artFileSeq) => + artFileSeq map { + case (art, f) => + // Deduce the classifier from the type if no classifier is present already + art.copy(classifier = art.classifier orElse typeClassifierMap.get(art.`type`)) -> f + } + } case Left(w) => throw w.resolveException } @@ -275,7 +304,7 @@ object IvyActions { { val arts = (artifacts collect { case (_, x, art, _) if sameModule(m, x) && art.classifier.isDefined => art }).distinct if (arts.isEmpty) None - else Some(m.copy(isTransitive = false, explicitArtifacts = arts)) + else Some(intransitiveModuleWithExplicitArts(m, arts)) } def hardcodedArtifacts = classifiedArtifacts(classifiers, exclude)(m) explicitArtifacts orElse hardcodedArtifacts @@ -284,8 +313,27 @@ object IvyActions { { val excluded = exclude getOrElse (restrictedCopy(m, false), Set.empty) val included = classifiers filterNot excluded - if (included.isEmpty) None else Some(m.copy(isTransitive = false, explicitArtifacts = classifiedArtifacts(m.name, included))) + if (included.isEmpty) None else { + Some(intransitiveModuleWithExplicitArts(module = m, arts = classifiedArtifacts(m.name, included))) + } } + + /** + * Explicitly set an "include all" rule (the default) because otherwise, if we declare ANY explicitArtifacts, + * [[org.apache.ivy.core.resolve.IvyNode#getArtifacts]] (in Ivy 2.3.0-rc1) will not merge in the descriptor's + * artifacts and will only keep the explicitArtifacts. + *
+ * Look for the comment saying {{{ + * // and now we filter according to include rules + * }}} + * in `IvyNode`, which iterates on `includes`, which will ordinarily be empty because higher up, in {{{ + * addAllIfNotNull(includes, usage.getDependencyIncludesSet(rootModuleConf)); + * }}} + * `usage.getDependencyIncludesSet` returns null if there are no (explicit) include rules. + */ + private def intransitiveModuleWithExplicitArts(module: ModuleID, arts: Seq[Artifact]): ModuleID = + module.copy(isTransitive = false, explicitArtifacts = arts, inclusions = InclExclRule.everything :: Nil) + def addExcluded(report: UpdateReport, classifiers: Seq[String], exclude: Map[ModuleID, Set[String]]): UpdateReport = report.addMissing { id => classifiedArtifacts(id.name, classifiers filter getExcluded(id, exclude)) } def classifiedArtifacts(name: String, classifiers: Seq[String]): Seq[Artifact] = @@ -300,11 +348,12 @@ object IvyActions { ModuleID(m.organization, m.name, m.revision, crossVersion = m.crossVersion, extraAttributes = m.extraAttributes, configurations = if (confs) m.configurations else None) .branch(m.branchName) - private[this] def resolve(logging: UpdateLogging.Value)(ivy: Ivy, module: DefaultModuleDescriptor, defaultConf: String): (ResolveReport, Option[ResolveException]) = + private[this] def resolve(logging: UpdateLogging.Value)(ivy: Ivy, module: DefaultModuleDescriptor, defaultConf: String, filter: ArtifactTypeFilter): (ResolveReport, Option[ResolveException]) = { val resolveOptions = new ResolveOptions val resolveId = ResolveOptions.getDefaultResolveId(module) resolveOptions.setResolveId(resolveId) + resolveOptions.setArtifactFilter(filter) resolveOptions.setLog(ivyLogLevel(logging)) ResolutionCache.cleanModule(module.getModuleRevisionId, resolveId, ivy.getSettings.getResolutionCacheManager) val resolveReport = ivy.resolve(module, resolveOptions) diff --git a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/RichUpdateReport.scala b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/RichUpdateReport.scala index 61675fd2d..e4005ef21 100644 --- a/librarymanagement/src/main/scala/sbt/internal/librarymanagement/RichUpdateReport.scala +++ b/librarymanagement/src/main/scala/sbt/internal/librarymanagement/RichUpdateReport.scala @@ -45,8 +45,8 @@ final class RichUpdateReport(report: UpdateReport) { moduleReportMap { (configuration, modReport) => val newArtifacts = f(configuration, modReport.module, modReport.artifacts) modReport.copy( - artifacts = f(configuration, modReport.module, modReport.artifacts), - missingArtifacts = Nil + artifacts = newArtifacts, + missingArtifacts = modReport.missingArtifacts ) } diff --git a/librarymanagement/src/main/scala/sbt/librarymanagement/Artifact.scala b/librarymanagement/src/main/scala/sbt/librarymanagement/Artifact.scala index 2d174f035..8bf97bc05 100644 --- a/librarymanagement/src/main/scala/sbt/librarymanagement/Artifact.scala +++ b/librarymanagement/src/main/scala/sbt/librarymanagement/Artifact.scala @@ -8,10 +8,10 @@ import java.net.URL import sbt.serialization._ final case class Artifact(name: String, `type`: String, extension: String, classifier: Option[String], configurations: Iterable[Configuration], url: Option[URL], extraAttributes: Map[String, String]) { - def extra(attributes: (String, String)*) = Artifact(name, `type`, extension, classifier, configurations, url, extraAttributes ++ ModuleID.checkE(attributes)) + def extra(attributes: (String, String)*) = copy(extraAttributes = extraAttributes ++ ModuleID.checkE(attributes)) } -import Configurations.{ config, Docs, Optional, Pom, Sources, Test } +import Configurations.{ config, Optional, Pom, Test } object Artifact { def apply(name: String): Artifact = Artifact(name, DefaultType, DefaultExtension, None, Nil, None) @@ -30,12 +30,23 @@ object Artifact { def javadoc(name: String) = classified(name, DocClassifier) def pom(name: String) = Artifact(name, PomType, PomType, None, Pom :: Nil, None) + // Possible ivy artifact types such that sbt will treat those artifacts at sources / docs + val DefaultSourceTypes = Set("src", "source", "sources") + val DefaultDocTypes = Set("doc", "docs", "javadoc", "javadocs") + val DocClassifier = "javadoc" val SourceClassifier = "sources" + + val TestsClassifier = "tests" + // Artifact types used when: + // * artifacts are explicitly created for Maven dependency resolution (see updateClassifiers) + // * declaring artifacts as part of creating Ivy files. val DocType = "doc" val SourceType = "src" val PomType = "pom" - val TestsClassifier = "tests" + + assert(DefaultDocTypes contains DocType) + assert(DefaultSourceTypes contains SourceType) def extract(url: URL, default: String): String = extract(url.toString, default) def extract(name: String, default: String): String = @@ -62,16 +73,22 @@ object Artifact { base + "-" + module.revision + classifierStr + "." + artifact.extension } - val classifierConfMap = Map(SourceClassifier -> Sources, DocClassifier -> Docs) val classifierTypeMap = Map(SourceClassifier -> SourceType, DocClassifier -> DocType) + @deprecated("Configuration should not be decided from the classifier.", "1.0") def classifierConf(classifier: String): Configuration = if (classifier.startsWith(TestsClassifier)) Test else - classifierConfMap.getOrElse(classifier, Optional) + Optional def classifierType(classifier: String): String = classifierTypeMap.getOrElse(classifier.stripPrefix(TestsClassifier + "-"), DefaultType) + + /** + * Create a classified explicit artifact, to be used when trying to resolve sources|javadocs from Maven. This is + * necessary because those artifacts are not published in the Ivy generated from the Pom of the module in question. + * The artifact is created under the default configuration. + */ def classified(name: String, classifier: String): Artifact = - Artifact(name, classifierType(classifier), DefaultExtension, Some(classifier), classifierConf(classifier) :: Nil, None) + Artifact(name, classifierType(classifier), DefaultExtension, Some(classifier), Nil, None) private val optStringPickler = implicitly[Pickler[Option[String]]] private val optStringUnpickler = implicitly[Unpickler[Option[String]]] diff --git a/librarymanagement/src/main/scala/sbt/librarymanagement/Configuration.scala b/librarymanagement/src/main/scala/sbt/librarymanagement/Configuration.scala index 50c432682..f860d4abc 100644 --- a/librarymanagement/src/main/scala/sbt/librarymanagement/Configuration.scala +++ b/librarymanagement/src/main/scala/sbt/librarymanagement/Configuration.scala @@ -10,7 +10,7 @@ object Configurations { def default: Seq[Configuration] = defaultMavenConfigurations def defaultMavenConfigurations: Seq[Configuration] = Seq(Compile, Runtime, Test, Provided, Optional) def defaultInternal: Seq[Configuration] = Seq(CompileInternal, RuntimeInternal, TestInternal) - def auxiliary: Seq[Configuration] = Seq(Sources, Docs, Pom) + def auxiliary: Seq[Configuration] = Seq(Pom) def names(cs: Seq[Configuration]) = cs.map(_.name) lazy val RuntimeInternal = optionalInternal(Runtime) @@ -34,10 +34,8 @@ object Configurations { lazy val Compile = config("compile") lazy val IntegrationTest = config("it") extend (Runtime) lazy val Provided = config("provided") - lazy val Docs = config("docs") lazy val Runtime = config("runtime") extend (Compile) lazy val Test = config("test") extend (Runtime) - lazy val Sources = config("sources") lazy val System = config("system") lazy val Optional = config("optional") lazy val Pom = config("pom") diff --git a/librarymanagement/src/main/scala/sbt/librarymanagement/IvyInterface.scala b/librarymanagement/src/main/scala/sbt/librarymanagement/IvyInterface.scala index b582a51ed..964fe7447 100644 --- a/librarymanagement/src/main/scala/sbt/librarymanagement/IvyInterface.scala +++ b/librarymanagement/src/main/scala/sbt/librarymanagement/IvyInterface.scala @@ -8,6 +8,8 @@ import java.net.{ URI, URL } import scala.xml.NodeSeq import org.apache.ivy.plugins.resolver.{ DependencyResolver, IBiblioResolver } import org.apache.ivy.util.url.CredentialsStore +import org.apache.ivy.core.module.descriptor +import org.apache.ivy.util.filter.{ Filter => IvyFilter } import sbt.serialization._ /** Additional information about a project module */ @@ -25,10 +27,40 @@ final case class ScmInfo(browseUrl: URL, connection: String, devConnection: Opti final case class Developer(id: String, name: String, email: String, url: URL) -/** Rule to exclude unwanted dependencies pulled in transitively by a module. */ -final case class ExclusionRule(organization: String = "*", name: String = "*", artifact: String = "*", configurations: Seq[String] = Nil) -object ExclusionRule { - implicit val pickler: Pickler[ExclusionRule] with Unpickler[ExclusionRule] = PicklerUnpickler.generate[ExclusionRule] +/** + * Rule to either: + * + * Which one depends on the parameter name which it is passed to, but the filter has the same fields in both cases. + */ +final case class InclExclRule(organization: String = "*", name: String = "*", artifact: String = "*", configurations: Seq[String] = Nil) +object InclExclRule { + def everything = InclExclRule("*", "*", "*", Nil) + + implicit val pickler: Pickler[InclExclRule] with Unpickler[InclExclRule] = PicklerUnpickler.generate[InclExclRule] +} + +/** + * Work around the inadequacy of Ivy's ArtifactTypeFilter (that it cannot reverse a filter) + * @param types represents the artifact types that we should try to resolve for (as in the allowed values of + * `artifact[type]` from a dependency `` section). One can use this to filter + * source / doc artifacts. + * @param inverted whether to invert the types filter (i.e. allow only types NOT in the set) + */ +case class ArtifactTypeFilter(types: Set[String], inverted: Boolean) { + def invert = copy(inverted = !inverted) + def apply(a: descriptor.Artifact): Boolean = (types contains a.getType) ^ inverted +} + +object ArtifactTypeFilter { + def allow(types: Set[String]) = ArtifactTypeFilter(types, false) + def forbid(types: Set[String]) = ArtifactTypeFilter(types, true) + + implicit def toIvyFilter(f: ArtifactTypeFilter): IvyFilter = new IvyFilter { + override def accept(o: Object): Boolean = Option(o) exists { case a: descriptor.Artifact => f.apply(a) } + } } final case class ModuleConfiguration(organization: String, name: String, revision: String, resolver: Resolver) diff --git a/librarymanagement/src/main/scala/sbt/librarymanagement/IvyScala.scala b/librarymanagement/src/main/scala/sbt/librarymanagement/IvyScala.scala index e5808eef9..d2df9b4cf 100644 --- a/librarymanagement/src/main/scala/sbt/librarymanagement/IvyScala.scala +++ b/librarymanagement/src/main/scala/sbt/librarymanagement/IvyScala.scala @@ -6,8 +6,7 @@ package sbt.librarymanagement import java.util.Collections.emptyMap import scala.collection.mutable.HashSet -import org.apache.ivy.core.module.descriptor.{ DefaultExcludeRule, ExcludeRule } -import org.apache.ivy.core.module.descriptor.{ DependencyDescriptor, DefaultModuleDescriptor, ModuleDescriptor, OverrideDependencyDescriptorMediator } +import org.apache.ivy.core.module.descriptor._ import org.apache.ivy.core.module.id.{ ArtifactId, ModuleId, ModuleRevisionId } import org.apache.ivy.plugins.matcher.ExactPatternMatcher import sbt.util.Logger @@ -125,4 +124,16 @@ private[sbt] object IvyScala { configurationNames.foreach(rule.addConfiguration) rule } + + /** + * Creates an IncludeRule that includes artifacts with the given module organization and name for + * the given configurations. + */ + private[sbt] def includeRule(organization: String, name: String, configurationNames: Iterable[String], includeTypePattern: String): IncludeRule = + { + val artifact = new ArtifactId(ModuleId.newInstance(organization, name), "*", includeTypePattern, "*") + val rule = new DefaultIncludeRule(artifact, ExactPatternMatcher.INSTANCE, emptyMap[AnyRef, AnyRef]) + configurationNames.foreach(rule.addConfiguration) + rule + } } diff --git a/librarymanagement/src/main/scala/sbt/librarymanagement/ModuleID.scala b/librarymanagement/src/main/scala/sbt/librarymanagement/ModuleID.scala index 1da5a1e0a..2cbf37138 100644 --- a/librarymanagement/src/main/scala/sbt/librarymanagement/ModuleID.scala +++ b/librarymanagement/src/main/scala/sbt/librarymanagement/ModuleID.scala @@ -8,7 +8,7 @@ import java.net.URL import sbt.internal.librarymanagement.mavenint.SbtPomExtraProperties import sbt.serialization._ -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, branchName: Option[String] = None) { +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, inclusions: Seq[InclusionRule] = Nil, exclusions: Seq[ExclusionRule] = Nil, extraAttributes: Map[String, String] = Map.empty, crossVersion: CrossVersion = CrossVersion.Disabled, branchName: Option[String] = None) { override def toString: String = organization + ":" + name + ":" + revision + (configurations match { case Some(s) => ":" + s; case None => "" }) + @@ -70,10 +70,10 @@ final case class ModuleID(organization: String, name: String, revision: String, * Applies the provided exclusions to dependencies of this module. Note that only exclusions that specify * both the exact organization and name and nothing else will be included in a pom.xml. */ - def excludeAll(rules: ExclusionRule*) = copy(exclusions = this.exclusions ++ rules) + def excludeAll(rules: InclExclRule*) = copy(exclusions = this.exclusions ++ rules) /** Excludes the dependency with organization `org` and `name` from being introduced by this dependency during resolution. */ - def exclude(org: String, name: String) = excludeAll(ExclusionRule(org, name)) + def exclude(org: String, name: String) = excludeAll(InclExclRule(org, name)) /** * Adds extra attributes for this module. All keys are prefixed with `e:` if they are not already so prefixed. diff --git a/librarymanagement/src/main/scala/sbt/librarymanagement/package.scala b/librarymanagement/src/main/scala/sbt/librarymanagement/package.scala new file mode 100644 index 000000000..24e6eee19 --- /dev/null +++ b/librarymanagement/src/main/scala/sbt/librarymanagement/package.scala @@ -0,0 +1,9 @@ +package sbt + +package object librarymanagement { + type ExclusionRule = InclExclRule + val ExclusionRule = InclExclRule + + type InclusionRule = InclExclRule + val InclusionRule = InclExclRule +} diff --git a/librarymanagement/src/test/resources/test-ivy-repo/com.test/module-with-srcs/0.1.00/ivys/ivy.xml b/librarymanagement/src/test/resources/test-ivy-repo/com.test/module-with-srcs/0.1.00/ivys/ivy.xml new file mode 100755 index 000000000..ab045d5cb --- /dev/null +++ b/librarymanagement/src/test/resources/test-ivy-repo/com.test/module-with-srcs/0.1.00/ivys/ivy.xml @@ -0,0 +1,23 @@ + + + + + Just a test module that publishes both a binary jar and a src jar in the 'compile' configuration. + + + + + + + + + + + + + + + + + + diff --git a/librarymanagement/src/test/resources/test-ivy-repo/com.test/module-with-srcs/0.1.00/jars/libmodule.jar b/librarymanagement/src/test/resources/test-ivy-repo/com.test/module-with-srcs/0.1.00/jars/libmodule.jar new file mode 100644 index 000000000..b21d53c7b Binary files /dev/null and b/librarymanagement/src/test/resources/test-ivy-repo/com.test/module-with-srcs/0.1.00/jars/libmodule.jar differ diff --git a/librarymanagement/src/test/resources/test-ivy-repo/com.test/module-with-srcs/0.1.00/srcs/libmodule-source.jar b/librarymanagement/src/test/resources/test-ivy-repo/com.test/module-with-srcs/0.1.00/srcs/libmodule-source.jar new file mode 100644 index 000000000..b21d53c7b Binary files /dev/null and b/librarymanagement/src/test/resources/test-ivy-repo/com.test/module-with-srcs/0.1.00/srcs/libmodule-source.jar differ diff --git a/librarymanagement/src/test/scala/BaseIvySpecification.scala b/librarymanagement/src/test/scala/BaseIvySpecification.scala index b0744fd3b..260ac3517 100644 --- a/librarymanagement/src/test/scala/BaseIvySpecification.scala +++ b/librarymanagement/src/test/scala/BaseIvySpecification.scala @@ -58,10 +58,14 @@ trait BaseIvySpecification extends UnitSpec { new InlineIvyConfiguration(paths, resolvers, other, moduleConfs, off, None, check, Some(resCacheDir), uo, log) } + def makeUpdateConfiguration: UpdateConfiguration = { + val retrieveConfig = new RetrieveConfiguration(currentManaged, Resolver.defaultRetrievePattern, false) + new UpdateConfiguration(Some(retrieveConfig), false, UpdateLogging.Full, ArtifactTypeFilter.forbid(Set("src", "doc"))) + } + def ivyUpdateEither(module: IvySbt#Module): Either[UnresolvedWarning, UpdateReport] = { // IO.delete(currentTarget) - val retrieveConfig = new RetrieveConfiguration(currentManaged, Resolver.defaultRetrievePattern, false) - val config = new UpdateConfiguration(Some(retrieveConfig), false, UpdateLogging.Full) + val config = makeUpdateConfiguration IvyActions.updateEither(module, config, UnresolvedWarningConfiguration(), LogicalClock.unknown, Some(currentDependency), log) } diff --git a/librarymanagement/src/test/scala/sbt/internal/librarymanagement/IvyRepoSpec.scala b/librarymanagement/src/test/scala/sbt/internal/librarymanagement/IvyRepoSpec.scala new file mode 100644 index 000000000..9e15fbc01 --- /dev/null +++ b/librarymanagement/src/test/scala/sbt/internal/librarymanagement/IvyRepoSpec.scala @@ -0,0 +1,89 @@ +package sbt.internal.librarymanagement + +import org.scalatest.Inside +import sbt.internal.librarymanagement.impl.DependencyBuilders +import sbt.librarymanagement._ + +class IvyRepoSpec extends BaseIvySpecification with DependencyBuilders { + + val ourModuleID = ModuleID("com.example", "foo", "0.1.0", Some("compile")) + + def makeModuleForDepWithSources = { + // By default a module seems to only have [compile, test, runtime], yet deps automatically map to + // default->compile(default) ... so I guess we have to explicitly use e.g. "compile" + val dep = "com.test" % "module-with-srcs" % "0.1.00" % "compile" + + module( + ourModuleID, + Seq(dep), None //, UpdateOptions().withCachedResolution(true) + ) + } + + "ivyUpdate from ivy repository" should "resolve only binary artifact from module which also contains a sources artifact under the same configuration." in { + cleanIvyCache() + + val m = makeModuleForDepWithSources + + val report = ivyUpdate(m) + + import Inside._ + inside(report.configuration("compile").map(_.modules)) { + case Some(Seq(mr)) => + inside(mr.artifacts) { + case Seq((ar, _)) => + ar.`type` shouldBe "jar" + ar.extension shouldBe "jar" + } + } + } + + it should "resolve only sources artifact of an acceptable artifact type, \"src\", when calling updateClassifiers." in { + cleanIvyCache() + + val m = makeModuleForDepWithSources + + // the "default" configuration used in updateEither. + val c = makeUpdateConfiguration + + val ivyScala = m.moduleSettings.ivyScala + val srcTypes = Set("src") + val docTypes = Set("javadoc") + // These will be the default classifiers that SBT should try, in case a dependency is Maven. + // In this case though, they will be tried and should fail gracefully - only the + val attemptedClassifiers = Seq("sources", "javadoc") + + // The dep that we want to get the "classifiers" (i.e. sources / docs) for. + // We know it has only one source artifact in the "compile" configuration. + val dep = "com.test" % "module-with-srcs" % "0.1.00" % "compile" + + val clMod = { + import language.implicitConversions + implicit val key = (m: ModuleID) => (m.organization, m.name, m.revision) + val externalModules = Seq(dep) + // Note: need to extract ourModuleID so we can plug it in here, can't fish it back out of the IvySbt#Module (`m`) + GetClassifiersModule(ourModuleID, externalModules, Seq(Configurations.Compile), attemptedClassifiers) + } + + val gcm = GetClassifiersConfiguration(clMod, Map.empty, c.copy(artifactFilter = c.artifactFilter.invert), ivyScala, srcTypes, docTypes) + + val report2 = IvyActions.updateClassifiers(m.owner, gcm, UnresolvedWarningConfiguration(), LogicalClock.unknown, None, Vector(), log) + + import Inside._ + inside(report2.configuration("compile").map(_.modules)) { + case Some(Seq(mr)) => + inside(mr.artifacts) { + case Seq((ar, _)) => + ar.name shouldBe "libmodule-source" + ar.`type` shouldBe "src" + ar.extension shouldBe "jar" + } + } + } + + override lazy val resolvers: Seq[Resolver] = Seq(testIvy) + + lazy val testIvy = { + val repoUrl = getClass.getResource("/test-ivy-repo") + Resolver.url("Test Repo", repoUrl)(Resolver.ivyStylePatterns) + } +}