Merge pull request #55 from alexarchambault/develop

Last developments
This commit is contained in:
Alexandre Archambault 2015-07-04 16:55:19 +02:00
commit 9a164f7b75
18 changed files with 706 additions and 324 deletions

View File

@ -4,32 +4,69 @@ package cli
import java.io.File
import caseapp._
import coursier.core.{Fetch, Parse, Repository}, Repository.CachePolicy
import coursier.core.{ MavenRepository, Parse, CachePolicy }
import scalaz.{ \/-, -\/ }
import scalaz.concurrent.Task
case class Coursier(scope: List[String],
keepOptional: Boolean,
fetch: Boolean,
@ExtraName("v") verbose: List[Unit],
@ExtraName("N") maxIterations: Int = 100) extends App {
case class Coursier(
scope: List[String],
keepOptional: Boolean,
fetch: Boolean,
@ExtraName("J") default: Boolean,
@ExtraName("S") sources: Boolean,
@ExtraName("D") javadoc: Boolean,
@ExtraName("P") @ExtraName("cp") classpath: Boolean,
@ExtraName("c") offline: Boolean,
@ExtraName("f") force: Boolean,
@ExtraName("q") quiet: Boolean,
@ExtraName("v") verbose: List[Unit],
@ExtraName("N") maxIterations: Int = 100,
@ExtraName("r") repository: List[String],
@ExtraName("n") parallel: Option[Int]
) extends App {
val verbose0 = verbose.length
val verbose0 = {
verbose.length +
(if (quiet) 1 else 0)
}
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/metadata/central")
val centralFilesCacheDir = new File(sys.props("user.home") + "/.coursier/cache/files/central")
def fileRepr(f: File) = f.toString
val logger: Fetch.Logger with FilesLogger =
new Fetch.Logger with FilesLogger {
def println(s: String) = Console.err.println(s)
def println(s: String) = Console.err.println(s)
if (force && offline) {
println("Error: --offline (-c) and --force (-f) options can't be specified at the same time.")
sys.exit(255)
}
def defaultLogger: MavenRepository.Logger with Files.Logger =
new MavenRepository.Logger with Files.Logger {
def downloading(url: String) =
println(s"Downloading $url")
def downloaded(url: String, success: Boolean) =
if (!success)
println(s"Failed to download $url")
def readingFromCache(f: File) = {}
def puttingInCache(f: File) = {}
def foundLocally(f: File) = {}
def downloadingArtifact(url: String) =
println(s"Downloading $url")
def downloadedArtifact(url: String, success: Boolean) =
if (!success)
println(s"Failed to download $url")
}
def verboseLogger: MavenRepository.Logger with Files.Logger =
new MavenRepository.Logger with Files.Logger {
def downloading(url: String) =
println(s"Downloading $url")
def downloaded(url: String, success: Boolean) =
@ -54,20 +91,62 @@ case class Coursier(scope: List[String],
)
}
val cachedMavenCentral = Repository.mavenCentral.copy(
fetch = Repository.mavenCentral.fetch.copy(
cache = Some(centralCacheDir),
logger = if (verbose0 <= 1) None else Some(logger)
val logger =
if (verbose0 < 0)
None
else if (verbose0 == 0)
Some(defaultLogger)
else
Some(verboseLogger)
implicit val cachePolicy =
if (offline)
CachePolicy.LocalOnly
else if (force)
CachePolicy.ForceDownload
else
CachePolicy.Default
val cache = Cache.default
val repositoryIds = {
val repository0 = repository
.flatMap(_.split(','))
.map(_.trim)
.filter(_.nonEmpty)
if (repository0.isEmpty)
cache.default()
else
repository0
}
val existingRepo = cache
.list()
.map(_._1)
.toSet
if (repositoryIds.exists(!existingRepo(_))) {
val notFound = repositoryIds
.filter(!existingRepo(_))
Console.err.println(
(if (notFound.lengthCompare(1) == 1) "Repository" else "Repositories") +
" not found: " +
notFound.mkString(", ")
)
)
val repositories = Seq[Repository](
cachedMavenCentral,
Repository.ivy2Local.copy(
fetch = Repository.ivy2Local.fetch.copy(
logger = if (verbose0 <= 1) None else Some(logger)
)
)
)
sys.exit(1)
}
val (repositories0, fileCaches) = cache
.list()
.map{case (_, repo, cacheEntry) => (repo, cacheEntry)}
.unzip
val repositories = repositories0
.map(_.copy(logger = logger))
val (splitDependencies, malformed) = remainingArgs.toList
.map(_.split(":", 3).toSeq)
@ -79,7 +158,7 @@ case class Coursier(scope: List[String],
}
if (malformed.nonEmpty) {
Console.err.println(s"Malformed dependencies:\n${malformed.map(_.mkString(":")).mkString("\n")}")
println(s"Malformed dependencies:\n${malformed.map(_.mkString(":")).mkString("\n")}")
sys exit 1
}
@ -109,13 +188,16 @@ case class Coursier(scope: List[String],
print.flatMap(_ => fetchQuiet(modVers))
}
if (verbose0 >= 0)
println(s"Resolving\n" + moduleVersions.map{case (mod, ver) => s" $mod:$ver"}.mkString("\n"))
val res = startRes
.process
.run(fetch0, maxIterations)
.run
if (!res.isDone) {
Console.err.println(s"Maximum number of iteration reached!")
println(s"Maximum number of iteration reached!")
sys exit 1
}
@ -131,7 +213,8 @@ case class Coursier(scope: List[String],
val trDeps = res.minDependencies.toList.sortBy(repr)
println("\n" + trDeps.map(repr).distinct.mkString("\n"))
if (verbose0 >= 0)
println("\n" + trDeps.map(repr).distinct.mkString("\n"))
if (res.conflicts.nonEmpty) {
// Needs test
@ -146,25 +229,56 @@ case class Coursier(scope: List[String],
}
}
if (fetch) {
println()
if (fetch || classpath) {
println("")
val cachePolicy: CachePolicy = CachePolicy.Default
val artifacts0 = res.artifacts
val default0 = default || (!sources && !javadoc)
val artifacts = artifacts0
.flatMap{ artifact =>
var l = List.empty[Artifact]
if (sources)
l = artifact.extra.get("sources").toList ::: l
if (javadoc)
l = artifact.extra.get("javadoc").toList ::: l
if (default0)
l = artifact :: l
val artifacts = res.artifacts
l
}
val files = new Files(
Seq(
cachedMavenCentral.fetch.root -> centralFilesCacheDir
),
() => ???,
if (verbose0 <= 0) None else Some(logger)
val files = {
var files0 = cache
.files()
.copy(logger = logger)
for (n <- parallel)
files0 = files0.copy(concurrentDownloadCount = n)
files0
}
val tasks = artifacts.map(artifact => files.file(artifact, cachePolicy).run.map(artifact.->))
def printTask = Task{
if (verbose0 >= 0 && artifacts.nonEmpty)
println(s"Found ${artifacts.length} artifacts")
}
val task = printTask.flatMap(_ => Task.gatherUnordered(tasks))
val results = task.run
val errors = results.collect{case (artifact, -\/(err)) => artifact -> err }
val files0 = results.collect{case (artifact, \/-(f)) => f }
if (errors.nonEmpty) {
println(s"${errors.size} error(s):")
for ((artifact, error) <- errors) {
println(s" ${artifact.url}: $error")
}
}
Console.out.println(
files0
.map(_.toString)
.mkString(if (classpath) File.pathSeparator else "\n")
)
val tasks = artifacts.map(files.file(_, cachePolicy).run)
val task = Task.gatherUnordered(tasks)
task.run
}
}

View File

@ -0,0 +1,87 @@
package coursier.cli
import coursier.Cache
import caseapp._
// TODO: allow removing a repository (with confirmations, etc.)
case class Repositories(
@ValueDescription("id:baseUrl") @ExtraName("a") add: List[String],
@ExtraName("L") list: Boolean,
@ExtraName("l") defaultList: Boolean,
ivyLike: Boolean
) extends App {
if (add.exists(!_.contains(":"))) {
CaseApp.printUsage[Repositories](err = true)
sys.exit(255)
}
val add0 = add
.map{ s =>
val Seq(id, baseUrl) = s.split(":", 2).toSeq
id -> baseUrl
}
if (
add0.exists(_._1.contains("/")) ||
add0.exists(_._1.startsWith(".")) ||
add0.exists(_._1.isEmpty)
) {
CaseApp.printUsage[Repositories](err = true)
sys.exit(255)
}
val cache = Cache.default
if (cache.cache.exists() && !cache.cache.isDirectory) {
Console.err.println(s"Error: ${cache.cache} not a directory")
sys.exit(1)
}
if (!cache.cache.exists())
cache.init()
val current = cache.list().map(_._1).toSet
val alreadyAdded = add0
.map(_._1)
.filter(current)
if (alreadyAdded.nonEmpty) {
Console.err.println(s"Error: already added: ${alreadyAdded.mkString(", ")}")
sys.exit(1)
}
for ((id, baseUrl0) <- add0) {
val baseUrl =
if (baseUrl0.endsWith("/"))
baseUrl0
else
baseUrl0 + "/"
cache.add(id, baseUrl, ivyLike = ivyLike)
}
if (defaultList) {
val map = cache.repositoryMap()
for (id <- cache.default(withNotFound = true))
map.get(id) match {
case Some(repo) =>
println(s"$id: ${repo.root}" + (if (repo.ivyLike) " (Ivy-like)" else ""))
case None =>
println(s"$id (not found)")
}
}
if (list)
for ((id, repo, _) <- cache.list().sortBy(_._1)) {
println(s"$id: ${repo.root}" + (if (repo.ivyLike) " (Ivy-like)" else ""))
}
}
object Repositories extends AppOf[Repositories] {
val parser = default
}

View File

@ -1,18 +1,18 @@
package coursier
package core
import org.scalajs.dom.raw.{Event, XMLHttpRequest}
import org.scalajs.dom.raw.{ Event, XMLHttpRequest }
import scala.concurrent.{ExecutionContext, Promise, Future}
import scalaz.{-\/, \/-, EitherT}
import scala.concurrent.{ ExecutionContext, Promise, Future }
import scalaz.{ -\/, \/-, EitherT }
import scalaz.concurrent.Task
import scala.scalajs.js
import js.Dynamic.{global => g}
import js.Dynamic.{ global => g }
import scala.scalajs.js.timers._
object Fetch {
object MavenRepository {
def encodeURIComponent(s: String): String =
g.encodeURIComponent(s).asInstanceOf[String]
@ -84,18 +84,24 @@ object Fetch {
}
case class Fetch(root: String,
logger: Option[Fetch.Logger] = None) {
case class MavenRepository(
root: String,
ivyLike: Boolean = false,
logger: Option[MavenRepository.Logger] = None
) extends BaseMavenRepository(root, ivyLike) {
def apply(artifact: Artifact,
cachePolicy: Repository.CachePolicy): EitherT[Task, String, String] = {
def fetch(
artifact: Artifact,
cachePolicy: CachePolicy
): EitherT[Task, String, String] = {
val url0 = root + artifact.url
EitherT(
Task { implicit ec =>
Future(logger.foreach(_.fetching(url0)))
.flatMap(_ => Fetch.get(url0))
.flatMap(_ => MavenRepository.get(url0))
.map{ s => logger.foreach(_.fetched(url0)); \/-(s) }
.recover{case e: Exception =>
logger.foreach(_.other(url0, e.getMessage))

View File

@ -1,7 +1,7 @@
package coursier.core
import scala.scalajs.js
import js.Dynamic.{global => g}
import js.Dynamic.{ global => g }
import org.scalajs.dom.raw.NodeList
package object compatibility {
@ -21,25 +21,16 @@ package object compatibility {
def letter: Boolean = between(c, 'a', 'z') || between(c, 'A', 'Z')
}
lazy val DOMParser = {
import js.Dynamic.{global => g}
import js.DynamicImplicits._
def newFromXmlDomOrGlobal(name: String) = {
var defn = g.selectDynamic(name)
if (js.isUndefined(defn))
defn = g.require("xmldom").selectDynamic(name)
val defn =
if (js.isUndefined(g.DOMParser)) g.require("xmldom").DOMParser
else g.DOMParser
js.Dynamic.newInstance(defn)()
}
lazy val XMLSerializer = {
import js.Dynamic.{global => g}
import js.DynamicImplicits._
val defn =
if (js.isUndefined(g.XMLSerializer)) g.require("xmldom").XMLSerializer
else g.XMLSerializer
js.Dynamic.newInstance(defn)()
}
lazy val DOMParser = newFromXmlDomOrGlobal("DOMParser")
lazy val XMLSerializer = newFromXmlDomOrGlobal("XMLSerializer")
// Can't find these from node
val ELEMENT_NODE = 1 // org.scalajs.dom.raw.Node.ELEMENT_NODE

View File

@ -1,6 +1,6 @@
package scalaz
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.{ ExecutionContext, Future }
/** Minimal Future-based Task */
package object concurrent {

View File

@ -1,7 +1,7 @@
package coursier
package test
import coursier.core.{Fetch, Repository}
import coursier.core.{Repository, MavenRepository}
import coursier.test.compatibility._
import utest._
@ -18,7 +18,7 @@ object JsTests extends TestSuite {
}
'get{
Fetch.get("http://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.1.3/logback-classic-1.1.3.pom")
MavenRepository.get("http://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.1.3/logback-classic-1.1.3.pom")
.map(core.compatibility.xmlParse)
.map{ xml =>
assert(xml.right.toOption.exists(_.label == "project"))
@ -26,6 +26,8 @@ object JsTests extends TestSuite {
}
'getProj{
implicit val cachePolicy = CachePolicy.Default
Repository.mavenCentral
.find(Module("ch.qos.logback", "logback-classic"), "1.1.3")
.map{case (_, proj) =>

View File

@ -2,20 +2,25 @@ package coursier
package core
import java.io._
import java.net.{URI, URL}
import java.net.{ URI, URL }
import scala.io.Codec
import scalaz._, Scalaz._
import scalaz.concurrent.Task
case class Fetch(root: String,
cache: Option[File] = None,
logger: Option[Fetch.Logger] = None) {
case class MavenRepository(
root: String,
cache: Option[File] = None,
ivyLike: Boolean = false,
logger: Option[MavenRepository.Logger] = None
) extends BaseMavenRepository(root, ivyLike) {
val isLocal = root.startsWith("file:///")
def apply(artifact: Artifact,
cachePolicy: Repository.CachePolicy): EitherT[Task, String, String] = {
def fetch(
artifact: Artifact,
cachePolicy: CachePolicy
): EitherT[Task, String, String] = {
def locally(eitherFile: String \/ File) = {
Task {
@ -44,7 +49,7 @@ case class Fetch(root: String,
val url = new URL(urlStr)
def log = Task(logger.foreach(_.downloading(urlStr)))
def get = Fetch.readFully(url.openStream())
def get = MavenRepository.readFully(url.openStream())
log.flatMap(_ => get)
}
@ -70,7 +75,7 @@ case class Fetch(root: String,
}
object Fetch {
object MavenRepository {
trait Logger {
def downloading(url: String): Unit

View File

@ -1,6 +1,6 @@
package coursier.test
import coursier.core.Fetch
import coursier.core.MavenRepository
import scala.concurrent.{ExecutionContext, Future}
import scalaz.concurrent.Task
@ -14,10 +14,12 @@ package object compatibility {
}
def textResource(path: String)(implicit ec: ExecutionContext): Future[String] = Future {
def is = getClass.getClassLoader
.getResource(path).openStream()
def is = getClass
.getClassLoader
.getResource(path)
.openStream()
new String(Fetch.readFullySync(is), "UTF-8")
new String(MavenRepository.readFullySync(is), "UTF-8")
}
}

View File

@ -101,29 +101,12 @@ object Versions {
case class Artifact(
url: String,
extra: Map[String, String],
checksumUrls: Map[String, String],
extra: Map[String, Artifact],
attributes: Attributes
)
object Artifact {
val md5 = "md5"
val sha1 = "sha1"
val sig = "pgp"
val sigMd5 = "md5-pgp"
val sigSha1 = "sha1-pgp"
val sources = "src"
val sourcesMd5 = "md5-src"
val sourcesSha1 = "sha1-src"
val sourcesSig = "src-pgp"
val sourcesSigMd5 = "md5-src-pgp"
val sourcesSigSha1 = "sha1-src-pgp"
val javadoc = "javadoc"
val javadocMd5 = "md5-javadoc"
val javadocSha1 = "sha1-javadoc"
val javadocSig = "javadoc-pgp"
val javadocSigMd5 = "md5-javadoc-pgp"
val javadocSigSha1 = "sha1-javadoc-pgp"
trait Source {
def artifacts(dependency: Dependency, project: Project): Seq[Artifact]
}

View File

@ -1,26 +1,27 @@
package coursier.core
import coursier.core.Resolution.ModuleVersion
import scalaz.{-\/, \/-, \/, EitherT}
import scalaz.{ -\/, \/-, \/, EitherT }
import scalaz.concurrent.Task
import coursier.core.compatibility.encodeURIComponent
trait Repository {
def find(module: Module,
version: String,
cachePolicy: Repository.CachePolicy = Repository.CachePolicy.Default): EitherT[Task, String, (Artifact.Source, Project)]
def find(
module: Module,
version: String
)(implicit
cachePolicy: CachePolicy
): EitherT[Task, String, (Artifact.Source, Project)]
}
object Repository {
val mavenCentral = MavenRepository(Fetch("https://repo1.maven.org/maven2/"))
val mavenCentral = MavenRepository("https://repo1.maven.org/maven2/")
val sonatypeReleases = MavenRepository(Fetch("https://oss.sonatype.org/content/repositories/releases/"))
val sonatypeSnapshots = MavenRepository(Fetch("https://oss.sonatype.org/content/repositories/snapshots/"))
val sonatypeReleases = MavenRepository("https://oss.sonatype.org/content/repositories/releases/")
val sonatypeSnapshots = MavenRepository("https://oss.sonatype.org/content/repositories/snapshots/")
lazy val ivy2Local = MavenRepository(Fetch("file://" + sys.props("user.home") + "/.ivy2/local/"), ivyLike = true)
lazy val ivy2Local = MavenRepository("file://" + sys.props("user.home") + "/.ivy2/local/", ivyLike = true)
/**
@ -34,111 +35,144 @@ object Repository {
* version (e.g. version interval). Which version get chosen depends on
* the repository implementation.
*/
def find(repositories: Seq[Repository],
module: Module,
version: String): EitherT[Task, Seq[String], (Artifact.Source, Project)] = {
def find(
repositories: Seq[Repository],
module: Module,
version: String
)(implicit
cachePolicy: CachePolicy
): EitherT[Task, Seq[String], (Artifact.Source, Project)] = {
val lookups = repositories.map(repo => repo -> repo.find(module, version).run)
val task = lookups.foldLeft(Task.now(-\/(Nil)): Task[Seq[String] \/ (Artifact.Source, Project)]) {
case (acc, (repo, t)) =>
acc.flatMap {
case -\/(errors) =>
t.map(res => res
.flatMap{case (source, project) =>
if (project.module == module) \/-((source, project))
else -\/(s"Wrong module returned (expected: $module, got: ${project.module})")
}
.leftMap(error => error +: errors)
)
val lookups = repositories
.map(repo => repo -> repo.find(module, version).run)
case res @ \/-(_) =>
Task.now(res)
}
}
val task = lookups
.foldLeft(Task.now(-\/(Nil)): Task[Seq[String] \/ (Artifact.Source, Project)]) {
case (acc, (repo, eitherProjTask)) =>
acc
.flatMap {
case -\/(errors) =>
eitherProjTask
.map(res => res
.flatMap{case (source, project) =>
if (project.module == module) \/-((source, project))
else -\/(s"Wrong module returned (expected: $module, got: ${project.module})")
}
.leftMap(error => error +: errors)
)
EitherT(task.map(_.leftMap(_.reverse))).map {case x @ (_, proj) =>
assert(proj.module == module)
x
}
}
case res @ \/-(_) =>
Task.now(res)
}
}
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
}
EitherT(task.map(_.leftMap(_.reverse)))
.map {case x @ (_, proj) =>
assert(proj.module == module)
x
}
}
implicit class ArtifactExtensions(val underlying: Artifact) extends AnyVal {
def withDefaultChecksums: Artifact =
underlying.copy(extra = underlying.extra ++ Seq(
Artifact.md5 -> (underlying.url + ".md5"),
Artifact.sha1 -> (underlying.url + ".sha1")
underlying.copy(checksumUrls = underlying.checksumUrls ++ Seq(
"md5" -> (underlying.url + ".md5"),
"sha1" -> (underlying.url + ".sha1")
))
def withDefaultSignature: Artifact =
underlying.copy(extra = underlying.extra ++ Seq(
Artifact.sigMd5 -> (underlying.url + ".asc.md5"),
Artifact.sigSha1 -> (underlying.url + ".asc.sha1"),
Artifact.sig -> (underlying.url + ".asc")
"sig" ->
Artifact(underlying.url + ".asc", Map.empty, Map.empty, Attributes("asc", ""))
.withDefaultChecksums
))
def withJavadocSources: Artifact = {
val base = underlying.url.stripSuffix(".jar")
underlying.copy(extra = underlying.extra ++ Seq(
Artifact.sourcesMd5 -> (base + "-sources.jar.md5"),
Artifact.sourcesSha1 -> (base + "-sources.jar.sha1"),
Artifact.sources -> (base + "-sources.jar"),
Artifact.sourcesSigMd5 -> (base + "-sources.jar.asc.md5"),
Artifact.sourcesSigSha1 -> (base + "-sources.jar.asc.sha1"),
Artifact.sourcesSig -> (base + "-sources.jar.asc"),
Artifact.javadocMd5 -> (base + "-javadoc.jar.md5"),
Artifact.javadocSha1 -> (base + "-javadoc.jar.sha1"),
Artifact.javadoc -> (base + "-javadoc.jar"),
Artifact.javadocSigMd5 -> (base + "-javadoc.jar.asc.md5"),
Artifact.javadocSigSha1 -> (base + "-javadoc.jar.asc.sha1"),
Artifact.javadocSig -> (base + "-javadoc.jar.asc")
"sources" -> Artifact(base + "-sources.jar", Map.empty, Map.empty, Attributes("jar", "src")) // Are these the right attributes?
.withDefaultChecksums
.withDefaultSignature,
"javadoc" -> Artifact(base + "-javadoc.jar", Map.empty, Map.empty, Attributes("jar", "javadoc")) // Same comment as above
.withDefaultChecksums
.withDefaultSignature
))
}
}
}
object MavenRepository {
case class MavenSource(root: String, ivyLike: Boolean) extends Artifact.Source {
import Repository._
def ivyLikePath(org: String,
name: String,
version: String,
subDir: String,
baseSuffix: String,
ext: String) =
def artifacts(
dependency: Dependency,
project: Project
): Seq[Artifact] = {
def ivyLikePath0(subDir: String, baseSuffix: String, ext: String) =
BaseMavenRepository.ivyLikePath(
dependency.module.organization,
dependency.module.name,
project.version,
subDir,
baseSuffix,
ext
)
val path =
if (ivyLike)
ivyLikePath0(dependency.attributes.`type` + "s", "", dependency.attributes.`type`)
else
dependency.module.organization.split('.').toSeq ++ Seq(
dependency.module.name,
project.version,
s"${dependency.module.name}-${project.version}${Some(dependency.attributes.classifier).filter(_.nonEmpty).map("-"+_).mkString}.${dependency.attributes.`type`}"
)
var artifact =
Artifact(
root + path.mkString("/"),
Map.empty,
Map.empty,
dependency.attributes
)
.withDefaultChecksums
if (dependency.attributes.`type` == "jar") {
artifact = artifact.withDefaultSignature
artifact =
if (ivyLike) {
val srcPath = root + ivyLikePath0("srcs", "-sources", "jar").mkString("/")
val javadocPath = root + ivyLikePath0("docs", "-javadoc", "jar").mkString("/")
artifact
.copy(
extra = artifact.extra ++ Map(
"sources" -> Artifact(srcPath, Map.empty, Map.empty, Attributes("jar", "src")) // Are these the right attributes?
.withDefaultChecksums
.withDefaultSignature,
"javadoc" -> Artifact(javadocPath, Map.empty, Map.empty, Attributes("jar", "javadoc")) // Same comment as above
.withDefaultChecksums
.withDefaultSignature
))
} else
artifact
.withJavadocSources
}
Seq(artifact)
}
}
object BaseMavenRepository {
def ivyLikePath(
org: String,
name: String,
version: String,
subDir: String,
baseSuffix: String,
ext: String
) =
Seq(
org,
name,
@ -147,74 +181,25 @@ object MavenRepository {
s"$name$baseSuffix.$ext"
)
case class Source(root: String, ivyLike: Boolean) extends Artifact.Source {
import Repository._
def artifacts(dependency: Dependency,
project: Project): Seq[Artifact] = {
def ivyLikePath0(subDir: String, baseSuffix: String, ext: String) =
MavenRepository.ivyLikePath(dependency.module.organization, dependency.module.name, project.version, subDir, baseSuffix, ext)
val path =
if (ivyLike)
ivyLikePath0(dependency.attributes.`type` + "s", "", dependency.attributes.`type`)
else
dependency.module.organization.split('.').toSeq ++ Seq(
dependency.module.name,
project.version,
s"${dependency.module.name}-${project.version}${Some(dependency.attributes.classifier).filter(_.nonEmpty).map("-"+_).mkString}.${dependency.attributes.`type`}"
)
var artifact =
Artifact(
root + path.mkString("/"),
Map.empty,
dependency.attributes
)
.withDefaultChecksums
if (dependency.attributes.`type` == "jar") {
artifact = artifact.withDefaultSignature
artifact =
if (ivyLike) {
val srcPath = root + ivyLikePath0("srcs", "-sources", "jar").mkString("/")
val javadocPath = root + ivyLikePath0("docs", "-javadoc", "jar").mkString("/")
artifact
.copy(extra = artifact.extra ++ Map(
Artifact.sourcesMd5 -> (srcPath + ".md5"),
Artifact.sourcesSha1 -> (srcPath + ".sha1"),
Artifact.sources -> srcPath,
Artifact.sourcesSigMd5 -> (srcPath + ".asc.md5"),
Artifact.sourcesSigSha1 -> (srcPath + ".asc.sha1"),
Artifact.sourcesSig -> (srcPath + ".asc"),
Artifact.javadocMd5 -> (javadocPath + ".md5"),
Artifact.javadocSha1 -> (javadocPath + ".sha1"),
Artifact.javadoc -> javadocPath,
Artifact.javadocSigMd5 -> (javadocPath + ".asc.md5"),
Artifact.javadocSigSha1 -> (javadocPath + ".asc.sha1"),
Artifact.javadocSig -> (javadocPath + ".asc")
))
} else artifact.withJavadocSources
}
Seq(artifact)
}
}
}
case class MavenRepository(fetch: Fetch,
ivyLike: Boolean = false) extends Repository {
abstract class BaseMavenRepository(
root: String,
ivyLike: Boolean
) extends Repository {
def fetch(
artifact: Artifact,
cachePolicy: CachePolicy
): EitherT[Task, String, String]
import Repository._
import MavenRepository._
import BaseMavenRepository._
val source = MavenRepository.Source(fetch.root, ivyLike)
val source = MavenSource(root, ivyLike)
def projectArtifact(module: Module, version: String): Artifact = {
val path = (
if (ivyLike)
ivyLikePath(module.organization, module.name, version, "poms", "", "pom")
@ -228,10 +213,8 @@ case class MavenRepository(fetch: Fetch,
Artifact(
path.mkString("/"),
Map(
Artifact.md5 -> "",
Artifact.sha1 -> ""
),
Map.empty,
Map.empty,
Attributes("pom", "")
)
.withDefaultSignature
@ -251,6 +234,7 @@ case class MavenRepository(fetch: Fetch,
Artifact(
path.mkString("/"),
Map.empty,
Map.empty,
Attributes("pom", "")
)
.withDefaultChecksums
@ -258,8 +242,10 @@ case class MavenRepository(fetch: Fetch,
Some(artifact)
}
def versions(module: Module,
cachePolicy: CachePolicy = CachePolicy.Default): EitherT[Task, String, Versions] = {
def versions(
module: Module,
cachePolicy: CachePolicy = CachePolicy.Default
): EitherT[Task, String, Versions] = {
EitherT(
versionsArtifact(module) match {
@ -279,9 +265,11 @@ case class MavenRepository(fetch: Fetch,
)
}
def findNoInterval(module: Module,
version: String,
cachePolicy: CachePolicy): EitherT[Task, String, Project] = {
def findNoInterval(
module: Module,
version: String,
cachePolicy: CachePolicy
): EitherT[Task, String, Project] = {
EitherT {
fetch(projectArtifact(module, version), cachePolicy)
@ -297,38 +285,80 @@ case class MavenRepository(fetch: Fetch,
}
}
def find(module: Module,
version: String,
cachePolicy: CachePolicy): EitherT[Task, String, (Artifact.Source, Project)] = {
def find(
module: Module,
version: String
)(implicit
cachePolicy: CachePolicy
): EitherT[Task, String, (Artifact.Source, Project)] = {
Parse.versionInterval(version).filter(_.isValid) match {
case None => findNoInterval(module, version, cachePolicy).map((source, _))
case Some(itv) =>
versions(module, cachePolicy)
.flatMap { versions0 =>
val eitherVersion = {
val release = Version(versions0.release)
Parse.versionInterval(version)
.filter(_.isValid) match {
case None =>
findNoInterval(module, version, cachePolicy).map((source, _))
case Some(itv) =>
versions(module, cachePolicy)
.flatMap { versions0 =>
val eitherVersion = {
val release = Version(versions0.release)
if (itv.contains(release)) \/-(versions0.release)
else {
val inInterval = versions0.available
.map(Version(_))
.filter(itv.contains)
if (itv.contains(release)) \/-(versions0.release)
else {
val inInterval = versions0.available
.map(Version(_))
.filter(itv.contains)
if (inInterval.isEmpty) -\/(s"No version found for $version")
else \/-(inInterval.max.repr)
if (inInterval.isEmpty) -\/(s"No version found for $version")
else \/-(inInterval.max.repr)
}
}
eitherVersion match {
case -\/(reason) => EitherT[Task, String, (Artifact.Source, Project)](Task.now(-\/(reason)))
case \/-(version0) =>
findNoInterval(module, version0, cachePolicy)
.map(_.copy(versions = Some(versions0)))
.map((source, _))
}
}
eitherVersion match {
case -\/(reason) => EitherT[Task, String, (Artifact.Source, Project)](Task.now(-\/(reason)))
case \/-(version0) =>
findNoInterval(module, version0, cachePolicy)
.map(_.copy(versions = Some(versions0)))
.map((source, _))
}
}
}
}
}
}
sealed trait CachePolicy {
def apply[E,T](local: => Task[E \/ T])
(remote: => Task[E \/ T]): Task[E \/ T]
def saving[E,T](local: => Task[E \/ T])
(remote: => Task[E \/ T])
(save: => T => Task[Unit]): Task[E \/ T] =
apply(local)(CachePolicy.saving(remote)(save))
}
object CachePolicy {
def saving[E,T](remote: => Task[E \/ T])
(save: T => Task[Unit]): Task[E \/ T] = {
for {
res <- remote
_ <- res.fold(_ => Task.now(()), t => save(t))
} yield res
}
case object Default extends CachePolicy {
def apply[E,T](local: => Task[E \/ T])
(remote: => Task[E \/ T]): Task[E \/ T] =
local
.flatMap(res => if (res.isLeft) remote else Task.now(res))
}
case object LocalOnly extends CachePolicy {
def apply[E,T](local: => Task[E \/ T])
(remote: => Task[E \/ T]): Task[E \/ T] =
local
}
case object ForceDownload extends CachePolicy {
def apply[E,T](local: => Task[E \/ T])
(remote: => Task[E \/ T]): Task[E \/ T] =
remote
}
}

View File

@ -52,6 +52,9 @@ package object coursier {
type Scope = core.Scope
val Scope: core.Scope.type = core.Scope
type CachePolicy = core.CachePolicy
val CachePolicy: core.CachePolicy.type = core.CachePolicy
type Repository = core.Repository
val Repository: core.Repository.type = core.Repository
@ -91,6 +94,8 @@ package object coursier {
implicit def fetch(
repositories: Seq[core.Repository]
)(implicit
cachePolicy: CachePolicy
): ResolutionProcess.Fetch[Task] = {
modVers =>

View File

@ -13,6 +13,8 @@ object CentralTests extends TestSuite {
Repository.mavenCentral
)
implicit val cachePolicy = CachePolicy.Default
def resolve(deps: Set[Dependency], filter: Option[Dependency => Boolean] = None, extraRepo: Option[Repository] = None) = {
val repositories0 = extraRepo.toSeq ++ repositories

View File

@ -9,6 +9,8 @@ import coursier.test.compatibility._
object ResolutionTests extends TestSuite {
implicit val cachePolicy = CachePolicy.Default
def resolve0(deps: Set[Dependency], filter: Option[Dependency => Boolean] = None) = {
Resolution(deps, filter = filter)
.process

View File

@ -11,7 +11,7 @@ class TestRepository(projects: Map[(Module, String), Project]) extends Repositor
val source = new core.Artifact.Source {
def artifacts(dependency: Dependency, project: Project) = ???
}
def find(module: Module, version: String, cachePolicy: Repository.CachePolicy) =
def find(module: Module, version: String)(implicit cachePolicy: CachePolicy) =
EitherT(Task.now(
projects.get((module, version)).map((source, _)).toRightDisjunction("Not found")
))

View File

@ -0,0 +1,139 @@
package coursier
import java.io.{PrintWriter, File}
import coursier.core.MavenRepository
import scala.io.Source
object Cache {
def mavenRepository(lines: Seq[String]): Option[MavenRepository] = {
def isMaven =
lines
.find(_.startsWith("maven:"))
.map(_.stripPrefix("maven:").trim)
.toSeq
.contains("true")
def ivyLike =
lines
.find(_.startsWith("ivy-like:"))
.map(_.stripPrefix("ivy-like:").trim)
.toSeq
.contains("true")
def base =
lines
.find(_.startsWith("base:"))
.map(_.stripPrefix("base:").trim)
.filter(_.nonEmpty)
if (isMaven)
base.map(MavenRepository(_, ivyLike = ivyLike))
else
None
}
lazy val default = Cache(new File(sys.props("user.home") + "/.coursier/cache"))
}
case class Cache(cache: File) {
import Cache._
lazy val repoDir = new File(cache, "repositories")
lazy val metadataBase = new File(cache, "metadata")
lazy val fileBase = new File(cache, "files")
lazy val defaultFile = new File(repoDir, ".default")
def add(id: String, base: String, ivyLike: Boolean): Unit = {
repoDir.mkdirs()
val f = new File(repoDir, id)
val w = new PrintWriter(f)
try w.println((Seq("maven: true", s"base: $base") ++ (if (ivyLike) Seq("ivy-like: true") else Nil)).mkString("\n"))
finally w.close()
}
def addCentral(): Unit =
add("central", "https://repo1.maven.org/maven2/", ivyLike = false)
def addIvy2Local(): Unit =
add("ivy2local", "file://" + sys.props("user.home") + "/.ivy2/local/", ivyLike = true)
def init(ifEmpty: Boolean = true): Unit =
if (!ifEmpty || !cache.exists()) {
repoDir.mkdirs()
metadataBase.mkdirs()
fileBase.mkdirs()
addCentral()
addIvy2Local()
setDefault("ivy2local", "central")
}
def setDefault(ids: String*): Unit = {
defaultFile.getParentFile.mkdirs()
val w = new PrintWriter(defaultFile)
try w.println(ids.mkString("\n"))
finally w.close()
}
def list(): Seq[(String, MavenRepository, (String, File))] =
Option(repoDir.listFiles())
.map(_.toSeq)
.getOrElse(Nil)
.filter(f => f.isFile && !f.getName.startsWith("."))
.flatMap { f =>
val name = f.getName
val lines = Source.fromFile(f).getLines().toList
mavenRepository(lines)
.map(repo =>
(name, repo.copy(cache = Some(new File(metadataBase, name))), (repo.root, new File(fileBase, name)))
)
}
def map(): Map[String, (MavenRepository, (String, File))] =
list()
.map{case (id, repo, fileCache) => id -> (repo, fileCache) }
.toMap
def repositories(): Seq[MavenRepository] =
list().map(_._2)
def repositoryMap(): Map[String, MavenRepository] =
list()
.map{case (id, repo, _) => id -> repo}
.toMap
def fileCaches(): Seq[(String, File)] =
list().map(_._3)
def default(withNotFound: Boolean = false): Seq[String] =
if (defaultFile.exists()) {
val default0 =
Source.fromFile(defaultFile)
.getLines()
.map(_.trim)
.filter(_.nonEmpty)
.toList
val found = list()
.map(_._1)
.toSet
default0
.filter(found)
} else
Nil
def files(): Files = {
val map0 = map()
val default0 = default()
new Files(default0.map(map0(_)._2), () => ???)
}
}

View File

@ -1,28 +1,30 @@
package coursier
import java.net.{URI, URL}
import coursier.core.Repository.CachePolicy
import java.net.{ URI, URL }
import java.util.concurrent.{ Executors, ExecutorService }
import scala.annotation.tailrec
import scalaz.{-\/, \/-, \/, EitherT}
import scalaz.concurrent.Task
import scalaz.{ -\/, \/-, \/, EitherT }
import scalaz.concurrent.{ Task, Strategy }
import java.io._
// FIXME This kind of side-effecting API is lame, we should aim at a more functional one.
trait FilesLogger {
def foundLocally(f: File): Unit
def downloadingArtifact(url: String): Unit
def downloadedArtifact(url: String, success: Boolean): Unit
}
case class Files(
cache: Seq[(String, File)],
tmp: () => File,
logger: Option[Files.Logger] = None,
concurrentDownloadCount: Int = Files.defaultConcurrentDownloadCount
) {
case class Files(cache: Seq[(String, File)],
tmp: () => File,
logger: Option[FilesLogger] = None) {
lazy val defaultPool =
Executors.newFixedThreadPool(concurrentDownloadCount, Strategy.DefaultDaemonThreadFactory)
def file(artifact: Artifact,
cachePolicy: CachePolicy): EitherT[Task, String, File] = {
def file(
artifact: Artifact,
cachePolicy: CachePolicy
)(implicit
pool: ExecutorService = defaultPool
): EitherT[Task, String, File] = {
if (artifact.url.startsWith("file:///")) {
val f = new File(new URI(artifact.url) .getPath)
@ -99,6 +101,15 @@ case class Files(cache: Seq[(String, File)],
}
object Files {
val defaultConcurrentDownloadCount = 6
// FIXME This kind of side-effecting API is lame, we should aim at a more functional one.
trait Logger {
def foundLocally(f: File): Unit
def downloadingArtifact(url: String): Unit
def downloadedArtifact(url: String, success: Boolean): Unit
}
var bufferSize = 1024*1024

View File

@ -59,7 +59,8 @@ object CoursierBuild extends Build {
organization := "com.github.alexarchambault",
scalaVersion := "2.11.6",
crossScalaVersions := Seq("2.10.5", "2.11.6"),
resolvers += "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases"
resolvers += "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases",
resolvers += Resolver.sonatypeRepo("snapshots")
) ++ publishingSettings
private lazy val commonCoreSettings = commonSettings ++ Seq[Setting[_]](
@ -123,7 +124,7 @@ object CoursierBuild extends Build {
.settings(
name := "coursier-cli",
libraryDependencies ++= Seq(
"com.github.alexarchambault" %% "case-app" % "0.2.2",
"com.github.alexarchambault" %% "case-app" % "0.3.0-SNAPSHOT",
"ch.qos.logback" % "logback-classic" % "1.1.3"
) ++ {
if (scalaVersion.value startsWith "2.10.")

View File

@ -1,10 +1,10 @@
package coursier
package web
import coursier.core.{Repository, MavenRepository, Fetch}
import japgolly.scalajs.react.vdom.{TagMod, Attr}
import coursier.core.{ Repository, MavenRepository, MavenSource }
import japgolly.scalajs.react.vdom.{ TagMod, Attr }
import japgolly.scalajs.react.vdom.Attrs.dangerouslySetInnerHtml
import japgolly.scalajs.react.{ReactEventI, ReactComponentB, BackendScope}
import japgolly.scalajs.react.{ ReactEventI, ReactComponentB, BackendScope }
import japgolly.scalajs.react.vdom.prefix_<^._
import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
import org.scalajs.jquery.jQuery
@ -12,7 +12,7 @@ import org.scalajs.jquery.jQuery
import scala.concurrent.Future
import scala.scalajs.js
import js.Dynamic.{global => g}
import js.Dynamic.{ global => g }
case class ResolutionOptions(followOptional: Boolean = false,
keepTest: Boolean = false)
@ -111,7 +111,7 @@ class Backend($: BackendScope[Unit, State]) {
g.$("#resLogTab a:last").tab("show")
$.modState(_.copy(resolving = true, log = Nil))
val logger: Fetch.Logger = new Fetch.Logger {
val logger: MavenRepository.Logger = new MavenRepository.Logger {
def fetched(url: String) = {
println(s"<- $url")
$.modState(s => s.copy(log = s"<- $url" +: s.log))
@ -133,9 +133,11 @@ class Backend($: BackendScope[Unit, State]) {
filter = Some(dep => (s.options.followOptional || !dep.optional) && (s.options.keepTest || dep.scope != Scope.Test))
)
implicit val cachePolicy = CachePolicy.Default
res
.process
.run(fetch(s.repositories.map(r => r.copy(fetch = r.fetch.copy(logger = Some(logger))))), 100)
.run(s.repositories.map(r => r.copy(logger = Some(logger))), 100)
}
// For reasons that are unclear to me, not delaying this when using the runNow execution context
@ -246,7 +248,7 @@ object App {
)),
<.td(Seq[Seq[TagMod]](
res.projectCache.get(dep.moduleVersion) match {
case Some((source: MavenRepository.Source, proj)) if !source.ivyLike =>
case Some((source: MavenSource, proj)) if !source.ivyLike =>
// FIXME Maven specific, generalize with source.artifacts
val version0 = finalVersionOpt getOrElse dep.version
val relPath =
@ -401,14 +403,14 @@ object App {
def repoItem(repo: MavenRepository) =
<.tr(
<.td(
<.a(^.href := repo.fetch.root,
repo.fetch.root
<.a(^.href := repo.root,
repo.root
)
)
)
val sortedRepos = repos
.sortBy(repo => repo.fetch.root)
.sortBy(repo => repo.root)
<.table(^.`class` := "table",
<.thead(