From 1c34362b6fb7aa210b9963da62792c844f43e515 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sun, 3 Jul 2016 17:21:17 +0200 Subject: [PATCH] Better handling of Ivy patterns --- build.sbt | 26 ++ cache/src/main/scala/coursier/Cache.scala | 8 +- .../src/main/scala/coursier/CacheParse.scala | 23 +- .../scala/coursier/ivy/IvyRepository.scala | 137 ++++++-- .../src/main/scala/coursier/ivy/Pattern.scala | 298 ++++++++++-------- .../src/main/scala/coursier/util/Parse.scala | 21 +- .../src/main/scala-2.10/coursier/Tasks.scala | 5 +- .../test/scala/coursier/test/IvyTests.scala | 4 +- .../coursier_2.11/1.0.0-SNAPSHOT | 3 + .../coursier/test/IvyPatternParserTests.scala | 135 ++++++++ 10 files changed, 485 insertions(+), 175 deletions(-) create mode 100644 tests/shared/src/test/scala/coursier/test/IvyPatternParserTests.scala diff --git a/build.sbt b/build.sbt index 1b1df7f12..827671710 100644 --- a/build.sbt +++ b/build.sbt @@ -119,6 +119,7 @@ lazy val core = crossProject .settings(mimaDefaultSettings: _*) .settings( name := "coursier", + libraryDependencies += "com.lihaoyi" %%% "fastparse" % "0.3.7", resourceGenerators.in(Compile) += { (target, version).map { (dir, ver) => import sys.process._ @@ -152,6 +153,31 @@ lazy val core = crossProject ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Project.apply"), ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Project.copy"), ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Project.this"), + // Reworked Ivy pattern handling + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.pattern"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.copy"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.properties"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.parts"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.substitute"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.this"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.substituteProperties"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.propertyRegex"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.apply"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.variableRegex"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.optionalPartRegex"), + ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$Literal$"), + ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart"), + ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.ivy.IvyRepository.apply"), + ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$Optional$"), + ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$Literal"), + ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$Optional"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("coursier.ivy.IvyRepository.pattern"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.ivy.IvyRepository.copy"), + ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.IvyRepository.properties"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("coursier.ivy.IvyRepository.metadataPattern"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.ivy.IvyRepository.this"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("coursier.util.Parse.repository"), // Since 1.0.0-M12 // Extra `authentication` field ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Artifact.apply"), diff --git a/cache/src/main/scala/coursier/Cache.scala b/cache/src/main/scala/coursier/Cache.scala index 8b9fa63fc..4a2e959f6 100644 --- a/cache/src/main/scala/coursier/Cache.scala +++ b/cache/src/main/scala/coursier/Cache.scala @@ -864,12 +864,12 @@ object Cache { str + "/" } - lazy val ivy2Local = IvyRepository( - ivy2HomeUri + "local/" + coursier.ivy.Pattern.default, + lazy val ivy2Local = IvyRepository.fromPattern( + (ivy2HomeUri + "local/") +: coursier.ivy.Pattern.default, dropInfoAttributes = true ) - lazy val ivy2Cache = IvyRepository( + lazy val ivy2Cache = IvyRepository.parse( ivy2HomeUri + "cache/" + "(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]", metadataPatternOpt = Some( @@ -879,6 +879,8 @@ object Cache { withChecksums = false, withSignatures = false, dropInfoAttributes = true + ).getOrElse( + throw new Exception("Cannot happen") ) lazy val default = new File( diff --git a/cache/src/main/scala/coursier/CacheParse.scala b/cache/src/main/scala/coursier/CacheParse.scala index 9bf1467ce..8ebc79234 100644 --- a/cache/src/main/scala/coursier/CacheParse.scala +++ b/cache/src/main/scala/coursier/CacheParse.scala @@ -18,17 +18,21 @@ object CacheParse { else { val repo = Parse.repository(s) - val url = repo match { + val url = repo.map { case m: MavenRepository => m.root case i: IvyRepository => - i.pattern + // FIXME We're not handling metadataPattern here + i.pattern.chunks.takeWhile { + case _: coursier.ivy.Pattern.Chunk.Const => true + case _ => false + }.map(_.string).mkString case r => sys.error(s"Unrecognized repository: $r") } val validatedUrl = try { - Cache.url(url).success + url.map(Cache.url).validation } catch { case e: MalformedURLException => ("Error parsing URL " + url + Option(e.getMessage).fold("")(" (" + _ + ")")).failure @@ -37,7 +41,7 @@ object CacheParse { validatedUrl.flatMap { url => Option(url.getUserInfo) match { case None => - repo.success + repo.validation case Some(userInfo) => userInfo.split(":", 2) match { case Array(user, password) => @@ -48,7 +52,7 @@ object CacheParse { url.getFile ).toString - val repo0 = repo match { + repo.validation.map { case m: MavenRepository => m.copy( root = baseUrl, @@ -56,15 +60,18 @@ object CacheParse { ) case i: IvyRepository => i.copy( - pattern = baseUrl, + pattern = coursier.ivy.Pattern( + coursier.ivy.Pattern.Chunk.Const(baseUrl) +: i.pattern.chunks.dropWhile { + case _: coursier.ivy.Pattern.Chunk.Const => true + case _ => false + } + ), authentication = Some(Authentication(user, password)) ) case r => sys.error(s"Unrecognized repository: $r") } - repo0.success - case _ => s"No password found in user info of URL $url".failure } diff --git a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala index f309ad872..fd3d562b2 100644 --- a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala +++ b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala @@ -4,40 +4,35 @@ import coursier.Fetch import coursier.core._ import scalaz._ -import scalaz.Scalaz.ToEitherOps +import scalaz.Scalaz._ case class IvyRepository( - pattern: String, - metadataPatternOpt: Option[String] = None, - changing: Option[Boolean] = None, - properties: Map[String, String] = Map.empty, - withChecksums: Boolean = true, - withSignatures: Boolean = true, - withArtifacts: Boolean = true, + pattern: Pattern, + metadataPatternOpt: Option[Pattern], + changing: Option[Boolean], + withChecksums: Boolean, + withSignatures: Boolean, + withArtifacts: Boolean, // hack for SBT putting infos in properties - dropInfoAttributes: Boolean = false, - authentication: Option[Authentication] = None + dropInfoAttributes: Boolean, + authentication: Option[Authentication] ) extends Repository { - def metadataPattern: String = metadataPatternOpt.getOrElse(pattern) + def metadataPattern: Pattern = metadataPatternOpt.getOrElse(pattern) - import Repository._ + lazy val revisionListingPatternOpt: Option[Pattern] = { + val idx = metadataPattern.chunks.indexWhere { chunk => + chunk == Pattern.Chunk.Var("revision") + } - private val pattern0 = Pattern(pattern, properties) - private val metadataPattern0 = Pattern(metadataPattern, properties) - - private val revisionListingPatternOpt = { - val idx = metadataPattern.indexOf("[revision]/") if (idx < 0) None else - // FIXME A bit too permissive... we should check that [revision] indeed begins - // a path component (that is, has a '/' before it no matter what) - // This is trickier than simply checking for a '/' character before it in metadataPattern, - // because of optional parts in it. - Some(Pattern(metadataPattern.take(idx), properties)) + Some(Pattern(metadataPattern.chunks.take(idx))) } + import Repository._ + // See http://ant.apache.org/ivy/history/latest-milestone/concept.html for a // list of variables that should be supported. // Some are missing (branch, conf, originalName). @@ -92,14 +87,14 @@ case class IvyRepository( } val retainedWithUrl = retained.flatMap { p => - pattern0.substitute(variables( + pattern.substituteVariables(variables( dependency.module, Some(project.actualVersion), p.`type`, p.name, p.ext, Some(p.classifier).filter(_.nonEmpty) - )).toList.map(p -> _) + )).toList.map(p -> _) // FIXME Validation errors are ignored } retainedWithUrl.map { case (p, url) => @@ -143,13 +138,13 @@ case class IvyRepository( case None => findNoInverval(module, version, fetch) case Some(itv) => - val listingUrl = revisionListingPattern.substitute( + val listingUrl = revisionListingPattern.substituteVariables( variables(module, None, "ivy", "ivy", "xml", None) ).flatMap { s => if (s.endsWith("/")) s.right else - s"Don't know how to list revisions of $metadataPattern".left + s"Don't know how to list revisions of ${metadataPattern.string}".left } def fromWebPage(s: String) = { @@ -194,7 +189,7 @@ case class IvyRepository( val eitherArtifact: String \/ Artifact = for { - url <- metadataPattern0.substitute( + url <- metadataPattern.substituteVariables( variables(module, Some(version), "ivy", "ivy", "xml", None) ) } yield { @@ -257,3 +252,91 @@ case class IvyRepository( } } + +object IvyRepository { + def parse( + pattern: String, + metadataPatternOpt: Option[String] = None, + changing: Option[Boolean] = None, + properties: Map[String, String] = Map.empty, + withChecksums: Boolean = true, + withSignatures: Boolean = true, + withArtifacts: Boolean = true, + // hack for SBT putting infos in properties + dropInfoAttributes: Boolean = false, + authentication: Option[Authentication] = None + ): String \/ IvyRepository = + + for { + propertiesPattern <- PropertiesPattern.parse(pattern) + metadataPropertiesPatternOpt <- metadataPatternOpt.fold(Option.empty[PropertiesPattern].right[String])(PropertiesPattern.parse(_).map(Some(_))) + + pattern <- propertiesPattern.substituteProperties(properties) + metadataPatternOpt <- metadataPropertiesPatternOpt.fold(Option.empty[Pattern].right[String])(_.substituteProperties(properties).map(Some(_))) + + } yield + IvyRepository( + pattern, + metadataPatternOpt, + changing, + withChecksums, + withSignatures, + withArtifacts, + dropInfoAttributes, + authentication + ) + + // because of the compatibility apply method below, we can't give default values + // to the default constructor of IvyPattern + // this method accepts the same arguments as this constructor, with default values when possible + def fromPattern( + pattern: Pattern, + metadataPatternOpt: Option[Pattern] = None, + changing: Option[Boolean] = None, + withChecksums: Boolean = true, + withSignatures: Boolean = true, + withArtifacts: Boolean = true, + // hack for SBT putting infos in properties + dropInfoAttributes: Boolean = false, + authentication: Option[Authentication] = None + ): IvyRepository = + IvyRepository( + pattern, + metadataPatternOpt, + changing, + withChecksums, + withSignatures, + withArtifacts, + dropInfoAttributes, + authentication + ) + + @deprecated("Can now raise exceptions - use parse instead", "1.0.0-M13") + def apply( + pattern: String, + metadataPatternOpt: Option[String] = None, + changing: Option[Boolean] = None, + properties: Map[String, String] = Map.empty, + withChecksums: Boolean = true, + withSignatures: Boolean = true, + withArtifacts: Boolean = true, + // hack for SBT putting infos in properties + dropInfoAttributes: Boolean = false, + authentication: Option[Authentication] = None + ): IvyRepository = + parse( + pattern, + metadataPatternOpt, + changing, + properties, + withChecksums, + withSignatures, + withArtifacts, + dropInfoAttributes, + authentication + ) match { + case \/-(repo) => repo + case -\/(msg) => + throw new IllegalArgumentException(s"Error while parsing Ivy patterns: $msg") + } +} \ No newline at end of file diff --git a/core/shared/src/main/scala/coursier/ivy/Pattern.scala b/core/shared/src/main/scala/coursier/ivy/Pattern.scala index b702c1b3f..586a29d06 100644 --- a/core/shared/src/main/scala/coursier/ivy/Pattern.scala +++ b/core/shared/src/main/scala/coursier/ivy/Pattern.scala @@ -1,146 +1,192 @@ package coursier.ivy -import scala.annotation.tailrec +import scalaz._, Scalaz._ -import scalaz._ +import fastparse.all._ -import scala.util.matching.Regex -import java.util.regex.Pattern.quote +case class PropertiesPattern(chunks: Seq[PropertiesPattern.ChunkOrProperty]) { -object Pattern { + def string: String = chunks.map(_.string).mkString - val default = - "[organisation]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/" + - "[artifact](-[classifier]).[ext]" + import PropertiesPattern.ChunkOrProperty - val propertyRegex = (quote("${") + "[^" + quote("{[()]}") + "]*" + quote("}")).r - val optionalPartRegex = (quote("(") + "[^" + quote("{()}") + "]*" + quote(")")).r - val variableRegex = (quote("[") + "[^" + quote("{[()]}") + "]*" + quote("]")).r + def substituteProperties(properties: Map[String, String]): String \/ Pattern = { - 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 { - final 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) - } - } - final 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) - } - -} - -final 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) + val validation = chunks.toVector.traverseU { + case ChunkOrProperty.Prop(name, alternativesOpt) => + properties.get(name) match { + case Some(value) => + Seq(Pattern.Chunk.Const(value)).successNel + case None => + alternativesOpt match { + case Some(alt) => + PropertiesPattern(alt) + .substituteProperties(properties) + .map(_.chunks) + .validation + .toValidationNel + case None => + name.failureNel } } - helper(0, optionalParts, Nil) + case ChunkOrProperty.Opt(l @ _*) => + PropertiesPattern(l) + .substituteProperties(properties) + .map(l => Seq(Pattern.Chunk.Opt(l.chunks: _*))) + .validation + .toValidationNel + + case ChunkOrProperty.Var(name) => + Seq(Pattern.Chunk.Var(name)).successNel + + case ChunkOrProperty.Const(value) => + Seq(Pattern.Chunk.Const(value)).successNel + + }.map(_.flatten).map(Pattern(_)) + + validation.disjunction.leftMap { notFoundProps => + s"Property(ies) not found: ${notFoundProps.toList.mkString(", ")}" + } + } +} + +case class Pattern(chunks: Seq[Pattern.Chunk]) { + + def +:(chunk: Pattern.Chunk): Pattern = + Pattern(chunk +: chunks) + + import Pattern.Chunk + + def string: String = chunks.map(_.string).mkString + + def substituteVariables(variables: Map[String, String]): String \/ String = { + + def helper(chunks: Seq[Chunk]): ValidationNel[String, Seq[Chunk.Const]] = + chunks.toVector.traverseU[ValidationNel[String, Seq[Chunk.Const]]] { + case Chunk.Var(name) => + variables.get(name) match { + case Some(value) => + Seq(Chunk.Const(value)).successNel + case None => + name.failureNel + } + case Chunk.Opt(l @ _*) => + val res = helper(l) + if (res.isSuccess) + res + else + Seq().successNel + case c: Chunk.Const => + Seq(c).successNel + }.map(_.flatten) + + val validation = helper(chunks) + + validation match { + case Failure(notFoundVariables) => + s"Variables not found: ${notFoundVariables.toList.mkString(", ")}".left + case Success(constants) => + val b = new StringBuilder + constants.foreach(b ++= _.value) + b.result().right + } + } +} + +object PropertiesPattern { + + sealed abstract class ChunkOrProperty extends Product with Serializable { + def string: String } - 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) + object ChunkOrProperty { + case class Prop(name: String, alternative: Option[Seq[ChunkOrProperty]]) extends ChunkOrProperty { + def string: String = + s"$${" + name + alternative.fold("")(alt => "-" + alt.map(_.string).mkString) + "}" + } + case class Var(name: String) extends ChunkOrProperty { + def string: String = "[" + name + "]" + } + case class Opt(content: ChunkOrProperty*) extends ChunkOrProperty { + def string: String = "(" + content.map(_.string).mkString + ")" + } + case class Const(value: String) extends ChunkOrProperty { + def string: String = value + } + + implicit def fromString(s: String): ChunkOrProperty = Const(s) } - private val substituteHelpers = parts.map { part => - part(pattern0.substring(part.effectiveStart, part.effectiveEnd)) + private object Parser { + + private val notIn = s"[]{}()$$".toSet + private val chars = P(CharsWhile(c => !notIn(c)).!) + private val noHyphenChars = P(CharsWhile(c => !notIn(c) && c != '-').!) + + private val constant = P(chars).map(ChunkOrProperty.Const) + + private lazy val property: Parser[ChunkOrProperty.Prop] = + P(s"$${" ~ noHyphenChars ~ ("-" ~ chunks).? ~ "}") + .map { case (name, altOpt) => ChunkOrProperty.Prop(name, altOpt) } + + private lazy val variable: Parser[ChunkOrProperty.Var] = P("[" ~ chars ~ "]").map(ChunkOrProperty.Var) + + private lazy val optional: Parser[ChunkOrProperty.Opt] = P("(" ~ chunks ~ ")") + .map(l => ChunkOrProperty.Opt(l: _*)) + + lazy val chunks: Parser[Seq[ChunkOrProperty]] = P((constant | property | variable | optional).rep) + .map(_.toVector) // "Vector" is more readable than "ArrayBuffer" } - def substitute(variables: Map[String, String]): String \/ String = - substituteHelpers.foldLeft[String \/ String](\/-("")) { - case (acc0, helper) => - for { - acc <- acc0 - s <- helper(variables) - } yield acc + s + def parser: Parser[Seq[ChunkOrProperty]] = Parser.chunks + + + def parse(pattern: String): String \/ PropertiesPattern = + parser.parse(pattern) match { + case f: Parsed.Failure => + f.msg.left + case Parsed.Success(v, _) => + PropertiesPattern(v).right } } + +object Pattern { + + sealed abstract class Chunk extends Product with Serializable { + def string: String + } + + object Chunk { + case class Var(name: String) extends Chunk { + def string: String = "[" + name + "]" + } + case class Opt(content: Chunk*) extends Chunk { + def string: String = "(" + content.map(_.string).mkString + ")" + } + case class Const(value: String) extends Chunk { + def string: String = value + } + + implicit def fromString(s: String): Chunk = Const(s) + } + + import Chunk.{ Var, Opt } + + // Corresponds to + // [organisation]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext] + + val default = Pattern( + Seq( + Var("organisation"), "/", + Var("module"), "/", + Opt("scala_", Var("scalaVersion"), "/"), + Opt("sbt_", Var("sbtVersion"), "/"), + Var("revision"), "/", + Var("type"), "s/", + Var("artifact"), Opt("-", Var("classifier")), ".", Var("ext") + ) + ) + +} diff --git a/core/shared/src/main/scala/coursier/util/Parse.scala b/core/shared/src/main/scala/coursier/util/Parse.scala index 1ef8b693b..25ba942ce 100644 --- a/core/shared/src/main/scala/coursier/util/Parse.scala +++ b/core/shared/src/main/scala/coursier/util/Parse.scala @@ -6,6 +6,9 @@ import coursier.maven.MavenRepository import scala.collection.mutable.ArrayBuffer +import scalaz.\/ +import scalaz.Scalaz.ToEitherOps + object Parse { /** @@ -127,21 +130,21 @@ object Parse { def moduleVersionConfigs(l: Seq[String]): (Seq[String], Seq[(Module, String, Option[String])]) = valuesAndErrors(moduleVersionConfig, l) - def repository(s: String): Repository = + def repository(s: String): String \/ Repository = if (s == "central") - MavenRepository("https://repo1.maven.org/maven2") + MavenRepository("https://repo1.maven.org/maven2").right else if (s.startsWith("sonatype:")) - MavenRepository(s"https://oss.sonatype.org/content/repositories/${s.stripPrefix("sonatype:")}") + MavenRepository(s"https://oss.sonatype.org/content/repositories/${s.stripPrefix("sonatype:")}").right else if (s.startsWith("bintray:")) - MavenRepository(s"https://dl.bintray.com/${s.stripPrefix("bintray:")}/maven") + MavenRepository(s"https://dl.bintray.com/${s.stripPrefix("bintray:")}/maven").right else if (s.startsWith("typesafe:ivy-")) - IvyRepository( - s"https://repo.typesafe.com/typesafe/ivy-" + s.stripPrefix("typesafe:ivy-") + "/" + + IvyRepository.fromPattern( + (s"https://repo.typesafe.com/typesafe/ivy-" + s.stripPrefix("typesafe:ivy-") + "/") +: coursier.ivy.Pattern.default - ) + ).right else if (s.startsWith("ivy:")) - IvyRepository(s.stripPrefix("ivy:")) + IvyRepository.parse(s.stripPrefix("ivy:")) else - MavenRepository(s) + MavenRepository(s).right } diff --git a/plugin/src/main/scala-2.10/coursier/Tasks.scala b/plugin/src/main/scala-2.10/coursier/Tasks.scala index 99e6420c7..9e0246c7d 100644 --- a/plugin/src/main/scala-2.10/coursier/Tasks.scala +++ b/plugin/src/main/scala-2.10/coursier/Tasks.scala @@ -436,7 +436,10 @@ object Tasks { m case i: IvyRepository => if (i.authentication.isEmpty) { - val base = i.pattern.takeWhile(c => c != '[' && c != '(' && c != '$') + val base = i.pattern.chunks.takeWhile { + case _: coursier.ivy.Pattern.Chunk.Const => true + case _ => false + }.map(_.string).mkString httpHost(base).flatMap(credentials.get).fold(i) { auth => i.copy(authentication = Some(auth)) diff --git a/tests/jvm/src/test/scala/coursier/test/IvyTests.scala b/tests/jvm/src/test/scala/coursier/test/IvyTests.scala index ab50e3694..0e6e51c59 100644 --- a/tests/jvm/src/test/scala/coursier/test/IvyTests.scala +++ b/tests/jvm/src/test/scala/coursier/test/IvyTests.scala @@ -9,11 +9,13 @@ object IvyTests extends TestSuite { // only tested on the JVM for lack of support of XML attributes in the platform-dependent XML stubs - val sbtRepo = IvyRepository( + val sbtRepo = IvyRepository.parse( "https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/" + "[organisation]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" + "[revision]/[type]s/[artifact](-[classifier]).[ext]", dropInfoAttributes = true + ).getOrElse( + throw new Exception("Cannot happen") ) val tests = TestSuite { diff --git a/tests/shared/src/test/resources/resolutions/io.get-coursier/coursier_2.11/1.0.0-SNAPSHOT b/tests/shared/src/test/resources/resolutions/io.get-coursier/coursier_2.11/1.0.0-SNAPSHOT index c28e9eb22..f943301c1 100644 --- a/tests/shared/src/test/resources/resolutions/io.get-coursier/coursier_2.11/1.0.0-SNAPSHOT +++ b/tests/shared/src/test/resources/resolutions/io.get-coursier/coursier_2.11/1.0.0-SNAPSHOT @@ -1,3 +1,6 @@ +com.lihaoyi:fastparse-utils_2.11:0.3.7:default +com.lihaoyi:fastparse_2.11:0.3.7:default +com.lihaoyi:sourcecode_2.11:0.1.1:default io.get-coursier:coursier_2.11:1.0.0-SNAPSHOT:compile org.jsoup:jsoup:1.9.2:default org.scala-lang:scala-library:2.11.8:default diff --git a/tests/shared/src/test/scala/coursier/test/IvyPatternParserTests.scala b/tests/shared/src/test/scala/coursier/test/IvyPatternParserTests.scala new file mode 100644 index 000000000..55d7742f1 --- /dev/null +++ b/tests/shared/src/test/scala/coursier/test/IvyPatternParserTests.scala @@ -0,0 +1,135 @@ +package coursier.test + +import coursier.ivy.PropertiesPattern +import coursier.ivy.PropertiesPattern.ChunkOrProperty +import coursier.ivy.PropertiesPattern.ChunkOrProperty._ + +import utest._ + +import scalaz.Scalaz.ToEitherOps + +object IvyPatternParserTests extends TestSuite { + + val tests = TestSuite { + + 'plugin - { + val strPattern = "[organization]/[module](/scala_[scalaVersion])(/sbt_[sbtVersion])/[revision]/resolved.xml.[ext]" + val expectedChunks = Seq[ChunkOrProperty]( + Var("organization"), + "/", Var("module"), + Opt("/scala_", Var("scalaVersion")), + Opt("/sbt_", Var("sbtVersion")), + "/", Var("revision"), + "/resolved.xml.", Var("ext") + ) + + assert(PropertiesPattern.parse(strPattern).map(_.chunks) == expectedChunks.right) + } + + 'activatorLaunchLocal - { + val strPattern = + "file://${activator.local.repository-${activator.home-${user.home}/.activator}/repository}" + + "/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" + + "[revision]/[type]s/[artifact](-[classifier]).[ext]" + val expectedChunks = Seq[ChunkOrProperty]( + "file://", + Prop("activator.local.repository", Some(Seq( + Prop("activator.home", Some(Seq( + Prop("user.home", None), + "/.activator" + ))), + "/repository" + ))), "/", + Var("organization"), "/", + Var("module"), "/", + Opt("scala_", Var("scalaVersion"), "/"), + Opt("sbt_", Var("sbtVersion"), "/"), + Var("revision"), "/", + Var("type"), "s/", + Var("artifact"), Opt("-", Var("classifier")), ".", Var("ext") + ) + + val pattern0 = PropertiesPattern.parse(strPattern) + assert(pattern0.map(_.chunks) == expectedChunks.right) + + val pattern = pattern0.toOption.get + + * - { + val varPattern = pattern.substituteProperties(Map( + "activator.local.repository" -> "xyz" + )).map(_.string) + + val expectedVarPattern = + "file://xyz" + + "/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" + + "[revision]/[type]s/[artifact](-[classifier]).[ext]" + + assert(varPattern == expectedVarPattern.right) + } + + * - { + val varPattern = pattern.substituteProperties(Map( + "activator.local.repository" -> "xyz", + "activator.home" -> "aaaa" + )).map(_.string) + + val expectedVarPattern = + "file://xyz" + + "/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" + + "[revision]/[type]s/[artifact](-[classifier]).[ext]" + + assert(varPattern == expectedVarPattern.right) + } + + * - { + val varPattern = pattern.substituteProperties(Map( + "activator.home" -> "aaaa" + )).map(_.string) + + val expectedVarPattern = + "file://aaaa/repository" + + "/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" + + "[revision]/[type]s/[artifact](-[classifier]).[ext]" + + assert(varPattern == expectedVarPattern.right) + } + + * - { + val varPattern0 = pattern.substituteProperties(Map( + "user.home" -> "homez" + )) + + val expectedVarPattern = + "file://homez/.activator/repository" + + "/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" + + "[revision]/[type]s/[artifact](-[classifier]).[ext]" + + assert(varPattern0.map(_.string) == expectedVarPattern.right) + + val varPattern = varPattern0.toOption.get + + * - { + val res = varPattern.substituteVariables(Map( + "organization" -> "org", + "module" -> "mod", + "revision" -> "1.1.x", + "type" -> "jarr", + "artifact" -> "art", + "classifier" -> "docc", + "ext" -> "jrr" + )).map(_.string) + val expectedRes = "file://homez/.activator/repository/org/mod/1.1.x/jarrs/art-docc.jrr" + + assert(res == expectedRes.right) + } + } + + * - { + val varPattern = pattern.substituteProperties(Map()) + assert(varPattern.isLeft) + } + } + + } + +}