Handling version interval properly

Fixes https://github.com/alexarchambault/coursier/issues/2
This commit is contained in:
Alexandre Archambault 2015-06-22 22:34:19 +01:00
parent 35b5790be7
commit 4391859a72
14 changed files with 214 additions and 71 deletions

View File

@ -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)

View File

@ -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
}
})
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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]
}

View File

@ -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)))
}
}
}

View File

@ -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 {

View File

@ -209,7 +209,8 @@ object Xml {
parentModuleOpt.map((_, parentVersionOpt.getOrElse(""))),
depMgmts,
properties.toMap,
profiles
profiles,
None
)
}

View File

@ -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

View File

@ -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")
}

View File

@ -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]"

View File

@ -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)
}

View File

@ -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)))
)
)
}