mirror of https://github.com/sbt/sbt.git
Initial commit
This commit is contained in:
commit
5e25748d83
|
|
@ -0,0 +1 @@
|
|||
target/
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
@ -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.
|
||||
|
|
@ -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")
|
||||
}
|
||||
```
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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] = ???
|
||||
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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/")
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package coursier.test
|
||||
|
||||
package object compatibility {
|
||||
|
||||
implicit val executionContext = scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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/"))
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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])
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 )
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
))
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
sbt.version=0.13.8
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
version in ThisBuild := "0.1.0-SNAPSHOT"
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
|
|
@ -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("×"))
|
||||
),
|
||||
<.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
|
||||
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue