Initial commit

This commit is contained in:
Alexandre Archambault 2015-06-16 20:17:07 +02:00
commit 5e25748d83
51 changed files with 15339 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

124
.idea/uiDesigner.xml Normal file
View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

11
.travis.yml Normal file
View File

@ -0,0 +1,11 @@
language: scala
scala:
- 2.11.6
- 2.10.5
jdk:
- oraclejdk7
- oraclejdk8
install:
- npm install xmldom
- npm install xhr2
script: project/travis.sh "$TRAVIS_SCALA_VERSION" "$TRAVIS_PULL_REQUEST" "$TRAVIS_BRANCH"

22
README.md Normal file
View File

@ -0,0 +1,22 @@
# Coursier
*Pure Scala Artifact Fetching*
A pure Scala substitute for [Aether](http://www.eclipse.org/aether/)
Work in progress:
* full list of dependencies / version conflict resolution working, mildly to well tested,
* downloading/caching of JARs in its early stages.
Implements fancy Maven features like
* [POM inheritance](http://books.sonatype.com/mvnref-book/reference/pom-relationships-sect-project-relationships.html#pom-relationships-sect-project-inheritance),
* [dependency management](http://books.sonatype.com/mvnex-book/reference/optimizing-sect-dependencies.html),
* [import scope](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Importing_Dependencies),
* [properties](http://books.sonatype.com/mvnref-book/reference/resource-filtering-sect-properties.html).
Restricted to Maven resolution and repositories for now. Support for Ivy seems definitely at reach, just not done yet.
Both a JVM library and a Scala JS one.
Released under the Apache license, v2.

43
USAGE.md Normal file
View File

@ -0,0 +1,43 @@
# API
Ensure you have a dependency on its artifact, e.g. add in `build.sbt`,
```scala
resolvers += Resolver.sonatypeRepo("snapshots")
libraryDependencies +=
"eu.frowning-lambda" %% "coursier" % "0.1.0-SNAPSHOT"
```
Then,
```scala
import coursier._
val repositories = Seq(
repository.mavenCentral
)
val dependencies = Set(
Dependency(Module("com.lihaoyi", "ammonite-pprint_2.11", "0.3.2")),
Dependency(Module("org.scala-lang", "scala-reflect", "2.11.6"))
)
val resolution =
resolve(dependencies, fetchFrom(repositories)).run
assert(resolution.isDone) // Check that resolution converged
// Printing the results
for (dep <- resolution.dependencies if resolution.projectsCache.contains(dep.module))
println(resolution.projectsCache(dep.module))
for (dep <- resolution.dependencies if resolution.errors.contains(dep.module))
println(resolution.errors(dep.module))
import coursier.core.ArtifactDownloader
val dl = ArtifactDownloader(repository.mavenCentral.root, new java.io.File("cache"))
for (dep <- resolution.dependencies if resolution.projectsCache.contains(dep.module))
dl.artifact(dep).run.run match {
case -\/(err) => println(s"Failed to download ${dep.module}: $err")
case \/-(file) => println(s"${dep.module}: $file")
}
```

View File

@ -0,0 +1,27 @@
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${java.io.tmpdir}/coursier.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>${java.io.tmpdir}/coursier.%i.log.zip</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%date{YYYY-MM-dd HH:mm:ss} %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="FILE" />
</root>
</configuration>

View File

@ -0,0 +1,143 @@
package coursier
package cli
import java.io.File
import caseapp._
import coursier.core.{CachePolicy, Parse}
import coursier.core.{ArtifactDownloaderLogger, RemoteLogger, ArtifactDownloader}
import scalaz.concurrent.Task
import scalaz.{-\/, \/-}
case class Coursier(scope: List[String],
keepOptional: Boolean,
fetch: Boolean) extends App {
val scopes0 =
if (scope.isEmpty) List(Scope.Compile, Scope.Runtime)
else scope.map(Parse.scope)
val scopes = scopes0.toSet
val centralCacheDir = new File(sys.props("user.home") + "/.coursier/cache/central")
val base = centralCacheDir.toURI
def fileRepr(f: File) =
base.relativize(f.toURI).getPath
val logger: RemoteLogger with ArtifactDownloaderLogger = new RemoteLogger with ArtifactDownloaderLogger {
def downloading(url: String) =
println(s"Downloading $url")
def downloaded(url: String, success: Boolean) =
println(
if (success) s"Downloaded $url"
else s"Failed to download $url"
)
def readingFromCache(f: File) = {
println(s"Reading ${fileRepr(f)} from cache")
}
def puttingInCache(f: File) =
println(s"Writing ${fileRepr(f)} in cache")
def foundLocally(f: File) =
println(s"Found locally ${fileRepr(f)}")
def downloadingArtifact(url: String) =
println(s"Downloading $url")
def downloadedArtifact(url: String, success: Boolean) =
println(
if (success) s"Downloaded $url"
else s"Failed to download $url"
)
}
val cachedMavenCentral = repository.mavenCentral.copy(cache = Some(centralCacheDir), logger = Some(logger))
val repositories = Seq[Repository](
cachedMavenCentral
)
lazy val downloaders = Map[Repository, ArtifactDownloader](
cachedMavenCentral -> ArtifactDownloader(repository.mavenCentral.root, centralCacheDir, logger = Some(logger))
)
val (splitArtifacts, malformed) = remainingArgs.toList
.map(_.split(":", 3).toSeq)
.partition(_.length == 3)
if (splitArtifacts.isEmpty) {
Console.err.println("Usage: coursier artifacts...")
sys exit 1
}
if (malformed.nonEmpty) {
Console.err.println(s"Malformed artifacts:\n${malformed.map(_.mkString(":")).mkString("\n")}")
sys exit 1
}
val modules = splitArtifacts.map{
case Seq(org, name, version) =>
Module(org, name, version)
}
val deps = modules.map(mod =>
Dependency(mod, scope = Scope.Runtime)
)
val res = resolve(
deps.toSet,
fetchFrom(repositories),
filter = Some(dep => (keepOptional || !dep.optional) && scopes(dep.scope))
).run
def repr(dep: Dependency) =
s"${dep.module.organization}:${dep.module.name}:${dep.`type`}:${Some(dep.classifier).filter(_.nonEmpty).map(_+":").mkString}${dep.module.version}"
val trDeps = res.dependencies.toList.map(repr).sorted
println("\n" + trDeps.mkString("\n"))
if (fetch) {
println()
val cachePolicy: CachePolicy = CachePolicy.Default
val m = res.dependencies.groupBy(dep => res.projectsCache.get(dep.module).map(_._1))
val (notFound, remaining0) = m.partition(_._1.isEmpty)
if (notFound.nonEmpty) {
val notFound0 = notFound.values.flatten.toList.map(repr).sorted
println(s"Not found:${notFound0.mkString("\n")}")
}
val (remaining, downloaderNotFound) = remaining0.partition(t => downloaders.contains(t._1.get))
if (downloaderNotFound.nonEmpty) {
val downloaderNotFound0 = downloaderNotFound.values.flatten.toList.map(repr).sorted
println(s"Don't know how to download:${downloaderNotFound0.mkString("\n")}")
}
val sorted = remaining
.toList
.map{ case (Some(repo), deps) => repo -> deps.toList.sortBy(repr) }
.sortBy(_._1.toString) // ...
val tasks =
for {
(repo, deps) <- sorted
dl = downloaders(repo)
dep <- deps
} yield {
dl.artifact(dep, cachePolicy = cachePolicy).run.map {
case -\/(err) =>
println(s"Failed to get ${repr(dep)}: $err")
case \/-(f) =>
println(s"${repr(dep)}:\n ${fileRepr(f)}")
}
}
val task = Task.gatherUnordered(tasks)
task.run
}
}
object Coursier extends AppOf[Coursier] {
val parser = default
}

View File

@ -0,0 +1,107 @@
package coursier
package core
import org.scalajs.dom.raw.{Event, XMLHttpRequest}
import scala.concurrent.{ExecutionContext, Promise, Future}
import scalaz.{-\/, \/-, \/, EitherT}
import scalaz.concurrent.Task
import scala.scalajs.js
import js.Dynamic.{global => g}
object Remote {
def encodeURIComponent(s: String): String =
g.encodeURIComponent(s).asInstanceOf[String]
lazy val jsonpAvailable = js.isUndefined(g.jsonp)
def proxiedJsonp(url: String)(implicit executionContext: ExecutionContext): Future[String] =
if (url.contains("{")) Future.successful("") // jsonp callback never gets executed in this case (empty response)
else {
// FIXME url is put between quotes in the YQL query, which could sometimes require some extra encoding
val url0 =
"https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20xml%20where%20url%3D%22" +
encodeURIComponent(url) +
"%22&format=jsonp&diagnostics=true"
val p = Promise[String]()
// FIXME Promise may not be always completed in case of failure
g.jsonp(url0, (res: js.Dynamic) => {
val success = !js.isUndefined(res) && !js.isUndefined(res.results)
if (success)
p.success(res.results.asInstanceOf[js.Array[String]].mkString("\n"))
else
p.failure(new Exception(s"Fetching $url ($url0)"))
})
p.future
}
def get(url: String)(implicit executionContext: ExecutionContext): Future[Either[String, Xml.Node]] =
if (jsonpAvailable) {
// Assume we're running on node, and that node package xhr2 is installed
val xhr = g.require("xhr2")
val p = Promise[Either[String, Xml.Node]]()
val req = js.Dynamic.newInstance(xhr)().asInstanceOf[XMLHttpRequest]
val f = { _: Event =>
p.success(compatibility.xmlParse(req.responseText))
}
req.onload = f
req.open("GET", url)
req.send()
p.future
} else {
proxiedJsonp(url).map(compatibility.xmlParse)
}
}
trait Logger {
def fetching(url: String): Unit
def fetched(url: String): Unit
def other(url: String, msg: String): Unit
}
case class Remote(base: String, logger: Option[Logger] = None) extends Repository {
def find(module: Module,
cachePolicy: CachePolicy): EitherT[Task, String, Project] = {
val relPath =
module.organization.split('.').toSeq ++ Seq(
module.name,
module.version,
s"${module.name}-${module.version}.pom"
)
val url = base + relPath.mkString("/")
EitherT(Task{ implicit ec =>
logger.foreach(_.fetching(url))
Remote.get(url).map{ eitherXml =>
logger.foreach(_.fetched(url))
for {
xml <- \/.fromEither(eitherXml)
_ = logger.foreach(_.other(url, "is XML"))
_ <- if (xml.label == "project") \/-(()) else -\/("Project definition not found")
_ = logger.foreach(_.other(url, "project definition found"))
proj <- Xml.project(xml)
_ = logger.foreach(_.other(url, "project definition ok"))
} yield proj
}
})
}
def versions(organization: String,
name: String,
cachePolicy: CachePolicy): EitherT[Task, String, Versions] = ???
}

View File

@ -0,0 +1,263 @@
package coursier.core.compatibility
// Cut-n-pasted from http4s 0.7.0
// Replaced Writer usages with StringBuilder
final class DateTime private (val year: Int, // the year
val month: Int, // the month of the year. January is 1.
val day: Int, // the day of the month. The first day is 1.
val hour: Int, // the hour of the day. The first hour is 0.
val minute: Int, // the minute of the hour. The first minute is 0.
val second: Int, // the second of the minute. The first second is 0.
val weekday: Int, // the day of the week. Sunday is 0.
val clicks: Long) // milliseconds since January 1, 1970, 00:00:00 GMT
extends Ordered[DateTime] with Product6[Int, Int, Int, Int, Int, Int] {
import DateTime.StringBuilderExtensions
/**
* The day of the week as a 3 letter abbreviation:
* `Sun`, `Mon`, `Tue`, `Wed`, `Thu`, `Fri` or `Sat`
*/
def weekdayStr: String = DateTime.WEEKDAYS(weekday)
/**
* The day of the month as a 3 letter abbreviation:
* `Jan`, `Feb`, `Mar`, `Apr`, `May`, `Jun`, `Jul`, `Aug`, `Sep`, `Oct`, `Nov` or `Dec`
*/
def monthStr: String = DateTime.MONTHS(month - 1)
/**
* Creates a new `DateTime` that represents the point in time the given number of ms later.
*/
def +(millis: Long): DateTime = DateTime(clicks + millis)
/**
* Creates a new `DateTime` that represents the point in time the given number of ms earlier.
*/
def -(millis: Long): DateTime = DateTime(clicks - millis)
/**
* `yyyy-mm-ddThh:mm:ss`
*/
override def toString = toIsoDateTimeString
/**
* `yyyy-mm-dd`
*/
def renderIsoDate(w: StringBuilder): w.type = {
put_##(put_##(w << year << '-', month) << '-', day)
w
}
/**
* `yyyy-mm-dd`
*/
def toIsoDateString = { val w = new StringBuilder; renderIsoDate(w).result() }
/**
* `yyyy-mm-ddThh:mm:ss`
*/
def renderIsoDateTimeString(w: StringBuilder): w.type = {
put_##(put_##(put_##(renderIsoDate(w) << 'T', hour) << ':', minute) << ':', second)
w
}
/**
* `yyyy-mm-ddThh:mm:ss`
*/
def toIsoDateTimeString = { val w = new StringBuilder; renderIsoDateTimeString(w).result() }
/**
* `yyyy-mm-dd hh:mm:ss`
*/
def renderIsoLikeDateTimeString(w: StringBuilder): w.type = {
put_##(put_##(put_##(renderIsoDate(w) << ' ', hour) << ':', minute) << ':', second)
w
}
/**
* `yyyy-mm-dd hh:mm:ss`
*/
def toIsoLikeDateTimeString = { val w = new StringBuilder; renderIsoLikeDateTimeString(w).result() }
/**
* RFC1123 date string, e.g. `Sun, 06 Nov 1994 08:49:37 GMT`
*/
def renderRfc1123DateTimeString(w: StringBuilder): w.type = {
w ++= weekdayStr
w ++= ", "
put_##(put_##(put_##(put_##(w << weekdayStr << ',' << ' ', day) << ' ' << monthStr << ' ' << year << ' ', hour) << ':', minute) << ':', second) << " GMT"
w
}
/**
* RFC1123 date string, e.g. `Sun, 06 Nov 1994 08:49:37 GMT`
*/
def toRfc1123DateTimeString = { val w = new StringBuilder; renderRfc1123DateTimeString(w).result() }
private def put_##(w: StringBuilder, i: Int): w.type = {
w << (i / 10 + '0').toChar << (i % 10 + '0').toChar
w
}
def compare(that: DateTime): Int = math.signum(clicks - that.clicks).toInt
override def hashCode() = clicks.##
override def equals(obj: Any) = obj match {
case x: DateTime x.clicks == clicks
case _ false
}
override def _1: Int = year
override def _2: Int = month
override def _3: Int = day
override def _4: Int = hour
override def _5: Int = minute
override def _6: Int = second
override def canEqual(that: Any): Boolean = that.isInstanceOf[DateTime]
}
object DateTime {
private implicit class StringBuilderExtensions(val b: StringBuilder) extends AnyVal {
def <<(s: String): b.type = b ++= s
def <<(c: Char): b.type = b += c
def <<(n: Int): b.type = b ++= n.toString
}
val WEEKDAYS = Array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
val MONTHS = Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
val MinValue = DateTime(1800, 1, 1)
val MaxValue = DateTime(2199, 12, 31, 23, 59, 59)
/**
* Creates a new `DateTime` with the given properties.
* Note that this implementation discards milliseconds (i.e. rounds down to full seconds).
*/
def apply(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0): DateTime = {
require(1800 <= year && year <= 9999, "year must be >= 1800 and <= 9999")
require(1 <= month && month <= 12, "month must be >= 1 and <= 12")
require(1 <= day && day <= 31, "day must be >= 1 and <= 31")
require(0 <= hour && hour <= 23, "hour must be >= 0 and <= 23")
require(0 <= minute && minute <= 59, "minute_ must be >= 0 and <= 59")
require(0 <= second && second <= 59, "second must be >= 0 and <= 59")
// compute yearday from month/monthday
val m = month - 1
var d = (m % 7) * 30 + (m % 7 + 1) / 2 + day
val isLeap = ((year % 4 == 0) && !(year % 100 == 0)) || (year % 400 == 0)
if (m >= 7) d += 214
if (d >= 61) d -= 1 // skip non-existent Feb 30
if (!isLeap && (d >= 60)) d -= 1 // skip non-existent Feb 29
// convert year/yearday to days since Jan 1, 1970, 00:00:00
val y = year - 1
d += y * 365 + y / 4 - y / 100 + y / 400
val dn = d - (1969 * 365 + 492 - 19 + 4)
val c = (dn - 1) * 86400L + hour * 3600L + minute * 60L + second // seconds since Jan 1, 1970, 00:00:00
new DateTime(year, month, day, hour, minute, second, weekday = d % 7, clicks = c * 1000)
}
/**
* Creates a new `DateTime` from the number of milli seconds
* since the start of "the epoch", namely January 1, 1970, 00:00:00 GMT.
* Note that this implementation discards milliseconds (i.e. rounds down to full seconds).
*/
def apply(clicks: Long): DateTime = {
require(DateTime.MinValue.clicks <= clicks && clicks <= DateTime.MaxValue.clicks,
"DateTime value must be >= " + DateTime.MinValue + " and <= " + DateTime.MaxValue)
// based on a fast RFC1123 implementation (C) 2000 by Tim Kientzle <kientzle@acm.org>
val c = clicks - clicks % 1000
// compute day number, seconds since beginning of day
var s = c
if (s >= 0) s /= 1000 // seconds since 1 Jan 1970
else s = (s - 999) / 1000 // floor(sec/1000)
var dn = (s / 86400).toInt
s %= 86400 // positive seconds since beginning of day
if (s < 0) { s += 86400; dn -= 1 }
dn += 1969 * 365 + 492 - 19 + 4 // days since "1 Jan, year 1"
// convert days since 1 Jan, year 1 to year/yearday
var y = 400 * (dn / 146097).toInt + 1
var d = dn % 146097
if (d == 146096) { y += 399; d = 365 } // last year of 400 is long
else {
y += 100 * (d / 36524)
d %= 36524
y += 4 * (d / 1461)
d %= 1461
if (d == 1460) { y += 3; d = 365 } // last year out of 4 is long
else {
y += d / 365
d %= 365
}
}
val isLeap = ((y % 4 == 0) && !(y % 100 == 0)) || (y % 400 == 0)
// compute month/monthday from year/yearday
if (!isLeap && (d >= 59)) d += 1 // skip non-existent Feb 29
if (d >= 60) d += 1 // skip non-existent Feb 30
var mon = ((d % 214) / 61) * 2 + ((d % 214) % 61) / 31
if (d > 213) mon += 7
d = ((d % 214) % 61) % 31 + 1
// convert second to hour/min/sec
var m = (s / 60).toInt
val h = m / 60
m %= 60
s %= 60
val w = (dn + 1) % 7 // day of week, 0==Sun
new DateTime(year = y, month = mon + 1, day = d, hour = h, minute = m, second = s.toInt, weekday = w, clicks = c)
}
/**
* Creates a new `DateTime` instance for the current point in time.
* Note that this implementation discards milliseconds (i.e. rounds down to full seconds).
*/
def now: DateTime = apply(System.currentTimeMillis)
/**
* Creates a new DateTime instance from the given String,
* if it adheres to the format `yyyy-mm-ddThh:mm:ss[.SSSZ]`.
* Note that this implementation discards milliseconds (i.e. rounds down to full seconds).
*/
def fromIsoDateTimeString(string: String): Option[DateTime] = {
def c(ix: Int) = string.charAt(ix)
def isDigit(c: Char) = '0' <= c && c <= '9'
def i(ix: Int) = {
val x = c(ix)
require(isDigit(x))
x - '0'
}
def check(len: Int): Boolean =
len match {
case 19 c(4) == '-' && c(7) == '-' && c(10) == 'T' && c(13) == ':' && c(16) == ':'
case 24 check(19) && c(19) == '.' && isDigit(c(20)) && isDigit(c(21)) && isDigit(c(22)) && c(23) == 'Z'
case _ false
}
if (check(string.length)) {
try {
val year = i(0) * 1000 + i(1) * 100 + i(2) * 10 + i(3)
val month = i(5) * 10 + i(6)
val day = i(8) * 10 + i(9)
val hour = i(11) * 10 + i(12)
val min = i(14) * 10 + i(15)
val sec = i(17) * 10 + i(18)
Some(DateTime(year, month, day, hour, min, sec))
} catch { case _: IllegalArgumentException None }
} else None
}
val UnixEpoch = DateTime(0L)
def unapply(dt: DateTime): Option[(Int, Int, Int, Int, Int, Int)] =
Some((dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second))
}

View File

@ -0,0 +1,71 @@
package coursier.core
import scala.scalajs.js
import org.scalajs.dom.raw.NodeList
package object compatibility {
def option[A](a: js.Dynamic): Option[A] =
if (js.isUndefined(a)) None
else Some(a.asInstanceOf[A])
def dynOption(a: js.Dynamic): Option[js.Dynamic] =
if (js.isUndefined(a)) None
else Some(a)
private def between(c: Char, lower: Char, upper: Char) = lower <= c && c <= upper
implicit class RichChar(val c: Char) extends AnyVal {
def letterOrDigit: Boolean = {
between(c, '0', '9') || letter
}
def letter: Boolean = between(c, 'a', 'z') || between(c, 'A', 'Z')
}
lazy val DOMParser = {
import js.Dynamic.{global => g}
import js.DynamicImplicits._
val defn =
if (js.isUndefined(g.DOMParser)) g.require("xmldom").DOMParser
else g.DOMParser
js.Dynamic.newInstance(defn)()
}
def fromNode(node: org.scalajs.dom.raw.Node): Xml.Node = {
val node0 = node.asInstanceOf[js.Dynamic]
new Xml.Node {
def label =
option[String](node0.nodeName)
.getOrElse("")
def child =
option[NodeList](node0.childNodes)
.map(l => List.tabulate(l.length)(l.item).map(fromNode))
.getOrElse(Nil)
def isText =
option[Int](node0.nodeType)
.exists(_ == 3) //org.scalajs.dom.raw.Node.TEXT_NODE
def textContent =
option(node0.textContent)
.getOrElse("")
def isElement =
option[Int](node0.nodeType)
.exists(_ == 1) // org.scalajs.dom.raw.Node.ELEMENT_NODE
}
}
def xmlParse(s: String): Either[String, Xml.Node] = {
val doc = {
if (s.isEmpty) None
else
dynOption(DOMParser.parseFromString(s, "text/xml"))
.flatMap(t => dynOption(t.childNodes))
.flatMap(l => l.asInstanceOf[js.Array[js.Dynamic]].headOption.flatMap(option[org.scalajs.dom.raw.Node]))
}
Right(doc.fold(Xml.Node.empty)(fromNode))
}
}

View File

@ -0,0 +1,13 @@
package coursier
package object repository {
type Remote = core.Remote
val Remote: core.Remote.type = core.Remote
val mavenCentral = Remote("https://repo1.maven.org/maven2/")
val sonatypeReleases = Remote("https://oss.sonatype.org/content/repositories/releases/")
val sonatypeSnapshots = Remote("https://oss.sonatype.org/content/repositories/snapshots/")
}

View File

@ -0,0 +1,42 @@
package scalaz
import scala.concurrent.{ExecutionContext, Future}
/** Minimal Future-based Task */
package object concurrent {
trait Task[T] { self =>
def map[U](f: T => U): Task[U] =
new Task[U] {
def runF(implicit ec: ExecutionContext) = self.runF.map(f)
}
def flatMap[U](f: T => Task[U]): Task[U] =
new Task[U] {
def runF(implicit ec: ExecutionContext) = self.runF.flatMap(f(_).runF)
}
def runF(implicit ec: ExecutionContext): Future[T]
}
object Task {
def now[A](a: A): Task[A] =
new Task[A] {
def runF(implicit ec: ExecutionContext) = Future.successful(a)
}
def apply[A](f: ExecutionContext => Future[A]): Task[A] =
new Task[A] {
def runF(implicit ec: ExecutionContext) = f(ec)
}
def gatherUnordered[T](tasks: Seq[Task[T]], exceptionCancels: Boolean = false): Task[Seq[T]] =
new Task[Seq[T]] {
def runF(implicit ec: ExecutionContext) = Future.traverse(tasks)(_.runF)
}
implicit val taskFunctor: Functor[Task] =
new Functor[Task] {
def map[A, B](fa: Task[A])(f: A => B): Task[B] =
fa.map(f)
}
}
}

View File

@ -0,0 +1,41 @@
package coursier
package test
import coursier.core.Remote
import coursier.test.compatibility._
import utest._
import scala.concurrent.{Future, Promise}
object JsTests extends TestSuite {
val tests = TestSuite {
'promise {
val p = Promise[Unit]()
Future(p.success(()))
p.future
}
'get{
Remote.get("http://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.1.3/logback-classic-1.1.3.pom")
.map{ xml =>
assert(xml.right.toOption.exists(_.label == "project"))
}
}
'getProj{
repository.mavenCentral
.find(Module("ch.qos.logback", "logback-classic", "1.1.3"))
.map{ proj =>
assert(proj.parent == Some(Module("ch.qos.logback", "logback-parent", "1.1.3")))
}
.run
.runF
.map{ res =>
assert(res.isRight)
}
}
}
}

View File

@ -0,0 +1,7 @@
package coursier.test
package object compatibility {
implicit val executionContext = scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
}

View File

@ -0,0 +1,229 @@
package coursier
package core
import java.io._
import java.net.URL
import org.http4s.{EntityDecoder, Status, Uri}
import scala.annotation.tailrec
import scala.io.Codec
import scalaz._, Scalaz._
import scalaz.concurrent.Task
// FIXME This kind of side-effecting API is lame, we should aim at a more functional one.
trait ArtifactDownloaderLogger {
def foundLocally(f: File): Unit
def downloadingArtifact(url: String): Unit
def downloadedArtifact(url: String, success: Boolean): Unit
}
case class ArtifactDownloader(root: Uri, cache: File, logger: Option[ArtifactDownloaderLogger] = None) {
var bufferSize = 1024*1024
def artifact(module: Module,
classifier: String,
`type`: String,
cachePolicy: CachePolicy): EitherT[Task, String, File] = {
val relPath =
module.organization.split('.').toSeq ++ Seq(
module.name,
module.version,
s"${module.name}-${module.version}${Some(classifier).filter(_.nonEmpty).map("-"+_).mkString}.${`type`}"
)
val file = (cache /: relPath)(new File(_, _))
def locally = {
Task {
if (file.exists()) {
logger.foreach(_.foundLocally(file))
\/-(file)
}
else -\/("Not found in cache")
}
}
def remote = {
// FIXME A lot of things can go wrong here and are not properly handled:
// - checksums should be validated
// - what if the connection gets closed during the transfer (partial file on disk)?
// - what if someone is trying to write this file at the same time? (no locking of any kind yet)
// - ...
val urlStr = (root resolve Uri(path = relPath.mkString("./", "/", ""))).renderString
Task {
try {
file.getParentFile.mkdirs()
logger.foreach(_.downloadingArtifact(urlStr))
val url = new URL(urlStr)
val b = Array.fill[Byte](bufferSize)(0)
val in = new BufferedInputStream(url.openStream(), bufferSize)
try {
val w = new FileOutputStream(file)
try {
@tailrec
def helper(): Unit = {
val read = in.read(b)
if (read >= 0) {
w.write(b, 0, read)
helper()
}
}
helper()
} finally w.close()
} finally in.close()
logger.foreach(_.downloadedArtifact(urlStr, success = true))
\/-(file)
}
catch { case e: Exception =>
logger.foreach(_.downloadedArtifact(urlStr, success = false))
-\/(e.getMessage)
}
}
}
EitherT(cachePolicy(locally)(remote))
}
def artifact(dependency: Dependency,
cachePolicy: CachePolicy = CachePolicy.Default): EitherT[Task, String, File] =
artifact(dependency.module, dependency.classifier, dependency.`type`, cachePolicy = cachePolicy)
}
// FIXME Comment of ArtifactDownloaderLogger applies here too
trait RemoteLogger {
def downloading(url: String): Unit
def downloaded(url: String, success: Boolean): Unit
def readingFromCache(f: File): Unit
def puttingInCache(f: File): Unit
}
case class Remote(root: Uri, cache: Option[File] = None, logger: Option[RemoteLogger] = None) extends Repository {
lazy val client = org.http4s.client.blaze.defaultClient
def find(module: Module,
cachePolicy: CachePolicy): EitherT[Task, String, Project] = {
val relPath =
module.organization.split('.').toSeq ++ Seq(
module.name,
module.version,
s"${module.name}-${module.version}.pom"
)
def localFile = {
for {
cache0 <- cache.toRightDisjunction("No cache")
f = (cache0 /: relPath)(new File(_, _))
} yield f
}
def locally = {
Task {
for {
f0 <- localFile
f <- Some(f0).filter(_.exists()).toRightDisjunction("Not found in cache")
content <- \/.fromTryCatchNonFatal{
logger.foreach(_.readingFromCache(f))
scala.io.Source.fromFile(f)(Codec.UTF8).mkString
}.leftMap(_.getMessage)
} yield content
}
}
def remote = {
val uri = root resolve Uri(path = relPath.mkString("./", "/", ""))
val log = Task(logger.foreach(_.downloading(uri.renderString)))
val get = log.flatMap(_ => client(uri))
get.flatMap{ resp =>
val success = resp.status == Status.Ok
logger.foreach(_.downloaded(uri.renderString, success))
if (success)
EntityDecoder.text.decode(resp).run.map(_.leftMap(_.sanitized))
else
Task.now(-\/(s"Unhandled or bad status ${resp.status.code} from repository (${resp.status.code} ${resp.status.reason})"))
}
}
def save(s: String) = {
localFile.fold(_ => Task.now(()), f =>
Task {
if (!f.exists()) {
logger.foreach(_.puttingInCache(f))
f.getParentFile.mkdirs()
val w = new PrintWriter(f)
try w.write(s)
finally w.close()
()
}
}
)
}
val task = cachePolicy.saving(locally)(remote)(save)
.map(eitherStr =>
for {
str <- eitherStr
xml <- \/.fromEither(compatibility.xmlParse(str))
_ <- if (xml.label == "project") \/-(()) else -\/("Project definition not found")
proj <- Xml.project(xml)
} yield proj
)
EitherT(task)
}
def versions(organization: String,
name: String,
cachePolicy: CachePolicy): EitherT[Task, String, Versions] = {
val relPath =
organization.split('.').toSeq ++ Seq(
name,
"maven-metadata.xml"
)
def locally = {
???
}
def remote = {
val respTask = client(root resolve Uri(path = relPath.mkString("./", "/", "")))
respTask.flatMap{ resp =>
if (resp.status == Status.Ok)
EntityDecoder.text.decode(resp).run.map(_.leftMap(_.sanitized))
else
Task.now(-\/(s"Unhandled or bad status ${resp.status.code} from repository (${resp.status.code} ${resp.status.reason})"))
}
}
def save(s: String) = {
// TODO
Task.now(())
}
val task = cachePolicy.saving(locally)(remote)(save)
.map(eitherStr =>
for {
str <- eitherStr
xml <- \/.fromEither(compatibility.xmlParse(str))
_ <- if (xml.label == "metadata") \/-(()) else -\/("Metadata not found")
versions <- Xml.versions(xml)
} yield versions
)
EitherT(task)
}
}

View File

@ -0,0 +1,34 @@
package coursier.core
package object compatibility {
type DateTime = org.http4s.DateTime
val DateTime: org.http4s.DateTime.type = org.http4s.DateTime
implicit class RichChar(val c: Char) extends AnyVal {
def letterOrDigit = c.isLetterOrDigit
def letter = c.isLetter
}
def xmlParse(s: String): Either[String, Xml.Node] = {
def parse =
try Right(scala.xml.XML.loadString(s))
catch { case e: Exception => Left(e.getMessage) }
def fromNode(node: scala.xml.Node): Xml.Node =
new Xml.Node {
def label = node.label
def child = node.child.map(fromNode)
def isText = node match { case _: scala.xml.Text => true; case _ => false }
def textContent = node match {
case t: scala.xml.Text => t.data
case _ => throw new NoSuchElementException("text of non text node")
}
def isElement = node match { case _: scala.xml.Elem => true; case _ => false }
}
parse.right
.map(fromNode)
}
}

View File

@ -0,0 +1,31 @@
package coursier
import scalaz.EitherT
import scalaz.concurrent.Task
package object core {
def resolution(dependencies: Set[Dependency],
fetch: Module => EitherT[Task, List[String], (Repository, Project)],
filter: Option[Dependency => Boolean],
profileActivation: Option[(String, Activation, Map[String, String]) => Boolean]): Stream[Resolution] = {
val startResolution = Resolution(
dependencies, dependencies, Set.empty,
Map.empty, Map.empty,
filter,
profileActivation
)
def helper(resolution: Resolution): Stream[Resolution] = {
if (resolution.isDone) Stream()
else {
val nextRes = resolution.next(fetch).run
nextRes #:: helper(nextRes)
}
}
startResolution #:: helper(startResolution)
}
}

View File

@ -0,0 +1,15 @@
package coursier
import org.http4s.Http4s._
package object repository {
type Remote = core.Remote
val Remote: core.Remote.type = core.Remote
val mavenCentral = Remote(uri("https://repo1.maven.org/maven2/"))
val sonatypeReleases = Remote(uri("https://oss.sonatype.org/content/repositories/releases/"))
val sonatypeSnapshots = Remote(uri("https://oss.sonatype.org/content/repositories/snapshots/"))
}

View File

@ -0,0 +1,14 @@
package coursier.test
import scala.concurrent.Future
import scalaz.concurrent.Task
package object compatibility {
implicit val executionContext = scala.concurrent.ExecutionContext.global
implicit class TaskExtensions[T](val underlying: Task[T]) extends AnyVal {
def runF: Future[T] = Future.successful(underlying.run)
}
}

View File

@ -0,0 +1,205 @@
package coursier.core
import scala.annotation.tailrec
import coursier.core.compatibility._
/** Same kind of ordering as aether-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java */
object ComparableVersion {
sealed trait Item extends Ordered[Item] {
def compare(other: Item): Int =
(this, other) match {
case (Number(a), Number(b)) => a.compare(b)
case (BigNumber(a), BigNumber(b)) => a.compare(b)
case (Number(a), BigNumber(b)) => -b.compare(a)
case (BigNumber(a), Number(b)) => a.compare(b)
case (Qualifier(_, a), Qualifier(_, b)) => a.compare(b)
case (Literal(a), Literal(b)) => a.compareToIgnoreCase(b)
case _ =>
val rel0 = compareToEmpty
val rel1 = other.compareToEmpty
if (rel0 == rel1) order.compare(other.order)
else rel0.compare(rel1)
}
def order: Int
def isEmpty: Boolean = compareToEmpty == 0
def compareToEmpty: Int = 1
}
sealed trait Numeric extends Item
case class Number(value: Int) extends Numeric {
val order = 0
override def compareToEmpty = value.compare(0)
}
case class BigNumber(value: BigInt) extends Numeric {
val order = 0
override def compareToEmpty = value.compare(0)
}
case class Qualifier(value: String, level: Int) extends Item {
val order = -2
override def compareToEmpty = level.compare(0)
}
case class Literal(value: String) extends Item {
val order = -1
override def compareToEmpty = if (value.isEmpty) 0 else 1
}
case object Min extends Item {
val order = -8
override def compareToEmpty = -1
}
case object Max extends Item {
val order = 8
}
val empty = Number(0)
val qualifiers = Seq[Qualifier](
Qualifier("alpha", -5),
Qualifier("beta", -4),
Qualifier("milestone", -3),
Qualifier("cr", -2),
Qualifier("rc", -2),
Qualifier("snapshot", -1),
Qualifier("ga", 0),
Qualifier("final", 0),
Qualifier("sp", 1)
)
val qualifiersMap = qualifiers.map(q => q.value -> q).toMap
object Tokenizer {
sealed trait Separator
case object Dot extends Separator
case object Hyphen extends Separator
case object Underscore extends Separator
case object None extends Separator
def apply(s: String): (Item, Stream[(Separator, Item)]) = {
def parseItem(s: Stream[Char]): (Item, Stream[Char]) = {
if (s.isEmpty || !s.head.letterOrDigit) (empty, s)
else if (s.head.isDigit) {
def digits(b: StringBuilder, s: Stream[Char]): (String, Stream[Char]) =
if (s.isEmpty || !s.head.isDigit) (b.result(), s)
else digits(b + s.head, s.tail)
val (digits0, rem) = digits(new StringBuilder, s)
val item =
if (digits0.length >= 10) BigNumber(BigInt(digits0))
else Number(digits0.toInt)
(item, rem)
} else {
assert(s.head.letter)
def letters(b: StringBuilder, s: Stream[Char]): (String, Stream[Char]) =
if (s.isEmpty || !s.head.letter) (b.result().toLowerCase, s)
else letters(b + s.head, s.tail)
val (letters0, rem) = letters(new StringBuilder, s)
val item =
qualifiersMap.getOrElse(letters0, Literal(letters0))
(item, rem)
}
}
def parseSeparator(s: Stream[Char]): (Separator, Stream[Char]) = {
assert(s.nonEmpty)
s.head match {
case '.' => (Dot, s.tail)
case '-' => (Hyphen, s.tail)
case '_' => (Underscore, s.tail)
case _ => (None, s)
}
}
def helper(s: Stream[Char]): Stream[(Separator, Item)] = {
if (s.isEmpty) Stream()
else {
val (sep, rem0) = parseSeparator(s)
val (item, rem) = parseItem(rem0)
(sep, item) #:: helper(rem)
}
}
val (first, rem) = parseItem(s.toStream)
(first, helper(rem))
}
}
def parse(s: String): ComparableVersion = {
val (first, tokens) = Tokenizer(s)
def isNumeric(item: Item) = item match { case _: Numeric => true; case _ => false }
def postProcess(prevIsNumeric: Option[Boolean], item: Item, tokens0: Stream[(Tokenizer.Separator, Item)]): Stream[Item] = {
val tokens = {
var _tokens = tokens0
if (isNumeric(item)) {
val nextNonDotZero = _tokens.dropWhile{case (Tokenizer.Dot, n: Numeric) => n.isEmpty; case _ => false }
if (nextNonDotZero.forall(t => t._1 == Tokenizer.Hyphen || ((t._1 == Tokenizer.Dot || t._1 == Tokenizer.None) && !isNumeric(t._2)))) { // Dot && isNumeric(t._2)
_tokens = nextNonDotZero
}
}
_tokens
}
def ifFollowedByNumberElse(ifFollowedByNumber: Item, default: Item) = {
val followedByNumber = tokens.headOption
.exists{ case (Tokenizer.None, num: Numeric) if !num.isEmpty => true; case _ => false }
if (followedByNumber) ifFollowedByNumber
else default
}
def next =
if (tokens.isEmpty) Stream()
else postProcess(Some(isNumeric(item)), tokens.head._2, tokens.tail)
item match {
case Literal("min") => Min #:: next
case Literal("max") => Max #:: next
case Literal("a") =>
ifFollowedByNumberElse(qualifiersMap("alpha"), item) #:: next
case Literal("b") =>
ifFollowedByNumberElse(qualifiersMap("beta"), item) #:: next
case Literal("m") =>
ifFollowedByNumberElse(qualifiersMap("milestone"), item) #:: next
case _ =>
item #:: next
}
}
ComparableVersion(postProcess(None, first, tokens).toList)
}
@tailrec
def listCompare(first: List[Item], second: List[Item]): Int = {
if (first.isEmpty && second.isEmpty) 0
else if (first.isEmpty) {
assert(second.nonEmpty)
-second.dropWhile(_.isEmpty).headOption.fold(0)(_.compareToEmpty)
} else if (second.isEmpty) {
assert(first.nonEmpty)
first.dropWhile(_.isEmpty).headOption.fold(0)(_.compareToEmpty)
} else {
val rel = first.head.compare(second.head)
if (rel == 0) listCompare(first.tail, second.tail)
else rel
}
}
}
case class ComparableVersion(items: List[ComparableVersion.Item]) extends Ordered[ComparableVersion] {
def compare(other: ComparableVersion) = ComparableVersion.listCompare(items, other.items)
def isEmpty = items.forall(_.isEmpty)
}

View File

@ -0,0 +1,47 @@
package coursier.core
case class Module(organization: String,
name: String,
version: String) {
def trim: Module = copy(
organization = organization.trim,
name = name.trim,
version = version.trim
)
override def toString = s"$organization:$name:$version"
}
sealed abstract class Scope(val name: String)
case class Dependency(module: Module,
scope: Scope,
`type`: String,
classifier: String,
exclusions: Set[(String, String)],
optional: Boolean)
case class Project(module: Module,
dependencies: Seq[Dependency],
parent: Option[Module],
dependencyManagement: Seq[Dependency],
properties: Map[String, String],
profiles: Seq[Profile])
object Scope {
case object Compile extends Scope("compile")
case object Runtime extends Scope("runtime")
case object Test extends Scope("test")
case object Provided extends Scope("provided")
case object Import extends Scope("import")
case class Other(override val name: String) extends Scope(name)
}
case class Activation(properties: Seq[(String, Option[String])])
case class Profile(id: String,
activeByDefault: Option[Boolean],
activation: Activation,
dependencies: Seq[Dependency],
dependencyManagement: Seq[Dependency],
properties: Map[String, String])

View File

@ -0,0 +1,43 @@
package coursier.core
import coursier.core.compatibility._
object Parse {
def scope(s: String): Scope = s match {
case "compile" => Scope.Compile
case "runtime" => Scope.Runtime
case "test" => Scope.Test
case "provided" => Scope.Provided
case "import" => Scope.Import
case other => Scope.Other(other)
}
def version(s: String): Option[Version] = {
if (s.isEmpty || s.exists(c => c != '.' && c != '-' && c != '_' && !c.letterOrDigit)) None
else Some(Version(s))
}
def versionInterval(s: String): Option[VersionInterval] = {
for {
fromIncluded <- if (s.startsWith("[")) Some(true) else if (s.startsWith("(")) Some(false) else None
toIncluded <- if (s.endsWith("]")) Some(true) else if (s.endsWith(")")) Some(false) else None
s0 = s.drop(1).dropRight(1)
commaIdx = s0.indexOf(',')
if commaIdx >= 0
strFrom = s0.take(commaIdx)
strTo = s0.drop(commaIdx + 1)
from <- if (strFrom.isEmpty) Some(None) else version(strFrom).map(Some(_))
to <- if (strTo.isEmpty) Some(None) else version(strTo).map(Some(_))
} yield VersionInterval(from.filterNot(_.cmp.isEmpty), to.filterNot(_.cmp.isEmpty), fromIncluded, toIncluded)
}
def versionConstraint(s: String): Option[VersionConstraint] = {
def noConstraint = if (s.isEmpty) Some(VersionConstraint.None) else None
noConstraint
.orElse(version(s).map(VersionConstraint.Preferred))
.orElse(versionInterval(s).map(VersionConstraint.Interval))
}
}

View File

@ -0,0 +1,38 @@
package coursier.core
import scalaz.{\/, EitherT}
import scalaz.concurrent.Task
trait Repository {
def find(module: Module, cachePolicy: CachePolicy = CachePolicy.Default): EitherT[Task, String, Project]
def versions(organization: String, name: String, cachePolicy: CachePolicy = CachePolicy.Default): EitherT[Task, String, Versions]
}
sealed trait CachePolicy {
def apply[E,T](local: => Task[E \/ T])(remote: => Task[E \/ T]): Task[E \/ T]
def saving[E,T](local: => Task[E \/ T])(remote: => Task[E \/ T])(save: => T => Task[Unit]): Task[E \/ T] =
apply(local)(CachePolicy.saving(remote)(save))
}
object CachePolicy {
def saving[E,T](remote: => Task[E \/ T])(save: T => Task[Unit]): Task[E \/ T] = {
for {
res <- remote
_ <- res.fold(_ => Task.now(()), t => save(t))
} yield res
}
case object Default extends CachePolicy {
def apply[E,T](local: => Task[E \/ T])(remote: => Task[E \/ T]): Task[E \/ T] =
local.flatMap(res => if (res.isLeft) remote else Task.now(res))
}
case object LocalOnly extends CachePolicy {
def apply[E,T](local: => Task[E \/ T])(remote: => Task[E \/ T]): Task[E \/ T] =
local
}
case object ForceDownload extends CachePolicy {
def apply[E,T](local: => Task[E \/ T])(remote: => Task[E \/ T]): Task[E \/ T] =
remote
}
}

View File

@ -0,0 +1,717 @@
package coursier.core
import java.util.regex.Pattern.quote
import scala.annotation.tailrec
import scalaz.concurrent.Task
import scalaz.{EitherT, \/-, \/, -\/}
object Resolver {
/**
* Try to find `module` among `repositories`.
*/
def find(repositories: Seq[Repository],
module: Module): EitherT[Task, List[String], (Repository, Project)] = {
val lookups = repositories.map(repo => repo -> repo.find(module).run)
val task = lookups.foldLeft(Task.now(-\/(Nil)): Task[List[String] \/ (Repository, Project)]) {
case (acc, (repo, t)) =>
acc.flatMap {
case -\/(errors) =>
t.map(res => res
.flatMap(project =>
if (project.module == module) \/-((repo, project))
else -\/(s"Wrong module returned (expected: $module, got: ${project.module})")
)
.leftMap(error => error :: errors)
)
case res @ \/-(_) =>
Task.now(res)
}
}
EitherT(task.map(_.leftMap(_.reverse))).map { case x @ (_, proj) =>
assert(proj.module == module)
x
}
}
/**
* Get the active profiles of `project`, using the current properties `properties`,
* and `profileActivation` stating if a profile is active.
*/
def profiles(project: Project,
properties: Map[String, String],
profileActivation: (String, Activation, Map[String, String]) => Boolean): Seq[Profile] = {
val activated = project.profiles
.filter(p => profileActivation(p.id, p.activation, properties))
def default = project.profiles
.filter(_.activeByDefault.toSeq.contains(true))
if (activated.isEmpty) default
else activated
}
type DepMgmtKey = (String, String, String)
def dependencyManagementKey(dep: Dependency): DepMgmtKey =
(dep.module.organization, dep.module.name, dep.`type`)
def dependencyManagementAdd(m: Map[DepMgmtKey, Dependency], dep: Dependency): Map[DepMgmtKey, Dependency] = {
val key = dependencyManagementKey(dep)
if (m.contains(key)) m else m + (key -> dep)
}
def dependencyManagementAddSeq(m: Map[DepMgmtKey, Dependency], deps: Seq[Dependency]): Map[DepMgmtKey, Dependency] =
(m /: deps)(dependencyManagementAdd)
def mergeProperties(m: Map[String, String], other: Map[String, String]): Map[String, String] = {
m ++ other.filterKeys(!m.contains(_))
}
def addDependencies(deps: Seq[Seq[Dependency]]): Seq[Dependency] = {
val res =
deps.foldRight((Set.empty[DepMgmtKey], Seq.empty[Dependency])) {
case (deps0, (set, acc)) =>
val deps = deps0.filter(dep => !set(dependencyManagementKey(dep)))
(set ++ deps.map(dependencyManagementKey), acc ++ deps)
}
res._2
}
val propRegex = (quote("${") + "([a-zA-Z0-9-.]*)" + quote("}")).r
/**
* Substitutes `properties` in `dependencies`.
*/
def withProperties(dependencies: Seq[Dependency],
properties: Map[String, String]): Seq[Dependency] = {
def substituteProps(s: String) = {
val matches = propRegex.findAllMatchIn(s).toList.reverse
if (matches.isEmpty) s
else {
val output = (new StringBuilder(s) /: matches) {
(b, m) => properties.get(m.group(1)).fold(b)(b.replace(m.start, m.end, _))
}
output.result()
}
}
dependencies.map{ dep =>
dep.copy(
module = dep.module.copy(
organization = substituteProps(dep.module.organization),
name = substituteProps(dep.module.name),
version = substituteProps(dep.module.version)
),
`type` = substituteProps(dep.`type`),
scope = Parse.scope(substituteProps(dep.scope.name)),
exclusions = dep.exclusions
.map{case (org, name) => (substituteProps(org), substituteProps(name))}
// FIXME The content of the optional tag may also be a property in the original POM.
// Maybe not parse it that earlier?
)
}
}
/**
* Merge several version constraints together. Returns `None` in case of conflict.
*/
def mergeVersions(versions: Seq[String]): Option[String] = {
val (nonParsedConstraints, parsedConstraints) =
versions
.map(v => v -> Parse.versionConstraint(v))
.partition(_._2.isEmpty)
if (nonParsedConstraints.nonEmpty)
Console.err.println(s"Ignoring unparsed versions: ${nonParsedConstraints.map(_._1)}")
val constraintOpt =
(Option(VersionInterval.zero) /: parsedConstraints.map(_._2.get.interval)) {
case (acc, itv) => acc.flatMap(_.merge(itv))
} .map(_.constraint)
constraintOpt.map(_.repr)
}
/**
* Merge several dependencies, solving version constraints of duplicated modules.
* Returns the conflicted dependencies, and the (solved) others.
*/
def merge(dependencies: TraversableOnce[Dependency]): (Seq[Dependency], Seq[Dependency]) = {
val m = dependencies
.toList
.groupBy(dep => (dep.module.organization, dep.module.name))
.mapValues{ deps =>
if (deps.lengthCompare(1) == 0) List(\/-(deps.head))
else {
val scopeTypeClassifiers = (Set.empty[(Scope, String, String)] /: deps)((acc, dep) => acc + ((dep.scope, dep.`type`, dep.classifier)))
val versions = deps.map(_.module.version).distinct
val versionOpt = mergeVersions(versions)
scopeTypeClassifiers.toList.flatMap{case (scope, type0, classifier) =>
val scopeTypeClassifierDeps =
deps.filter(dep =>
dep.scope == scope && dep.`type` == type0 && dep.classifier == classifier
)
def dep(version: String) = {
Dependency(
deps.head.module.copy(version = version),
scope,
type0,
classifier,
scopeTypeClassifierDeps.map(_.exclusions.toSet).reduce(exclusionsIntersect),
scopeTypeClassifierDeps.forall(_.optional)
)
}
versionOpt match {
case Some(version) => List(\/-(dep(version)))
case None => versions.map(version => -\/(dep(version)))
}
}
}
}
val l = m.values.toList.flatten
(l.collect{case -\/(dep) => dep}, l.collect{case \/-(dep) => dep})
}
/**
* If one of our dependency has scope `base`, and a transitive dependency of it has scope `transitive`,
* return the scope of the latter for us, if any. If empty, means the transitive dependency
* should not be considered a dependency for us.
*
* See https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Scope.
*/
def resolveScope(base: Scope,
transitive: Scope): Option[Scope] =
(base, transitive) match {
case (Scope.Compile, other) => Some(other)
case (Scope.Runtime, Scope.Compile) => Some(Scope.Runtime)
case (Scope.Runtime, other) => Some(other)
case _ => None
}
/**
* Applies `dependencyManagement` to `dependencies`.
*
* Fill empty version / scope / exclusions, for dependencies found in `dependencyManagement`.
*/
def depsWithDependencyManagement(dependencies: Seq[Dependency],
dependencyManagement: Seq[Dependency]): Seq[Dependency] = {
// See http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Management
lazy val m = dependencyManagementAddSeq(Map.empty, dependencyManagement)
dependencies.map { dep0 =>
var dep = dep0
for (mgmtDep <- m.get(dependencyManagementKey(dep0))) {
if (dep.module.version.isEmpty)
dep = dep.copy(module = dep.module.copy(version = mgmtDep.module.version))
if (dep.scope.name.isEmpty)
dep = dep.copy(scope = mgmtDep.scope)
if (dep.exclusions.isEmpty)
dep = dep.copy(exclusions = mgmtDep.exclusions)
}
dep
}
}
/**
* Addition of exclusions. A module is excluded by the result if it is excluded
* by `first`, by `second`, or by both.
*/
def exclusionsAdd(first: Set[(String, String)],
second: Set[(String, String)]): Set[(String, String)] = {
val (firstAll, firstNonAll) = first.partition{case ("*", "*") => true; case _ => false }
val (secondAll, secondNonAll) = second.partition{case ("*", "*") => true; case _ => false }
if (firstAll.nonEmpty || secondAll.nonEmpty) Set(("*", "*"))
else {
val firstOrgWildcards = firstNonAll.collect{ case ("*", name) => name }
val firstNameWildcards = firstNonAll.collect{ case (org, "*") => org }
val secondOrgWildcards = secondNonAll.collect{ case ("*", name) => name }
val secondNameWildcards = secondNonAll.collect{ case (org, "*") => org }
val orgWildcards = firstOrgWildcards ++ secondOrgWildcards
val nameWildcards = firstNameWildcards ++ secondNameWildcards
val firstRemaining = firstNonAll.filter{ case (org, name) => org != "*" && name != "*" }
val secondRemaining = secondNonAll.filter{ case (org, name) => org != "*" && name != "*" }
val remaining = (firstRemaining ++ secondRemaining).filterNot{case (org, name) => orgWildcards(name) || nameWildcards(org) }
orgWildcards.map(name => ("*", name)) ++ nameWildcards.map(org => (org, "*")) ++ remaining
}
}
/**
* Intersection of exclusions. A module is excluded by the result if it is excluded
* by both `first` and `second`.
*/
def exclusionsIntersect(first: Set[(String, String)],
second: Set[(String, String)]): Set[(String, String)] = {
val (firstAll, firstNonAll) = first.partition{case ("*", "*") => true; case _ => false }
val (secondAll, secondNonAll) = second.partition{case ("*", "*") => true; case _ => false }
if (firstAll.nonEmpty && secondAll.nonEmpty) Set(("*", "*"))
else {
val firstOrgWildcards = firstNonAll.collect{ case ("*", name) => name }
val firstNameWildcards = firstNonAll.collect{ case (org, "*") => org }
val secondOrgWildcards = secondNonAll.collect{ case ("*", name) => name }
val secondNameWildcards = secondNonAll.collect{ case (org, "*") => org }
val orgWildcards =
(firstOrgWildcards intersect secondOrgWildcards) ++
(if (secondAll.nonEmpty) firstOrgWildcards else Set.empty) ++
(if (firstAll.nonEmpty) secondOrgWildcards else Set.empty)
val nameWildcards =
(firstNameWildcards intersect secondNameWildcards) ++
(if (secondAll.nonEmpty) firstNameWildcards else Set.empty) ++
(if (firstAll.nonEmpty) secondNameWildcards else Set.empty)
val firstRemaining = firstNonAll.filter{ case (org, name) => org != "*" && name != "*" }
val secondRemaining = secondNonAll.filter{ case (org, name) => org != "*" && name != "*" }
val remaining =
(firstRemaining intersect secondRemaining) ++
(if (secondAll.nonEmpty) firstRemaining else Set.empty) ++
(if (firstAll.nonEmpty) secondRemaining else Set.empty) ++
(if (secondOrgWildcards.nonEmpty) firstRemaining.filter(e => secondOrgWildcards(e._2)) else Set.empty) ++
(if (firstOrgWildcards.nonEmpty) secondRemaining.filter(e => firstOrgWildcards(e._2)) else Set.empty) ++
(if (secondNameWildcards.nonEmpty) firstRemaining.filter(e => secondNameWildcards(e._1)) else Set.empty) ++
(if (firstNameWildcards.nonEmpty) secondRemaining.filter(e => firstNameWildcards(e._1)) else Set.empty)
orgWildcards.map(name => ("*", name)) ++ nameWildcards.map(org => (org, "*")) ++ remaining
}
}
def withDefaultScope(dep: Dependency): Dependency =
if (dep.scope.name.isEmpty) dep.copy(scope = Scope.Compile)
else dep
/**
* Filters `deps` with `exclusions`.
*/
def withExclusions(dependencies: Seq[Dependency],
exclusions: Set[(String, String)]): Seq[Dependency] = {
val (all, notAll) = exclusions.partition{case ("*", "*") => true; case _ => false}
val orgWildcards = notAll.collect{case ("*", name) => name }
val nameWildcards = notAll.collect{case (org, "*") => org }
val remaining = notAll.filterNot{case (org, name) => org == "*" || name == "*" }
dependencies
.filter(dep =>
all.isEmpty &&
!orgWildcards(dep.module.name) &&
!nameWildcards(dep.module.organization) &&
!remaining((dep.module.organization, dep.module.name))
)
.map(dep =>
dep.copy(exclusions = exclusionsAdd(dep.exclusions, exclusions))
)
}
/**
* Get the dependencies of `project`, knowing that it came from dependency `from` (that is,
* `from.module == project.module`).
*
* Substitute properties, update scopes, apply exclusions, and get extra parameters from
* dependency management along the way.
*/
def finalDependencies(from: Dependency,
project: Project): Seq[Dependency] = {
// Here, we're substituting properties also in dependencies that come from parents
// or dependency management. This may not be the right thing to do.
val deps =
withExclusions(
withProperties(
depsWithDependencyManagement(
project.dependencies,
project.dependencyManagement
),
mergeProperties(
project.properties,
Map(
"project.groupId" -> project.module.organization,
"project.artifactId" -> project.module.name,
"project.version" -> project.module.version
)
)
),
from.exclusions
)
.map(withDefaultScope)
deps.flatMap { trDep =>
resolveScope(from.scope, trDep.scope)
.map(scope => trDep.copy(scope = scope, optional = trDep.optional || from.optional))
}
}
/**
* State of a dependency resolution.
*
* Done if `isDone` is `true`.
*
* @param dependencies: current set of dependencies
* @param conflicts: conflicting dependencies
* @param projectsCache: cache of known projects
* @param errors: keeps track of the modules whose project definition could not be found
*/
case class Resolution(rootDependencies: Set[Dependency],
dependencies: Set[Dependency],
conflicts: Set[Dependency],
projectsCache: Map[Module, (Repository, Project)],
errors: Map[Module, Seq[String]],
filter: Option[Dependency => Boolean],
profileActivation: Option[(String, Activation, Map[String, String]) => Boolean]) {
/**
* Transitive dependencies of the current dependencies, according to what there currently is in cache.
* No attempt is made to solve version conflicts here.
*/
def transitiveDependencies =
for {
dep <- (dependencies -- conflicts).toList
(_, proj) <- projectsCache.get(dep.module).toSeq
trDep <- finalDependencies(dep, proj).filter(filter getOrElse defaultFilter)
} yield trDep
/**
* The "next" dependency set, made of the current dependencies and their transitive dependencies,
* trying to solve version conflicts. Transitive dependencies are calculated with the current cache.
*
* Returns a tuple made of the conflicting dependencies, and all the dependencies.
*/
def nextDependenciesAndConflicts = {
merge(dependencies ++ transitiveDependencies)
}
/**
* The modules we miss some info about.
*/
def missingFromCache: Set[Module] = {
val modules = dependencies.map(_.module)
val nextModules = nextDependenciesAndConflicts._2.map(_.module)
(modules ++ nextModules)
.filterNot(mod => projectsCache.contains(mod) || errors.contains(mod))
}
/**
* Whether the resolution is done.
*/
def isDone: Boolean = {
def isFixPoint = {
val (nextConflicts, nextDependencies) = nextDependenciesAndConflicts
dependencies == (nextDependencies ++ nextConflicts).toSet && conflicts == nextConflicts.toSet
}
missingFromCache.isEmpty && isFixPoint
}
private def key(dep: Dependency) =
(dep.module.organization, dep.module.name, dep.scope)
/**
* Returns a map giving the dependency that brought each of the dependency of the "next" dependency set,
* along with the exclusions that the source dependency adds to it.
*/
def reverseDependenciesAndExclusions = {
val (updatedConflicts, updatedDeps) = nextDependenciesAndConflicts
val trDepsSeq =
for {
dep <- updatedDeps
(_, proj) <- projectsCache.get(dep.module).toList
trDep <- finalDependencies(dep, proj).filter(filter getOrElse defaultFilter)
} yield key(trDep) -> (key(dep), trDep.exclusions)
val knownDeps = (updatedDeps ++ updatedConflicts).map(key).toSet
trDepsSeq
.groupBy(_._1)
.mapValues(_.map(_._2).toVector)
.filterKeys(knownDeps)
.toList.toMap // Eagerly evaluate filterKeys/mapValues
}
/**
* Returns a map, whose keys are the dependencies from the "next" dependency set,
* filtering out those that are no more required, and whose values are the exclusions
* added to them by the dependencies that brought them here.
*/
def remainingDependenciesAndExclusions = {
val rootDependenciesExclusions = rootDependencies
.map(dep => key(dep) -> dep.exclusions)
.toMap
type D = (String, String, Scope)
@tailrec
def helper[T](reverseDeps: Map[D, Vector[(D, T)]]): Map[D, Vector[(D, T)]] = {
val (toRemove, remaining) = reverseDeps.partition(kv => kv._2.isEmpty && !rootDependenciesExclusions.contains(kv._1))
if (toRemove.isEmpty) reverseDeps
else helper(remaining.mapValues(_.filter(x => remaining.contains(x._1) || rootDependenciesExclusions.contains(x._1))).toList.toMap)
}
val filteredReverseDependenciesAndExclusions = helper(reverseDependenciesAndExclusions)
(rootDependenciesExclusions.keySet ++ filteredReverseDependenciesAndExclusions.keySet)
.toList
.map{case dep => dep ->
(filteredReverseDependenciesAndExclusions.get(dep).map(_.map(_._2)).getOrElse(Nil) ++ rootDependenciesExclusions.get(dep))
.reduce(exclusionsIntersect)
}
.toMap
}
/**
* The final next dependency set, stripped of no more required ones.
*/
def newDependencies = {
val remainingDeps0 = remainingDependenciesAndExclusions
nextDependenciesAndConflicts._2
.filter(dep => remainingDeps0.contains(key(dep)))
.map(dep => dep.copy(exclusions = remainingDeps0(key(dep))))
.toSet
}
private def nextNoMissingUnsafe(): Resolution = {
val (newConflicts, _) = nextDependenciesAndConflicts
copy(dependencies = newDependencies ++ newConflicts, conflicts = newConflicts.toSet)
}
/**
* If no module info is missing, the next state of the resolution, which can be immediately calculated.
* Else, the current resolution itself.
*/
def nextIfNoMissing(): Resolution = {
val missing = missingFromCache
if (missing.isEmpty) nextNoMissingUnsafe()
else this
}
/**
* Do a new iteration, fetching the missing modules along the way.
*/
def next(fetchModule: Module => EitherT[Task, List[String], (Repository, Project)]): Task[Resolution] = {
val missing = missingFromCache
if (missing.isEmpty) Task.now(nextNoMissingUnsafe())
else fetch(missing.toList, fetchModule).map(_.nextIfNoMissing())
}
/**
* Required modules for the dependency management of `project`.
*/
def dependencyManagementRequirements(project: Project): Set[Module] = {
val approxProperties =
project.parent
.flatMap(projectsCache.get)
.map(_._2.properties)
.fold(project.properties)(mergeProperties(project.properties, _))
val profileDependencies =
profiles(project, approxProperties, profileActivation getOrElse defaultProfileActivation)
.flatMap(_.dependencies)
val modules =
(project.dependencies ++ profileDependencies)
.collect{ case dep if dep.scope == Scope.Import => dep.module } ++
project.parent
modules.toSet
}
/**
* Missing modules in cache, to get the full list of dependencies of `project`, taking
* dependency management / inheritance into account.
*
* Note that adding the missing modules to the cache may unveil other missing modules, so
* these modules should be added to the cache, and `dependencyManagementMissing` checked again
* for new missing modules.
*/
def dependencyManagementMissing(project: Project): Set[Module] = {
@tailrec
def helper(toCheck: Set[Module],
done: Set[Module],
missing: Set[Module]): Set[Module] = {
if (toCheck.isEmpty) missing
else if (toCheck.exists(done)) helper(toCheck -- done, done, missing)
else if (toCheck.exists(missing)) helper(toCheck -- missing, done, missing)
else if (toCheck.exists(projectsCache.contains)) {
val (checking, remaining) = toCheck.partition(projectsCache.contains)
val directRequirements = checking.flatMap(mod => dependencyManagementRequirements(projectsCache(mod)._2))
helper(remaining ++ directRequirements, done ++ checking, missing)
} else if (toCheck.exists(errors.contains)) {
val (errored, remaining) = toCheck.partition(errors.contains)
helper(remaining, done ++ errored, missing)
} else
helper(Set.empty, done, missing ++ toCheck)
}
helper(dependencyManagementRequirements(project), Set(project.module), Set.empty)
}
/**
* Add dependency management / inheritance related items to `project`, from what's available in cache.
* It is recommended to have fetched what `dependencyManagementMissing` returned prior to calling
* `withDependencyManagement`.
*/
def withDependencyManagement(project: Project): Project = {
val approxProperties =
project.parent
.filter(projectsCache.contains)
.map(projectsCache(_)._2.properties)
.fold(project.properties)(mergeProperties(project.properties, _))
val profiles0 = profiles(project, approxProperties, profileActivation getOrElse defaultProfileActivation)
val dependencies0 = addDependencies(project.dependencies +: profiles0.map(_.dependencies))
val properties0 = (project.properties /: profiles0)((acc, p) => mergeProperties(acc, p.properties))
val deps =
dependencies0
.collect{ case dep if dep.scope == Scope.Import && projectsCache.contains(dep.module) => dep.module } ++
project.parent.filter(projectsCache.contains)
val projs = deps.map(projectsCache(_)._2)
val depMgmt =
(project.dependencyManagement +: (profiles0.map(_.dependencyManagement) ++ projs.map(_.dependencyManagement)))
.foldLeft(Map.empty[DepMgmtKey, Dependency])(dependencyManagementAddSeq)
val depsSet = deps.toSet
project.copy(
dependencies = dependencies0
.filterNot(dep => dep.scope == Scope.Import && depsSet(dep.module)) ++
project.parent
.filter(projectsCache.contains)
.toSeq
.flatMap(projectsCache(_)._2.dependencies),
dependencyManagement = depMgmt.values.toSeq,
properties = project.parent
.filter(projectsCache.contains)
.map(projectsCache(_)._2.properties)
.fold(properties0)(mergeProperties(properties0, _))
)
}
/**
* Fetch `modules` with `fetchModules`, and add the resulting errors and projects to the cache.
*/
def fetch(modules: Seq[Module],
fetchModule: Module => EitherT[Task, List[String], (Repository, Project)]): Task[Resolution] = {
val lookups = modules.map(dep => fetchModule(dep).run.map(dep -> _))
val gatheredLookups = Task.gatherUnordered(lookups, exceptionCancels = true)
gatheredLookups.flatMap{ lookupResults =>
val errors0 = errors ++ lookupResults.collect{case (mod, -\/(repoErrors)) => mod -> repoErrors}
val newProjects = lookupResults.collect{case (mod, \/-(proj)) => mod -> proj}
/*
* newProjects are project definitions, fresh from the repositories. We need to add
* dependency management / inheritance-related bits to them.
*/
newProjects.foldLeft(Task.now(copy(errors = errors0))) { case (accTask, (mod, (repo, proj))) =>
for {
current <- accTask
updated <- current.fetch(current.dependencyManagementMissing(proj).toList, fetchModule)
proj0 = updated.withDependencyManagement(proj)
} yield updated.copy(projectsCache = updated.projectsCache + (proj0.module -> (repo, proj0)))
}
}
}
}
/**
* Default function checking whether a profile is active, given its id, activation conditions,
* and the properties of its project.
*/
def defaultProfileActivation(id: String,
activation: Activation,
props: Map[String, String]): Boolean = {
if (activation.properties.isEmpty) false
else {
activation.properties.forall { case (name, valueOpt) =>
props.get(name).exists{ v =>
valueOpt.forall { reqValue =>
if (reqValue.startsWith("!")) v != reqValue.drop(1)
else v == reqValue
}
}
}
}
}
/**
* Default dependency filter used during resolution.
*
* Only follows compile scope / non-optional dependencies.
*/
def defaultFilter(dep: Dependency): Boolean =
!dep.optional && dep.scope == Scope.Compile
/**
* Get all the transitive dependencies of `dependencies`, solving any dependency version mismatch.
*
* Iteratively fetches the missing info of the current dependencies / add newly discovered dependencies
* to the current ones. The maximum number of such iterations can be bounded with `maxIterations`.
*
* ...
*
*/
def resolve(dependencies: Set[Dependency],
fetch: Module => EitherT[Task, List[String], (Repository, Project)],
maxIterations: Option[Int],
filter: Option[Dependency => Boolean],
profileActivation: Option[(String, Activation, Map[String, String]) => Boolean]): Task[Resolution] = {
val dependencies0 = dependencies.map(withDefaultScope)
val startResolution = Resolution(
dependencies0, dependencies0, Set.empty,
Map.empty, Map.empty,
filter,
profileActivation
)
def helper(resolution: Resolution, remainingIter: Option[Int]): Task[(Resolution, Option[Int])] = {
if (resolution.isDone || remainingIter.exists(_ <= 0))
Task.now((resolution, remainingIter))
else
resolution.next(fetch).flatMap(helper(_, remainingIter.map(_ - 1)))
}
helper(startResolution, maxIterations).map(_._1)
}
}

View File

@ -0,0 +1,104 @@
package coursier.core
import coursier.core.compatibility._
case class Versions(latest: String,
release: String,
available: List[String],
lastUpdated: Option[DateTime])
/** Used internally by Resolver */
case class Version(repr: String) extends Ordered[Version] {
lazy val cmp = ComparableVersion.parse(repr)
def compare(other: Version): Int = {
cmp.compare(other.cmp)
}
}
case class VersionInterval(from: Option[Version],
to: Option[Version],
fromIncluded: Boolean,
toIncluded: Boolean) {
def isValid: Boolean = {
val fromToOrder =
for {
f <- from
t <- to
cmd = f.compare(t)
} yield cmd < 0 || (cmd == 0 && fromIncluded && toIncluded)
fromToOrder.forall(x => x) && (from.nonEmpty || !fromIncluded) && (to.nonEmpty || !toIncluded)
}
def merge(other: VersionInterval): Option[VersionInterval] = {
val (newFrom, newFromIncluded) =
(from, other.from) match {
case (Some(a), Some(b)) =>
val cmp = a.compare(b)
if (cmp < 0) (Some(b), other.fromIncluded)
else if (cmp > 0) (Some(a), fromIncluded)
else (Some(a), fromIncluded && other.fromIncluded)
case (Some(a), None) => (Some(a), fromIncluded)
case (None, Some(b)) => (Some(b), other.fromIncluded)
case (None, None) => (None, false)
}
val (newTo, newToIncluded) =
(to, other.to) match {
case (Some(a), Some(b)) =>
val cmp = a.compare(b)
if (cmp < 0) (Some(a), toIncluded)
else if (cmp > 0) (Some(b), other.toIncluded)
else (Some(a), toIncluded && other.toIncluded)
case (Some(a), None) => (Some(a), toIncluded)
case (None, Some(b)) => (Some(b), other.toIncluded)
case (None, None) => (None, false)
}
Some(VersionInterval(newFrom, newTo, newFromIncluded, newToIncluded))
.filter(_.isValid)
}
def constraint: VersionConstraint =
this match {
case VersionInterval.zero => VersionConstraint.None
case VersionInterval(Some(version), None, true, false) => VersionConstraint.Preferred(version)
case itv => VersionConstraint.Interval(itv)
}
def repr: String = Seq(
if (fromIncluded) "[" else "(",
from.map(_.repr).mkString,
",",
to.map(_.repr).mkString,
if (toIncluded) "]" else ")"
).mkString
}
object VersionInterval {
val zero = VersionInterval(None, None, fromIncluded = false, toIncluded = false)
}
sealed trait VersionConstraint {
def interval: VersionInterval
def repr: String
}
object VersionConstraint {
/** Currently treated as minimum... */
case class Preferred(version: Version) extends VersionConstraint {
def interval: VersionInterval = VersionInterval(Some(version), Option.empty, fromIncluded = true, toIncluded = false)
def repr: String = version.repr
}
case class Interval(interval: VersionInterval) extends VersionConstraint {
def repr: String = interval.repr
}
case object None extends VersionConstraint {
val interval = VersionInterval.zero
def repr: String = "" // Once parsed, "(,)" becomes "" because of this
}
}

View File

@ -0,0 +1,247 @@
package coursier.core
import coursier.core.compatibility.DateTime
import scalaz._
object Xml {
/** A representation of an XML node/document, with different implementations on the JVM and JS */
trait Node {
def label: String
def child: Seq[Node]
def isText: Boolean
def textContent: String
def isElement: Boolean
}
object Node {
val empty: Node =
new Node {
val isText = false
val isElement = false
val child = Nil
val label = ""
val textContent = ""
}
}
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")
}
private def property(elem: Node): String \/ (String, String) = {
elem.child match {
case Seq() => \/-(elem.label -> "")
case Seq(Text(t)) => \/-(elem.label -> t)
case _ => -\/(s"Can't parse property $elem")
}
}
// TODO Allow no version in some contexts
private def module(node: Node, groupIdIsOptional: Boolean = false): String \/ Module = {
for {
organization <- {
val e = text(node, "groupId", "Organization")
if (groupIdIsOptional) e.orElse(\/-(""))
else e
}
name <- text(node, "artifactId", "Name")
version = text(node, "version", "Version").getOrElse("")
} yield Module(organization, name, version).trim
}
private val defaultScope = Scope.Other("")
private val defaultType = "jar"
private val defaultClassifier = ""
def dependency(node: Node): String \/ Dependency = {
for {
mod <- module(node)
scopeOpt = text(node, "scope", "").toOption
.map(Parse.scope)
typeOpt = text(node, "type", "").toOption
classifierOpt = text(node, "classifier", "").toOption
xmlExclusions = node.child
.find(_.label == "exclusions")
.map(_.child.filter(_.label == "exclusion"))
.getOrElse(Seq.empty)
exclusions <- {
import Scalaz._
xmlExclusions.toList.traverseU(module(_))
}
optional = text(node, "optional", "").toOption.toSeq.contains("true")
} yield Dependency(
mod,
scopeOpt getOrElse defaultScope,
typeOpt getOrElse defaultType,
classifierOpt getOrElse defaultClassifier,
exclusions.map(mod => (mod.organization, mod.name)).toSet,
optional
)
}
private def profileActivation(node: Node): (Option[Boolean], Activation) = {
val byDefault =
text(node, "activeByDefault", "").toOption.flatMap{
case "true" => Some(true)
case "false" => Some(false)
case _ => None
}
val properties = node.child
.filter(_.label == "property")
.flatMap{ p =>
for{
name <- text(p, "name", "").toOption
valueOpt = text(p, "value", "").toOption
} yield (name, valueOpt)
}
(byDefault, Activation(properties))
}
def profile(node: Node): String \/ Profile = {
import Scalaz._
for {
id <- text(node, "id", "Profile ID")
xmlActivationOpt = node.child
.find(_.label == "activation")
(activeByDefault, activation) = xmlActivationOpt.fold((Option.empty[Boolean], Activation(Nil)))(profileActivation)
xmlDeps = node.child
.find(_.label == "dependencies")
.map(_.child.filter(_.label == "dependency"))
.getOrElse(Seq.empty)
deps <- xmlDeps.toList.traverseU(dependency)
xmlDepMgmts = node.child
.find(_.label == "dependencyManagement")
.flatMap(_.child.find(_.label == "dependencies"))
.map(_.child.filter(_.label == "dependency"))
.getOrElse(Seq.empty)
depMgmts <- xmlDepMgmts.toList.traverseU(dependency)
xmlProperties = node.child
.find(_.label == "properties")
.map(_.child.collect{case elem if elem.isElement => elem})
.getOrElse(Seq.empty)
properties <- {
import Scalaz._
xmlProperties.toList.traverseU(property)
}
} yield Profile(id, activeByDefault, activation, deps, depMgmts, properties.toMap)
}
def project(pom: Node): String \/ Project = {
import Scalaz._
for {
projModule <- module(pom, groupIdIsOptional = true)
parentOpt = pom.child
.find(_.label == "parent")
parentModuleOpt <- parentOpt
.map(module(_).map(Some(_)))
.getOrElse(\/-(None))
xmlDeps = pom.child
.find(_.label == "dependencies")
.map(_.child.filter(_.label == "dependency"))
.getOrElse(Seq.empty)
deps <- xmlDeps.toList.traverseU(dependency)
xmlDepMgmts = pom.child
.find(_.label == "dependencyManagement")
.flatMap(_.child.find(_.label == "dependencies"))
.map(_.child.filter(_.label == "dependency"))
.getOrElse(Seq.empty)
depMgmts <- xmlDepMgmts.toList.traverseU(dependency)
groupId <- Some(projModule.organization).filter(_.nonEmpty)
.orElse(parentModuleOpt.map(_.organization).filter(_.nonEmpty))
.toRightDisjunction("No organization found")
version <- Some(projModule.version).filter(_.nonEmpty)
.orElse(parentModuleOpt.map(_.version).filter(_.nonEmpty))
.toRightDisjunction("No version found")
_ <- parentModuleOpt
.map(mod => if (mod.version.isEmpty) -\/("Parent version missing") else \/-(()))
.getOrElse(\/-(()))
_ <- parentModuleOpt
.map(mod => if (mod.organization.isEmpty) -\/("Parent organization missing") else \/-(()))
.getOrElse(\/-(()))
xmlProperties = pom.child
.find(_.label == "properties")
.map(_.child.collect{case elem if elem.isElement => elem})
.getOrElse(Seq.empty)
properties <- xmlProperties.toList.traverseU(property)
xmlProfiles = pom.child
.find(_.label == "profiles")
.map(_.child.filter(_.label == "profile"))
.getOrElse(Seq.empty)
profiles <- xmlProfiles.toList.traverseU(profile)
} yield Project(
projModule.copy(organization = groupId, version = version),
deps,
parentModuleOpt,
depMgmts,
properties.toMap,
profiles
)
}
def versions(node: Node): String \/ Versions = {
import Scalaz._
for {
organization <- text(node, "groupId", "Organization") // Ignored
name <- text(node, "artifactId", "Name") // Ignored
xmlVersioning <- node.child
.find(_.label == "versioning")
.toRightDisjunction("Versioning info not found in metadata")
latest = text(xmlVersioning, "latest", "Latest version")
.getOrElse("")
release = text(xmlVersioning, "release", "Release version")
.getOrElse("")
versions <- xmlVersioning.child
.find(_.label == "versions")
.map(_.child.filter(_.label == "version").flatMap(_.child.collectFirst{case Text(t) => t}))
.toRightDisjunction("Version list not found in metadata")
lastUpdatedOpt = text(xmlVersioning, "lastUpdated", "Last update date and time")
.toOption
.filter(s => s.length == 14 && s.forall(_.isDigit))
.map(s => DateTime(
s.substring(0, 4).toInt,
s.substring(4, 6).toInt,
s.substring(6, 8).toInt,
s.substring(8, 10).toInt,
s.substring(10, 12).toInt,
s.substring(12, 14).toInt
))
} yield Versions(latest, release, versions.toList, lastUpdatedOpt)
}
}

View File

@ -0,0 +1,79 @@
import scalaz.EitherT
import scalaz.concurrent.Task
package object coursier {
type Dependency = core.Dependency
object Dependency {
def apply(module: Module,
scope: Scope = Scope.Other(""), // Subsituted by Resolver with its own default scope (compile)
`type`: String = "jar",
classifier: String = "",
exclusions: Set[(String, String)] = Set.empty,
optional: Boolean = false): Dependency =
core.Dependency(module, scope, `type`, classifier, exclusions, optional)
}
type Project = core.Project
object Project {
def apply(module: Module,
dependencies: Seq[Dependency] = Seq.empty,
parent: Option[Module] = None,
dependencyManagement: Seq[Dependency] = Seq.empty,
properties: Map[String, String] = Map.empty,
profiles: Seq[Profile] = Seq.empty): Project =
core.Project(module, dependencies, parent, dependencyManagement, properties, profiles)
}
type Profile = core.Profile
object Profile {
type Activation = core.Activation
object Activation {
def apply(properties: Seq[(String, Option[String])] = Nil): Activation =
core.Activation(properties)
}
def apply(id: String,
activeByDefault: Option[Boolean] = None,
activation: Activation = Activation(),
dependencies: Seq[Dependency] = Nil,
dependencyManagement: Seq[Dependency] = Nil,
properties: Map[String, String] = Map.empty) =
core.Profile(id, activeByDefault, activation, dependencies, dependencyManagement, properties)
}
type Module = core.Module
object Module {
def apply(organization: String, name: String, version: String): Module =
core.Module(organization, name, version)
}
type Scope = core.Scope
val Scope: core.Scope.type = core.Scope
type Repository = core.Repository
def fetchFrom(repositories: Seq[Repository]): Module => EitherT[Task, List[String], (Repository, Project)] =
core.Resolver.find(repositories, _)
type Resolution = core.Resolver.Resolution
object Resolution {
val empty = apply()
def apply(rootDependencies: Set[Dependency] = Set.empty,
dependencies: Set[Dependency] = Set.empty,
conflicts: Set[Dependency] = Set.empty,
projectsCache: Map[Module, (Repository, Project)] = Map.empty,
errors: Map[Module, Seq[String]] = Map.empty,
filter: Option[Dependency => Boolean] = None,
profileActivation: Option[(String, Profile.Activation, Map[String, String]) => Boolean] = None): Resolution =
core.Resolver.Resolution(rootDependencies, dependencies, conflicts, projectsCache, errors, filter, profileActivation)
}
def resolve(dependencies: Set[Dependency],
fetch: Module => EitherT[Task, List[String], (Repository, Project)],
maxIterations: Option[Int] = Some(200),
filter: Option[Dependency => Boolean] = None,
profileActivation: Option[(String, Profile.Activation, Map[String, String]) => Boolean] = None): Task[Resolution] = {
core.Resolver.resolve(dependencies, fetch, maxIterations, filter, profileActivation)
}
}

View File

@ -0,0 +1,42 @@
package coursier
package test
import utest._
import scala.async.Async.{async, await}
import coursier.test.compatibility._
object CentralTests extends TestSuite {
val repositories = Seq[Repository](
repository.mavenCentral
)
val tests = TestSuite {
'logback{
async {
val dep = Dependency(Module("ch.qos.logback", "logback-classic", "1.1.3"))
val res0 =
await(resolve(Set(dep), fetchFrom(repositories))
.runF)
val res = res0.copy(
projectsCache = Map.empty, errors = Map.empty, // No validating these here
dependencies = res0.dependencies.filter(dep => dep.scope == Scope.Compile && !dep.optional)
)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(
dep.withCompileScope,
Dependency(Module("ch.qos.logback", "logback-core", "1.1.3")).withCompileScope,
Dependency(Module("org.slf4j", "slf4j-api", "1.7.7")).withCompileScope
)
)
assert(res == expected)
}
}
}
}

View File

@ -0,0 +1,343 @@
package coursier
package test
import java.util.Locale
import utest._
object ComparableVersionTests extends TestSuite {
import core.ComparableVersion.parse
def compare(first: String, second: String) = parse(first).compare(parse(second))
def increasing(versions: String*): Boolean =
versions.iterator.sliding(2).withPartial(false).forall{case Seq(a, b) => compare(a, b) < 0 }
val tests = TestSuite {
'stackOverflow{
val s = "." * 100000
val v = parse(s)
assert(v.isEmpty)
}
'empty{
val v0 = parse("0")
val v = parse("")
assert(v0.isEmpty)
assert(v.isEmpty)
}
'numericOrdering{
assert(compare("1.2", "1.10") < 0)
}
// Adapted from aether-core/aether-util/src/test/java/org/eclipse/aether/util/version/GenericVersionTest.java
// Only one test doesn't pass (see FIXME below)
'EmptyVersion{
assert(compare("0", "" ) == 0)
}
'NumericOrdering
{
assert(compare("2", "10" ) < 0)
assert(compare("1.2", "1.10" ) < 0)
assert(compare("1.0.2", "1.0.10" ) < 0)
assert(compare("1.0.0.2", "1.0.0.10" ) < 0)
assert(compare("1.0.20101206.111434.1", "1.0.20101206.111435.1" ) < 0)
assert(compare("1.0.20101206.111434.2", "1.0.20101206.111434.10" ) < 0)
}
'Delimiters
{
assert(compare("1.0", "1-0" ) == 0)
assert(compare("1.0", "1_0" ) == 0)
assert(compare("1.a", "1a" ) == 0)
}
'LeadingZerosAreSemanticallyIrrelevant
{
assert(compare("1", "01" ) == 0)
assert(compare("1.2", "1.002" ) == 0)
assert(compare("1.2.3", "1.2.0003" ) == 0)
assert(compare("1.2.3.4", "1.2.3.00004" ) == 0)
}
'TrailingZerosAreSemanticallyIrrelevant
{
assert(compare("1", "1.0.0.0.0.0.0.0.0.0.0.0.0.0" ) == 0)
assert(compare("1", "1-0-0-0-0-0-0-0-0-0-0-0-0-0" ) == 0)
assert(compare("1", "1.0-0.0-0.0-0.0-0.0-0.0-0.0" ) == 0)
assert(compare("1", "1.0000000000000" ) == 0)
assert(compare("1.0", "1.0.0" ) == 0)
}
'TrailingZerosBeforeQualifierAreSemanticallyIrrelevant
{
assert(compare("1.0-ga", "1.0.0-ga" ) == 0)
assert(compare("1.0.ga", "1.0.0.ga" ) == 0)
assert(compare("1.0ga", "1.0.0ga" ) == 0)
assert(compare("1.0-alpha", "1.0.0-alpha" ) == 0)
assert(compare("1.0.alpha", "1.0.0.alpha" ) == 0)
assert(compare("1.0alpha", "1.0.0alpha" ) == 0)
assert(compare("1.0-alpha-snapshot", "1.0.0-alpha-snapshot" ) == 0)
assert(compare("1.0.alpha.snapshot", "1.0.0.alpha.snapshot" ) == 0)
assert(compare("1.x.0-alpha", "1.x.0.0-alpha" ) == 0)
assert(compare("1.x.0.alpha", "1.x.0.0.alpha" ) == 0)
assert(compare("1.x.0-alpha-snapshot", "1.x.0.0-alpha-snapshot" ) == 0)
assert(compare("1.x.0.alpha.snapshot", "1.x.0.0.alpha.snapshot" ) == 0)
}
'TrailingDelimitersAreSemanticallyIrrelevant
{
assert(compare("1", "1............." ) == 0)
assert(compare("1", "1-------------" ) == 0)
assert(compare("1.0", "1............." ) == 0)
assert(compare("1.0", "1-------------" ) == 0)
}
'InitialDelimiters
{
assert(compare("0.1", ".1" ) == 0)
assert(compare("0.0.1", "..1" ) == 0)
assert(compare("0.1", "-1" ) == 0)
assert(compare("0.0.1", "--1" ) == 0)
}
'ConsecutiveDelimiters
{
assert(compare("1.0.1", "1..1" ) == 0)
assert(compare("1.0.0.1", "1...1" ) == 0)
assert(compare("1.0.1", "1--1" ) == 0)
assert(compare("1.0.0.1", "1---1" ) == 0)
}
'UnlimitedNumberOfVersionComponents
{
assert(compare("1.0.1.2.3.4.5.6.7.8.9.0.1.2.10", "1.0.1.2.3.4.5.6.7.8.9.0.1.2.3" ) > 0)
}
'UnlimitedNumberOfDigitsInNumericComponent
{
assert(compare("1.1234567890123456789012345678901", "1.123456789012345678901234567891" ) > 0)
}
'TransitionFromDigitToLetterAndViceVersaIsEqualivantToDelimiter
{
assert(compare("1alpha10", "1.alpha.10" ) == 0)
assert(compare("1alpha10", "1-alpha-10" ) == 0)
assert(compare("1.alpha10", "1.alpha2" ) > 0)
assert(compare("10alpha", "1alpha" ) > 0)
}
'WellKnownQualifierOrdering
{
assert(compare("1-alpha1", "1-a1" ) == 0)
assert(compare("1-alpha", "1-beta" ) < 0)
assert(compare("1-beta1", "1-b1" ) == 0)
assert(compare("1-beta", "1-milestone" ) < 0)
assert(compare("1-milestone1", "1-m1" ) == 0)
assert(compare("1-milestone", "1-rc" ) < 0)
assert(compare("1-rc", "1-cr" ) == 0)
assert(compare("1-rc", "1-snapshot" ) < 0)
assert(compare("1-snapshot", "1" ) < 0)
assert(compare("1", "1-ga" ) == 0)
assert(compare("1", "1.ga.0.ga" ) == 0)
assert(compare("1.0", "1-ga" ) == 0)
assert(compare("1", "1-ga.ga" ) == 0)
assert(compare("1", "1-ga-ga" ) == 0)
assert(compare("A", "A.ga.ga" ) == 0)
assert(compare("A", "A-ga-ga" ) == 0)
assert(compare("1", "1-final" ) == 0)
assert(compare("1", "1-sp" ) < 0)
assert(compare("A.rc.1", "A.ga.1" ) < 0)
assert(compare("A.sp.1", "A.ga.1" ) > 0)
assert(compare("A.rc.x", "A.ga.x" ) < 0)
assert(compare("A.sp.x", "A.ga.x" ) > 0)
}
'WellKnownQualifierVersusUnknownQualifierOrdering
{
assert(compare("1-abc", "1-alpha" ) > 0)
assert(compare("1-abc", "1-beta" ) > 0)
assert(compare("1-abc", "1-milestone" ) > 0)
assert(compare("1-abc", "1-rc" ) > 0)
assert(compare("1-abc", "1-snapshot" ) > 0)
assert(compare("1-abc", "1" ) > 0)
assert(compare("1-abc", "1-sp" ) > 0)
}
'WellKnownSingleCharQualifiersOnlyRecognizedIfImmediatelyFollowedByNumber
{
assert(compare("1.0a", "1.0" ) > 0)
assert(compare("1.0-a", "1.0" ) > 0)
assert(compare("1.0.a", "1.0" ) > 0)
assert(compare("1.0b", "1.0" ) > 0)
assert(compare("1.0-b", "1.0" ) > 0)
assert(compare("1.0.b", "1.0" ) > 0)
assert(compare("1.0m", "1.0" ) > 0)
assert(compare("1.0-m", "1.0" ) > 0)
assert(compare("1.0.m", "1.0" ) > 0)
assert(compare("1.0a1", "1.0" ) < 0)
assert(compare("1.0-a1", "1.0" ) < 0)
assert(compare("1.0.a1", "1.0" ) < 0)
assert(compare("1.0b1", "1.0" ) < 0)
assert(compare("1.0-b1", "1.0" ) < 0)
assert(compare("1.0.b1", "1.0" ) < 0)
assert(compare("1.0m1", "1.0" ) < 0)
assert(compare("1.0-m1", "1.0" ) < 0)
assert(compare("1.0.m1", "1.0" ) < 0)
assert(compare("1.0a.1", "1.0" ) > 0)
assert(compare("1.0a-1", "1.0" ) > 0)
assert(compare("1.0b.1", "1.0" ) > 0)
assert(compare("1.0b-1", "1.0" ) > 0)
assert(compare("1.0m.1", "1.0" ) > 0)
assert(compare("1.0m-1", "1.0" ) > 0)
}
'UnknownQualifierOrdering
{
assert(compare("1-abc", "1-abcd" ) < 0)
assert(compare("1-abc", "1-bcd" ) < 0)
assert(compare("1-abc", "1-aac" ) > 0)
}
'CaseInsensitiveOrderingOfQualifiers
{
assert(compare("1.alpha", "1.ALPHA" ) == 0)
assert(compare("1.alpha", "1.Alpha" ) == 0)
assert(compare("1.beta", "1.BETA" ) == 0)
assert(compare("1.beta", "1.Beta" ) == 0)
assert(compare("1.milestone", "1.MILESTONE" ) == 0)
assert(compare("1.milestone", "1.Milestone" ) == 0)
assert(compare("1.rc", "1.RC" ) == 0)
assert(compare("1.rc", "1.Rc" ) == 0)
assert(compare("1.cr", "1.CR" ) == 0)
assert(compare("1.cr", "1.Cr" ) == 0)
assert(compare("1.snapshot", "1.SNAPSHOT" ) == 0)
assert(compare("1.snapshot", "1.Snapshot" ) == 0)
assert(compare("1.ga", "1.GA" ) == 0)
assert(compare("1.ga", "1.Ga" ) == 0)
assert(compare("1.final", "1.FINAL" ) == 0)
assert(compare("1.final", "1.Final" ) == 0)
assert(compare("1.sp", "1.SP" ) == 0)
assert(compare("1.sp", "1.Sp" ) == 0)
assert(compare("1.unknown", "1.UNKNOWN" ) == 0)
assert(compare("1.unknown", "1.Unknown" ) == 0)
}
'QualifierVersusNumberOrdering
{
assert(compare("1-ga", "1-1" ) < 0)
assert(compare("1.ga", "1.1" ) < 0)
assert(compare("1-ga", "1.0" ) == 0)
assert(compare("1.ga", "1.0" ) == 0)
assert(compare("1-ga-1", "1-0-1" ) < 0)
assert(compare("1.ga.1", "1.0.1" ) < 0)
assert(compare("1.sp", "1.0" ) > 0)
assert(compare("1.sp", "1.1" ) < 0)
assert(compare("1-abc", "1-1" ) < 0)
assert(compare("1.abc", "1.1" ) < 0)
assert(compare("1-xyz", "1-1" ) < 0)
assert(compare("1.xyz", "1.1" ) < 0)
}
'MinimumSegment
{
assert(compare("1.min", "1.0-alpha-1" ) < 0)
assert(compare("1.min", "1.0-SNAPSHOT" ) < 0)
assert(compare("1.min", "1.0" ) < 0)
assert(compare("1.min", "1.9999999999" ) < 0)
assert(compare("1.min", "1.MIN" ) == 0)
assert(compare("1.min", "0.99999" ) > 0)
assert(compare("1.min", "0.max" ) > 0)
}
'MaximumSegment
{
assert(compare("1.max", "1.0-alpha-1" ) > 0)
assert(compare("1.max", "1.0-SNAPSHOT" ) > 0)
assert(compare("1.max", "1.0" ) > 0)
assert(compare("1.max", "1.9999999999" ) > 0)
assert(compare("1.max", "1.MAX" ) == 0)
assert(compare("1.max", "2.0-alpha-1" ) < 0)
assert(compare("1.max", "2.min" ) < 0)
}
'VersionEvolution
{
assert(increasing( "0.9.9-SNAPSHOT", "0.9.9", "0.9.10-SNAPSHOT", "0.9.10", "1.0-alpha-2-SNAPSHOT", "1.0-alpha-2",
"1.0-alpha-10-SNAPSHOT", "1.0-alpha-10", "1.0-beta-1-SNAPSHOT", "1.0-beta-1",
"1.0-rc-1-SNAPSHOT", "1.0-rc-1", "1.0-SNAPSHOT", "1.0", "1.0-sp-1-SNAPSHOT", "1.0-sp-1"))
// FIXME Should pass?
// assert(compare("1.0-sp-1", "1.0.1-alpha-1-SNAPSHOT") < 0)
assert(increasing("1.0.1-alpha-1-SNAPSHOT",
"1.0.1-alpha-1", "1.0.1-beta-1-SNAPSHOT", "1.0.1-beta-1",
"1.0.1-rc-1-SNAPSHOT", "1.0.1-rc-1", "1.0.1-SNAPSHOT", "1.0.1", "1.1-SNAPSHOT", "1.1" ))
assert(increasing( "1.0-alpha", "1.0", "1.0-1" ))
assert(increasing( "1.0.alpha", "1.0", "1.0-1" ))
assert(increasing( "1.0-alpha", "1.0", "1.0.1" ))
assert(increasing( "1.0.alpha", "1.0", "1.0.1" ))
}
// 'CaseInsensitiveOrderingOfQualifiersIsLocaleIndependent
// {
// val orig = Locale.getDefault
// try {
// for ( locale <- Seq(Locale.ENGLISH, new Locale( "tr" )) ) {
// Locale.setDefault( locale )
// assert(compare("1-abcdefghijklmnopqrstuvwxyz", "1-ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) == 0)
// }
// }
// finally Locale.setDefault( orig )
// }
}
}

View File

@ -0,0 +1,124 @@
package coursier
package test
import utest._
import core.Resolver.{ exclusionsAdd, exclusionsIntersect }
object ExclusionsTests extends TestSuite {
val tests = TestSuite {
val e1 = Set(("org1", "name1"))
val e2 = Set(("org2", "name2"))
val enb = Set(("org1", "*"))
val eob = Set(("*", "name1"))
val eb = Set(("*", "*"))
'add{
'basicZero{
val result1l = exclusionsAdd(e1, Set.empty)
val result1r = exclusionsAdd(Set.empty, e1)
val result2l = exclusionsAdd(e2, Set.empty)
val result2r = exclusionsAdd(Set.empty, e2)
assert(result1l == e1)
assert(result1r == e1)
assert(result2l == e2)
assert(result2r == e2)
}
'basic{
val expected = e1 ++ e2
val result12 = exclusionsAdd(e1, e2)
val result21 = exclusionsAdd(e2, e1)
assert(result12 == expected)
assert(result21 == expected)
}
'nameBlob{
val result1b = exclusionsAdd(e1, enb)
val resultb1 = exclusionsAdd(enb, e1)
val result2b = exclusionsAdd(e2, enb)
val resultb2 = exclusionsAdd(enb, e2)
assert(result1b == enb)
assert(resultb1 == enb)
assert(result2b == (e2 ++ enb))
assert(resultb2 == (e2 ++ enb))
}
'orgBlob{
val result1b = exclusionsAdd(e1, eob)
val resultb1 = exclusionsAdd(eob, e1)
val result2b = exclusionsAdd(e2, eob)
val resultb2 = exclusionsAdd(eob, e2)
assert(result1b == eob)
assert(resultb1 == eob)
assert(result2b == (e2 ++ eob))
assert(resultb2 == (e2 ++ eob))
}
'blob{
val result1b = exclusionsAdd(e1, eb)
val resultb1 = exclusionsAdd(eb, e1)
val result2b = exclusionsAdd(e2, eb)
val resultb2 = exclusionsAdd(eb, e2)
assert(result1b == eb)
assert(resultb1 == eb)
assert(result2b == eb)
assert(resultb2 == eb)
}
}
'intersect{
'basicZero{
val result1l = exclusionsIntersect(e1, Set.empty)
val result1r = exclusionsIntersect(Set.empty, e1)
val result2l = exclusionsIntersect(e2, Set.empty)
val result2r = exclusionsIntersect(Set.empty, e2)
assert(result1l == Set.empty)
assert(result1r == Set.empty)
assert(result2l == Set.empty)
assert(result2r == Set.empty)
}
'basic{
val expected = e1 ++ e2
val result12 = exclusionsIntersect(e1, e2)
val result21 = exclusionsIntersect(e2, e1)
assert(result12 == Set.empty)
assert(result21 == Set.empty)
}
'nameBlob{
val result1b = exclusionsIntersect(e1, enb)
val resultb1 = exclusionsIntersect(enb, e1)
val result2b = exclusionsIntersect(e2, enb)
val resultb2 = exclusionsIntersect(enb, e2)
assert(result1b == e1)
assert(resultb1 == e1)
assert(result2b == Set.empty)
assert(resultb2 == Set.empty)
}
'orgBlob{
val result1b = exclusionsIntersect(e1, eob)
val resultb1 = exclusionsIntersect(eob, e1)
val result2b = exclusionsIntersect(e2, eob)
val resultb2 = exclusionsIntersect(eob, e2)
assert(result1b == e1)
assert(resultb1 == e1)
assert(result2b == Set.empty)
assert(resultb2 == Set.empty)
}
'blob{
val result1b = exclusionsIntersect(e1, eb)
val resultb1 = exclusionsIntersect(eb, e1)
val result2b = exclusionsIntersect(e2, eb)
val resultb2 = exclusionsIntersect(eb, e2)
assert(result1b == e1)
assert(resultb1 == e1)
assert(result2b == e2)
assert(resultb2 == e2)
}
}
}
}

View File

@ -0,0 +1,143 @@
package coursier
package test
import utest._
import scalaz._
import coursier.core.Xml
import coursier.Profile.Activation
import coursier.core.compatibility._
object PomParsingTests extends TestSuite {
val tests = TestSuite {
'readClassifier{
val depNode ="""
<dependency>
<groupId>comp</groupId>
<artifactId>lib</artifactId>
<version>2.1</version>
<classifier>extra</classifier>
</dependency>
"""
val expected = \/-(Dependency(Module("comp", "lib", "2.1"), classifier = "extra"))
val result = Xml.dependency(xmlParse(depNode).right.get)
assert(result == expected)
}
'readProfileWithNoActivation{
val profileNode ="""
<profile>
<id>profile1</id>
</profile>
"""
val expected = \/-(Profile("profile1", None, Activation(Nil), Nil, Nil, Map.empty))
val result = Xml.profile(xmlParse(profileNode).right.get)
assert(result == expected)
}
'readProfileActivatedByDefault{
val profileNode ="""
<profile>
<id>profile1</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
"""
val expected = \/-(Profile("profile1", Some(true), Activation(Nil), Nil, Nil, Map.empty))
val result = Xml.profile(xmlParse(profileNode).right.get)
assert(result == expected)
}
'readProfileDependencies{
val profileNode ="""
<profile>
<id>profile1</id>
<dependencies>
<dependency>
<groupId>comp</groupId>
<artifactId>lib</artifactId>
<version>0.2</version>
</dependency>
</dependencies>
</profile>
"""
val expected = \/-(Profile(
"profile1",
None,
Activation(Nil),
Seq(
Dependency(Module("comp", "lib", "0.2"))),
Nil,
Map.empty
))
val result = Xml.profile(xmlParse(profileNode).right.get)
assert(result == expected)
}
'readProfileDependenciesMgmt{
val profileNode ="""
<profile>
<id>profile1</id>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>comp</groupId>
<artifactId>lib</artifactId>
<version>0.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
"""
val expected = \/-(Profile(
"profile1",
None,
Activation(Nil),
Nil,
Seq(
Dependency(Module("comp", "lib", "0.2"), scope = Scope.Test)),
Map.empty
))
val result = Xml.profile(xmlParse(profileNode).right.get)
assert(result == expected)
}
'readProfileProperties{
val profileNode ="""
<profile>
<id>profile1</id>
<properties>
<first.prop>value1</first.prop>
</properties>
</profile>
"""
val expected = \/-(Profile(
"profile1",
None,
Activation(Nil),
Nil,
Nil,
Map("first.prop" -> "value1")
))
val result = Xml.profile(xmlParse(profileNode).right.get)
assert(result == expected)
}
}
}

View File

@ -0,0 +1,464 @@
package coursier
package test
import coursier.core.Resolver
import utest._
import scala.async.Async.{async, await}
import coursier.test.compatibility._
object ResolverTests extends TestSuite {
implicit class ProjectOps(val p: Project) extends AnyVal {
def kv: (Module, (Repository, Project)) = p.module -> (testRepository, p)
}
val projects = Seq(
Project(Module("acme", "config", "1.3.0")),
Project(Module("acme", "play", "2.4.0"), Seq(
Dependency(Module("acme", "play-json", "2.4.0")))),
Project(Module("acme", "play-json", "2.4.0")),
Project(Module("acme", "play", "2.4.1"),
Seq(
Dependency(Module("acme", "play-json", "${playJsonVersion}")),
Dependency(Module("${project.groupId}", "${configName}", "1.3.0"))),
properties = Map(
"playJsonVersion" -> "2.4.0",
"configName" -> "config")),
Project(Module("acme", "play-extra-no-config", "2.4.1"),
Seq(
Dependency(Module("acme", "play", "2.4.1"),
exclusions = Set(("acme", "config"))))),
Project(Module("acme", "play-extra-no-config-no", "2.4.1"),
Seq(
Dependency(Module("acme", "play", "2.4.1"),
exclusions = Set(("*", "config"))))),
Project(Module("hudsucker", "mail", "10.0"),
Seq(
Dependency(Module("${project.groupId}", "test-util", "${project.version}"),
scope = Scope.Test))),
Project(Module("hudsucker", "test-util", "10.0")),
Project(Module("se.ikea", "parent", "18.0"),
dependencyManagement = Seq(
Dependency(Module("acme", "play", "2.4.0"),
exclusions = Set(("acme", "play-json"))))),
Project(Module("se.ikea", "billy", "18.0"),
Seq(
Dependency(Module("acme", "play", ""))
),
parent = Some(Module("se.ikea", "parent", "18.0"))),
Project(Module("org.gnome", "parent", "7.0"),
Seq(
Dependency(Module("org.gnu", "glib", "13.4")))),
Project(Module("org.gnome", "panel-legacy", "7.0"),
Seq(
Dependency(Module("org.gnome", "desktop", "${project.version}"))),
parent = Some(Module("org.gnome", "parent", "7.0"))),
Project(Module("gov.nsa", "secure-pgp", "10.0"),
Seq(
Dependency(Module("gov.nsa", "crypto", "536.89")))),
Project(Module("com.mailapp", "mail-client", "2.1"),
Seq(
Dependency(Module("gov.nsa", "secure-pgp", "10.0"),
exclusions = Set(("*", "${crypto.name}")))),
properties = Map("crypto.name" -> "crypto", "dummy" -> "2")),
Project(Module("com.thoughtworks.paranamer", "paranamer-parent", "2.6"),
Seq(
Dependency(Module("junit", "junit", ""))),
dependencyManagement = Seq(
Dependency(Module("junit", "junit", "4.11"), scope = Scope.Test))),
Project(Module("com.thoughtworks.paranamer", "paranamer", "2.6"),
parent = Some(Module("com.thoughtworks.paranamer", "paranamer-parent", "2.6"))),
Project(Module("com.github.dummy", "libb", "0.3.3"),
profiles = Seq(
Profile("default", activeByDefault = Some(true), dependencies = Seq(
Dependency(Module("org.escalier", "librairie-standard", "2.11.6"))
))
)),
Project(Module("com.github.dummy", "libb", "0.4.2"),
Seq(
Dependency(Module("org.scalaverification", "scala-verification", "1.12.4"))
),
profiles = Seq(
Profile("default", activeByDefault = Some(true), dependencies = Seq(
Dependency(Module("org.escalier", "librairie-standard", "2.11.6")),
Dependency(Module("org.scalaverification", "scala-verification", "1.12.4"), scope = Scope.Test)
))
)),
Project(Module("com.github.dummy", "libb", "0.5.3"),
properties = Map("special" -> "true"),
profiles = Seq(
Profile("default", activation = Profile.Activation(properties = Seq("special" -> None)), dependencies = Seq(
Dependency(Module("org.escalier", "librairie-standard", "2.11.6"))
))
)),
Project(Module("com.github.dummy", "libb", "0.5.4"),
properties = Map("special" -> "true"),
profiles = Seq(
Profile("default", activation = Profile.Activation(properties = Seq("special" -> Some("true"))), dependencies = Seq(
Dependency(Module("org.escalier", "librairie-standard", "2.11.6"))
))
)),
Project(Module("com.github.dummy", "libb", "0.5.5"),
properties = Map("special" -> "true"),
profiles = Seq(
Profile("default", activation = Profile.Activation(properties = Seq("special" -> Some("!false"))), dependencies = Seq(
Dependency(Module("org.escalier", "librairie-standard", "2.11.6"))
))
)),
Project(Module("com.github.dummy", "libb-parent", "0.5.6"),
properties = Map("special" -> "true")),
Project(Module("com.github.dummy", "libb", "0.5.6"),
parent = Some(Module("com.github.dummy", "libb-parent", "0.5.6")),
properties = Map("special" -> "true"),
profiles = Seq(
Profile("default", activation = Profile.Activation(properties = Seq("special" -> Some("!false"))), dependencies = Seq(
Dependency(Module("org.escalier", "librairie-standard", "2.11.6"))
))
))
)
val projectsMap = projects.map(p => p.module -> p).toMap
val testRepository: Repository = new TestRepository(projectsMap)
val repositories = Seq[Repository](
testRepository
)
val tests = TestSuite {
'empty{
async{
val res = await(resolve(
Set.empty,
fetchFrom(repositories)
).runF
)
assert(res == Resolution.empty)
}
}
'notFound{
async {
val dep = Dependency(Module("acme", "playy", "2.4.0"))
val res = await(resolve(
Set(dep),
fetchFrom(repositories)
).runF)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope),
errors = Map(dep.module -> Seq("Not found"))
)
assert(res == expected)
}
}
'single{
async {
val dep = Dependency(Module("acme", "config", "1.3.0"))
val res = await(resolve(
Set(dep),
fetchFrom(repositories)
).runF)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope),
projectsCache = Map(dep.module -> (testRepository, projectsMap(dep.module)))
)
assert(res == expected)
}
}
'oneTransitiveDependency{
async {
val dep = Dependency(Module("acme", "play", "2.4.0"))
val trDep = Dependency(Module("acme", "play-json", "2.4.0"))
val res = await(resolve(
Set(dep),
fetchFrom(repositories)
).runF)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope, trDep.withCompileScope),
projectsCache = Map(
projectsMap(dep.module).kv,
projectsMap(trDep.module).kv
)
)
assert(res == expected)
}
}
'twoTransitiveDependencyWithProps{
async {
val dep = Dependency(Module("acme", "play", "2.4.1"))
val trDeps = Seq(
Dependency(Module("acme", "play-json", "2.4.0")),
Dependency(Module("acme", "config", "1.3.0"))
)
val res = await(resolve(
Set(dep),
fetchFrom(repositories)
).runF)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope),
projectsCache = Map(
projectsMap(dep.module).kv
) ++ trDeps.map(trDep => projectsMap(trDep.module).kv)
)
assert(res == expected)
}
}
'exclude{
async {
val dep = Dependency(Module("acme", "play-extra-no-config", "2.4.1"))
val trDeps = Seq(
Dependency(Module("acme", "play", "2.4.1"),
exclusions = Set(("acme", "config"))),
Dependency(Module("acme", "play-json", "2.4.0"),
exclusions = Set(("acme", "config")))
)
val res = await(resolve(
Set(dep),
fetchFrom(repositories)
).runF)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope),
projectsCache = Map(
projectsMap(dep.module).kv
) ++ trDeps.map(trDep => projectsMap(trDep.module).kv)
)
assert(res == expected)
}
}
'excludeOrgWildcard{
async {
val dep = Dependency(Module("acme", "play-extra-no-config-no", "2.4.1"))
val trDeps = Seq(
Dependency(Module("acme", "play", "2.4.1"),
exclusions = Set(("*", "config"))),
Dependency(Module("acme", "play-json", "2.4.0"),
exclusions = Set(("*", "config")))
)
val res = await(resolve(
Set(dep),
fetchFrom(repositories)
).runF)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope),
projectsCache = Map(
projectsMap(dep.module).kv
) ++ trDeps.map(trDep => projectsMap(trDep.module).kv)
)
assert(res == expected)
}
}
'filter{
async {
val dep = Dependency(Module("hudsucker", "mail", "10.0"))
val res = await(resolve(
Set(dep),
fetchFrom(repositories),
filter = Some(_.scope == Scope.Compile)
).runF).copy(filter = None)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope),
projectsCache = Map(
projectsMap(dep.module).kv
)
)
assert(res == expected)
}
}
'parentDepMgmt{
async {
val dep = Dependency(Module("se.ikea", "billy", "18.0"))
val trDeps = Seq(
Dependency(Module("acme", "play", "2.4.0"),
exclusions = Set(("acme", "play-json")))
)
val res = await(resolve(
Set(dep),
fetchFrom(repositories),
filter = Some(_.scope == Scope.Compile)
).runF).copy(filter = None, projectsCache = Map.empty)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope)
)
assert(res == expected)
}
}
'parentDependencies{
async {
val dep = Dependency(Module("org.gnome", "panel-legacy", "7.0"))
val trDeps = Seq(
Dependency(Module("org.gnu", "glib", "13.4")),
Dependency(Module("org.gnome", "desktop", "7.0")))
val res = await(resolve(
Set(dep),
fetchFrom(repositories),
filter = Some(_.scope == Scope.Compile)
).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope)
)
assert(res == expected)
}
}
'propertiesInExclusions{
async {
val dep = Dependency(Module("com.mailapp", "mail-client", "2.1"))
val trDeps = Seq(
Dependency(Module("gov.nsa", "secure-pgp", "10.0"), exclusions = Set(("*", "crypto"))))
val res = await(resolve(
Set(dep),
fetchFrom(repositories),
filter = Some(_.scope == Scope.Compile)
).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope)
)
assert(res == expected)
}
}
'depMgmtInParentDeps{
async {
val dep = Dependency(Module("com.thoughtworks.paranamer", "paranamer", "2.6"))
val res = await(resolve(
Set(dep),
fetchFrom(repositories),
filter = Some(_.scope == Scope.Compile)
).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope)
)
assert(res == expected)
}
}
'depsFromDefaultProfile{
async {
val dep = Dependency(Module("com.github.dummy", "libb", "0.3.3"))
val trDeps = Seq(
Dependency(Module("org.escalier", "librairie-standard", "2.11.6")))
val res = await(resolve(
Set(dep),
fetchFrom(repositories),
filter = Some(_.scope == Scope.Compile)
).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope)
)
assert(res == expected)
}
}
'depsFromPropertyActivatedProfile{
val f =
for (version <- Seq("0.5.3", "0.5.4", "0.5.5", "0.5.6")) yield {
async {
val dep = Dependency(Module("com.github.dummy", "libb", version))
val trDeps = Seq(
Dependency(Module("org.escalier", "librairie-standard", "2.11.6")))
val res = await(resolve(
Set(dep),
fetchFrom(repositories),
filter = Some(_.scope == Scope.Compile)
).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope)
)
assert(res == expected)
}
}
scala.concurrent.Future.sequence(f)
}
'depsScopeOverrideFromProfile{
async {
// Like com.google.inject:guice:3.0 with org.sonatype.sisu.inject:cglib
val dep = Dependency(Module("com.github.dummy", "libb", "0.4.2"))
val trDeps = Seq(
Dependency(Module("org.escalier", "librairie-standard", "2.11.6")))
val res = await(resolve(
Set(dep),
fetchFrom(repositories),
filter = Some(_.scope == Scope.Compile)
).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty)
val expected = Resolution(
rootDependencies = Set(dep.withCompileScope),
dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope)
)
assert(res == expected)
}
}
'parts{
'propertySubstitution{
val res =
Resolver.withProperties(
Seq(Dependency(Module("a-company", "a-name", "${a.property}"))),
Map("a.property" -> "a-version"))
val expected = Seq(Dependency(Module("a-company", "a-name", "a-version")))
assert(res == expected)
}
}
}
}

View File

@ -0,0 +1,19 @@
package coursier
package test
import coursier.core.{Versions, CachePolicy}
import scalaz.{-\/, \/, EitherT}
import scalaz.concurrent.Task
import scalaz.Scalaz._
class TestRepository(projects: Map[Module, Project]) extends Repository {
def find(module: Module, cachePolicy: CachePolicy) =
EitherT(Task.now(
projects.get(module).toRightDisjunction("Not found")
))
def versions(organization: String, name: String, cachePolicy: CachePolicy) =
EitherT(Task.now[String \/ Versions](
-\/("Not supported")
))
}

View File

@ -0,0 +1,56 @@
package coursier
package test
import coursier.core._
import utest._
object VersionConstraintTests extends TestSuite {
val tests = TestSuite {
'parse{
'empty{
val c0 = Parse.versionConstraint("")
assert(c0 == Some(VersionConstraint.None))
}
'basicVersion{
val c0 = Parse.versionConstraint("1.2")
assert(c0 == Some(VersionConstraint.Preferred(Version("1.2"))))
}
'basicVersionInterval{
val c0 = Parse.versionConstraint("(,1.2]")
assert(c0 == Some(VersionConstraint.Interval(VersionInterval(None, Some(Version("1.2")), false, true))))
}
}
'repr{
'empty{
val s0 = VersionConstraint.None.repr
assert(s0 == "")
}
'preferred{
val s0 = VersionConstraint.Preferred(Version("2.1")).repr
assert(s0 == "2.1")
}
'interval{
val s0 = VersionConstraint.Interval(VersionInterval(None, Some(Version("2.1")), false, true)).repr
assert(s0 == "(,2.1]")
}
}
'interval{
'empty{
val s0 = VersionConstraint.None.interval
assert(s0 == VersionInterval.zero)
}
'preferred{
val s0 = VersionConstraint.Preferred(Version("2.1")).interval
assert(s0 == VersionInterval(Some(Version("2.1")), None, true, false))
}
'interval{
val s0 = VersionConstraint.Interval(VersionInterval(None, Some(Version("2.1")), false, true)).interval
assert(s0 == VersionInterval(None, Some(Version("2.1")), false, true))
}
}
}
}

View File

@ -0,0 +1,318 @@
package coursier
package test
import coursier.core._
import utest._
object VersionIntervalTests extends TestSuite {
val tests = TestSuite{
'invalid{
'basic{
assert(VersionInterval.zero.isValid)
val itv1 = VersionInterval(None, None, true, true)
val itv2 = VersionInterval(None, None, false, true)
val itv3 = VersionInterval(None, None, true, false)
assert(!itv1.isValid)
assert(!itv2.isValid)
assert(!itv3.isValid)
}
'halfBounded{
val itv1 = VersionInterval(Some(Version("1.2")), None, true, true)
val itv2 = VersionInterval(Some(Version("1.2")), None, false, true)
val itv3 = VersionInterval(None, Some(Version("1.2")), true, true)
val itv4 = VersionInterval(None, Some(Version("1.2")), true, false)
assert(!itv1.isValid)
assert(!itv2.isValid)
assert(!itv3.isValid)
assert(!itv4.isValid)
}
'order{
val itv1 = VersionInterval(Some(Version("2")), Some(Version("1")), true, true)
val itv2 = VersionInterval(Some(Version("2")), Some(Version("1")), false, true)
val itv3 = VersionInterval(Some(Version("2")), Some(Version("1")), true, false)
val itv4 = VersionInterval(Some(Version("2")), Some(Version("1")), false, false)
assert(!itv1.isValid)
assert(!itv2.isValid)
assert(!itv3.isValid)
assert(!itv4.isValid)
}
'bound{
val itv1 = VersionInterval(Some(Version("2")), Some(Version("2")), false, true)
val itv2 = VersionInterval(Some(Version("2")), Some(Version("2")), true, false)
val itv3 = VersionInterval(Some(Version("2")), Some(Version("2")), false, false)
assert(!itv1.isValid)
assert(!itv2.isValid)
assert(!itv3.isValid)
val itv4 = VersionInterval(Some(Version("2")), Some(Version("2")), true, true)
assert(itv4.isValid)
}
}
'merge{
'basic{
val itv0m = VersionInterval.zero.merge(VersionInterval.zero)
assert(itv0m == Some(VersionInterval.zero))
val itv1 = VersionInterval(Some(Version("1")), Some(Version("2")), false, true)
val itv1m = itv1.merge(VersionInterval.zero)
val itv1m0 = VersionInterval.zero.merge(itv1)
assert(itv1m == Some(itv1))
assert(itv1m0 == Some(itv1))
}
'noIntersec{
val itv1 = VersionInterval(Some(Version("1")), Some(Version("2")), true, false)
val itv2 = VersionInterval(Some(Version("3")), Some(Version("5")), false, true)
val itvm = itv1 merge itv2
val itvm0 = itv2 merge itv1
assert(itvm == None)
assert(itvm0 == None)
}
'noIntersecSameFrontierOpenClose{
val itv1 = VersionInterval(Some(Version("1")), Some(Version("2")), true, false)
val itv2 = VersionInterval(Some(Version("2")), Some(Version("4")), true, true)
val itvm = itv1 merge itv2
val itvm0 = itv2 merge itv1
assert(itvm == None)
assert(itvm0 == None)
}
'noIntersecSameFrontierCloseOpen{
val itv1 = VersionInterval(Some(Version("1")), Some(Version("2")), true, true)
val itv2 = VersionInterval(Some(Version("2")), Some(Version("4")), false, true)
val itvm = itv1 merge itv2
val itvm0 = itv2 merge itv1
assert(itvm == None)
assert(itvm0 == None)
}
'noIntersecSameFrontierOpenOpen{
val itv1 = VersionInterval(Some(Version("1")), Some(Version("2")), true, false)
val itv2 = VersionInterval(Some(Version("2")), Some(Version("4")), false, true)
val itvm = itv1 merge itv2
val itvm0 = itv2 merge itv1
assert(itvm == None)
assert(itvm0 == None)
}
'intersecSameFrontierCloseClose{
val itv1 = VersionInterval(Some(Version("1")), Some(Version("2")), true, true)
val itv2 = VersionInterval(Some(Version("2")), Some(Version("4")), true, true)
val itvm = itv1 merge itv2
val itvm0 = itv2 merge itv1
val expected = VersionInterval(Some(Version("2")), Some(Version("2")), true, true)
assert(itvm == Some(expected))
assert(itvm0 == Some(expected))
}
'intersec{
val bools = Seq(true, false)
for (l1 <- bools; l2 <- bools; r1 <- bools; r2 <- bools) {
val itv1 = VersionInterval(Some(Version("1")), Some(Version("3")), l1, r1)
val itv2 = VersionInterval(Some(Version("2")), Some(Version("4")), l2, r2)
val itvm = itv1 merge itv2
val itvm0 = itv2 merge itv1
val expected = VersionInterval(Some(Version("2")), Some(Version("3")), l2, r1)
assert(itvm == Some(expected))
assert(itvm0 == Some(expected))
}
}
}
'parse{
'malformed{
val s1 = "[1.1]"
val itv1 = Parse.versionInterval(s1)
assert(itv1 == None)
val s2 = "(1.1)"
val itv2 = Parse.versionInterval(s2)
assert(itv2 == None)
val s3 = "()"
val itv3 = Parse.versionInterval(s3)
assert(itv3 == None)
val s4 = "[1.1,1.3"
val itv4 = Parse.versionInterval(s4)
assert(itv4 == None)
val s5 = "1.1,1.3)"
val itv5 = Parse.versionInterval(s5)
assert(itv5 == None)
}
'basic {
val s1 = "[1.1,1.3]"
val itv1 = Parse.versionInterval(s1)
assert(itv1 == Some(VersionInterval(Some(Version("1.1")), Some(Version("1.3")), true, true)))
val s2 = "(1.1,1.3]"
val itv2 = Parse.versionInterval(s2)
assert(itv2 == Some(VersionInterval(Some(Version("1.1")), Some(Version("1.3")), false, true)))
val s3 = "[1.1,1.3)"
val itv3 = Parse.versionInterval(s3)
assert(itv3 == Some(VersionInterval(Some(Version("1.1")), Some(Version("1.3")), true, false)))
val s4 = "(1.1,1.3)"
val itv4 = Parse.versionInterval(s4)
assert(itv4 == Some(VersionInterval(Some(Version("1.1")), Some(Version("1.3")), false, false)))
}
'leftEmptyVersions {
val s1 = "[,1.3]"
val itv1 = Parse.versionInterval(s1)
assert(itv1 == Some(VersionInterval(None, Some(Version("1.3")), true, true)))
assert(!itv1.get.isValid)
val s2 = "(,1.3]"
val itv2 = Parse.versionInterval(s2)
assert(itv2 == Some(VersionInterval(None, Some(Version("1.3")), false, true)))
assert(itv2.get.isValid)
val s3 = "[,1.3)"
val itv3 = Parse.versionInterval(s3)
assert(itv3 == Some(VersionInterval(None, Some(Version("1.3")), true, false)))
assert(!itv3.get.isValid)
val s4 = "(,1.3)"
val itv4 = Parse.versionInterval(s4)
assert(itv4 == Some(VersionInterval(None, Some(Version("1.3")), false, false)))
assert(itv4.get.isValid)
}
'rightEmptyVersions {
val s1 = "[1.3,]"
val itv1 = Parse.versionInterval(s1)
assert(itv1 == Some(VersionInterval(Some(Version("1.3")), None, true, true)))
assert(!itv1.get.isValid)
val s2 = "(1.3,]"
val itv2 = Parse.versionInterval(s2)
assert(itv2 == Some(VersionInterval(Some(Version("1.3")), None, false, true)))
assert(!itv2.get.isValid)
val s3 = "[1.3,)"
val itv3 = Parse.versionInterval(s3)
assert(itv3 == Some(VersionInterval(Some(Version("1.3")), None, true, false)))
assert(itv3.get.isValid)
val s4 = "(1.3,)"
val itv4 = Parse.versionInterval(s4)
assert(itv4 == Some(VersionInterval(Some(Version("1.3")), None, false, false)))
assert(itv4.get.isValid)
}
'bothEmptyVersions {
val s1 = "[,]"
val itv1 = Parse.versionInterval(s1)
assert(itv1 == Some(VersionInterval(None, None, true, true)))
assert(!itv1.get.isValid)
val s2 = "(,]"
val itv2 = Parse.versionInterval(s2)
assert(itv2 == Some(VersionInterval(None, None, false, true)))
assert(!itv2.get.isValid)
val s3 = "[,)"
val itv3 = Parse.versionInterval(s3)
assert(itv3 == Some(VersionInterval(None, None, true, false)))
assert(!itv3.get.isValid)
val s4 = "(,]"
val itv4 = Parse.versionInterval(s4)
assert(itv4 == Some(VersionInterval(None, None, false, true)))
assert(!itv4.get.isValid)
}
}
'repr{
'basic {
val s1 = "[1.1,1.3]"
val repr1 = Parse.versionInterval(s1).map(_.repr)
assert(repr1 == Some(s1))
val s2 = "(1.1,1.3]"
val repr2 = Parse.versionInterval(s2).map(_.repr)
assert(repr2 == Some(s2))
val s3 = "[1.1,1.3)"
val repr3 = Parse.versionInterval(s3).map(_.repr)
assert(repr3 == Some(s3))
val s4 = "(1.1,1.3)"
val repr4 = Parse.versionInterval(s4).map(_.repr)
assert(repr4 == Some(s4))
}
'leftEmptyVersions {
val s1 = "[,1.3]"
val repr1 = Parse.versionInterval(s1).map(_.repr)
assert(repr1 == Some(s1))
val s2 = "(,1.3]"
val repr2 = Parse.versionInterval(s2).map(_.repr)
assert(repr2 == Some(s2))
val s3 = "[,1.3)"
val repr3 = Parse.versionInterval(s3).map(_.repr)
assert(repr3 == Some(s3))
val s4 = "(,1.3)"
val repr4 = Parse.versionInterval(s4).map(_.repr)
assert(repr4 == Some(s4))
}
'rightEmptyVersions {
val s1 = "[1.3,]"
val repr1 = Parse.versionInterval(s1).map(_.repr)
assert(repr1 == Some(s1))
val s2 = "(1.3,]"
val repr2 = Parse.versionInterval(s2).map(_.repr)
assert(repr2 == Some(s2))
val s3 = "[1.3,)"
val repr3 = Parse.versionInterval(s3).map(_.repr)
assert(repr3 == Some(s3))
val s4 = "(1.3,)"
val repr4 = Parse.versionInterval(s4).map(_.repr)
assert(repr4 == Some(s4))
}
'bothEmptyVersions {
val s1 = "[,]"
val repr1 = Parse.versionInterval(s1).map(_.repr)
assert(repr1 == Some(s1))
val s2 = "(,]"
val repr2 = Parse.versionInterval(s2).map(_.repr)
assert(repr2 == Some(s2))
val s3 = "[,)"
val repr3 = Parse.versionInterval(s3).map(_.repr)
assert(repr3 == Some(s3))
val s4 = "(,]"
val repr4 = Parse.versionInterval(s4).map(_.repr)
assert(repr4 == Some(s4))
}
}
'constraint{
'none{
val s1 = "(,)"
val c1 = Parse.versionInterval(s1).map(_.constraint)
assert(c1 == Some(VersionConstraint.None))
}
'preferred{
val s1 = "[1.3,)"
val c1 = Parse.versionInterval(s1).map(_.constraint)
assert(c1 == Some(VersionConstraint.Preferred(Parse.version("1.3").get)))
}
'interval{
val s1 = "[1.3,2.4)"
val c1 = Parse.versionInterval(s1).map(_.constraint)
assert(c1 == Some(VersionConstraint.Interval(VersionInterval(Parse.version("1.3"), Parse.version("2.4"), true, false))))
}
}
}
}

View File

@ -0,0 +1,9 @@
package coursier
package object test {
implicit class DependencyOps(val underlying: Dependency) extends AnyVal {
def withCompileScope: Dependency = underlying.copy(scope = Scope.Compile)
}
}

121
project/Coursier.scala Normal file
View File

@ -0,0 +1,121 @@
import org.scalajs.sbtplugin.ScalaJSPlugin
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
import sbt._, Keys._
import sbtrelease.ReleasePlugin.releaseSettings
import sbtrelease.ReleasePlugin.ReleaseKeys.{ publishArtifactsAction, versionBump }
import sbtrelease.Version.Bump
import com.typesafe.sbt.pgp.PgpKeys
object CoursierBuild extends Build {
lazy val publishingSettings = Seq[Setting[_]](
publishMavenStyle := true,
publishTo := {
val nexus = "https://oss.sonatype.org/"
if (isSnapshot.value)
Some("snapshots" at nexus + "content/repositories/snapshots")
else
Some("releases" at nexus + "service/local/staging/deploy/maven2")
},
pomExtra := {
<url>https://github.com/alexarchambault/coursier</url>
<licenses>
<license>
<name>Apache 2.0</name>
<url>http://opensource.org/licenses/Apache-2.0</url>
</license>
</licenses>
<scm>
<connection>scm:git:github.com/alexarchambault/coursier.git</connection>
<developerConnection>scm:git:git@github.com:alexarchambault/coursier.git</developerConnection>
<url>github.com/alexarchambault/coursier.git</url>
</scm>
<developers>
<developer>
<id>alexarchambault</id>
<name>Alexandre Archambault</name>
<url>https://github.com/alexarchambault</url>
</developer>
</developers>
},
credentials += {
Seq("SONATYPE_USER", "SONATYPE_PASS").map(sys.env.get) match {
case Seq(Some(user), Some(pass)) =>
Credentials("Sonatype Nexus Repository Manager", "oss.sonatype.org", user, pass)
case _ =>
Credentials(Path.userHome / ".ivy2" / ".credentials")
}
},
versionBump := Bump.Bugfix,
publishArtifactsAction := PgpKeys.publishSigned.value
) ++ releaseSettings
lazy val commonSettings = Seq[Setting[_]](
organization := "eu.frowning-lambda",
version := "0.1.0-SNAPSHOT",
scalaVersion := "2.11.6",
crossScalaVersions := Seq("2.10.5", "2.11.6"),
resolvers += "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases"
) ++ publishingSettings
private lazy val commonCoreSettings = Seq[Setting[_]](
name := "coursier",
libraryDependencies += "org.scala-lang.modules" %% "scala-async" % "0.9.1" % "provided",
unmanagedSourceDirectories in Compile += (baseDirectory in LocalRootProject).value / "core" / "src" / "main" / "scala",
unmanagedSourceDirectories in Test += (baseDirectory in LocalRootProject).value / "core" / "src" / "test" / "scala",
testFrameworks += new TestFramework("utest.runner.Framework")
)
lazy val coreJvm = Project(id = "core-jvm", base = file("core-jvm"))
.settings(commonSettings ++ commonCoreSettings: _*)
.settings(
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-blazeclient" % "0.7.0",
"com.lihaoyi" %% "utest" % "0.3.0" % "test"
) ++ {
if (scalaVersion.value.startsWith("2.10.")) Seq()
else Seq(
"org.scala-lang.modules" %% "scala-xml" % "1.0.3"
)
}
)
lazy val coreJs = Project(id = "core-js", base = file("core-js"))
.settings(commonSettings ++ commonCoreSettings: _*)
.settings(
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "0.8.0",
"com.github.japgolly.fork.scalaz" %%% "scalaz-core" % (if (scalaVersion.value.startsWith("2.10.")) "7.1.1" else "7.1.2"),
"be.doeraene" %%% "scalajs-jquery" % "0.8.0",
"com.lihaoyi" %%% "utest" % "0.3.0" % "test"
),
postLinkJSEnv := NodeJSEnv().value,
scalaJSStage in Global := FastOptStage
)
.enablePlugins(ScalaJSPlugin)
lazy val cli = Project(id = "cli", base = file("cli"))
.dependsOn(coreJvm)
.settings(commonSettings ++ xerial.sbt.Pack.packAutoSettings: _*)
.settings(
libraryDependencies ++= Seq(
"com.github.alexarchambault" %% "case-app" % "0.2.2",
"ch.qos.logback" % "logback-classic" % "1.1.3"
)
)
lazy val web = Project(id = "web", base = file("web"))
.dependsOn(coreJs)
.settings(commonSettings: _*)
.settings(
libraryDependencies ++= Seq(
"com.github.japgolly.scalajs-react" %%% "core" % "0.9.0"
),
jsDependencies +=
"org.webjars" % "react" % "0.12.2" / "react-with-addons.js" commonJSName "React"
)
.enablePlugins(ScalaJSPlugin)
}

1
project/build.properties Normal file
View File

@ -0,0 +1 @@
sbt.version=0.13.8

7
project/plugins.sbt Normal file
View File

@ -0,0 +1,7 @@
addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.6.8")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.3")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
addSbtPlugin("com.github.gseitz" % "sbt-release" % "0.8.5")

40
project/travis.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
set -ev
TRAVIS_SCALA_VERSION="$1"
shift
TRAVIS_PULL_REQUEST="$1"
shift
TRAVIS_BRANCH="$1"
shift
function isPr() {
[ "$TRAVIS_PULL_REQUEST" = "false" ]
}
function isJdk7() {
[ "$JAVA_HOME" = "$(jdk_switcher home oraclejdk7)" ]
}
function isMaster() {
[ "$TRAVIS_BRANCH" = "master" ]
}
if echo "$TRAVIS_SCALA_VERSION" | grep -q "^2\.10"; then
if isPr && isJdk7 && isMaster; then
EXTRA_SBT_ARGS="core-jvm/publish core-js/publish cli/publish"
else
EXTRA_SBT_ARGS=""
fi
sbt ++${TRAVIS_SCALA_VERSION} core-jvm/test core-js/test cli/test $EXTRA_SBT_ARGS
else
if isPr && isJdk7 && isMaster; then
EXTRA_SBT_ARGS="publish"
else
EXTRA_SBT_ARGS=""
fi
sbt ++${TRAVIS_SCALA_VERSION} test $EXTRA_SBT_ARGS
fi

1
version.sbt Normal file
View File

@ -0,0 +1 @@
version in ThisBuild := "0.1.0-SNAPSHOT"

View File

@ -0,0 +1,37 @@
/* =========================================================
* bootstrap-treeview.css v1.2.0
* =========================================================
* Copyright 2013 Jonathan Miles
* Project URL : http://www.jondmiles.com/bootstrap-treeview
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================= */
.treeview .list-group-item {
cursor: pointer;
}
.treeview span.indent {
margin-left: 10px;
margin-right: 10px;
}
.treeview span.icon {
width: 12px;
margin-right: 5px;
}
.treeview .node-disabled {
color: silver;
cursor: not-allowed;
}

View File

@ -0,0 +1,143 @@
<!DOCTYPE html>
<html>
<head>
<title>Dependency resolution with Coursier</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<link rel="stylesheet" href="css/bootstrap-treeview.css">
<style>
.icon-action {
cursor: pointer;
}
#resolveButton {
margin-top: 20px;
margin-bottom: 20px;
}
#results {
display: none;
}
.customButton {
margin: 5px;
}
</style>
</head>
<body style="margin: 0px">
<div class="container theme-showcase" role="main">
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#mainTab" data-toggle="tab">Coursier</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<!-- class="active" -->
<li><a href="#demo" data-toggle="tab">Demo</a></li>
<li><a href="https://github.com/alexarchambault/coursier/blob/master/USAGE.md">Usage</a></li>
<!-- <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Usage <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#usageCli" data-toggle="tab">Command-line</a></li>
<li><a href="#usageApi" data-toggle="tab">API</a></li>
</ul>
</li> -->
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="https://github.com/alexarchambault/coursier">Github</a></li>
</ul>
</div>
</div>
</nav>
<div class="tab-content" id="mainTabContent">
<div class="tab-pane fade in active" id="mainTab">
<div class="jumbotron">
<h1>Coursier</h1>
<p>Pure Scala Artifact Fetching</p>
<p><a href="https://github.com/alexarchambault/coursier">Project Page</a></p>
</div>
</div>
<div class="tab-pane fade" id="demo">
<div id="demoContent"></div>
</div>
<div class="tab-pane fade" id="usageCli">
<p>CLI</p>
</div>
<div class="tab-pane fade" id="usageApi">
<p>API</p>
</div>
</div>
</div>
<script>
// From scalajs-react examples
function jsonp(url, callback) {
var callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());
window[callbackName] = function(data) {
delete window[callbackName];
callback(data);
};
var script = document.createElement('script');
script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + callbackName;
document.body.appendChild(script);
}
</script>
<script type="text/javascript" src="../web-jsdeps.js"></script>
<script type="text/javascript" src="../web-fastopt.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<script src="js/bootstrap-treeview.js"></script>
<script type="text/javascript" src="js/raphael.js"></script>
<script type="text/javascript" src="js/dracula_graph.js"></script>
<script type="text/javascript" src="js/dracula_graffle.js"></script>
<script type="text/javascript" src="js/routie.min.js"></script>
<script>
// http://stackoverflow.com/a/13413044/3714539
routie({
demo: function() {
$('#accordionOne').collapse('show');
},
accordionTwo: function() {
$('#accordionTwo').collapse('show');
},
accordionThree: function() {
$('#accordionThree').collapse('show');
}
});
</script>
<script>
coursier.web.Main().main();
</script>
</body>
</html>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Dependency resolution with Coursier</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body style="margin: 0px">
<div>
<canvas style="display: block" id="canvas" width="255" height="255"/>
</div>
<script type="text/javascript" src="../web-opt.js"></script>
<script>
example.ScalaJSExample().main();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
/**
* Originally grabbed from the official RaphaelJS Documentation
* http://raphaeljs.com/graffle.html
* Adopted (arrows) and commented by Philipp Strathausen http://blog.ameisenbar.de
* Licenced under the MIT licence.
*/
/**
* Usage:
* connect two shapes
* parameters:
* source shape [or connection for redrawing],
* target shape,
* style with { fg : linecolor, bg : background color, directed: boolean }
* returns:
* connection { draw = function() }
*/
Raphael.fn.connection = function Connection(obj1, obj2, style) {
var selfRef = this;
/* create and return new connection */
var edge = {/*
from : obj1,
to : obj2,
style : style,*/
draw : function() {
/* get bounding boxes of target and source */
var bb1 = obj1.getBBox();
var bb2 = obj2.getBBox();
var off1 = 0;
var off2 = 0;
/* coordinates for potential connection coordinates from/to the objects */
var p = [
/* NORTH 1 */
{ x: bb1.x + bb1.width / 2, y: bb1.y - off1 },
/* SOUTH 1 */
{ x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + off1 },
/* WEST */
{ x: bb1.x - off1, y: bb1.y + bb1.height / 2 },
/* EAST 1 */
{ x: bb1.x + bb1.width + off1, y: bb1.y + bb1.height / 2 },
/* NORTH 2 */
{ x: bb2.x + bb2.width / 2, y: bb2.y - off2 },
/* SOUTH 2 */
{ x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + off2 },
/* WEST 2 */
{ x: bb2.x - off2, y: bb2.y + bb2.height / 2 },
/* EAST 2 */
{ x: bb2.x + bb2.width + off2, y: bb2.y + bb2.height / 2 }
];
/* distances between objects and according coordinates connection */
var d = {}, dis = [], dx, dy;
/*
* find out the best connection coordinates by trying all possible ways
*/
/* loop the first object's connection coordinates */
for (var i = 0; i < 4; i++) {
/* loop the seond object's connection coordinates */
for (var j = 4; j < 8; j++) {
dx = Math.abs(p[i].x - p[j].x);
dy = Math.abs(p[i].y - p[j].y);
if ((i === j - 4) || (((i !== 3 && j !== 6) || p[i].x < p[j].x) &&
((i !== 2 && j !== 7) || p[i].x > p[j].x) &&
((i !== 0 && j !== 5) || p[i].y > p[j].y) &&
((i !== 1 && j !== 4) || p[i].y < p[j].y)))
{
dis.push(dx + dy);
d[dis[dis.length - 1].toFixed(3)] = [i, j];
}
}
}
var res = dis.length === 0 ? [0, 4] : d[Math.min.apply(Math, dis).toFixed(3)];
/* bezier path */
var x1 = p[res[0]].x,
y1 = p[res[0]].y,
x4 = p[res[1]].x,
y4 = p[res[1]].y;
dx = Math.max(Math.abs(x1 - x4) / 2, 10);
dy = Math.max(Math.abs(y1 - y4) / 2, 10);
var x2 = [ x1, x1, x1 - dx, x1 + dx ][res[0]].toFixed(3),
y2 = [ y1 - dy, y1 + dy, y1, y1 ][res[0]].toFixed(3),
x3 = [ 0, 0, 0, 0, x4, x4, x4 - dx, x4 + dx ][res[1]].toFixed(3),
y3 = [ 0, 0, 0, 0, y1 + dy, y1 - dy, y4, y4 ][res[1]].toFixed(3);
/* assemble path and arrow */
var path = [ "M" + x1.toFixed(3), y1.toFixed(3),
"C" + x2, y2, x3, y3, x4.toFixed(3), y4.toFixed(3) ].join(",");
/* arrow */
if(style && style.directed) {
// magnitude, length of the last path vector
var mag = Math.sqrt((y4 - y3) * (y4 - y3) + (x4 - x3) * (x4 - x3));
// vector normalisation to specified length
var norm = function(x,l){return (-x*(l||5)/mag);};
// calculate array coordinates (two lines orthogonal to the path vector)
var arr = [
{ x:(norm(x4-x3)+norm(y4-y3)+x4).toFixed(3),
y:(norm(y4-y3)+norm(x4-x3)+y4).toFixed(3) },
{ x:(norm(x4-x3)-norm(y4-y3)+x4).toFixed(3),
y:(norm(y4-y3)-norm(x4-x3)+y4).toFixed(3) }
];
path = path + ",M" + arr[0].x + "," + arr[0].y + ",L" + x4 + "," +
y4 + ",L" + arr[1].x + "," + arr[1].y;
}
/* function to be used for moving existent path(s), e.g. animate() or attr() */
var move = "attr";
/* applying path(s) */
edge.fg && edge.fg[move]({path:path}) ||
(edge.fg = selfRef.path(path)
.attr({ stroke: style && style.stroke || "#000", fill: "none" })
.toBack());
edge.bg && edge.bg[move]({path:path}) ||
style && style.fill && (edge.bg = style.fill.split &&
selfRef.path(path)
.attr({ stroke: style.fill.split("|")[0], fill: "none",
"stroke-width": style.fill.split("|")[1] || 3 }).toBack());
/* setting label */
style && style.label &&
(edge.label && edge.label.attr({x:(x1+x4)/2, y:(y1+y4)/2}) ||
(edge.label = selfRef.text((x1+x4)/2, (y1+y4)/2, style.label)
.attr({fill: "#000", "font-size": style["font-size"] || "12px"})));
style && style.label && style["label-style"] && edge.label &&
edge.label.attr(style["label-style"]);
style && style.callback && style.callback(edge);
}
};
edge.draw();
return edge;
};

View File

@ -0,0 +1,685 @@
/*
* Dracula Graph Layout and Drawing Framework 0.0.3alpha
* (c) 2010 Johann Philipp Strathausen <strathausen@gmail.com>
* http://strathausen.eu
*
* Contributions by Jake Stothard <stothardj@gmail.com>.
*
* based on the Graph JavaScript framework, version 0.0.1
* (c) 2006 Aslak Hellesoy <aslak.hellesoy@gmail.com>
* (c) 2006 Dave Hoover <dave.hoover@gmail.com>
*
* Ported from Graph::Layouter::Spring in
* http://search.cpan.org/~pasky/Graph-Layderer-0.02/
* The algorithm is based on a spring-style layouter of a Java-based social
* network tracker PieSpy written by Paul Mutton <paul@jibble.org>.
*
* This code is freely distributable under the MIT license. Commercial use is
* hereby granted without any cost or restriction.
*
* Links:
*
* Graph Dracula JavaScript Framework:
* http://graphdracula.net
*
/*--------------------------------------------------------------------------*/
/*
* Dracula
*/
var Dracula = function() {
this.nodes = {};
this.edges = [];
this.snapshots = []; // previous graph states TODO to be implemented
};
var Graph = Dracula;
Dracula.prototype = {
/*
* add a node
* @id the node's ID (string or number)
* @content (optional, dictionary) can contain any information that is
* being interpreted by the layout algorithm or the graph
* representation
*/
addNode: function(id, content) {
/* testing if node is already existing in the graph */
if(this.nodes[id] === undefined) {
this.nodes[id] = new Dracula.Node(id, content);
}
return this.nodes[id];
},
addEdge: function(source, target, style) {
var s = this.addNode(source);
var t = this.addNode(target);
var edge = new Dracula.Edge({ source: s, target: t, style: style });
s.edges.push(edge);
this.edges.push(edge);
// NOTE: Even directed edges are added to both nodes.
t.edges.push(edge);
return edge;
},
/* TODO to be implemented
* Preserve a copy of the graph state (nodes, positions, ...)
* @comment a comment describing the state
*/
snapShot: function(comment) {
// FIXME
//var graph = new Dracula();
//graph.nodes = jQuery.extend(true, {}, this.nodes);
//graph.edges = jQuery.extend(true, {}, this.edges);
//this.snapshots.push({comment: comment, graph: graph});
},
removeNode: function(id) {
if (this.nodes[id] && this.node[id].shape) {
this.nodes[id].shape.remove();
delete this.nodes[id];
}
for(var i = 0; i < this.edges.length; i++) {
if (this.edges[i].source.id == id || this.edges[i].target.id == id) {
this.edges[i].connection.fg.remove();
this.edges[i].connection.label.remove();
this.edges.splice(i, 1);
i--;
}
}
}
};
/*
* Edge
*/
Dracula.Edge = function DraculaEdge(opts) {
this.source = opts.source;
this.target = opts.target;
if (opts.style) {
this.style = jQuery.extend(true, {}, Dracula.Edge.style, opts.style);
} else {
this.style = jQuery.extend(true, {}, Dracula.Edge.style);
}
};
Dracula.Edge.style = {
directed: false
};
Dracula.Edge.prototype = {
weight: 0,
hide: function hideEdge() {
this.connection.fg.hide();
this.connection.bg && this.bg.connection.hide();
}
};
/*
* Node
*/
Dracula.Node = function DraculaNode(id, node){
var i;
node = node || {};
node.id = id;
node.edges = [];
node.hide = function() {
var i;
this.hidden = true;
this.shape && this.shape.hide(); /* FIXME this is representation specific code and should be elsewhere */
for(i in this.edges)
(this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].hide && this.edges[i].hide();
};
node.show = function() {
var i;
this.hidden = false;
this.shape && this.shape.show();
for(i in this.edges)
(this.edges[i].source.id == id || this.edges[i].target == id) &&
this.edges[i].show && this.edges[i].show();
};
return node;
};
Dracula.Node.prototype = {
};
/*
* Renderer Base Class
*/
Dracula.Renderer = {};
/*
* Renderer implementation using RaphaelJS
*/
Dracula.Renderer.Raphael = function(element, graph, width, height) {
this.width = width || 400;
this.height = height || 400;
this.r = Raphael(element, this.width, this.height);
this.radius = 40; /* max dimension of a node */
this.graph = graph;
this.mouse_in = false;
/* TODO default node rendering function */
if(!this.graph.render) {
this.graph.render = function() {
return;
};
}
this.draw();
};
/* Moved this default node renderer function out of the main prototype code
* so it can be override by default */
Dracula.Renderer.defaultRenderFunc = function(r, node) {
/* the default node drawing */
var color = Raphael.getColor();
var ellipse = r.ellipse(0, 0, 30, 20).attr({
fill: node.fill || color,
stroke: node.stroke || color,
"stroke-width": 2
});
/* set DOM node ID */
ellipse.node.id = node.id || node.label;
if(node.class)ellipse.node.classList.add(node.class);
shape = r.set().push(ellipse).push(r.text(0, 30, node.label || node.id));
return shape;
};
Dracula.Renderer.Raphael.prototype = {
translate: function(point) {
return [
(point[0] - this.graph.layoutMinX) * this.factorX + this.radius,
(point[1] - this.graph.layoutMinY) * this.factorY + this.radius
];
},
rotate: function(point, length, angle) {
var dx = length * Math.cos(angle);
var dy = length * Math.sin(angle);
return [point[0]+dx, point[1]+dy];
},
draw: function() {
var i;
this.factorX = (this.width - 2 * this.radius) / (this.graph.layoutMaxX - this.graph.layoutMinX);
this.factorY = (this.height - 2 * this.radius) / (this.graph.layoutMaxY - this.graph.layoutMinY);
for (i in this.graph.nodes) {
this.drawNode(this.graph.nodes[i]);
}
for (i = 0; i < this.graph.edges.length; i++) {
this.drawEdge(this.graph.edges[i]);
}
},
drawNode: function(node) {
var point = this.translate([node.layoutPosX, node.layoutPosY]);
node.point = point;
var r = this.r;
var graph = this.graph;
/* if node has already been drawn, move the nodes */
if(node.shape) {
var oBBox = node.shape.getBBox();
var opoint = {
x: oBBox.x + oBBox.width / 2,
y: oBBox.y + oBBox.height / 2
};
node.shape.translate(
Math.round(point[0] - opoint.x), Math.round(point[1] - opoint.y)
);
this.r.safari();
return node;
}/* else, draw new nodes */
var shape;
/* if a node renderer function is provided by the user, then use it
or the default render function instead */
if(!node.render) {
node.render = Dracula.Renderer.defaultRenderFunc;
}
/* or check for an ajax representation of the nodes */
if(node.shapes) {
// TODO ajax representation evaluation
}
var selfRef = this;
shape = node.render(this.r, node).hide();
shape.attr({"fill-opacity": 0.6});
/* re-reference to the node an element belongs to, needed for dragging all elements of a node */
shape.items.forEach(function(item) {
item.set = shape;
item.node.style.cursor = "move";
});
shape.drag(
function dragMove(dx, dy, x, y) {
dx = this.set.ox;
dy = this.set.oy;
var bBox = this.set.getBBox();
var newX = x - dx + (bBox.x + bBox.width / 2);
var newY = y - dy + (bBox.y + bBox.height / 2);
var clientX =
x - (newX < 20 ? newX - 20 : newX > r.width - 20 ? newX - r.width + 20 : 0);
var clientY =
y - (newY < 20 ? newY - 20 : newY > r.height - 20 ? newY - r.height + 20 : 0);
this.set.translate(clientX - Math.round(dx), clientY - Math.round(dy));
for (var i in selfRef.graph.edges) {
selfRef.graph.edges[i] &&
selfRef.graph.edges[i].connection && selfRef.graph.edges[i].connection.draw();
}
r.safari();
this.set.ox = clientX;
this.set.oy = clientY;
},
function dragEnter(x, y) {
this.set.ox = x;
this.set.oy = y;
this.animate({ 'fill-opacity': 0.2 }, 500);
},
function dragOut() {
this.animate({ 'fill-opacity': 0.6 }, 500);
}
);
var box = shape.getBBox();
shape.translate(
Math.round(point[0] - (box.x + box.width / 2)),
Math.round(point[1] - (box.y + box.height / 2))
);
node.hidden || shape.show();
node.shape = shape;
},
drawEdge: function(edge) {
/* if this edge already exists the other way around and is undirected */
if(edge.backedge)
return;
if(edge.source.hidden || edge.target.hidden) {
edge.connection && edge.connection.fg.hide();
edge.connection.bg && edge.connection.bg.hide();
return;
}
/* if edge already has been drawn, only refresh the edge */
if(!edge.connection) {
edge.style && edge.style.callback && edge.style.callback(edge); // TODO move this somewhere else
edge.connection = this.r.connection(edge.source.shape, edge.target.shape, edge.style);
return;
}
//FIXME showing doesn't work well
edge.connection.fg.show();
edge.connection.bg && edge.connection.bg.show();
edge.connection.draw();
}
};
Dracula.Layout = {};
Dracula.Layout.Spring = function(graph) {
this.graph = graph;
this.iterations = 500;
this.maxRepulsiveForceDistance = 6;
this.k = 2;
this.c = 0.01;
this.maxVertexMovement = 0.5;
this.layout();
};
Dracula.Layout.Spring.prototype = {
layout: function() {
this.layoutPrepare();
for (var i = 0; i < this.iterations; i++) {
this.layoutIteration();
}
this.layoutCalcBounds();
},
layoutPrepare: function() {
var i;
for (i in this.graph.nodes) {
var node = this.graph.nodes[i];
node.layoutPosX = 0;
node.layoutPosY = 0;
node.layoutForceX = 0;
node.layoutForceY = 0;
}
},
layoutCalcBounds: function() {
var minx = Infinity, maxx = -Infinity,
miny = Infinity, maxy = -Infinity;
var i;
for (i in this.graph.nodes) {
var x = this.graph.nodes[i].layoutPosX;
var y = this.graph.nodes[i].layoutPosY;
if(x > maxx) maxx = x;
if(x < minx) minx = x;
if(y > maxy) maxy = y;
if(y < miny) miny = y;
}
this.graph.layoutMinX = minx;
this.graph.layoutMaxX = maxx;
this.graph.layoutMinY = miny;
this.graph.layoutMaxY = maxy;
},
layoutIteration: function() {
// Forces on nodes due to node-node repulsions
var prev = [];
for(var c in this.graph.nodes) {
var node1 = this.graph.nodes[c];
for (var d in prev) {
var node2 = this.graph.nodes[prev[d]];
this.layoutRepulsive(node1, node2);
}
prev.push(c);
}
// Forces on nodes due to edge attractions
for (var i = 0; i < this.graph.edges.length; i++) {
var edge = this.graph.edges[i];
this.layoutAttractive(edge);
}
// Move by the given force
for (i in this.graph.nodes) {
var node = this.graph.nodes[i];
var xmove = this.c * node.layoutForceX;
var ymove = this.c * node.layoutForceY;
var max = this.maxVertexMovement;
if(xmove > max) xmove = max;
if(xmove < -max) xmove = -max;
if(ymove > max) ymove = max;
if(ymove < -max) ymove = -max;
node.layoutPosX += xmove;
node.layoutPosY += ymove;
node.layoutForceX = 0;
node.layoutForceY = 0;
}
},
layoutRepulsive: function(node1, node2) {
if (typeof node1 == 'undefined' || typeof node2 == 'undefined')
return;
var dx = node2.layoutPosX - node1.layoutPosX;
var dy = node2.layoutPosY - node1.layoutPosY;
var d2 = dx * dx + dy * dy;
if(d2 < 0.01) {
dx = 0.1 * Math.random() + 0.1;
dy = 0.1 * Math.random() + 0.1;
d2 = dx * dx + dy * dy;
}
var d = Math.sqrt(d2);
if(d < this.maxRepulsiveForceDistance) {
var repulsiveForce = this.k * this.k / d;
node2.layoutForceX += repulsiveForce * dx / d;
node2.layoutForceY += repulsiveForce * dy / d;
node1.layoutForceX -= repulsiveForce * dx / d;
node1.layoutForceY -= repulsiveForce * dy / d;
}
},
layoutAttractive: function(edge) {
var node1 = edge.source;
var node2 = edge.target;
var dx = node2.layoutPosX - node1.layoutPosX;
var dy = node2.layoutPosY - node1.layoutPosY;
var d2 = dx * dx + dy * dy;
if(d2 < 0.01) {
dx = 0.1 * Math.random() + 0.1;
dy = 0.1 * Math.random() + 0.1;
d2 = dx * dx + dy * dy;
}
var d = Math.sqrt(d2);
if(d > this.maxRepulsiveForceDistance) {
d = this.maxRepulsiveForceDistance;
d2 = d * d;
}
var attractiveForce = (d2 - this.k * this.k) / this.k;
if(edge.attraction === undefined) edge.attraction = 1;
attractiveForce *= Math.log(edge.attraction) * 0.5 + 1;
node2.layoutForceX -= attractiveForce * dx / d;
node2.layoutForceY -= attractiveForce * dy / d;
node1.layoutForceX += attractiveForce * dx / d;
node1.layoutForceY += attractiveForce * dy / d;
}
};
Dracula.Layout.Ordered = function(graph, order) {
this.graph = graph;
this.order = order;
this.layout();
};
Dracula.Layout.Ordered.prototype = {
layout: function() {
this.layoutPrepare();
this.layoutCalcBounds();
},
layoutPrepare: function(order) {
var node, i;
for (i in this.graph.nodes) {
node = this.graph.nodes[i];
node.layoutPosX = 0;
node.layoutPosY = 0;
}
var counter = 0;
for (i in this.order) {
node = this.order[i];
node.layoutPosX = counter;
node.layoutPosY = Math.random();
counter++;
}
},
layoutCalcBounds: function() {
var minx = Infinity, maxx = -Infinity,
miny = Infinity, maxy = -Infinity;
var i;
for (i in this.graph.nodes) {
var x = this.graph.nodes[i].layoutPosX;
var y = this.graph.nodes[i].layoutPosY;
if(x > maxx) maxx = x;
if(x < minx) minx = x;
if(y > maxy) maxy = y;
if(y < miny) miny = y;
}
this.graph.layoutMinX = minx;
this.graph.layoutMaxX = maxx;
this.graph.layoutMinY = miny;
this.graph.layoutMaxY = maxy;
}
};
Dracula.Layout.OrderedTree = function(graph, order) {
this.graph = graph;
this.order = order;
this.layout();
};
/*
* OrderedTree is like Ordered but assumes there is one root
* This way we can give non random positions to nodes on the Y-axis
* it assumes the ordered nodes are of a perfect binary tree
*/
Dracula.Layout.OrderedTree.prototype = {
layout: function() {
this.layoutPrepare();
this.layoutCalcBounds();
},
layoutPrepare: function(order) {
var node, i;
for (i in this.graph.nodes) {
node = this.graph.nodes[i];
node.layoutPosX = 0;
node.layoutPosY = 0;
}
//to reverse the order of rendering, we need to find out the
//absolute number of levels we have. simple log math applies.
var numNodes = this.order.length;
var totalLevels = Math.floor(Math.log(numNodes) / Math.log(2));
var counter = 1;
for (i in this.order) {
node = this.order[i];
//rank aka x coordinate
var rank = Math.floor(Math.log(counter) / Math.log(2));
//file relative to top
var file = counter - Math.pow(rank, 2);
node.layoutPosX = totalLevels - rank;
node.layoutPosY = file;
counter++;
}
},
layoutCalcBounds: function() {
var minx = Infinity, maxx = -Infinity,
miny = Infinity, maxy = -Infinity;
var i;
for (i in this.graph.nodes) {
var x = this.graph.nodes[i].layoutPosX;
var y = this.graph.nodes[i].layoutPosY;
if(x > maxx) maxx = x;
if(x < minx) minx = x;
if(y > maxy) maxy = y;
if(y < miny) miny = y;
}
this.graph.layoutMinX = minx;
this.graph.layoutMaxX = maxx;
this.graph.layoutMinY = miny;
this.graph.layoutMaxY = maxy;
}
};
Dracula.Layout.TournamentTree = function(graph, order) {
this.graph = graph;
this.order = order;
this.layout();
};
/*
* TournamentTree looks more like a binary tree
*/
Dracula.Layout.TournamentTree.prototype = {
layout: function() {
this.layoutPrepare();
this.layoutCalcBounds();
},
layoutPrepare: function(order) {
var node, i;
for (i in this.graph.nodes) {
node = this.graph.nodes[i];
node.layoutPosX = 0;
node.layoutPosY = 0;
}
//to reverse the order of rendering, we need to find out the
//absolute number of levels we have. simple log math applies.
var numNodes = this.order.length;
var totalLevels = Math.floor(Math.log(numNodes) / Math.log(2));
var counter = 1;
for (i in this.order) {
node = this.order[i];
var depth = Math.floor(Math.log(counter) / Math.log(2));
var xpos = counter - Math.pow(depth, 2);
var offset = Math.pow(2, totalLevels - depth);
var final_x = offset + (counter - Math.pow(2, depth)) *
Math.pow(2, (totalLevels - depth) + 1);
node.layoutPosX = final_x;
node.layoutPosY = depth;
counter++;
}
},
layoutCalcBounds: function() {
var minx = Infinity, maxx = -Infinity,
miny = Infinity, maxy = -Infinity;
var i;
for (i in this.graph.nodes) {
var x = this.graph.nodes[i].layoutPosX;
var y = this.graph.nodes[i].layoutPosY;
if(x > maxx) maxx = x;
if(x < minx) minx = x;
if(y > maxy) maxy = y;
if(y < miny) miny = y;
}
this.graph.layoutMinX = minx;
this.graph.layoutMaxX = maxx;
this.graph.layoutMinY = miny;
this.graph.layoutMaxY = maxy;
}
};
/*
* Raphael Tooltip Plugin
* - attaches an element as a tooltip to another element
*
* Usage example, adding a rectangle as a tooltip to a circle:
*
* paper.circle(100,100,10).tooltip(paper.rect(0,0,20,30));
*
* If you want to use more shapes, you'll have to put them into a set.
*
*/
Raphael.el.tooltip = function (tp) {
this.tp = tp;
this.tp.o = {x: 0, y: 0};
this.tp.hide();
this.hover(
function(event){
this.mousemove(function(event){
this.tp.translate(event.clientX -
this.tp.o.x,event.clientY - this.tp.o.y);
this.tp.o = {x: event.clientX, y: event.clientY};
});
this.tp.show().toFront();
},
function(event){
this.tp.hide();
this.unmousemove();
});
return this;
};
/* For IE */
if (!Array.prototype.forEach)
{
Array.prototype.forEach = function(fun /*, thisp*/)
{
var len = this.length;
if (typeof fun != "function")
throw new TypeError();
var thisp = arguments[1];
for (var i = 0; i < len; i++)
{
if (i in this)
fun.call(thisp, this[i], i, this);
}
};
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
/*!
* routie - a tiny hash router
* v0.3.2
* http://projects.jga.me/routie
* copyright Greg Allen 2013
* MIT License
*/
(function(n){var e=[],t={},r="routie",o=n[r],i=function(n,e){this.name=e,this.path=n,this.keys=[],this.fns=[],this.params={},this.regex=a(this.path,this.keys,!1,!1)};i.prototype.addHandler=function(n){this.fns.push(n)},i.prototype.removeHandler=function(n){for(var e=0,t=this.fns.length;t>e;e++){var r=this.fns[e];if(n==r)return this.fns.splice(e,1),void 0}},i.prototype.run=function(n){for(var e=0,t=this.fns.length;t>e;e++)this.fns[e].apply(this,n)},i.prototype.match=function(n,e){var t=this.regex.exec(n);if(!t)return!1;for(var r=1,o=t.length;o>r;++r){var i=this.keys[r-1],a="string"==typeof t[r]?decodeURIComponent(t[r]):t[r];i&&(this.params[i.name]=a),e.push(a)}return!0},i.prototype.toURL=function(n){var e=this.path;for(var t in n)e=e.replace("/:"+t,"/"+n[t]);if(e=e.replace(/\/:.*\?/g,"/").replace(/\?/g,""),-1!=e.indexOf(":"))throw Error("missing parameters for url: "+e);return e};var a=function(n,e,t,r){return n instanceof RegExp?n:(n instanceof Array&&(n="("+n.join("|")+")"),n=n.concat(r?"":"/?").replace(/\/\(/g,"(?:/").replace(/\+/g,"__plus__").replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g,function(n,t,r,o,i,a){return e.push({name:o,optional:!!a}),t=t||"",""+(a?"":t)+"(?:"+(a?t:"")+(r||"")+(i||r&&"([^/.]+?)"||"([^/]+?)")+")"+(a||"")}).replace(/([\/.])/g,"\\$1").replace(/__plus__/g,"(.+)").replace(/\*/g,"(.*)"),RegExp("^"+n+"$",t?"":"i"))},s=function(n,r){var o=n.split(" "),a=2==o.length?o[0]:null;n=2==o.length?o[1]:o[0],t[n]||(t[n]=new i(n,a),e.push(t[n])),t[n].addHandler(r)},h=function(n,e){if("function"==typeof e)s(n,e),h.reload();else if("object"==typeof n){for(var t in n)s(t,n[t]);h.reload()}else e===void 0&&h.navigate(n)};h.lookup=function(n,t){for(var r=0,o=e.length;o>r;r++){var i=e[r];if(i.name==n)return i.toURL(t)}},h.remove=function(n,e){var r=t[n];r&&r.removeHandler(e)},h.removeAll=function(){t={},e=[]},h.navigate=function(n,e){e=e||{};var t=e.silent||!1;t&&l(),setTimeout(function(){window.location.hash=n,t&&setTimeout(function(){p()},1)},1)},h.noConflict=function(){return n[r]=o,h};var f=function(){return window.location.hash.substring(1)},c=function(n,e){var t=[];return e.match(n,t)?(e.run(t),!0):!1},u=h.reload=function(){for(var n=f(),t=0,r=e.length;r>t;t++){var o=e[t];if(c(n,o))return}},p=function(){n.addEventListener?n.addEventListener("hashchange",u,!1):n.attachEvent("onhashchange",u)},l=function(){n.removeEventListener?n.removeEventListener("hashchange",u):n.detachEvent("onhashchange",u)};p(),n[r]=h})(window);

View File

@ -0,0 +1,535 @@
package coursier
package web
import coursier.core.{Resolver, Logger, Remote}
import japgolly.scalajs.react.vdom.{TagMod, Attr}
import japgolly.scalajs.react.vdom.Attrs.dangerouslySetInnerHtml
import japgolly.scalajs.react.{ReactKeyboardEventI, ReactEventI, ReactComponentB, BackendScope}
import japgolly.scalajs.react.vdom.prefix_<^._
import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
import org.scalajs.jquery.jQuery
import scala.scalajs.js
import js.Dynamic.{global => g}
case class ResolutionOptions(followOptional: Boolean = false,
keepTest: Boolean = false)
case class State(modules: Seq[Dependency],
repositories: Seq[Remote],
options: ResolutionOptions,
resolutionOpt: Option[Resolution],
editModuleIdx: Int,
resolving: Boolean,
reverseTree: Boolean,
log: Seq[String])
class Backend($: BackendScope[Unit, State]) {
def updateDepGraph(resolution: Resolution) = {
println("Rendering canvas")
val graph = js.Dynamic.newInstance(g.Graph)()
var nodes = Set.empty[String]
def addNode(name: String) =
if (!nodes(name)) {
graph.addNode(name)
nodes += name
}
for {
((org, name, scope), par) <- resolution.reverseDependenciesAndExclusions.toList
from = s"$org:$name:${scope.name}"
_ = addNode(from)
((parOrg, parName, parScope), _) <- par
to = s"$parOrg:$parName:${parScope.name}"
_ = addNode(to)
} {
graph.addEdge(from, to)
}
val layouter = js.Dynamic.newInstance(g.Graph.Layout.Spring)(graph)
layouter.layout()
val width = jQuery("#dependencies").width()
val height = math.max(jQuery("#dependencies").height().asInstanceOf[Int], 400)
println(s"width: $width, height: $height")
jQuery("#depgraphcanvas").html("") //empty()
val renderer = js.Dynamic.newInstance(g.Graph.Renderer.Raphael)("depgraphcanvas", graph, width, height)
renderer.draw()
println("Rendered canvas")
}
def updateDepGraphBtn(resolution: Resolution)(e: ReactEventI) = {
updateDepGraph(resolution)
}
def updateTree(resolution: Resolution, target: String, reverse: Boolean) = {
def depsOf(dep: Dependency) =
resolution.projectsCache.get(dep.module).toSeq.flatMap(t => Resolver.finalDependencies(dep, t._2).filter(resolution.filter getOrElse Resolver.defaultFilter))
lazy val reverseDeps = {
var m = Map.empty[Module, Seq[Dependency]]
for {
dep <- resolution.dependencies
trDep <- depsOf(dep)
} {
m += trDep.module -> (m.getOrElse(trDep.module, Nil) :+ dep)
}
m
}
def tree(dep: Dependency): js.Dictionary[js.Any] =
js.Dictionary(Seq(
"text" -> (s"${dep.module}": js.Any)
) ++ {
val deps = if (reverse) reverseDeps.getOrElse(dep.module, Nil) else depsOf(dep)
if (deps.isEmpty) Seq()
else Seq("nodes" -> js.Array(deps.map(tree): _*))
}: _*)
println(resolution.dependencies.toList.map(tree).map(js.JSON.stringify(_)))
g.$(target).treeview(js.Dictionary("data" -> js.Array(resolution.dependencies.toList.map(tree): _*)))
}
def resolve(action: => Unit = ()) = {
g.$("#resLogTab a:last").tab("show")
$.modState(_.copy(resolving = true, log = Nil))
val logger: Logger = new Logger {
def fetched(url: String) = {
println(s"Fetching $url")
$.modState(s => s.copy(log = s"Fetching $url" +: s.log))
}
def fetching(url: String) = {
println(s"Fetched $url")
$.modState(s => s.copy(log = s"Fetched $url" +: s.log))
}
def other(url: String, msg: String) = {
println(s"$url: $msg")
$.modState(s => s.copy(log = s"$url: $msg" +: s.log))
}
}
val s = $.state
val task = coursier.resolve(
s.modules.toSet,
fetchFrom(s.repositories.map(_.copy(logger = Some(logger)))),
filter = Some(dep => (s.options.followOptional || !dep.optional) && (s.options.keepTest || dep.scope != Scope.Test))
)
task.runF.foreach { res: Resolution =>
$.modState{ s => updateDepGraph(res); updateTree(res, "#deptree", reverse = s.reverseTree); s.copy(resolutionOpt = Some(res), resolving = false)}
g.$("#resResTab a:last").tab("show")
}
}
def handleResolve(e: ReactEventI) = {
println(s"Resolving")
e.preventDefault()
jQuery("#results").css("display", "block")
resolve()
}
def clearLog(e: ReactEventI) = {
$.modState(_.copy(log = Nil))
}
def toggleReverseTree(e: ReactEventI) = {
$.modState{ s =>
for (res <- s.resolutionOpt)
updateTree(res, "#deptree", reverse = !s.reverseTree)
s.copy(reverseTree = !s.reverseTree)
}
}
def editModule(idx: Int)(e: ReactEventI) = {
e.preventDefault()
$.modState(_.copy(editModuleIdx = idx))
}
def removeModule(idx: Int)(e: ReactEventI) = {
e.preventDefault()
$.modState(s => s.copy(modules = s.modules.zipWithIndex.filter(_._2 != idx).map(_._1)))
}
def updateModule(moduleIdx: Int, update: (Dependency, String) => Dependency)(e: ReactEventI) = {
if (moduleIdx >= 0) {
$.modState{ state =>
val dep = state.modules(moduleIdx)
state.copy(modules = state.modules.updated(moduleIdx, update(dep, e.target.value)))
}
}
}
def addModule(e: ReactEventI) = {
e.preventDefault()
$.modState{ state =>
val modules = state.modules :+ Dependency(Module("", "", ""))
println(s"Modules:\n${modules.mkString("\n")}")
state.copy(modules = modules, editModuleIdx = modules.length - 1)
}
}
object options {
def toggleOptional(e: ReactEventI) = {
$.modState(s => s.copy(options = s.options.copy(followOptional = !s.options.followOptional)))
}
def toggleTest(e: ReactEventI) = {
$.modState(s => s.copy(options = s.options.copy(keepTest = !s.options.keepTest)))
}
}
}
object App {
lazy val arbor = g.arbor
val resultDependencies = ReactComponentB[Resolution]("Result")
.render{ res =>
def depItem(dep: Dependency) =
<.tr(
^.`class` := (if (res.errors.contains(dep.module)) "danger" else ""),
<.td(dep.module.organization),
<.td(dep.module.name),
<.td(dep.module.version),
<.td(Seq[Seq[TagMod]](
if (dep.scope == Scope.Compile) Seq() else Seq(<.span(^.`class` := "label label-info", dep.scope.name)),
if (dep.`type`.isEmpty || dep.`type` == "jar") Seq() else Seq(<.span(^.`class` := "label label-info", dep.`type`)),
if (dep.classifier.isEmpty) Seq() else Seq(<.span(^.`class` := "label label-info", dep.classifier)),
if (dep.optional) Seq(<.span(^.`class` := "label label-info", dep.classifier)) else Seq()
)),
<.td(Seq[Seq[TagMod]](
res.projectsCache.get(dep.module) match {
case Some((repo: Remote, _)) =>
// FIXME Maven specific, generalize if/when adding support for Ivy
val relPath =
dep.module.organization.split('.').toSeq ++ Seq(
dep.module.name,
dep.module.version,
s"${dep.module.name}-${dep.module.version}"
)
Seq(
<.a(^.href := s"${repo.base}${relPath.mkString("/")}.pom",
<.span(^.`class` := "label label-info", "POM")
),
<.a(^.href := s"${repo.base}${relPath.mkString("/")}.jar",
<.span(^.`class` := "label label-info", "JAR")
)
)
case _ => Seq()
}
))
)
val sortedDeps = res.dependencies.toList
.sortBy(dep => coursier.core.Module.unapply(dep.module).get)
<.table(^.`class` := "table",
<.thead(
<.tr(
<.th("Organization"),
<.th("Name"),
<.th("Version"),
<.th("Extra"),
<.th("Links")
)
),
<.tbody(
sortedDeps.map(depItem)
)
// <.div(dangerouslySetInnerHtml("<script>$(function () { $('[data-toggle=\"popover\"]').popover() })</script>"))
)
}
.build
object icon {
def apply(id: String) = <.span(^.`class` := s"glyphicon glyphicon-$id", ^.aria.hidden := "true")
def ok = apply("ok")
def edit = apply("pencil")
def remove = apply("remove")
}
val moduleEditModal = ReactComponentB[(Module, Int, Backend)]("EditModule")
.render{ P =>
val (module, moduleIdx, backend) = P
<.div(^.`class` := "modal fade", ^.id := "moduleEdit", ^.role := "dialog", ^.aria.labelledby := "moduleEditTitle",
<.div(^.`class` := "modal-dialog", <.div(^.`class` := "modal-content",
<.div(^.`class` := "modal-header",
<.button(^.`type` := "button", ^.`class` := "close", Attr("data-dismiss") := "modal", ^.aria.label := "Close",
<.span(^.aria.hidden := "true", dangerouslySetInnerHtml("&times;"))
),
<.h4(^.`class` := "modal-title", ^.id := "moduleEditTitle", "Dependency")
),
<.div(^.`class` := "modal-body",
<.form(
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputOrganization", "Organization"),
<.input(^.`class` := "form-control", ^.id := "inputOrganization", ^.placeholder := "Organization",
^.onChange ==> backend.updateModule(moduleIdx, (dep, value) => dep.copy(module = dep.module.copy(organization = value))),
^.value := module.organization
)
),
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputName", "Name"),
<.input(^.`class` := "form-control", ^.id := "inputName", ^.placeholder := "Name",
^.onChange ==> backend.updateModule(moduleIdx, (dep, value) => dep.copy(module = dep.module.copy(name = value))),
^.value := module.name
)
),
<.div(^.`class` := "form-group",
<.label(^.`for` := "inputVersion", "Version"),
<.input(^.`class` := "form-control", ^.id := "inputVersion", ^.placeholder := "Version",
^.onChange ==> backend.updateModule(moduleIdx, (dep, value) => dep.copy(module = dep.module.copy(version = value))),
^.value := module.version
)
),
<.div(^.`class` := "modal-footer",
<.button(^.`type` := "submit", ^.`class` := "btn btn-primary", Attr("data-dismiss") := "modal", "Done")
)
)
)
))
)
}
.build
def dependenciesTable(name: String) = ReactComponentB[(Seq[Dependency], Int, Backend)](name)
.render{ P =>
val (deps, editModuleIdx, backend) = P
def depItem(dep: Dependency, idx: Int) =
<.tr(
<.td(dep.module.organization),
<.td(dep.module.name),
<.td(dep.module.version),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#moduleEdit", ^.`class` := "icon-action",
^.onClick ==> backend.editModule(idx),
icon.edit
)
),
<.td(
<.a(Attr("data-toggle") := "modal", Attr("data-target") := "#moduleRemove", ^.`class` := "icon-action",
^.onClick ==> backend.removeModule(idx),
icon.remove
)
)
)
<.div(
<.p(
<.button(^.`type` := "button", ^.`class` := "btn btn-default customButton",
^.onClick ==> backend.addModule,
Attr("data-toggle") := "modal", Attr("data-target") := "#moduleEdit",
"Add"
)
),
<.table(^.`class` := "table",
<.thead(
<.tr(
<.th("Organization"),
<.th("Name"),
<.th("Version"),
<.th(""),
<.th("")
)
),
<.tbody(
deps.zipWithIndex.map((depItem _).tupled)
)
),
moduleEditModal((deps.lift(editModuleIdx).fold(Module("", "", ""))(_.module), editModuleIdx, backend))
)
}
.build
val modules = dependenciesTable("Dependencies")
val repositories = ReactComponentB[Seq[Remote]]("Repositories")
.render{ repos =>
def repoItem(repo: Remote) =
<.tr(
<.td(
<.a(^.href := repo.base,
repo.base
)
)
)
val sortedRepos = repos
.sortBy(repo => repo.base)
<.table(^.`class` := "table",
<.thead(
<.tr(
<.th("Base URL")
)
),
<.tbody(
sortedRepos.map(repoItem)
)
)
}
.build
val options = ReactComponentB[(ResolutionOptions, Backend)]("ResolutionOptions")
.render{ P =>
val (options, backend) = P
<.div(
<.div(^.`class` := "checkbox",
<.label(
<.input(^.`type` := "checkbox",
^.onChange ==> backend.options.toggleOptional,
if (options.followOptional) Seq(^.checked := "checked") else Seq(),
"Follow optional dependencies"
)
)
),
<.div(^.`class` := "checkbox",
<.label(
<.input(^.`type` := "checkbox",
^.onChange ==> backend.options.toggleTest,
if (options.keepTest) Seq(^.checked := "checked") else Seq(),
"Keep test dependencies"
)
)
)
)
}
.build
val resolution = ReactComponentB[Option[Resolution]]("Resolution")
.render(resOpt =>
resOpt match {
case Some(res) =>
<.div(
<.div(^.`class` := "page-header",
<.h1("Resolution")
),
resultDependencies(res)
)
case None =>
<.div()
}
)
.build
val initialState = State(Nil, Seq(coursier.repository.mavenCentral), ResolutionOptions(), None, -1, resolving = false, reverseTree = false, log = Nil)
val app = ReactComponentB[Unit]("Coursier")
.initialState(initialState)
.backend(new Backend(_))
.render((_,S,B) =>
<.div(
<.div(^.role := "tabpanel",
<.ul(^.`class` := "nav nav-tabs", ^.role := "tablist",
<.li(^.role := "presentation", ^.`class` := "active",
<.a(^.href := "#dependencies", ^.aria.controls := "dependencies", ^.role := "tab", Attr("data-toggle") := "tab",
s"Dependencies (${S.modules.length})"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#repositories", ^.aria.controls := "repositories", ^.role := "tab", Attr("data-toggle") := "tab",
s"Repositories (${S.repositories.length})"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#options", ^.aria.controls := "options", ^.role := "tab", Attr("data-toggle") := "tab",
"Options"
)
)
),
<.div(^.`class` := "tab-content",
<.div(^.role := "tabpanel", ^.`class` := "tab-pane active", ^.id := "dependencies",
modules((S.modules, S.editModuleIdx, B))
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "repositories",
repositories(S.repositories)
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "options",
options((S.options, B))
)
)
),
<.div(<.form(^.onSubmit ==> B.handleResolve,
<.button(^.`type` := "submit", ^.id := "resolveButton", ^.`class` := "btn btn-lg btn-primary",
if (S.resolving) ^.disabled := "true" else Attr("active") := "true",
if (S.resolving) "Resolving..." else "Resolve"
)
)),
<.div(^.role := "tabpanel", ^.id := "results",
<.ul(^.`class` := "nav nav-tabs", ^.role := "tablist", ^.id := "resTabs",
<.li(^.role := "presentation", ^.id := "resResTab",
<.a(^.href := "#resolution", ^.aria.controls := "resolution", ^.role := "tab", Attr("data-toggle") := "tab",
"Resolution"
)
),
<.li(^.role := "presentation", ^.id := "resLogTab",
<.a(^.href := "#log", ^.aria.controls := "log", ^.role := "tab", Attr("data-toggle") := "tab",
"Log"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#depgraph", ^.aria.controls := "depgraph", ^.role := "tab", Attr("data-toggle") := "tab",
"Graph"
)
),
<.li(^.role := "presentation",
<.a(^.href := "#deptreepanel", ^.aria.controls := "deptreepanel", ^.role := "tab", Attr("data-toggle") := "tab",
"Tree"
)
)
),
<.div(^.`class` := "tab-content",
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "resolution",
resolution(S.resolutionOpt)
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "log",
<.button(^.`type` := "button", ^.`class` := "btn btn-default",
^.onClick ==> B.clearLog,
"Clear"
),
<.div(^.`class` := "well",
<.ul(^.`class` := "log",
S.log.map(e => <.li(e))
)
)
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "depgraph",
<.button(^.`type` := "button", ^.`class` := "btn btn-default",
^.onClick ==> B.updateDepGraphBtn(S.resolutionOpt.getOrElse(Resolution.empty)),
"Redraw"
),
<.div(^.id := "depgraphcanvas")
),
<.div(^.role := "tabpanel", ^.`class` := "tab-pane", ^.id := "deptreepanel",
<.div(^.`class` := "checkbox",
<.label(
<.input(^.`type` := "checkbox",
^.onChange ==> B.toggleReverseTree,
if (S.reverseTree) Seq(^.checked := "checked") else Seq(),
"Reverse"
)
)
),
<.div(^.id := "deptree")
)
)
)
)
)
.buildU
}

View File

@ -0,0 +1,13 @@
package coursier.web
import japgolly.scalajs.react.React
import scala.scalajs.js.annotation.JSExport
import org.scalajs.dom.document
@JSExport
object Main {
@JSExport
def main(): Unit = {
React.render(App.app("Coursier"), document.getElementById("demoContent"))
}
}