From 3b4b773c6433de070d19239b1ad182037e5b413c Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Wed, 30 Dec 2015 01:34:32 +0100 Subject: [PATCH] Add Ivy repository support --- .../main/scala/coursier/cli/Coursier.scala | 2 + cli/src/main/scala/coursier/cli/Helper.scala | 107 +++++---- .../coursier/core/compatibility/package.scala | 4 +- .../coursier/core/compatibility/package.scala | 17 +- .../scala/coursier/core/Definitions.scala | 29 ++- .../src/main/scala/coursier/core/Orders.scala | 16 +- .../src/main/scala/coursier/core/Parse.scala | 17 ++ .../main/scala/coursier/core/Resolution.scala | 23 +- .../scala/coursier/ivy/IvyRepository.scala | 214 ++++++++++++++++++ .../src/main/scala/coursier/ivy/IvyXml.scala | 127 +++++++++++ .../coursier/maven/MavenRepository.scala | 3 +- .../src/main/scala/coursier/maven/Pom.scala | 82 +++---- .../src/main/scala/coursier/package.scala | 2 +- .../src/main/scala/coursier/util/Xml.scala | 30 ++- .../scala/coursier/test/PomParsingTests.scala | 2 +- .../test/scala/coursier/test/package.scala | 61 +++-- 16 files changed, 601 insertions(+), 135 deletions(-) create mode 100644 core/shared/src/main/scala/coursier/ivy/IvyRepository.scala create mode 100644 core/shared/src/main/scala/coursier/ivy/IvyXml.scala diff --git a/cli/src/main/scala/coursier/cli/Coursier.scala b/cli/src/main/scala/coursier/cli/Coursier.scala index 9dd707773..bc7d2eb8f 100644 --- a/cli/src/main/scala/coursier/cli/Coursier.scala +++ b/cli/src/main/scala/coursier/cli/Coursier.scala @@ -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") diff --git a/cli/src/main/scala/coursier/cli/Helper.scala b/cli/src/main/scala/coursier/cli/Helper.scala index 3f1a98d4d..459d32f84 100644 --- a/cli/src/main/scala/coursier/cli/Helper.scala +++ b/cli/src/main/scala/coursier/cli/Helper.scala @@ -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) diff --git a/core/js/src/main/scala/coursier/core/compatibility/package.scala b/core/js/src/main/scala/coursier/core/compatibility/package.scala index 9226f6d07..f7659c987 100644 --- a/core/js/src/main/scala/coursier/core/compatibility/package.scala +++ b/core/js/src/main/scala/coursier/core/compatibility/package.scala @@ -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) diff --git a/core/jvm/src/main/scala/coursier/core/compatibility/package.scala b/core/jvm/src/main/scala/coursier/core/compatibility/package.scala index 153b376e0..b4874ba94 100644 --- a/core/jvm/src/main/scala/coursier/core/compatibility/package.scala +++ b/core/jvm/src/main/scala/coursier/core/compatibility/package.scala @@ -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 } diff --git a/core/shared/src/main/scala/coursier/core/Definitions.scala b/core/shared/src/main/scala/coursier/core/Definitions.scala index 44550acf0..e9b52a889 100644 --- a/core/shared/src/main/scala/coursier/core/Definitions.scala +++ b/core/shared/src/main/scala/coursier/core/Definitions.scala @@ -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], diff --git a/core/shared/src/main/scala/coursier/core/Orders.scala b/core/shared/src/main/scala/coursier/core/Orders.scala index 7df4a87eb..2952604df 100644 --- a/core/shared/src/main/scala/coursier/core/Orders.scala +++ b/core/shared/src/main/scala/coursier/core/Orders.scala @@ -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)(_ ++ _) } diff --git a/core/shared/src/main/scala/coursier/core/Parse.scala b/core/shared/src/main/scala/coursier/core/Parse.scala index 3259d1ef1..a813b26f1 100644 --- a/core/shared/src/main/scala/coursier/core/Parse.scala +++ b/core/shared/src/main/scala/coursier/core/Parse.scala @@ -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 + } + } diff --git a/core/shared/src/main/scala/coursier/core/Resolution.scala b/core/shared/src/main/scala/coursier/core/Resolution.scala index 891afee00..89306f1ff 100644 --- a/core/shared/src/main/scala/coursier/core/Resolution.scala +++ b/core/shared/src/main/scala/coursier/core/Resolution.scala @@ -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 diff --git a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala new file mode 100644 index 000000000..c49054e60 --- /dev/null +++ b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala @@ -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) + } + +} \ No newline at end of file diff --git a/core/shared/src/main/scala/coursier/ivy/IvyXml.scala b/core/shared/src/main/scala/coursier/ivy/IvyXml.scala new file mode 100644 index 000000000..617c7f796 --- /dev/null +++ b/core/shared/src/main/scala/coursier/ivy/IvyXml.scala @@ -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 -> _) + } + ) + +} \ No newline at end of file diff --git a/core/shared/src/main/scala/coursier/maven/MavenRepository.scala b/core/shared/src/main/scala/coursier/maven/MavenRepository.scala index b2b13f3fe..f8f3c7981 100644 --- a/core/shared/src/main/scala/coursier/maven/MavenRepository.scala +++ b/core/shared/src/main/scala/coursier/maven/MavenRepository.scala @@ -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) } diff --git a/core/shared/src/main/scala/coursier/maven/Pom.scala b/core/shared/src/main/scala/coursier/maven/Pom.scala index 4cd412d2a..6ad669204 100644 --- a/core/shared/src/main/scala/coursier/maven/Pom.scala +++ b/core/shared/src/main/scala/coursier/maven/Pom.scala @@ -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 diff --git a/core/shared/src/main/scala/coursier/package.scala b/core/shared/src/main/scala/coursier/package.scala index c815d5dcd..3a7125b49 100644 --- a/core/shared/src/main/scala/coursier/package.scala +++ b/core/shared/src/main/scala/coursier/package.scala @@ -19,8 +19,8 @@ package object coursier { module, version, configuration, - attributes, exclusions, + attributes, optional ) } diff --git a/core/shared/src/main/scala/coursier/util/Xml.scala b/core/shared/src/main/scala/coursier/util/Xml.scala index 5518a58d2..a4e9c86df 100644 --- a/core/shared/src/main/scala/coursier/util/Xml.scala +++ b/core/shared/src/main/scala/coursier/util/Xml.scala @@ -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") + } + } diff --git a/tests/shared/src/test/scala/coursier/test/PomParsingTests.scala b/tests/shared/src/test/scala/coursier/test/PomParsingTests.scala index 833cabbbb..bb8452b79 100644 --- a/tests/shared/src/test/scala/coursier/test/PomParsingTests.scala +++ b/tests/shared/src/test/scala/coursier/test/PomParsingTests.scala @@ -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) diff --git a/tests/shared/src/test/scala/coursier/test/package.scala b/tests/shared/src/test/scala/coursier/test/package.scala index dc8d31315..83d17cf7d 100644 --- a/tests/shared/src/test/scala/coursier/test/package.scala +++ b/tests/shared/src/test/scala/coursier/test/package.scala @@ -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 + ) } }