Merge pull request #108 from alexarchambault/topic/ivy-cache

Add support for ~/.ivy2/cache as a repository
This commit is contained in:
Alexandre Archambault 2016-01-14 09:52:10 +01:00
commit b7a0c62c25
5 changed files with 205 additions and 141 deletions

View File

@ -16,6 +16,9 @@ import java.io.{ Serializable => _, _ }
object Cache { object Cache {
// Check SHA-1 if available, else be fine with no checksum
val defaultChecksums = Seq(Some("SHA-1"), None)
private def withLocal(artifact: Artifact, cache: Seq[(String, File)]): Artifact = { private def withLocal(artifact: Artifact, cache: Seq[(String, File)]): Artifact = {
def local(url: String) = def local(url: String) =
if (url.startsWith("file:///")) if (url.startsWith("file:///"))
@ -398,7 +401,7 @@ object Cache {
artifact: Artifact, artifact: Artifact,
cache: Seq[(String, File)] = default, cache: Seq[(String, File)] = default,
cachePolicy: CachePolicy = CachePolicy.FetchMissing, cachePolicy: CachePolicy = CachePolicy.FetchMissing,
checksums: Seq[Option[String]] = Seq(Some("SHA-1")), checksums: Seq[Option[String]] = defaultChecksums,
logger: Option[Logger] = None, logger: Option[Logger] = None,
pool: ExecutorService = defaultPool pool: ExecutorService = defaultPool
): EitherT[Task, FileError, File] = { ): EitherT[Task, FileError, File] = {
@ -449,7 +452,7 @@ object Cache {
def fetch( def fetch(
cache: Seq[(String, File)] = default, cache: Seq[(String, File)] = default,
cachePolicy: CachePolicy = CachePolicy.FetchMissing, cachePolicy: CachePolicy = CachePolicy.FetchMissing,
checksums: Seq[Option[String]] = Seq(Some("SHA-1")), checksums: Seq[Option[String]] = defaultChecksums,
logger: Option[Logger] = None, logger: Option[Logger] = None,
pool: ExecutorService = defaultPool pool: ExecutorService = defaultPool
): Fetch.Content[Task] = { ): Fetch.Content[Task] = {
@ -467,13 +470,33 @@ object Cache {
} }
} }
private lazy val ivy2HomeUri = {
// a bit touchy on Windows... - don't try to manually write down the URI with s"file://..."
val str = new File(sys.props("user.home") + "/.ivy2/").toURI.toString
if (str.endsWith("/"))
str
else
str + "/"
}
lazy val ivy2Local = IvyRepository( lazy val ivy2Local = IvyRepository(
// a bit touchy on Windows... - don't try to get the URI manually like s"file://..." ivy2HomeUri + "local/" +
new File(sys.props("user.home") + "/.ivy2/local/").toURI.toString +
"[organisation]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/" + "[organisation]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/" +
"[artifact](-[classifier]).[ext]" "[artifact](-[classifier]).[ext]"
) )
lazy val ivy2Cache = IvyRepository(
ivy2HomeUri + "cache/" +
"(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]",
metadataPatternOpt = Some(
ivy2HomeUri + "cache/" +
"(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[organisation]/[module]/[type]-[revision](-[classifier]).[ext]"
),
withChecksums = false,
withSignatures = false,
dropInfoAttributes = true
)
lazy val defaultBase = new File( lazy val defaultBase = new File(
sys.env.getOrElse( sys.env.getOrElse(
"COURSIER_CACHE", "COURSIER_CACHE",

View File

@ -10,6 +10,8 @@ object CacheParse {
def repository(s: String): Validation[String, Repository] = def repository(s: String): Validation[String, Repository] =
if (s == "ivy2local" || s == "ivy2Local") if (s == "ivy2local" || s == "ivy2Local")
Cache.ivy2Local.success Cache.ivy2Local.success
else if (s == "ivy2cache" || s == "ivy2Cache")
Cache.ivy2Cache.success
else { else {
val repo = Parse.repository(s) val repo = Parse.repository(s)

View File

@ -2,146 +2,27 @@ package coursier.ivy
import coursier.Fetch import coursier.Fetch
import coursier.core._ import coursier.core._
import scala.annotation.tailrec
import scala.util.matching.Regex
import scalaz._ import scalaz._
import java.util.regex.Pattern.quote
object IvyRepository {
val optionalPartRegex = (quote("(") + "[^" + quote("{()}") + "]*" + quote(")")).r
val variableRegex = (quote("[") + "[^" + quote("{[()]}") + "]*" + quote("]")).r
val propertyRegex = (quote("${") + "[^" + quote("{[()]}") + "]*" + quote("}")).r
sealed abstract class PatternPart(val effectiveStart: Int, val effectiveEnd: Int) extends Product with Serializable {
require(effectiveStart <= effectiveEnd)
def start = effectiveStart
def end = effectiveEnd
// FIXME Some kind of validation should be used here, to report all the missing variables,
// not only the first one missing.
def apply(content: String): Map[String, String] => String \/ String
}
object PatternPart {
case class Literal(override val effectiveStart: Int, override val effectiveEnd: Int) extends PatternPart(effectiveStart, effectiveEnd) {
def apply(content: String): Map[String, String] => String \/ String = {
assert(content.length == effectiveEnd - effectiveStart)
val matches = variableRegex.findAllMatchIn(content).toList
variables =>
@tailrec
def helper(idx: Int, matches: List[Regex.Match], b: StringBuilder): String \/ String =
if (idx >= content.length)
\/-(b.result())
else {
assert(matches.headOption.forall(_.start >= idx))
matches.headOption.filter(_.start == idx) match {
case Some(m) =>
val variableName = content.substring(m.start + 1, m.end - 1)
variables.get(variableName) match {
case None => -\/(s"Variable not found: $variableName")
case Some(value) =>
b ++= value
helper(m.end, matches.tail, b)
}
case None =>
val nextIdx = matches.headOption.fold(content.length)(_.start)
b ++= content.substring(idx, nextIdx)
helper(nextIdx, matches, b)
}
}
helper(0, matches, new StringBuilder)
}
}
case class Optional(start0: Int, end0: Int) extends PatternPart(start0 + 1, end0 - 1) {
override def start = start0
override def end = end0
def apply(content: String): Map[String, String] => String \/ String = {
assert(content.length == effectiveEnd - effectiveStart)
val inner = Literal(effectiveStart, effectiveEnd).apply(content)
variables =>
\/-(inner(variables).fold(_ => "", x => x))
}
}
}
def substituteProperties(s: String, properties: Map[String, String]): String =
propertyRegex.findAllMatchIn(s).toVector.foldRight(s) { case (m, s0) =>
val key = s0.substring(m.start + "${".length, m.end - "}".length)
val value = properties.getOrElse(key, "")
s0.take(m.start) + value + s0.drop(m.end)
}
}
case class IvyRepository( case class IvyRepository(
pattern: String, pattern: String,
metadataPatternOpt: Option[String] = None,
changing: Option[Boolean] = None, changing: Option[Boolean] = None,
properties: Map[String, String] = Map.empty, properties: Map[String, String] = Map.empty,
withChecksums: Boolean = true, withChecksums: Boolean = true,
withSignatures: Boolean = true, withSignatures: Boolean = true,
withArtifacts: Boolean = true withArtifacts: Boolean = true,
// hack for SBT putting infos in properties
dropInfoAttributes: Boolean = false
) extends Repository { ) extends Repository {
def metadataPattern: String = metadataPatternOpt.getOrElse(pattern)
import Repository._ import Repository._
import IvyRepository._
private val pattern0 = substituteProperties(pattern, properties) private val pattern0 = Pattern(pattern, properties)
private val metadataPattern0 = Pattern(metadataPattern, properties)
val parts = {
val optionalParts = optionalPartRegex.findAllMatchIn(pattern0).toList.map { m =>
PatternPart.Optional(m.start, m.end)
}
val len = pattern0.length
@tailrec
def helper(
idx: Int,
opt: List[PatternPart.Optional],
acc: List[PatternPart]
): Vector[PatternPart] =
if (idx >= len)
acc.toVector.reverse
else
opt match {
case Nil =>
helper(len, Nil, PatternPart.Literal(idx, len) :: acc)
case (opt0 @ PatternPart.Optional(start0, end0)) :: rem =>
if (idx < start0)
helper(start0, opt, PatternPart.Literal(idx, start0) :: acc)
else {
assert(idx == start0, s"idx: $idx, start0: $start0")
helper(end0, rem, opt0 :: acc)
}
}
helper(0, optionalParts, Nil)
}
assert(pattern0.isEmpty == parts.isEmpty)
if (pattern0.nonEmpty) {
for ((a, b) <- parts.zip(parts.tail))
assert(a.end == b.start)
assert(parts.head.start == 0)
assert(parts.last.end == pattern0.length)
}
private val substituteHelpers = parts.map { part =>
part(pattern0.substring(part.effectiveStart, part.effectiveEnd))
}
def substitute(variables: Map[String, String]): String \/ String =
substituteHelpers.foldLeft[String \/ String](\/-("")) {
case (acc0, helper) =>
for {
acc <- acc0
s <- helper(variables)
} yield acc + s
}
// See http://ant.apache.org/ivy/history/latest-milestone/concept.html for a // See http://ant.apache.org/ivy/history/latest-milestone/concept.html for a
// list of variables that should be supported. // list of variables that should be supported.
@ -194,7 +75,7 @@ case class IvyRepository(
} }
val retainedWithUrl = retained.flatMap { p => val retainedWithUrl = retained.flatMap { p =>
substitute(variables( pattern0.substitute(variables(
dependency.module, dependency.module,
dependency.version, dependency.version,
p.`type`, p.`type`,
@ -236,7 +117,7 @@ case class IvyRepository(
val eitherArtifact: String \/ Artifact = val eitherArtifact: String \/ Artifact =
for { for {
url <- substitute( url <- metadataPattern0.substitute(
variables(module, version, "ivy", "ivy", "xml", None) variables(module, version, "ivy", "ivy", "xml", None)
) )
} yield { } yield {
@ -259,14 +140,28 @@ case class IvyRepository(
for { for {
artifact <- EitherT(F.point(eitherArtifact)) artifact <- EitherT(F.point(eitherArtifact))
ivy <- fetch(artifact) ivy <- fetch(artifact)
proj <- EitherT(F.point { proj0 <- EitherT(F.point {
for { for {
xml <- \/.fromEither(compatibility.xmlParse(ivy)) xml <- \/.fromEither(compatibility.xmlParse(ivy))
_ <- if (xml.label == "ivy-module") \/-(()) else -\/("Module definition not found") _ <- if (xml.label == "ivy-module") \/-(()) else -\/("Module definition not found")
proj <- IvyXml.project(xml) proj <- IvyXml.project(xml)
} yield proj } yield proj
}) })
} yield (source, proj) } yield {
val proj =
if (dropInfoAttributes)
proj0.copy(
module = proj0.module.copy(
attributes = proj0.module.attributes.filter {
case (k, _) => !k.startsWith("info.")
}
)
)
else
proj0
(source, proj)
}
} }
} }

View File

@ -0,0 +1,142 @@
package coursier.ivy
import scala.annotation.tailrec
import scalaz._
import scala.util.matching.Regex
import java.util.regex.Pattern.quote
object Pattern {
val propertyRegex = (quote("${") + "[^" + quote("{[()]}") + "]*" + quote("}")).r
val optionalPartRegex = (quote("(") + "[^" + quote("{()}") + "]*" + quote(")")).r
val variableRegex = (quote("[") + "[^" + quote("{[()]}") + "]*" + quote("]")).r
sealed abstract class PatternPart(val effectiveStart: Int, val effectiveEnd: Int) extends Product with Serializable {
require(effectiveStart <= effectiveEnd)
def start = effectiveStart
def end = effectiveEnd
// FIXME Some kind of validation should be used here, to report all the missing variables,
// not only the first one missing.
def apply(content: String): Map[String, String] => String \/ String
}
object PatternPart {
case class Literal(override val effectiveStart: Int, override val effectiveEnd: Int) extends PatternPart(effectiveStart, effectiveEnd) {
def apply(content: String): Map[String, String] => String \/ String = {
assert(content.length == effectiveEnd - effectiveStart)
val matches = variableRegex.findAllMatchIn(content).toList
variables =>
@tailrec
def helper(idx: Int, matches: List[Regex.Match], b: StringBuilder): String \/ String =
if (idx >= content.length)
\/-(b.result())
else {
assert(matches.headOption.forall(_.start >= idx))
matches.headOption.filter(_.start == idx) match {
case Some(m) =>
val variableName = content.substring(m.start + 1, m.end - 1)
variables.get(variableName) match {
case None => -\/(s"Variable not found: $variableName")
case Some(value) =>
b ++= value
helper(m.end, matches.tail, b)
}
case None =>
val nextIdx = matches.headOption.fold(content.length)(_.start)
b ++= content.substring(idx, nextIdx)
helper(nextIdx, matches, b)
}
}
helper(0, matches, new StringBuilder)
}
}
case class Optional(start0: Int, end0: Int) extends PatternPart(start0 + 1, end0 - 1) {
override def start = start0
override def end = end0
def apply(content: String): Map[String, String] => String \/ String = {
assert(content.length == effectiveEnd - effectiveStart)
val inner = Literal(effectiveStart, effectiveEnd).apply(content)
variables =>
\/-(inner(variables).fold(_ => "", x => x))
}
}
}
def substituteProperties(s: String, properties: Map[String, String]): String =
propertyRegex.findAllMatchIn(s).toVector.foldRight(s) { case (m, s0) =>
val key = s0.substring(m.start + "${".length, m.end - "}".length)
val value = properties.getOrElse(key, "")
s0.take(m.start) + value + s0.drop(m.end)
}
}
case class Pattern(
pattern: String,
properties: Map[String, String]
) {
import Pattern._
private val pattern0 = substituteProperties(pattern, properties)
val parts = {
val optionalParts = optionalPartRegex.findAllMatchIn(pattern0).toList.map { m =>
PatternPart.Optional(m.start, m.end)
}
val len = pattern0.length
@tailrec
def helper(
idx: Int,
opt: List[PatternPart.Optional],
acc: List[PatternPart]
): Vector[PatternPart] =
if (idx >= len)
acc.toVector.reverse
else
opt match {
case Nil =>
helper(len, Nil, PatternPart.Literal(idx, len) :: acc)
case (opt0 @ PatternPart.Optional(start0, end0)) :: rem =>
if (idx < start0)
helper(start0, opt, PatternPart.Literal(idx, start0) :: acc)
else {
assert(idx == start0, s"idx: $idx, start0: $start0")
helper(end0, rem, opt0 :: acc)
}
}
helper(0, optionalParts, Nil)
}
assert(pattern0.isEmpty == parts.isEmpty)
if (pattern0.nonEmpty) {
for ((a, b) <- parts.zip(parts.tail))
assert(a.end == b.start)
assert(parts.head.start == 0)
assert(parts.last.end == pattern0.length)
}
private val substituteHelpers = parts.map { part =>
part(pattern0.substring(part.effectiveStart, part.effectiveEnd))
}
def substitute(variables: Map[String, String]): String \/ String =
substituteHelpers.foldLeft[String \/ String](\/-("")) {
case (acc0, helper) =>
for {
acc <- acc0
s <- helper(variables)
} yield acc + s
}
}

View File

@ -115,20 +115,22 @@ object FromSbt {
case sbt.FileRepository(_, _, patterns) case sbt.FileRepository(_, _, patterns)
if patterns.ivyPatterns.lengthCompare(1) == 0 && if patterns.ivyPatterns.lengthCompare(1) == 0 &&
patterns.ivyPatterns == patterns.artifactPatterns => patterns.artifactPatterns.lengthCompare(1) == 0 =>
Some(IvyRepository( Some(IvyRepository(
"file://" + patterns.ivyPatterns.head, "file://" + patterns.artifactPatterns.head,
metadataPatternOpt = Some("file://" + patterns.ivyPatterns.head),
changing = Some(true), changing = Some(true),
properties = ivyProperties properties = ivyProperties
)) ))
case sbt.URLRepository(_, patterns) case sbt.URLRepository(_, patterns)
if patterns.ivyPatterns.lengthCompare(1) == 0 && if patterns.ivyPatterns.lengthCompare(1) == 0 &&
patterns.ivyPatterns == patterns.artifactPatterns => patterns.artifactPatterns.lengthCompare(1) == 0 =>
Some(IvyRepository( Some(IvyRepository(
patterns.ivyPatterns.head, patterns.artifactPatterns.head,
metadataPatternOpt = Some(patterns.ivyPatterns.head),
changing = None, changing = None,
properties = ivyProperties properties = ivyProperties
)) ))