diff --git a/cli/src/main/scala/coursier/cli/Coursier.scala b/cli/src/main/scala/coursier/cli/Coursier.scala index e5ca9b188..2d2c292f7 100644 --- a/cli/src/main/scala/coursier/cli/Coursier.scala +++ b/cli/src/main/scala/coursier/cli/Coursier.scala @@ -101,7 +101,14 @@ case class Coursier(scope: List[String], val (type0, classifier) = dep.artifacts match { case maven: Artifacts.Maven => (maven.`type`, maven.classifier) } - s"${dep.module.organization}:${dep.module.name}:$type0:${Some(classifier).filter(_.nonEmpty).map(_+":").mkString}${dep.version}" + + // dep.version can be an interval, whereas the one from project can't + val version = res.projectsCache.get(dep.moduleVersion).map(_._2.version).getOrElse(dep.version) + val extra = + if (version == dep.version) "" + else s" ($version for ${dep.version})" + + s"${dep.module.organization}:${dep.module.name}:$type0:${Some(classifier).filter(_.nonEmpty).map(_+":").mkString}$version$extra" } val trDeps = res.dependencies.toList.sortBy(repr) diff --git a/core-js/src/main/scala/coursier/core/Remote.scala b/core-js/src/main/scala/coursier/core/Remote.scala index 9145eab9d..6aa2d2700 100644 --- a/core-js/src/main/scala/coursier/core/Remote.scala +++ b/core-js/src/main/scala/coursier/core/Remote.scala @@ -77,13 +77,13 @@ trait Logger { def other(url: String, msg: String): Unit } -case class Remote(base: String, logger: Option[Logger] = None) extends Repository { +case class Remote(base: String, logger: Option[Logger] = None) extends MavenRepository { - def find(module: Module, - version: String, - cachePolicy: CachePolicy): EitherT[Task, String, Project] = { + def findNoInterval(module: Module, + version: String, + cachePolicy: CachePolicy): EitherT[Task, String, Project] = { - val relPath = { + val path = { module.organization.split('.').toSeq ++ Seq( module.name, version, @@ -91,7 +91,7 @@ case class Remote(base: String, logger: Option[Logger] = None) extends Repositor ) } .map(Remote.encodeURIComponent) - val url = base + relPath.mkString("/") + val url = base + path.mkString("/") EitherT(Task{ implicit ec => logger.foreach(_.fetching(url)) @@ -111,6 +111,28 @@ case class Remote(base: String, logger: Option[Logger] = None) extends Repositor def versions(organization: String, name: String, - cachePolicy: CachePolicy): EitherT[Task, String, Versions] = ??? + cachePolicy: CachePolicy): EitherT[Task, String, Versions] = { + + val path = { + organization.split('.').toSeq ++ Seq( + name, + "maven-metadata.xml" + ) + } .map(Remote.encodeURIComponent) + + val url = base + path.mkString("/") + + EitherT(Task{ implicit ec => + logger.foreach(_.fetching(url)) + Remote.get(url).recover{case e: Exception => Left(e.getMessage)}.map{ eitherXml => + logger.foreach(_.fetched(url)) + for { + xml <- \/.fromEither(eitherXml) + _ <- if (xml.label == "metadata") \/-(()) else -\/("Metadata not found") + versions <- Xml.versions(xml) + } yield versions + } + }) + } } diff --git a/core-js/src/main/scala/scalaz/concurrent/package.scala b/core-js/src/main/scala/scalaz/concurrent/package.scala index c0ace3e11..095f06222 100644 --- a/core-js/src/main/scala/scalaz/concurrent/package.scala +++ b/core-js/src/main/scala/scalaz/concurrent/package.scala @@ -32,10 +32,10 @@ package object concurrent { def runF(implicit ec: ExecutionContext) = Future.traverse(tasks)(_.runF) } - implicit val taskFunctor: Functor[Task] = - new Functor[Task] { - def map[A, B](fa: Task[A])(f: A => B): Task[B] = - fa.map(f) + implicit val taskMonad: Monad[Task] = + new Monad[Task] { + def point[A](a: => A): Task[A] = Task.now(a) + def bind[A,B](fa: Task[A])(f: A => Task[B]): Task[B] = fa.flatMap(f) } } diff --git a/core-jvm/src/main/scala/coursier/core/Remote.scala b/core-jvm/src/main/scala/coursier/core/Remote.scala index d6673a9af..d26dd74e2 100644 --- a/core-jvm/src/main/scala/coursier/core/Remote.scala +++ b/core-jvm/src/main/scala/coursier/core/Remote.scala @@ -103,7 +103,8 @@ case class ArtifactDownloader(root: String, cache: File, logger: Option[Artifact val tasks = artifacts0 .map { artifact0 => - artifact(dependency.module, dependency.version, artifact0, cachePolicy = cachePolicy).run + // Important: using version from project, as the one from dependency can be an interval + artifact(dependency.module, project.version, artifact0, cachePolicy = cachePolicy).run } Task.gatherUnordered(tasks) @@ -151,23 +152,15 @@ object Remote { case class Remote(root: String, cache: Option[File] = None, - logger: Option[RemoteLogger] = None) extends Repository { + logger: Option[RemoteLogger] = None) extends MavenRepository { - def find(module: Module, - version: String, - cachePolicy: CachePolicy): EitherT[Task, String, Project] = { + private def get(path: Seq[String], + cachePolicy: CachePolicy): EitherT[Task, String, String] = { - val relPath = - module.organization.split('.').toSeq ++ Seq( - module.name, - version, - s"${module.name}-$version.pom" - ) - - def localFile = { + lazy val localFile = { for { cache0 <- cache.toRightDisjunction("No cache") - f = (cache0 /: relPath)(new File(_, _)) + f = (cache0 /: path)(new File(_, _)) } yield f } @@ -185,7 +178,7 @@ case class Remote(root: String, } def remote = { - val urlStr = root + relPath.mkString("/") + val urlStr = root + path.mkString("/") val url = new URL(urlStr) def log = Task(logger.foreach(_.downloading(urlStr))) @@ -209,7 +202,21 @@ case class Remote(root: String, ) } - val task = cachePolicy.saving(locally)(remote)(save) + EitherT(cachePolicy.saving(locally)(remote)(save)) + } + + def findNoInterval(module: Module, + version: String, + cachePolicy: CachePolicy): EitherT[Task, String, Project] = { + + val path = + module.organization.split('.').toSeq ++ Seq( + module.name, + version, + s"${module.name}-$version.pom" + ) + + val task = get(path, cachePolicy).run .map(eitherStr => for { str <- eitherStr @@ -226,29 +233,13 @@ case class Remote(root: String, name: String, cachePolicy: CachePolicy): EitherT[Task, String, Versions] = { - val relPath = + val path = organization.split('.').toSeq ++ Seq( name, "maven-metadata.xml" ) - def locally = { - ??? - } - - def remote = { - val urlStr = root + relPath.mkString("/") - val url = new URL(urlStr) - - Remote.readFully(url.openStream()) - } - - def save(s: String) = { - // TODO - Task.now(()) - } - - val task = cachePolicy.saving(locally)(remote)(save) + val task = get(path, cachePolicy).run .map(eitherStr => for { str <- eitherStr diff --git a/core/src/main/scala/coursier/core/Definitions.scala b/core/src/main/scala/coursier/core/Definitions.scala index 393a949fb..ee48bd04e 100644 --- a/core/src/main/scala/coursier/core/Definitions.scala +++ b/core/src/main/scala/coursier/core/Definitions.scala @@ -68,7 +68,8 @@ case class Project(module: Module, parent: Option[(Module, String)], dependencyManagement: Seq[Dependency], properties: Map[String, String], - profiles: Seq[Profile]) { + profiles: Seq[Profile], + versions: Option[Versions]) { def moduleVersion = (module, version) } @@ -89,3 +90,12 @@ case class Profile(id: String, dependencies: Seq[Dependency], dependencyManagement: Seq[Dependency], properties: Map[String, String]) + +case class Versions(latest: String, + release: String, + available: List[String], + lastUpdated: Option[Versions.DateTime]) + +object Versions { + case class DateTime(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) +} diff --git a/core/src/main/scala/coursier/core/Repository.scala b/core/src/main/scala/coursier/core/Repository.scala index 52ca12a8f..c953d64cc 100644 --- a/core/src/main/scala/coursier/core/Repository.scala +++ b/core/src/main/scala/coursier/core/Repository.scala @@ -1,6 +1,6 @@ package coursier.core -import scalaz.{\/, EitherT} +import scalaz.{-\/, \/-, \/, EitherT} import scalaz.concurrent.Task trait Repository { @@ -36,3 +36,38 @@ object CachePolicy { remote } } + +trait MavenRepository extends Repository { + + def find(module: Module, + version: String, + cachePolicy: CachePolicy): EitherT[Task, String, Project] = { + + Parse.versionInterval(version).filter(_.isValid) match { + case None => findNoInterval(module, version, cachePolicy) + case Some(itv) => + versions(module.organization, module.name, cachePolicy).flatMap { versions0 => + val eitherVersion = { + val release = Version(versions0.release) + if (itv.contains(release)) \/-(versions0.release) + else { + val inInterval = versions0.available.map(Version(_)).filter(itv.contains) + if (inInterval.isEmpty) -\/(s"No version found for $version") + else \/-(inInterval.max.repr) + } + } + + eitherVersion match { + case -\/(reason) => EitherT[Task, String, Project](Task.now(-\/(reason))) + case \/-(version0) => findNoInterval(module, version0, cachePolicy) + .map(_.copy(versions = Some(versions0))) + } + } + } + } + + def findNoInterval(module: Module, + version: String, + cachePolicy: CachePolicy): EitherT[Task, String, Project] + +} diff --git a/core/src/main/scala/coursier/core/Resolver.scala b/core/src/main/scala/coursier/core/Resolver.scala index 7b901afcd..0c2bd39da 100644 --- a/core/src/main/scala/coursier/core/Resolver.scala +++ b/core/src/main/scala/coursier/core/Resolver.scala @@ -591,20 +591,20 @@ object Resolver { val lookups = modules.map(dep => fetchModule(dep).run.map(dep -> _)) val gatheredLookups = Task.gatherUnordered(lookups, exceptionCancels = true) gatheredLookups.flatMap{ lookupResults => - val errors0 = errors ++ lookupResults.collect{case (mod, -\/(repoErrors)) => mod -> repoErrors} - val newProjects = lookupResults.collect{case (mod, \/-(proj)) => mod -> proj} + val errors0 = errors ++ lookupResults.collect{case (modVer, -\/(repoErrors)) => modVer -> repoErrors} + val newProjects = lookupResults.collect{case (modVer, \/-(proj)) => modVer -> proj} /* * newProjects are project definitions, fresh from the repositories. We need to add * dependency management / inheritance-related bits to them. */ - newProjects.foldLeft(Task.now(copy(errors = errors0))) { case (accTask, (mod, (repo, proj))) => + newProjects.foldLeft(Task.now(copy(errors = errors0))) { case (accTask, (modVer, (repo, proj))) => for { current <- accTask updated <- current.fetch(current.dependencyManagementMissing(proj).toList, fetchModule) proj0 = updated.withDependencyManagement(proj) - } yield updated.copy(projectsCache = updated.projectsCache + (proj0.moduleVersion -> (repo, proj0))) + } yield updated.copy(projectsCache = updated.projectsCache + (modVer -> (repo, proj0))) } } } diff --git a/core/src/main/scala/coursier/core/Versions.scala b/core/src/main/scala/coursier/core/Versions.scala index 024d953a0..ae4877200 100644 --- a/core/src/main/scala/coursier/core/Versions.scala +++ b/core/src/main/scala/coursier/core/Versions.scala @@ -1,16 +1,5 @@ package coursier.core -case class Versions(latest: String, - release: String, - available: List[String], - lastUpdated: Option[Versions.DateTime]) - -object Versions { - - case class DateTime(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) - -} - case class VersionInterval(from: Option[Version], to: Option[Version], fromIncluded: Boolean, @@ -27,6 +16,21 @@ case class VersionInterval(from: Option[Version], fromToOrder.forall(x => x) && (from.nonEmpty || !fromIncluded) && (to.nonEmpty || !toIncluded) } + def contains(version: Version): Boolean = { + val fromCond = + from.forall { from0 => + val cmp = from0.compare(version) + cmp < 0 || cmp == 0 && fromIncluded + } + lazy val toCond = + to.forall { to0 => + val cmp = version.compare(to0) + cmp < 0 || cmp == 0 && toIncluded + } + + fromCond && toCond + } + def merge(other: VersionInterval): Option[VersionInterval] = { val (newFrom, newFromIncluded) = (from, other.from) match { diff --git a/core/src/main/scala/coursier/core/Xml.scala b/core/src/main/scala/coursier/core/Xml.scala index 8c28485d2..621b3272f 100644 --- a/core/src/main/scala/coursier/core/Xml.scala +++ b/core/src/main/scala/coursier/core/Xml.scala @@ -209,7 +209,8 @@ object Xml { parentModuleOpt.map((_, parentVersionOpt.getOrElse(""))), depMgmts, properties.toMap, - profiles + profiles, + None ) } diff --git a/core/src/main/scala/coursier/package.scala b/core/src/main/scala/coursier/package.scala index c258a3ab2..01a2083b5 100644 --- a/core/src/main/scala/coursier/package.scala +++ b/core/src/main/scala/coursier/package.scala @@ -32,8 +32,9 @@ package object coursier { parent: Option[ModuleVersion] = None, dependencyManagement: Seq[Dependency] = Seq.empty, properties: Map[String, String] = Map.empty, - profiles: Seq[Profile] = Seq.empty): Project = - core.Project(module, version, dependencies, parent, dependencyManagement, properties, profiles) + profiles: Seq[Profile] = Seq.empty, + versions: Option[core.Versions] = None): Project = + core.Project(module, version, dependencies, parent, dependencyManagement, properties, profiles, versions) } type Profile = core.Profile diff --git a/core/src/test/scala/coursier/test/CentralTests.scala b/core/src/test/scala/coursier/test/CentralTests.scala index 46a3e635f..1d58286d6 100644 --- a/core/src/test/scala/coursier/test/CentralTests.scala +++ b/core/src/test/scala/coursier/test/CentralTests.scala @@ -67,6 +67,24 @@ object CentralTests extends TestSuite { assert(res == expected) } } + 'jodaVersionInterval{ + async { + val dep = Dependency(Module("joda-time", "joda-time"), "[2.2,2.8]") + val res0 = await(resolve(Set(dep), fetchFrom(repositories)).runF) + val res = res0.copy(projectsCache = Map.empty, errors = Map.empty) + + val expected = Resolution( + rootDependencies = Set(dep.withCompileScope), + dependencies = Set( + dep.withCompileScope)) + + assert(res == expected) + assert(res0.projectsCache.contains(dep.moduleVersion)) + + val (_, proj) = res0.projectsCache(dep.moduleVersion) + assert(proj.version == "2.8") + } + } 'spark{ resolutionCheck(Module("org.apache.spark", "spark-core_2.11"), "1.3.1") } diff --git a/core/src/test/scala/coursier/test/VersionIntervalTests.scala b/core/src/test/scala/coursier/test/VersionIntervalTests.scala index 4a7c06288..0d7b7fa15 100644 --- a/core/src/test/scala/coursier/test/VersionIntervalTests.scala +++ b/core/src/test/scala/coursier/test/VersionIntervalTests.scala @@ -121,6 +121,46 @@ object VersionIntervalTests extends TestSuite { } } + 'contains{ + val v21 = Version("2.1") + val v22 = Version("2.2") + val v23 = Version("2.3") + val v24 = Version("2.4") + val v25 = Version("2.5") + val v26 = Version("2.6") + val v27 = Version("2.7") + val v28 = Version("2.8") + + 'basic{ + val itv = Parse.versionInterval("[2.2,)").get + + assert(!itv.contains(v21)) + assert(itv.contains(v22)) + assert(itv.contains(v23)) + assert(itv.contains(v24)) + } + 'open{ + val itv = Parse.versionInterval("(2.2,)").get + + assert(!itv.contains(v21)) + assert(!itv.contains(v22)) + assert(itv.contains(v23)) + assert(itv.contains(v24)) + } + 'segment{ + val itv = Parse.versionInterval("[2.2,2.8]").get + + assert(!itv.contains(v21)) + assert(itv.contains(v22)) + assert(itv.contains(v23)) + assert(itv.contains(v24)) + assert(itv.contains(v25)) + assert(itv.contains(v26)) + assert(itv.contains(v27)) + assert(itv.contains(v28)) + } + } + 'parse{ 'malformed{ val s1 = "[1.1]" diff --git a/core/src/test/scala/coursier/test/VersionTests.scala b/core/src/test/scala/coursier/test/VersionTests.scala index 41bf57b44..5b1918f70 100644 --- a/core/src/test/scala/coursier/test/VersionTests.scala +++ b/core/src/test/scala/coursier/test/VersionTests.scala @@ -26,6 +26,19 @@ object VersionTests extends TestSuite { assert(v.isEmpty) } + 'max{ + val v21 = Version("2.1") + val v22 = Version("2.2") + val v23 = Version("2.3") + val v24 = Version("2.4") + val v241 = Version("2.4.1") + + val l = Seq(v21, v22, v23, v24, v241) + val max = l.max + + assert(max == v241) + } + 'numericOrdering{ assert(compare("1.2", "1.10") < 0) } diff --git a/web/src/main/scala/coursier/web/Backend.scala b/web/src/main/scala/coursier/web/Backend.scala index 0916c2d96..1b5c73f57 100644 --- a/web/src/main/scala/coursier/web/Backend.scala +++ b/web/src/main/scala/coursier/web/Backend.scala @@ -217,7 +217,7 @@ object App { label ) - def depItem(dep: Dependency) = { + def depItem(dep: Dependency, finalVersionOpt: Option[String]) = { val (type0, classifier) = dep.artifacts match { case maven: Artifacts.Maven => (maven.`type`, maven.classifier) } @@ -226,7 +226,7 @@ object App { ^.`class` := (if (res.errors.contains(dep.moduleVersion)) "danger" else ""), <.td(dep.module.organization), <.td(dep.module.name), - <.td(dep.version), + <.td(finalVersionOpt.fold(dep.version)(finalVersion => s"$finalVersion (for ${dep.version})")), <.td(Seq[Seq[TagMod]]( if (dep.scope == Scope.Compile) Seq() else Seq(infoLabel(dep.scope.name)), if (type0.isEmpty || type0 == "jar") Seq() else Seq(infoLabel(type0)), @@ -239,11 +239,12 @@ object App { res.projectsCache.get(dep.moduleVersion) match { case Some((repo: Remote, _)) => // FIXME Maven specific, generalize if/when adding support for Ivy + val version0 = finalVersionOpt getOrElse dep.version val relPath = dep.module.organization.split('.').toSeq ++ Seq( dep.module.name, - dep.version, - s"${dep.module.name}-${dep.version}" + version0, + s"${dep.module.name}-$version0" ) Seq( @@ -275,7 +276,7 @@ object App { ) ), <.tbody( - sortedDeps.map(depItem) + sortedDeps.map(dep => depItem(dep, res.projectsCache.get(dep.moduleVersion).map(_._2.version).filter(_ != dep.version))) ) ) }