Add Ivy repository support

This commit is contained in:
Alexandre Archambault 2015-12-30 01:34:32 +01:00
parent e4dfc862b4
commit 3b4b773c64
16 changed files with 601 additions and 135 deletions

View File

@ -30,6 +30,8 @@ case class CommonOptions(
@HelpMessage("Repositories - for multiple repositories, separate with comma and/or repeat this option (e.g. -r central,ivy2local -r sonatype-snapshots, or equivalently -r central,ivy2local,sonatype-snapshots)")
@ExtraName("r")
repository: List[String],
@HelpMessage("Do not add default repositories (~/.ivy2/local, and Central)")
noDefault: Boolean = false,
@HelpMessage("Force module version")
@ValueDescription("organization:name:forcedVersion")
@ExtraName("V")

View File

@ -4,6 +4,8 @@ package cli
import java.io.{ OutputStreamWriter, File }
import java.util.UUID
import coursier.ivy.IvyRepository
import scalaz.{ \/-, -\/ }
import scalaz.concurrent.Task
@ -62,69 +64,66 @@ class Helper(
else
CachePolicy.Default
val cache = Cache(new File(cacheOptions.cache))
cache.init(verbose = verbose0 >= 0)
val files =
Files(
Seq(
"http://" -> new File(new File(cacheOptions.cache), "http"),
"https://" -> new File(new File(cacheOptions.cache), "https")
),
() => ???,
concurrentDownloadCount = parallel
)
val repositoryIds = {
val repositoryIds0 = repository
.flatMap(_.split(','))
.map(_.trim)
.filter(_.nonEmpty)
val central = MavenRepository("https://repo1.maven.org/maven2/")
val ivy2Local = MavenRepository(
new File(sys.props("user.home") + "/.ivy2/local/").toURI.toString,
ivyLike = true
)
val defaultRepositories = Seq(
ivy2Local,
central
)
if (repositoryIds0.isEmpty)
cache.default()
else
repositoryIds0
}
val repositories0 = common.repository.map { repo =>
val repo0 = repo.toLowerCase
if (repo0 == "central")
Right(central)
else if (repo0 == "ivy2local")
Right(ivy2Local)
else if (repo0.startsWith("sonatype:"))
Right(
MavenRepository(s"https://oss.sonatype.org/content/repositories/${repo.drop("sonatype:".length)}")
)
else {
val (url, r) =
if (repo.startsWith("ivy:")) {
val url = repo.drop("ivy:".length)
(url, IvyRepository(url))
} else if (repo.startsWith("ivy-like:")) {
val url = repo.drop("ivy-like:".length)
(url, MavenRepository(url, ivyLike = true))
} else {
(repo, MavenRepository(repo))
}
val repoMap = cache.map()
val repoByBase = repoMap.map { case (_, v @ (m, _)) =>
m.root -> v
}
val repositoryIdsOpt0 = repositoryIds.map { id =>
repoMap.get(id) match {
case Some(v) => Right(v)
case None =>
if (id.contains("://")) {
val root0 = if (id.endsWith("/")) id else id + "/"
Right(
repoByBase.getOrElse(root0, {
val id0 = UUID.randomUUID().toString
if (verbose0 >= 1)
Console.err.println(s"Addding repository $id0 ($root0)")
// FIXME This could be done more cleanly
cache.add(id0, root0, ivyLike = false)
cache.map().getOrElse(id0,
sys.error(s"Adding repository $id0 ($root0)")
)
})
)
} else
Left(id)
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file:/"))
Right(r)
else
Left(repo -> s"Unrecognized protocol or repository: $url")
}
}
val notFoundRepositoryIds = repositoryIdsOpt0.collect {
case Left(id) => id
}
if (notFoundRepositoryIds.nonEmpty) {
errPrintln(
(if (notFoundRepositoryIds.lengthCompare(1) == 0) "Repository" else "Repositories") +
" not found: " +
notFoundRepositoryIds.mkString(", ")
)
val unrecognizedRepos = repositories0.collect { case Left(e) => e }
if (unrecognizedRepos.nonEmpty) {
errPrintln(s"${unrecognizedRepos.length} error(s) parsing repositories:")
for ((repo, err) <- unrecognizedRepos)
errPrintln(s"$repo: $err")
sys.exit(255)
}
val files = cache.files().copy(concurrentDownloadCount = parallel)
val (repositories, fileCaches) = repositoryIdsOpt0
.collect { case Right(v) => v }
.unzip
val repositories =
(if (common.noDefault) Nil else defaultRepositories) ++
repositories0.collect { case Right(r) => r }
val (rawDependencies, extraArgs) = {
val idxOpt = Some(remainingArgs.indexOf("--")).filter(_ >= 0)

View File

@ -46,11 +46,13 @@ package object compatibility {
def label =
option[String](node0.nodeName)
.getOrElse("")
def child =
def children =
option[NodeList](node0.childNodes)
.map(l => List.tabulate(l.length)(l.item).map(fromNode))
.getOrElse(Nil)
def attributes: Seq[(String, String)] = ???
// `exists` instead of `contains`, for scala 2.10
def isText =
option[Int](node0.nodeType)

View File

@ -2,6 +2,8 @@ package coursier.core
import coursier.util.Xml
import scala.xml.{ MetaData, Null }
package object compatibility {
implicit class RichChar(val c: Char) extends AnyVal {
@ -16,8 +18,21 @@ package object compatibility {
def fromNode(node: scala.xml.Node): Xml.Node =
new Xml.Node {
lazy val attributes = {
def helper(m: MetaData): Stream[(String, String)] =
m match {
case Null => Stream.empty
case attr =>
val value = attr.value.collect {
case scala.xml.Text(t) => t
}.mkString("")
(attr.key -> value) #:: helper(m.next)
}
helper(node.attributes).toVector
}
def label = node.label
def child = node.child.map(fromNode)
def children = node.child.map(fromNode)
def isText = node match { case _: scala.xml.Text => true; case _ => false }
def textContent = node.text
def isElement = node match { case _: scala.xml.Elem => true; case _ => false }

View File

@ -34,13 +34,16 @@ case class Dependency(
module: Module,
version: String,
configuration: String,
attributes: Attributes,
exclusions: Set[(String, String)],
// Maven-specific
attributes: Attributes,
optional: Boolean
) {
def moduleVersion = (module, version)
}
// Maven-specific
case class Attributes(
`type`: String,
classifier: String
@ -49,20 +52,30 @@ case class Attributes(
case class Project(
module: Module,
version: String,
// First String is configuration (scope for Maven)
dependencies: Seq[(String, Dependency)],
// For Maven, this is the standard scopes as an Ivy configuration
configurations: Map[String, Seq[String]],
// Maven-specific
parent: Option[(Module, String)],
dependencyManagement: Seq[(String, Dependency)],
configurations: Map[String, Seq[String]],
properties: Map[String, String],
profiles: Seq[Profile],
versions: Option[Versions],
snapshotVersioning: Option[SnapshotVersioning]
snapshotVersioning: Option[SnapshotVersioning],
// Ivy-specific
// First String is configuration
publications: Seq[(String, Publication)]
) {
def moduleVersion = (module, version)
}
// Maven-specific
case class Activation(properties: Seq[(String, Option[String])])
// Maven-specific
case class Profile(
id: String,
activeByDefault: Option[Boolean],
@ -72,6 +85,7 @@ case class Profile(
properties: Map[String, String]
)
// Maven-specific
case class Versions(
latest: String,
release: String,
@ -90,6 +104,7 @@ object Versions {
)
}
// Maven-specific
case class SnapshotVersion(
classifier: String,
extension: String,
@ -97,6 +112,7 @@ case class SnapshotVersion(
updated: Option[Versions.DateTime]
)
// Maven-specific
case class SnapshotVersioning(
module: Module,
version: String,
@ -109,6 +125,13 @@ case class SnapshotVersioning(
snapshotVersions: Seq[SnapshotVersion]
)
// Ivy-specific
case class Publication(
name: String,
`type`: String,
ext: String
)
case class Artifact(
url: String,
checksumUrls: Map[String, String],

View File

@ -115,15 +115,25 @@ object Orders {
}
}
private def fallbackConfigIfNecessary(dep: Dependency, configs: Set[String]): Dependency =
Parse.withFallbackConfig(dep.configuration) match {
case Some((main, fallback)) if !configs(main) && configs(fallback) =>
dep.copy(configuration = fallback)
case _ =>
dep
}
/**
* Assume all dependencies have same `module`, `version`, and `artifact`; see `minDependencies`
* if they don't.
*/
def minDependenciesUnsafe(
dependencies: Set[Dependency],
configs: ((Module, String)) => Map[String, Seq[String]]
configs: Map[String, Seq[String]]
): Set[Dependency] = {
val availableConfigs = configs.keySet
val groupedDependencies = dependencies
.map(fallbackConfigIfNecessary(_, availableConfigs))
.groupBy(dep => (dep.optional, dep.configuration))
.mapValues(deps => deps.head.copy(exclusions = deps.foldLeft(Exclusions.one)((acc, dep) => Exclusions.meet(acc, dep.exclusions))))
.toList
@ -132,7 +142,7 @@ object Orders {
for {
List(((xOpt, xScope), xDep), ((yOpt, yScope), yDep)) <- groupedDependencies.combinations(2)
optCmp <- optionalPartialOrder.tryCompare(xOpt, yOpt).iterator
scopeCmp <- configurationPartialOrder(configs(xDep.moduleVersion)).tryCompare(xScope, yScope).iterator
scopeCmp <- configurationPartialOrder(configs).tryCompare(xScope, yScope).iterator
if optCmp*scopeCmp >= 0
exclCmp <- exclusionsPartialOrder.tryCompare(xDep.exclusions, yDep.exclusions).iterator
if optCmp*exclCmp >= 0
@ -156,7 +166,7 @@ object Orders {
): Set[Dependency] = {
dependencies
.groupBy(_.copy(configuration = "", exclusions = Set.empty, optional = false))
.mapValues(minDependenciesUnsafe(_, configs))
.mapValues(deps => minDependenciesUnsafe(deps, configs(deps.head.moduleVersion)))
.valuesIterator
.fold(Set.empty)(_ ++ _)
}

View File

@ -1,5 +1,6 @@
package coursier.core
import java.util.regex.Pattern.quote
import coursier.core.compatibility._
object Parse {
@ -31,4 +32,20 @@ object Parse {
.orElse(versionInterval(s).map(VersionConstraint.Interval))
}
val fallbackConfigRegex = {
val noPar = "([^" + quote("()") + "]*)"
"^" + noPar + quote("(") + noPar + quote(")") + "$"
}.r
def withFallbackConfig(config: String): Option[(String, String)] =
Parse.fallbackConfigRegex.findAllMatchIn(config).toSeq match {
case Seq(m) =>
assert(m.groupCount == 2)
val main = config.substring(m.start(1), m.end(1))
val fallback = config.substring(m.start(2), m.end(2))
Some((main, fallback))
case _ =>
None
}
}

View File

@ -286,7 +286,18 @@ object Resolution {
helper(extraConfigs, acc ++ configs)
}
helper(Set(config), Set.empty)
val config0 = Parse.withFallbackConfig(config) match {
case Some((main, fallback)) =>
if (configurations.contains(main))
main
else if (configurations.contains(fallback))
fallback
else
main
case None => config
}
helper(Set(config0), Set.empty)
}
/**
@ -741,6 +752,16 @@ case class Resolution(
.artifacts(dep, proj)
} yield artifact
def artifactsByDep: Seq[(Dependency, Artifact)] =
for {
dep <- minDependencies.toSeq
(source, proj) <- projectCache
.get(dep.moduleVersion)
.toSeq
artifact <- source
.artifacts(dep, proj)
} yield dep -> artifact
def errors: Seq[(Dependency, Seq[String])] =
for {
dep <- dependencies.toSeq

View File

@ -0,0 +1,214 @@
package coursier.ivy
import coursier.core._
import scala.annotation.tailrec
import scala.util.matching.Regex
import scalaz._
import java.util.regex.Pattern.quote
object IvyRepository {
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))
}
}
}
}
case class IvyRepository(pattern: String) extends Repository {
import Repository._
import IvyRepository._
val parts = {
val optionalParts = optionalPartRegex.findAllMatchIn(pattern).toList.map { m =>
PatternPart.Optional(m.start, m.end)
}
val len = pattern.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(pattern.isEmpty == parts.isEmpty)
if (pattern.nonEmpty) {
for ((a, b) <- parts.zip(parts.tail))
assert(a.end == b.start)
assert(parts.head.start == 0)
assert(parts.last.end == pattern.length)
}
private val substituteHelpers = parts.map { part =>
part(pattern.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
}
// If attributes are added to `Module`, they should be added here
// 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).
private def variables(
org: String,
name: String,
version: String,
`type`: String,
artifact: String,
ext: String
) =
Map(
"organization" -> org,
"organisation" -> org,
"orgPath" -> org.replace('.', '/'),
"module" -> name,
"revision" -> version,
"type" -> `type`,
"artifact" -> artifact,
"ext" -> ext
)
val source: Artifact.Source = new Artifact.Source {
def artifacts(dependency: Dependency, project: Project) =
project
.publications
.collect { case (conf, p) if conf == "*" || conf == dependency.configuration => p }
.flatMap { p =>
substitute(variables(
dependency.module.organization,
dependency.module.name,
dependency.version,
p.`type`,
p.name,
p.ext
)).toList.map(p -> _)
}
.map { case (p, url) =>
Artifact(
url,
Map.empty,
Map.empty,
Attributes(p.`type`, p.ext)
)
.withDefaultChecksums
.withDefaultSignature
}
}
def find[F[_]](
module: Module,
version: String,
fetch: Repository.Fetch[F]
)(implicit
F: Monad[F]
): EitherT[F, String, (Artifact.Source, Project)] = {
val eitherArtifact: String \/ Artifact =
for {
url <- substitute(variables(module.organization, module.name, version, "ivy", "ivy", "xml"))
} yield
Artifact(
url,
Map.empty,
Map.empty,
Attributes("ivy", "")
)
.withDefaultChecksums
.withDefaultSignature
for {
artifact <- EitherT(F.point(eitherArtifact))
ivy <- fetch(artifact)
proj <- EitherT(F.point {
for {
xml <- \/.fromEither(compatibility.xmlParse(ivy))
_ <- if (xml.label == "ivy-module") \/-(()) else -\/("Module definition not found")
proj <- IvyXml.project(xml)
} yield proj
})
} yield (source, proj)
}
}

View File

@ -0,0 +1,127 @@
package coursier.ivy
import coursier.core._
import coursier.util.Xml._
import scalaz.{ Node => _, _ }, Scalaz._
object IvyXml {
private def info(node: Node): String \/ (Module, String) =
for {
org <- node.attribute("organisation")
name <- node.attribute("module")
version <- node.attribute("revision")
} yield (Module(org, name), version)
// FIXME Errors are ignored here
private def configurations(node: Node): Seq[(String, Seq[String])] =
node.children
.filter(_.label == "conf")
.flatMap { node =>
node.attribute("name").toOption.toSeq.map(_ -> node)
}
.map { case (name, node) =>
name -> node.attribute("extends").toOption.toSeq.flatMap(_.split(','))
}
// FIXME Errors ignored as above - warnings should be reported at least for anything suspicious
private def dependencies(node: Node): Seq[(String, Dependency)] =
node.children
.filter(_.label == "dependency")
.flatMap { node =>
// artifact and include sub-nodes are ignored here
val excludes = node.children
.filter(_.label == "exclude")
.flatMap { node0 =>
val org = node.attribute("org").getOrElse("*")
val name = node.attribute("module").orElse(node.attribute("name")).getOrElse("*")
val confs = node.attribute("conf").toOption.fold(Seq("*"))(_.split(','))
confs.map(_ -> (org, name))
}
.groupBy { case (conf, _) => conf }
.map { case (conf, l) => conf -> l.map { case (_, e) => e }.toSet }
val allConfsExcludes = excludes.getOrElse("*", Set.empty)
for {
org <- node.attribute("org").toOption.toSeq
name <- node.attribute("name").toOption.toSeq
version <- node.attribute("rev").toOption.toSeq
rawConf <- node.attribute("conf").toOption.toSeq
(fromConf, toConf) <- rawConf.split(',').toSeq.map(_.split("->", 2)).collect {
case Array(from, to) => from -> to
}
} yield fromConf -> Dependency(
Module(org, name),
version,
toConf,
allConfsExcludes ++ excludes.getOrElse(fromConf, Set.empty),
Attributes("jar", ""), // should come from possible artifact nodes
optional = false
)
}
private def publications(node: Node): Map[String, Seq[Publication]] =
node.children
.filter(_.label == "artifact")
.flatMap { node0 =>
val name = node.attribute("name").getOrElse("")
val type0 = node.attribute("type").getOrElse("jar")
val ext = node.attribute("ext").getOrElse(type0)
val confs = node.attribute("conf").toOption.fold(Seq("*"))(_.split(','))
confs.map(_ -> Publication(name, type0, ext))
}
.groupBy { case (conf, _) => conf }
.map { case (conf, l) => conf -> l.map { case (_, p) => p } }
def project(node: Node): String \/ Project =
for {
infoNode <- node.children
.find(_.label == "info")
.toRightDisjunction("Info not found")
(module, version) <- info(infoNode)
dependenciesNodeOpt = node.children
.find(_.label == "dependencies")
dependencies0 = dependenciesNodeOpt.map(dependencies).getOrElse(Nil)
configurationsNodeOpt = node.children
.find(_.label == "configurations")
configurationsOpt = configurationsNodeOpt.map(configurations)
configurations0 = configurationsOpt.getOrElse(Seq("default" -> Seq.empty[String]))
publicationsNodeOpt = node.children
.find(_.label == "publications")
publicationsOpt = publicationsNodeOpt.map(publications)
} yield
Project(
module,
version,
dependencies0,
configurations0.toMap,
None,
Nil,
Map.empty,
Nil,
None,
None,
if (publicationsOpt.isEmpty)
// no publications node -> default JAR artifact
Seq("*" -> Publication(module.name, "jar", "jar"))
else
// publications node is there -> only its content (if it is empty, no artifacts,
// as per the Ivy manual)
configurations0.flatMap { case (conf, _) =>
publicationsOpt.flatMap(_.get(conf)).getOrElse(Nil).map(conf -> _)
}
)
}

View File

@ -37,6 +37,7 @@ object MavenRepository {
val defaultConfigurations = Map(
"compile" -> Seq.empty,
"runtime" -> Seq("compile"),
"test" -> Seq("runtime")
)
@ -106,7 +107,7 @@ case class MavenRepository(
Attributes("pom", "")
)
.withDefaultChecksums
.withDefaultChecksums
.withDefaultSignature
Some(artifact)
}

View File

@ -7,21 +7,6 @@ import scalaz._
object Pom {
import coursier.util.Xml._
object Text {
def unapply(n: Node): Option[String] =
if (n.isText) Some(n.textContent)
else None
}
private def text(elem: Node, label: String, description: String) = {
import Scalaz.ToOptionOpsFromOption
elem.child
.find(_.label == label)
.flatMap(_.child.collectFirst{case Text(t) => t})
.toRightDisjunction(s"$description not found")
}
def property(elem: Node): String \/ (String, String) = {
// Not matching with Text, which fails on scala-js if the property value has xml comments
if (elem.isElement) \/-(elem.label -> elem.textContent)
@ -53,9 +38,9 @@ object Pom {
scopeOpt = text(node, "scope", "").toOption
typeOpt = text(node, "type", "").toOption
classifierOpt = text(node, "classifier", "").toOption
xmlExclusions = node.child
xmlExclusions = node.children
.find(_.label == "exclusions")
.map(_.child.filter(_.label == "exclusion"))
.map(_.children.filter(_.label == "exclusion"))
.getOrElse(Seq.empty)
exclusions <- {
import Scalaz._
@ -66,8 +51,8 @@ object Pom {
mod,
version0,
"",
Attributes(typeOpt getOrElse defaultType, classifierOpt getOrElse defaultClassifier),
exclusions.map(mod => (mod.organization, mod.name)).toSet,
Attributes(typeOpt getOrElse defaultType, classifierOpt getOrElse defaultClassifier),
optional
)
}
@ -80,7 +65,7 @@ object Pom {
case _ => None
}
val properties = node.child
val properties = node.children
.filter(_.label == "property")
.flatMap{ p =>
for{
@ -97,28 +82,28 @@ object Pom {
val id = text(node, "id", "Profile ID").getOrElse("")
val xmlActivationOpt = node.child
val xmlActivationOpt = node.children
.find(_.label == "activation")
val (activeByDefault, activation) = xmlActivationOpt.fold((Option.empty[Boolean], Activation(Nil)))(profileActivation)
val xmlDeps = node.child
val xmlDeps = node.children
.find(_.label == "dependencies")
.map(_.child.filter(_.label == "dependency"))
.map(_.children.filter(_.label == "dependency"))
.getOrElse(Seq.empty)
for {
deps <- xmlDeps.toList.traverseU(dependency)
xmlDepMgmts = node.child
xmlDepMgmts = node.children
.find(_.label == "dependencyManagement")
.flatMap(_.child.find(_.label == "dependencies"))
.map(_.child.filter(_.label == "dependency"))
.flatMap(_.children.find(_.label == "dependencies"))
.map(_.children.filter(_.label == "dependency"))
.getOrElse(Seq.empty)
depMgmts <- xmlDepMgmts.toList.traverseU(dependency)
xmlProperties = node.child
xmlProperties = node.children
.find(_.label == "properties")
.map(_.child.collect{case elem if elem.isElement => elem})
.map(_.children.collect{case elem if elem.isElement => elem})
.getOrElse(Seq.empty)
properties <- {
@ -136,7 +121,7 @@ object Pom {
projModule <- module(pom, groupIdIsOptional = true)
projVersion = readVersion(pom)
parentOpt = pom.child
parentOpt = pom.children
.find(_.label == "parent")
parentModuleOpt <- parentOpt
.map(module(_).map(Some(_)))
@ -144,16 +129,16 @@ object Pom {
parentVersionOpt = parentOpt
.map(readVersion)
xmlDeps = pom.child
xmlDeps = pom.children
.find(_.label == "dependencies")
.map(_.child.filter(_.label == "dependency"))
.map(_.children.filter(_.label == "dependency"))
.getOrElse(Seq.empty)
deps <- xmlDeps.toList.traverseU(dependency)
xmlDepMgmts = pom.child
xmlDepMgmts = pom.children
.find(_.label == "dependencyManagement")
.flatMap(_.child.find(_.label == "dependencies"))
.map(_.child.filter(_.label == "dependency"))
.flatMap(_.children.find(_.label == "dependencies"))
.map(_.children.filter(_.label == "dependency"))
.getOrElse(Seq.empty)
depMgmts <- xmlDepMgmts.toList.traverseU(dependency)
@ -171,15 +156,15 @@ object Pom {
.map(mod => if (mod.organization.isEmpty) -\/("Parent organization missing") else \/-(()))
.getOrElse(\/-(()))
xmlProperties = pom.child
xmlProperties = pom.children
.find(_.label == "properties")
.map(_.child.collect{case elem if elem.isElement => elem})
.map(_.children.collect{case elem if elem.isElement => elem})
.getOrElse(Seq.empty)
properties <- xmlProperties.toList.traverseU(property)
xmlProfiles = pom.child
xmlProfiles = pom.children
.find(_.label == "profiles")
.map(_.child.filter(_.label == "profile"))
.map(_.children.filter(_.label == "profile"))
.getOrElse(Seq.empty)
profiles <- xmlProfiles.toList.traverseU(profile)
@ -187,13 +172,14 @@ object Pom {
projModule.copy(organization = groupId),
version,
deps,
Map.empty,
parentModuleOpt.map((_, parentVersionOpt.getOrElse(""))),
depMgmts,
Map.empty,
properties.toMap,
profiles,
None,
None
None,
Nil
)
}
@ -217,7 +203,7 @@ object Pom {
organization <- text(node, "groupId", "Organization") // Ignored
name <- text(node, "artifactId", "Name") // Ignored
xmlVersioning <- node.child
xmlVersioning <- node.children
.find(_.label == "versioning")
.toRightDisjunction("Versioning info not found in metadata")
@ -226,9 +212,9 @@ object Pom {
release = text(xmlVersioning, "release", "Release version")
.getOrElse("")
versionsOpt = xmlVersioning.child
versionsOpt = xmlVersioning.children
.find(_.label == "versions")
.map(_.child.filter(_.label == "version").flatMap(_.child.collectFirst{case Text(t) => t}))
.map(_.children.filter(_.label == "version").flatMap(_.children.collectFirst{case Text(t) => t}))
lastUpdatedOpt = text(xmlVersioning, "lastUpdated", "Last update date and time")
.toOption
@ -268,7 +254,7 @@ object Pom {
name <- text(node, "artifactId", "Name")
version = readVersion(node)
xmlVersioning <- node.child
xmlVersioning <- node.children
.find(_.label == "versioning")
.toRightDisjunction("Versioning info not found in metadata")
@ -277,15 +263,15 @@ object Pom {
release = text(xmlVersioning, "release", "Release version")
.getOrElse("")
versionsOpt = xmlVersioning.child
versionsOpt = xmlVersioning.children
.find(_.label == "versions")
.map(_.child.filter(_.label == "version").flatMap(_.child.collectFirst{case Text(t) => t}))
.map(_.children.filter(_.label == "version").flatMap(_.children.collectFirst{case Text(t) => t}))
lastUpdatedOpt = text(xmlVersioning, "lastUpdated", "Last update date and time")
.toOption
.flatMap(parseDateTime)
xmlSnapshotOpt = xmlVersioning.child
xmlSnapshotOpt = xmlVersioning.children
.find(_.label == "snapshot")
timestamp = xmlSnapshotOpt
@ -313,9 +299,9 @@ object Pom {
case "false" => false
}
xmlSnapshotVersions = xmlVersioning.child
xmlSnapshotVersions = xmlVersioning.children
.find(_.label == "snapshotVersions")
.map(_.child.filter(_.label == "snapshotVersion"))
.map(_.children.filter(_.label == "snapshotVersion"))
.getOrElse(Seq.empty)
snapshotVersions <- xmlSnapshotVersions
.toList

View File

@ -19,8 +19,8 @@ package object coursier {
module,
version,
configuration,
attributes,
exclusions,
attributes,
optional
)
}

View File

@ -1,14 +1,24 @@
package coursier.util
import scalaz.{\/-, -\/, \/, Scalaz}
object Xml {
/** A representation of an XML node/document, with different implementations on JVM and JS */
trait Node {
def label: String
def child: Seq[Node]
def attributes: Seq[(String, String)]
def children: Seq[Node]
def isText: Boolean
def textContent: String
def isElement: Boolean
lazy val attributesMap = attributes.toMap
def attribute(name: String): String \/ String =
attributesMap.get(name) match {
case None => -\/(s"Missing attribute $name")
case Some(value) => \/-(value)
}
}
object Node {
@ -16,10 +26,26 @@ object Xml {
new Node {
val isText = false
val isElement = false
val child = Nil
val children = Nil
val label = ""
val attributes = Nil
val textContent = ""
}
}
object Text {
def unapply(n: Node): Option[String] =
if (n.isText) Some(n.textContent)
else None
}
def text(elem: Node, label: String, description: String) = {
import Scalaz.ToOptionOpsFromOption
elem.children
.find(_.label == label)
.flatMap(_.children.collectFirst{case Text(t) => t})
.toRightDisjunction(s"$description not found")
}
}

View File

@ -194,7 +194,7 @@ object PomParsingTests extends TestSuite {
val node = parsed.right.get
assert(node.label == "properties")
val children = node.child.collect{case elem if elem.isElement => elem}
val children = node.children.collect{case elem if elem.isElement => elem}
val props0 = children.toList.traverseU(Pom.property)
assert(props0.isRight)

View File

@ -13,27 +13,50 @@ package object test {
core.Activation(properties)
}
def apply(id: String,
activeByDefault: Option[Boolean] = None,
activation: Activation = Activation(),
dependencies: Seq[(String, Dependency)] = Nil,
dependencyManagement: Seq[(String, Dependency)] = Nil,
properties: Map[String, String] = Map.empty) =
core.Profile(id, activeByDefault, activation, dependencies, dependencyManagement, properties)
def apply(
id: String,
activeByDefault: Option[Boolean] = None,
activation: Activation = Activation(),
dependencies: Seq[(String, Dependency)] = Nil,
dependencyManagement: Seq[(String, Dependency)] = Nil,
properties: Map[String, String] = Map.empty
) =
core.Profile(
id,
activeByDefault,
activation,
dependencies,
dependencyManagement,
properties
)
}
object Project {
def apply(module: Module,
version: String,
dependencies: Seq[(String, Dependency)] = Seq.empty,
parent: Option[ModuleVersion] = None,
dependencyManagement: Seq[(String, Dependency)] = Seq.empty,
configurations: Map[String, Seq[String]] = Map.empty,
properties: Map[String, String] = Map.empty,
profiles: Seq[Profile] = Seq.empty,
versions: Option[core.Versions] = None,
snapshotVersioning: Option[core.SnapshotVersioning] = None
): Project =
core.Project(module, version, dependencies, parent, dependencyManagement, configurations, properties, profiles, versions, snapshotVersioning)
def apply(
module: Module,
version: String,
dependencies: Seq[(String, Dependency)] = Seq.empty,
parent: Option[ModuleVersion] = None,
dependencyManagement: Seq[(String, Dependency)] = Seq.empty,
configurations: Map[String, Seq[String]] = Map.empty,
properties: Map[String, String] = Map.empty,
profiles: Seq[Profile] = Seq.empty,
versions: Option[core.Versions] = None,
snapshotVersioning: Option[core.SnapshotVersioning] = None,
publications: Seq[(String, core.Publication)] = Nil
): Project =
core.Project(
module,
version,
dependencies,
configurations,
parent,
dependencyManagement,
properties,
profiles,
versions,
snapshotVersioning,
publications
)
}
}