diff --git a/project/push-gh-pages-helper.sh b/.ci/push-gh-pages-helper.sh
similarity index 100%
rename from project/push-gh-pages-helper.sh
rename to .ci/push-gh-pages-helper.sh
diff --git a/project/push-gh-pages.sh b/.ci/push-gh-pages.sh
similarity index 100%
rename from project/push-gh-pages.sh
rename to .ci/push-gh-pages.sh
diff --git a/project/travis.sh b/.ci/travis.sh
similarity index 78%
rename from project/travis.sh
rename to .ci/travis.sh
index e74211729..d9d91e885 100755
--- a/project/travis.sh
+++ b/.ci/travis.sh
@@ -28,7 +28,16 @@ function isMasterOrDevelop() {
}
# Required for ~/.ivy2/local repo tests
-~/sbt coreJVM/publish-local
+~/sbt coreJVM/publishLocal simple-web-server/publishLocal
+
+# Required for HTTP authentication tests
+./coursier launch \
+ io.get-coursier:simple-web-server_2.11:1.0.0-SNAPSHOT \
+ -r http://dl.bintray.com/scalaz/releases \
+ -- \
+ -d tests/jvm/src/test/resources/test-repo/http/abc.com \
+ -u user -P pass -r realm \
+ -v &
# TODO Add coverage once https://github.com/scoverage/sbt-scoverage/issues/111 is fixed
diff --git a/.travis.yml b/.travis.yml
index d7a69943d..2f6ffe43f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,7 +6,7 @@ install:
os:
- osx
script:
- - project/travis.sh "${TRAVIS_SCALA_VERSION:-2.11.8}" "$TRAVIS_PULL_REQUEST" "$TRAVIS_BRANCH" "$PUBLISH"
+ - .ci/travis.sh "${TRAVIS_SCALA_VERSION:-2.11.8}" "$TRAVIS_PULL_REQUEST" "$TRAVIS_BRANCH" "$PUBLISH"
# Uncomment once https://github.com/scoverage/sbt-scoverage/issues/111 is fixed
# after_success:
# - bash <(curl -s https://codecov.io/bash)
diff --git a/appveyor.yml b/appveyor.yml
index 48e52a3e2..8a39062dd 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -15,10 +15,11 @@ install:
- cmd: SET SBT_OPTS=-XX:MaxPermSize=2g -Xmx4g
- cmd: SET COURSIER_NO_TERM=1
build_script:
- - sbt ++2.11.8 clean compile coreJVM/publishLocal
+ - sbt ++2.11.8 clean compile coreJVM/publishLocal simple-web-server/publishLocal
- sbt ++2.10.6 clean compile
- sbt ++2.10.6 coreJVM/publishLocal cache/publishLocal # to make the scripted tests happy
test_script:
+ - ps: Start-Job { & java -jar -noverify C:\projects\coursier\coursier launch -r http://dl.bintray.com/scalaz/releases io.get-coursier:simple-web-server_2.11:1.0.0-SNAPSHOT -- -d /C:/projects/coursier/tests/jvm/src/test/resources/test-repo/http/abc.com -u user -P pass -r realm -v }
- sbt ++2.11.8 testsJVM/test # Would node be around for testsJS/test?
- sbt ++2.10.6 testsJVM/test plugin/scripted
cache:
diff --git a/build.sbt b/build.sbt
index 431c04701..8b490bb44 100644
--- a/build.sbt
+++ b/build.sbt
@@ -146,6 +146,21 @@ lazy val core = crossProject
import com.typesafe.tools.mima.core.ProblemFilters._
Seq(
+ // Since 1.0.0-M12
+ // Extra `authentication` field
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Artifact.apply"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Artifact.copy"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Artifact.this"),
+ ProblemFilters.exclude[MissingTypesProblem]("coursier.ivy.IvyRepository$"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.ivy.IvyRepository.apply"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.ivy.IvyRepository.copy"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.ivy.IvyRepository.this"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.maven.MavenRepository.copy"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.maven.MavenRepository.this"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.maven.MavenSource.apply"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.maven.MavenRepository.apply"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.maven.MavenSource.copy"),
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.maven.MavenSource.this"),
// Since 1.0.0-M11
// Extra parameter with default value added, problem for forward compatibility only
ProblemFilters.exclude[MissingMethodProblem]("coursier.core.ResolutionProcess.next"),
@@ -226,6 +241,9 @@ lazy val cache = project
import com.typesafe.tools.mima.core.ProblemFilters._
Seq(
+ // Since 1.0.0-M12
+ // Remove deprecated / unused helper method
+ ProblemFilters.exclude[MissingMethodProblem]("coursier.Cache.readFully"),
// Since 1.0.0-M11
// Add constructor parameter on FileError - shouldn't be built by users anyway
ProblemFilters.exclude[MissingMethodProblem]("coursier.FileError.this"),
@@ -488,13 +506,15 @@ lazy val plugin = project
scriptedBufferLog := false
)
+val http4sVersion = "0.8.6"
+
lazy val `simple-web-server` = project
.settings(commonSettings)
.settings(packAutoSettings)
.settings(
libraryDependencies ++= Seq(
- "org.http4s" %% "http4s-blaze-server" % "0.13.2",
- "org.http4s" %% "http4s-dsl" % "0.13.2",
+ "org.http4s" %% "http4s-blazeserver" % http4sVersion,
+ "org.http4s" %% "http4s-dsl" % http4sVersion,
"org.slf4j" % "slf4j-nop" % "1.7.19",
"com.github.alexarchambault" %% "case-app" % "1.0.0-RC2"
)
diff --git a/cache/src/main/scala/coursier/Cache.scala b/cache/src/main/scala/coursier/Cache.scala
index 3929dee0f..6226f1d03 100644
--- a/cache/src/main/scala/coursier/Cache.scala
+++ b/cache/src/main/scala/coursier/Cache.scala
@@ -8,7 +8,9 @@ import java.security.MessageDigest
import java.util.concurrent.{ ConcurrentHashMap, Executors, ExecutorService }
import java.util.regex.Pattern
+import coursier.core.Authentication
import coursier.ivy.IvyRepository
+import coursier.util.Base64.Encoder
import scala.annotation.tailrec
@@ -18,6 +20,10 @@ import scalaz.concurrent.{ Task, Strategy }
import java.io.{ Serializable => _, _ }
+trait AuthenticatedURLConnection extends URLConnection {
+ def authenticate(authentication: Authentication): Unit
+}
+
object Cache {
// Check SHA-1 if available, else be fine with no checksum
@@ -43,7 +49,7 @@ object Cache {
}
}
- private def localFile(url: String, cache: File): File = {
+ private def localFile(url: String, cache: File, user: Option[String]): File = {
val path =
if (url.startsWith("file:///"))
url.stripPrefix("file://")
@@ -62,7 +68,10 @@ object Cache {
else
throw new Exception(s"URL $url doesn't contain an absolute path")
- new File(cache, escape(protocol + "/" + remaining0)) .toString
+ new File(
+ cache,
+ escape(protocol + "/" + user.fold("")(_ + "@") + remaining0.dropWhile(_ == '/'))
+ ).toString
case _ =>
throw new Exception(s"No protocol found in URL $url")
@@ -204,7 +213,7 @@ object Cache {
-\/(FileError.ConcurrentDownload(url))
}
catch { case e: Exception =>
- -\/(FileError.DownloadError(s"Caught $e (${e.getMessage})"))
+ -\/(FileError.DownloadError(s"Caught $e${Option(e.getMessage).fold("")(" (" + _ + ")")}"))
}
private def temporaryFile(file: File): File = {
@@ -232,7 +241,7 @@ object Cache {
def printError(e: Exception): Unit =
scala.Console.err.println(
- s"Cannot instantiate $clsName: $e${Option(e.getMessage).map(" ("+_+")")}"
+ s"Cannot instantiate $clsName: $e${Option(e.getMessage).fold("")(" ("+_+")")}"
)
val handlerOpt = clsOpt.flatMap {
@@ -259,6 +268,17 @@ object Cache {
}
}
+ private val BasicRealm = (
+ "^" +
+ Pattern.quote("Basic realm=\"") +
+ "([^" + Pattern.quote("\"") + "]*)" +
+ Pattern.quote("\"") +
+ "$"
+ ).r
+
+ private def basicAuthenticationEncode(user: String, password: String): String =
+ (user + ":" + password).getBytes("UTF-8").toBase64
+
/**
* Returns a `java.net.URL` for `s`, possibly using the custom protocol handlers found under the
* `coursier.cache.protocol` namespace.
@@ -289,7 +309,7 @@ object Cache {
val referenceFileOpt = artifact
.extra
.get("metadata")
- .map(a => localFile(a.url, cache))
+ .map(a => localFile(a.url, cache, a.authentication.map(_.user)))
def referenceFileExists: Boolean = referenceFileOpt.exists(_.exists())
@@ -300,6 +320,20 @@ object Cache {
// (Maven 2 compatibility? - happens for snapshot versioning metadata,
// this is SO FSCKING CRAZY)
conn.setRequestProperty("User-Agent", "")
+
+ for (auth <- artifact.authentication)
+ conn match {
+ case authenticated: AuthenticatedURLConnection =>
+ authenticated.authenticate(auth)
+ case conn0: HttpURLConnection =>
+ conn0.setRequestProperty(
+ "Authorization",
+ "Basic " + basicAuthenticationEncode(auth.user, auth.password)
+ )
+ case _ =>
+ // FIXME Authentication is ignored
+ }
+
conn
}
@@ -384,15 +418,28 @@ object Cache {
}
}
- def is404(conn: URLConnection) =
+ def responseCode(conn: URLConnection): Option[Int] =
conn match {
case conn0: HttpURLConnection =>
- conn0.getResponseCode == 404
+ Some(conn0.getResponseCode)
case _ =>
- false
+ None
}
- def remote(file: File, url: String): EitherT[Task, FileError, Unit] =
+ def realm(conn: URLConnection): Option[String] =
+ conn match {
+ case conn0: HttpURLConnection =>
+ Option(conn0.getHeaderField("WWW-Authenticate")).collect {
+ case BasicRealm(realm) => realm
+ }
+ case _ =>
+ None
+ }
+
+ def remote(
+ file: File,
+ url: String
+ ): EitherT[Task, FileError, Unit] =
EitherT {
Task {
withLockFor(cache, file) {
@@ -421,8 +468,10 @@ object Cache {
case _ => (false, conn0)
}
- if (is404(conn))
+ if (responseCode(conn) == Some(404))
FileError.NotFound(url, permanent = Some(true)).left
+ else if (responseCode(conn) == Some(401))
+ FileError.Unauthorized(url, realm = realm(conn)).left
else {
for (len0 <- Option(conn.getContentLengthLong) if len0 >= 0L) {
val len = len0 + (if (partialDownload) alreadyDownloaded else 0L)
@@ -534,7 +583,7 @@ object Cache {
val tasks =
for (url <- urls) yield {
- val file = localFile(url, cache)
+ val file = localFile(url, cache, artifact.authentication.map(_.user))
val res =
if (url.startsWith("file:/")) {
@@ -618,12 +667,12 @@ object Cache {
implicit val pool0 = pool
- val localFile0 = localFile(artifact.url, cache)
+ val localFile0 = localFile(artifact.url, cache, artifact.authentication.map(_.user))
EitherT {
artifact.checksumUrls.get(sumType) match {
case Some(sumUrl) =>
- val sumFile = localFile(sumUrl, cache)
+ val sumFile = localFile(sumUrl, cache, artifact.authentication.map(_.user))
Task {
val sumOpt = parseChecksum(
@@ -730,7 +779,7 @@ object Cache {
checksums = checksums,
logger = logger,
pool = pool
- ).leftMap(_.message).map { f =>
+ ).leftMap(_.describe).map { f =>
// FIXME Catch error here?
new String(NioFiles.readAllBytes(f.toPath), "UTF-8")
}
@@ -811,18 +860,6 @@ object Cache {
buffer.toByteArray
}
- def readFully(is: => InputStream) =
- Task {
- \/.fromTryCatchNonFatal {
- val is0 = is
- val b =
- try readFullySync(is0)
- finally is0.close()
-
- new String(b, "UTF-8")
- } .leftMap(_.getMessage)
- }
-
def withContent(is: InputStream, f: (Array[Byte], Int) => Unit): Unit = {
val data = Array.ofDim[Byte](16384)
diff --git a/cache/src/main/scala/coursier/CacheParse.scala b/cache/src/main/scala/coursier/CacheParse.scala
index becd60d43..9bf1467ce 100644
--- a/cache/src/main/scala/coursier/CacheParse.scala
+++ b/cache/src/main/scala/coursier/CacheParse.scala
@@ -2,6 +2,7 @@ package coursier
import java.net.MalformedURLException
+import coursier.core.Authentication
import coursier.ivy.IvyRepository
import coursier.util.Parse
@@ -26,12 +27,48 @@ object CacheParse {
sys.error(s"Unrecognized repository: $r")
}
- try {
- Cache.url(url)
- repo.success
+ val validatedUrl = try {
+ Cache.url(url).success
} catch {
case e: MalformedURLException =>
- ("Error parsing URL " + url + Option(e.getMessage).map(" (" + _ + ")").mkString).failure
+ ("Error parsing URL " + url + Option(e.getMessage).fold("")(" (" + _ + ")")).failure
+ }
+
+ validatedUrl.flatMap { url =>
+ Option(url.getUserInfo) match {
+ case None =>
+ repo.success
+ case Some(userInfo) =>
+ userInfo.split(":", 2) match {
+ case Array(user, password) =>
+ val baseUrl = new java.net.URL(
+ url.getProtocol,
+ url.getHost,
+ url.getPort,
+ url.getFile
+ ).toString
+
+ val repo0 = repo match {
+ case m: MavenRepository =>
+ m.copy(
+ root = baseUrl,
+ authentication = Some(Authentication(user, password))
+ )
+ case i: IvyRepository =>
+ i.copy(
+ pattern = baseUrl,
+ authentication = Some(Authentication(user, password))
+ )
+ case r =>
+ sys.error(s"Unrecognized repository: $r")
+ }
+
+ repo0.success
+
+ case _ =>
+ s"No password found in user info of URL $url".failure
+ }
+ }
}
}
diff --git a/cache/src/main/scala/coursier/FileError.scala b/cache/src/main/scala/coursier/FileError.scala
index f1a3bce34..3f1788d61 100644
--- a/cache/src/main/scala/coursier/FileError.scala
+++ b/cache/src/main/scala/coursier/FileError.scala
@@ -5,7 +5,9 @@ import java.io.File
sealed abstract class FileError(
val `type`: String,
val message: String
-) extends Product with Serializable
+) extends Product with Serializable {
+ def describe: String = s"${`type`}: $message"
+}
object FileError {
@@ -22,6 +24,14 @@ object FileError {
file
)
+ final case class Unauthorized(
+ file: String,
+ realm: Option[String]
+ ) extends FileError(
+ "unauthorized",
+ file + realm.fold("")(" (" + _ + ")")
+ )
+
final case class ChecksumNotFound(
sumType: String,
file: String
diff --git a/cache/src/main/scala/coursier/Platform.scala b/cache/src/main/scala/coursier/Platform.scala
index 2e7d7b6d3..33acf1bde 100644
--- a/cache/src/main/scala/coursier/Platform.scala
+++ b/cache/src/main/scala/coursier/Platform.scala
@@ -32,10 +32,10 @@ object Platform {
new String(b, "UTF-8")
} .leftMap{
- case e: java.io.FileNotFoundException =>
+ case e: java.io.FileNotFoundException if e.getMessage != null =>
s"Not found: ${e.getMessage}"
case e =>
- s"$e: ${e.getMessage}"
+ s"$e${Option(e.getMessage).fold("")(" (" + _ + ")")}"
}
}
diff --git a/cache/src/main/scala/coursier/util/Base64.scala b/cache/src/main/scala/coursier/util/Base64.scala
new file mode 100644
index 000000000..0019a80c8
--- /dev/null
+++ b/cache/src/main/scala/coursier/util/Base64.scala
@@ -0,0 +1,106 @@
+package coursier.util
+
+import scala.collection.mutable.ArrayBuilder
+
+/**
+ * Base64 encoder
+ * @author Mark Lister
+ * This software is distributed under the 2-Clause BSD license. See the
+ * LICENSE file in the root of the repository.
+ *
+ * Copyright (c) 2014 - 2015 Mark Lister
+ *
+ * The repo for this Base64 encoder lives at https://github.com/marklister/base64
+ * Please send your issues, suggestions and pull requests there.
+ */
+
+object Base64 {
+
+ case class B64Scheme(encodeTable: Array[Char], strictPadding: Boolean = true,
+ postEncode: String => String = identity,
+ preDecode: String => String = identity) {
+ lazy val decodeTable = {
+ val b: Array[Int] = new Array[Int](256)
+ for (x <- encodeTable.zipWithIndex) {
+ b(x._1) = x._2.toInt
+ }
+ b
+ }
+ }
+
+ val base64 = new B64Scheme((('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ Seq('+', '/')).toArray)
+ val base64Url = new B64Scheme(base64.encodeTable.dropRight(2) ++ Seq('-', '_'), false,
+ _.replaceAllLiterally("=", "%3D"),
+ _.replaceAllLiterally("%3D", "="))
+
+ implicit class SeqEncoder(s: Seq[Byte]) {
+ def toBase64(implicit scheme: B64Scheme = base64): String = Encoder(s.toArray).toBase64
+ }
+
+ implicit class Encoder(b: Array[Byte]) {
+ val r = new StringBuilder((b.length + 3) * 4 / 3)
+ lazy val pad = (3 - b.length % 3) % 3
+
+ def toBase64(implicit scheme: B64Scheme = base64): String = {
+ def sixBits(x: Byte, y: Byte, z: Byte): Unit = {
+ val zz = (x & 0xff) << 16 | (y & 0xff) << 8 | (z & 0xff)
+ r += scheme.encodeTable(zz >> 18)
+ r += scheme.encodeTable(zz >> 12 & 0x3f)
+ r += scheme.encodeTable(zz >> 6 & 0x3f)
+ r += scheme.encodeTable(zz & 0x3f)
+ }
+ for (p <- 0 until b.length - 2 by 3) {
+ sixBits(b(p), b(p + 1), b(p + 2))
+ }
+ pad match {
+ case 0 =>
+ case 1 => sixBits(b(b.length - 2), b(b.length - 1), 0)
+ case 2 => sixBits(b(b.length - 1), 0, 0)
+ }
+ r.length = (r.length - pad)
+ r ++= "=" * pad
+ scheme.postEncode(r.toString())
+ }
+ }
+
+ implicit class Decoder(s: String) {
+
+ def toByteArray(implicit scheme: B64Scheme = base64): Array[Byte] = {
+ val pre = scheme.preDecode(s)
+ val cleanS = pre.replaceAll("=+$", "")
+ val pad = pre.length - cleanS.length
+ val computedPad = (4 - (cleanS.length % 4)) % 4
+ val r = new ArrayBuilder.ofByte
+
+ def threeBytes(a: Int, b: Int, c: Int, d: Int): Unit = {
+ val i = a << 18 | b << 12 | c << 6 | d
+ r += ((i >> 16).toByte)
+ r += ((i >> 8).toByte)
+ r += (i.toByte)
+ }
+ if (scheme.strictPadding) {
+ if (pad > 2) throw new java.lang.IllegalArgumentException("Invalid Base64 String: (excessive padding) " + s)
+ if (s.length % 4 != 0) throw new java.lang.IllegalArgumentException("Invalid Base64 String: (padding problem) " + s)
+ }
+ if (computedPad == 3) throw new java.lang.IllegalArgumentException("Invalid Base64 String: (string length) " + s)
+ try {
+ val s = (cleanS + "A" * computedPad)
+ for (x <- 0 until s.length - 1 by 4) {
+ val i = scheme.decodeTable(s.charAt(x)) << 18 |
+ scheme.decodeTable(s.charAt(x + 1)) << 12 |
+ scheme.decodeTable(s.charAt(x + 2)) << 6 |
+ scheme.decodeTable(s.charAt(x + 3))
+ r += ((i >> 16).toByte)
+ r += ((i >> 8).toByte)
+ r += (i.toByte)
+ }
+ } catch {
+ case e: NoSuchElementException => throw new java.lang.IllegalArgumentException("Invalid Base64 String: (invalid character)" + e.getMessage + s)
+ }
+ val res = r.result
+ res.slice(0, res.length - computedPad)
+ }
+
+ }
+
+}
diff --git a/cli/src/main/scala-2.11/coursier/cli/Bootstrap.scala b/cli/src/main/scala-2.11/coursier/cli/Bootstrap.scala
index dc6717e55..fa28ff693 100644
--- a/cli/src/main/scala-2.11/coursier/cli/Bootstrap.scala
+++ b/cli/src/main/scala-2.11/coursier/cli/Bootstrap.scala
@@ -193,7 +193,7 @@ case class Bootstrap(
try Files.write(output0.toPath, shellPreamble.getBytes("UTF-8") ++ buffer.toByteArray)
catch { case e: IOException =>
- Console.err.println(s"Error while writing $output0: ${e.getMessage}")
+ Console.err.println(s"Error while writing $output0${Option(e.getMessage).fold("")(" (" + _ + ")")}")
sys.exit(1)
}
@@ -218,7 +218,10 @@ case class Bootstrap(
case e: UnsupportedOperationException =>
// Ignored
case e: IOException =>
- Console.err.println(s"Error while making $output0 executable: ${e.getMessage}")
+ Console.err.println(
+ s"Error while making $output0 executable" +
+ Option(e.getMessage).fold("")(" (" + _ + ")")
+ )
sys.exit(1)
}
diff --git a/cli/src/main/scala-2.11/coursier/cli/Helper.scala b/cli/src/main/scala-2.11/coursier/cli/Helper.scala
index eb31ec8fb..baa6c5069 100644
--- a/cli/src/main/scala-2.11/coursier/cli/Helper.scala
+++ b/cli/src/main/scala-2.11/coursier/cli/Helper.scala
@@ -99,6 +99,18 @@ class Helper(
MavenRepository("https://repo1.maven.org/maven2")
)
+ val sourceDirectories = common.sources.map { path =>
+ val subDir = "target/repository"
+ val dir = new File(path)
+ val repoDir = new File(dir, subDir)
+ if (!dir.exists())
+ Console.err.println(s"Warning: sources $path not found")
+ else if (!repoDir.exists())
+ Console.err.println(s"Warning: directory $subDir not found under sources path $path")
+
+ repoDir
+ }
+
val repositoriesValidation = CacheParse.repositories(common.repository).map { repos0 =>
var repos = (if (common.noDefault) Nil else defaultRepositories) ++ repos0
@@ -119,10 +131,14 @@ class Helper(
}
val repositories = repositoriesValidation match {
- case Success(repos) => repos
+ case Success(repos) =>
+ val sourceRepositories = sourceDirectories.map(dir =>
+ MavenRepository(dir.toURI.toString, changing = Some(true))
+ )
+ sourceRepositories ++ repos
case Failure(errors) =>
prematureExit(
- s"Error parsing repositories:\n${errors.list.map(" "+_).mkString("\n")}"
+ s"Error with repositories:\n${errors.list.map(" "+_).mkString("\n")}"
)
}
@@ -140,8 +156,36 @@ class Helper(
s"Cannot parse forced versions:\n" + forceVersionErrors.map(" "+_).mkString("\n")
}
+ val sourceRepositoryForceVersions = sourceDirectories.flatMap { base =>
+
+ // FIXME Also done in the plugin module
+
+ def pomDirComponents(f: File, components: Vector[String]): Stream[Vector[String]] =
+ if (f.isDirectory) {
+ val components0 = components :+ f.getName
+ Option(f.listFiles()).toStream.flatten.flatMap(pomDirComponents(_, components0))
+ } else if (f.getName.endsWith(".pom"))
+ Stream(components)
+ else
+ Stream.empty
+
+ Option(base.listFiles())
+ .toVector
+ .flatten
+ .flatMap(pomDirComponents(_, Vector()))
+ // at least 3 for org / name / version - the contrary should not happen, but who knows
+ .filter(_.length >= 3)
+ .map { components =>
+ val org = components.dropRight(2).mkString(".")
+ val name = components(components.length - 2)
+ val version = components.last
+
+ Module(org, name) -> version
+ }
+ }
+
val forceVersions = {
- val grouped = forceVersions0
+ val grouped = (forceVersions0 ++ sourceRepositoryForceVersions)
.groupBy { case (mod, _) => mod }
.map { case (mod, l) => mod -> l.map { case (_, version) => version } }
@@ -354,10 +398,16 @@ class Helper(
lazy val projCache = res.projectCache.mapValues { case (_, p) => p }
- if (printResultStdout || verbosityLevel >= 1) {
- if ((printResultStdout && verbosityLevel >= 1) || verbosityLevel >= 2)
+ if (printResultStdout || verbosityLevel >= 1 || tree || reverseTree) {
+ if ((printResultStdout && verbosityLevel >= 1) || verbosityLevel >= 2 || tree || reverseTree)
errPrintln(s" Result:")
- val depsStr = Print.dependenciesUnknownConfigs(trDeps, projCache)
+
+ val depsStr =
+ if (reverseTree || tree)
+ Print.dependencyTree(dependencies, res, printExclusions = verbosityLevel >= 1, reverse = reverseTree)
+ else
+ Print.dependenciesUnknownConfigs(trDeps, projCache)
+
if (printResultStdout)
println(depsStr)
else
diff --git a/cli/src/main/scala-2.11/coursier/cli/Options.scala b/cli/src/main/scala-2.11/coursier/cli/Options.scala
index 9cb1b08c3..e790ad1c6 100644
--- a/cli/src/main/scala-2.11/coursier/cli/Options.scala
+++ b/cli/src/main/scala-2.11/coursier/cli/Options.scala
@@ -24,9 +24,12 @@ case class CommonOptions(
@Help("Maximum number of resolution iterations (specify a negative value for unlimited, default: 100)")
@Short("N")
maxIterations: Int = 100,
- @Help("Repositories - for multiple repositories, separate with comma and/or repeat this option (e.g. -r central,ivy2local -r sonatype-snapshots, or equivalently -r central,ivy2local,sonatype-snapshots)")
+ @Help("Repository - for multiple repositories, separate with comma and/or add this option multiple times (e.g. -r central,ivy2local -r sonatype-snapshots, or equivalently -r central,ivy2local,sonatype-snapshots)")
@Short("r")
repository: List[String],
+ @Help("Source repository - for multiple repositories, separate with comma and/or add this option multiple times")
+ @Short("R")
+ sources: List[String],
@Help("Do not add default repositories (~/.ivy2/local, and Central)")
noDefault: Boolean = false,
@Help("Modify names in Maven repository paths for SBT plugins")
@@ -61,6 +64,12 @@ case class CommonOptions(
@Short("B")
@Value("Number of warm-up resolutions - if negative, doesn't print per iteration benchmark (less overhead)")
benchmark: Int,
+ @Help("Print dependencies as a tree")
+ @Short("t")
+ tree: Boolean,
+ @Help("Print dependencies as an inversed tree (dependees as children)")
+ @Short("T")
+ reverseTree: Boolean,
@Recurse
cacheOptions: CacheOptions
) {
diff --git a/core/jvm/src/main/scala/coursier/core/compatibility/package.scala b/core/jvm/src/main/scala/coursier/core/compatibility/package.scala
index 3a8f6dc49..c290caaa5 100644
--- a/core/jvm/src/main/scala/coursier/core/compatibility/package.scala
+++ b/core/jvm/src/main/scala/coursier/core/compatibility/package.scala
@@ -14,7 +14,7 @@ package object compatibility {
def xmlParse(s: String): Either[String, Xml.Node] = {
def parse =
try Right(scala.xml.XML.loadString(s))
- catch { case e: Exception => Left(e.getMessage) }
+ catch { case e: Exception => Left(e.toString + Option(e.getMessage).fold("")(" (" + _ + ")")) }
def fromNode(node: scala.xml.Node): Xml.Node =
new Xml.Node {
diff --git a/core/jvm/src/main/scala/coursier/maven/WritePom.scala b/core/jvm/src/main/scala/coursier/maven/WritePom.scala
new file mode 100644
index 000000000..26817608f
--- /dev/null
+++ b/core/jvm/src/main/scala/coursier/maven/WritePom.scala
@@ -0,0 +1,77 @@
+package coursier.maven
+
+import coursier.core.{ Dependency, Project }
+
+object WritePom {
+
+ def project(proj: Project, packaging: Option[String]) = {
+
+ def dependencyNode(config: String, dep: Dependency) = {
+
+ {dep.module.organization}
+ {dep.module.name}
+ {
+ if (dep.version.isEmpty)
+ Nil
+ else
+ Seq({dep.version})
+ }
+ {
+ if (config.isEmpty)
+ Nil
+ else
+ Seq({config})
+ }
+
+ }
+
+
+ // parent
+ {proj.module.organization}
+ {proj.module.name}
+ {
+ packaging
+ .map(p => {p})
+ .toSeq
+ }
+ {proj.info.description}
+ {proj.info.homePage}
+ {proj.version}
+ // licenses
+ {proj.module.name}
+
+ {proj.module.name}
+ {proj.info.homePage}
+
+ // SCM
+ // developers
+ {
+ if (proj.dependencies.isEmpty)
+ Nil
+ else
+ {
+ proj.dependencies.map {
+ case (config, dep) =>
+ dependencyNode(config, dep)
+ }
+ }
+ }
+ {
+ if (proj.dependencyManagement.isEmpty)
+ Nil
+ else
+
+ {
+ proj.dependencyManagement.map {
+ case (config, dep) =>
+ dependencyNode(config, dep)
+ }
+ }
+
+ }
+ // properties
+ // repositories
+
+ }
+
+}
diff --git a/core/shared/src/main/scala/coursier/core/Definitions.scala b/core/shared/src/main/scala/coursier/core/Definitions.scala
index 085b00fe3..ea77c0b4e 100644
--- a/core/shared/src/main/scala/coursier/core/Definitions.scala
+++ b/core/shared/src/main/scala/coursier/core/Definitions.scala
@@ -183,7 +183,8 @@ final case class Artifact(
checksumUrls: Map[String, String],
extra: Map[String, Artifact],
attributes: Attributes,
- changing: Boolean
+ changing: Boolean,
+ authentication: Option[Authentication]
)
object Artifact {
@@ -205,3 +206,11 @@ object Artifact {
}
}
}
+
+case class Authentication(
+ user: String,
+ password: String
+) {
+ override def toString: String =
+ s"Authentication($user, *******)"
+}
\ No newline at end of file
diff --git a/core/shared/src/main/scala/coursier/core/Repository.scala b/core/shared/src/main/scala/coursier/core/Repository.scala
index f52917b15..8e5b6b55c 100644
--- a/core/shared/src/main/scala/coursier/core/Repository.scala
+++ b/core/shared/src/main/scala/coursier/core/Repository.scala
@@ -34,7 +34,8 @@ object Repository {
Map.empty,
Map.empty,
Attributes("asc", ""),
- changing = underlying.changing
+ changing = underlying.changing,
+ authentication = underlying.authentication
)
.withDefaultChecksums
))
diff --git a/core/shared/src/main/scala/coursier/core/Resolution.scala b/core/shared/src/main/scala/coursier/core/Resolution.scala
index eaca558d5..1762db013 100644
--- a/core/shared/src/main/scala/coursier/core/Resolution.scala
+++ b/core/shared/src/main/scala/coursier/core/Resolution.scala
@@ -529,6 +529,16 @@ final case class Resolution(
} else
Nil
+ def dependenciesOf(dep: Dependency, withReconciledVersions: Boolean = true): Seq[Dependency] =
+ if (withReconciledVersions)
+ finalDependencies0(dep).map { trDep =>
+ trDep.copy(
+ version = reconciledVersions.getOrElse(trDep.module, trDep.version)
+ )
+ }
+ else
+ finalDependencies0(dep)
+
/**
* Transitive dependencies of the current dependencies, according to
* what there currently is in cache.
@@ -558,6 +568,9 @@ final case class Resolution(
forceVersions
)
+ def reconciledVersions: Map[Module, String] =
+ nextDependenciesAndConflicts._3
+
/**
* The modules we miss some info about.
*/
@@ -974,10 +987,9 @@ final case class Resolution(
* @param dependencies: the dependencies to keep from this `Resolution`
*/
def subset(dependencies: Set[Dependency]): Resolution = {
- val (_, _, finalVersions) = nextDependenciesAndConflicts
def updateVersion(dep: Dependency): Dependency =
- dep.copy(version = finalVersions.getOrElse(dep.module, dep.version))
+ dep.copy(version = reconciledVersions.getOrElse(dep.module, dep.version))
@tailrec def helper(current: Set[Dependency]): Set[Dependency] = {
val newDeps = current ++ current
diff --git a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala
index 433a0674f..8df652abd 100644
--- a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala
+++ b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala
@@ -14,7 +14,8 @@ case class IvyRepository(
withSignatures: Boolean = true,
withArtifacts: Boolean = true,
// hack for SBT putting infos in properties
- dropInfoAttributes: Boolean = false
+ dropInfoAttributes: Boolean = false,
+ authentication: Option[Authentication] = None
) extends Repository {
def metadataPattern: String = metadataPatternOpt.getOrElse(pattern)
@@ -92,7 +93,8 @@ case class IvyRepository(
Map.empty,
Map.empty,
p.attributes,
- changing = changing.getOrElse(project.version.contains("-SNAPSHOT")) // could be more reliable
+ changing = changing.getOrElse(project.version.contains("-SNAPSHOT")), // could be more reliable
+ authentication = authentication
)
if (withChecksums)
@@ -127,7 +129,8 @@ case class IvyRepository(
Map.empty,
Map.empty,
Attributes("ivy", ""),
- changing = changing.getOrElse(version.contains("-SNAPSHOT"))
+ changing = changing.getOrElse(version.contains("-SNAPSHOT")),
+ authentication = authentication
)
if (withChecksums)
diff --git a/core/shared/src/main/scala/coursier/maven/MavenRepository.scala b/core/shared/src/main/scala/coursier/maven/MavenRepository.scala
index cfe9021f8..f408333ea 100644
--- a/core/shared/src/main/scala/coursier/maven/MavenRepository.scala
+++ b/core/shared/src/main/scala/coursier/maven/MavenRepository.scala
@@ -70,14 +70,15 @@ case class MavenRepository(
root: String,
changing: Option[Boolean] = None,
/** Hackish hack for sbt plugins mainly - what this does really sucks */
- sbtAttrStub: Boolean = false
+ sbtAttrStub: Boolean = false,
+ authentication: Option[Authentication] = None
) extends Repository {
import Repository._
import MavenRepository._
val root0 = if (root.endsWith("/")) root else root + "/"
- val source = MavenSource(root0, changing, sbtAttrStub)
+ val source = MavenSource(root0, changing, sbtAttrStub, authentication)
def projectArtifact(
module: Module,
@@ -96,7 +97,8 @@ case class MavenRepository(
Map.empty,
Map.empty,
Attributes("pom", ""),
- changing = changing.getOrElse(version.contains("-SNAPSHOT"))
+ changing = changing.getOrElse(version.contains("-SNAPSHOT")),
+ authentication = authentication
)
.withDefaultChecksums
.withDefaultSignature
@@ -115,7 +117,8 @@ case class MavenRepository(
Map.empty,
Map.empty,
Attributes("pom", ""),
- changing = true
+ changing = true,
+ authentication = authentication
)
.withDefaultChecksums
.withDefaultSignature
@@ -140,7 +143,8 @@ case class MavenRepository(
Map.empty,
Map.empty,
Attributes("pom", ""),
- changing = true
+ changing = true,
+ authentication = authentication
)
.withDefaultChecksums
.withDefaultSignature
diff --git a/core/shared/src/main/scala/coursier/maven/MavenSource.scala b/core/shared/src/main/scala/coursier/maven/MavenSource.scala
index e28cf9b0b..09d74b2e3 100644
--- a/core/shared/src/main/scala/coursier/maven/MavenSource.scala
+++ b/core/shared/src/main/scala/coursier/maven/MavenSource.scala
@@ -6,7 +6,8 @@ case class MavenSource(
root: String,
changing: Option[Boolean] = None,
/** See doc on MavenRepository */
- sbtAttrStub: Boolean
+ sbtAttrStub: Boolean,
+ authentication: Option[Authentication]
) extends Artifact.Source {
import Repository._
@@ -21,7 +22,8 @@ case class MavenSource(
Map.empty,
Map.empty,
Attributes("jar", "src"), // Are these the right attributes?
- changing = underlying.changing
+ changing = underlying.changing,
+ authentication = authentication
)
.withDefaultChecksums
.withDefaultSignature,
@@ -30,7 +32,8 @@ case class MavenSource(
Map.empty,
Map.empty,
Attributes("jar", "javadoc"), // Same comment as above
- changing = underlying.changing
+ changing = underlying.changing,
+ authentication = authentication
)
.withDefaultChecksums
.withDefaultSignature
@@ -65,7 +68,8 @@ case class MavenSource(
Map.empty,
Map.empty,
publication.attributes,
- changing = changing0
+ changing = changing0,
+ authentication = authentication
)
.withDefaultChecksums
diff --git a/core/shared/src/main/scala/coursier/util/Print.scala b/core/shared/src/main/scala/coursier/util/Print.scala
index bdd380b06..9dbb5b52f 100644
--- a/core/shared/src/main/scala/coursier/util/Print.scala
+++ b/core/shared/src/main/scala/coursier/util/Print.scala
@@ -1,6 +1,6 @@
package coursier.util
-import coursier.core.{Module, Project, Orders, Dependency}
+import coursier.core.{ Attributes, Dependency, Module, Orders, Project, Resolution }
object Print {
@@ -41,4 +41,154 @@ object Print {
deps1.map(dependency).mkString("\n")
}
+ private def compatibleVersions(first: String, second: String): Boolean = {
+ // too loose for now
+ // e.g. RCs and milestones should not be considered compatible with subsequent non-RC or
+ // milestone versions - possibly not with each other either
+
+ first.split('.').take(2).toSeq == second.split('.').take(2).toSeq
+ }
+
+ def dependencyTree(
+ roots: Seq[Dependency],
+ resolution: Resolution,
+ printExclusions: Boolean,
+ reverse: Boolean
+ ): String = {
+
+ case class Elem(dep: Dependency, excluded: Boolean) {
+
+ lazy val reconciledVersion = resolution.reconciledVersions
+ .getOrElse(dep.module, dep.version)
+
+ lazy val repr =
+ if (excluded)
+ resolution.reconciledVersions.get(dep.module) match {
+ case None =>
+ s"${Console.YELLOW}(excluded)${Console.RESET} ${dep.module}:${dep.version}"
+ case Some(version) =>
+ val versionMsg =
+ if (version == dep.version)
+ "this version"
+ else
+ s"version $version"
+
+ s"${dep.module}:${dep.version} " +
+ s"${Console.RED}(excluded, $versionMsg present anyway)${Console.RESET}"
+ }
+ else {
+ val versionStr =
+ if (reconciledVersion == dep.version)
+ dep.version
+ else {
+ val assumeCompatibleVersions = compatibleVersions(dep.version, reconciledVersion)
+
+ (if (assumeCompatibleVersions) Console.YELLOW else Console.RED) +
+ s"${dep.version} -> $reconciledVersion" +
+ Console.RESET
+ }
+
+ s"${dep.module}:$versionStr"
+ }
+
+ lazy val children: Seq[Elem] =
+ if (excluded)
+ Nil
+ else {
+ val dep0 = dep.copy(version = reconciledVersion)
+
+ val dependencies = resolution.dependenciesOf(
+ dep0,
+ withReconciledVersions = false
+ ).sortBy { trDep =>
+ (trDep.module.organization, trDep.module.name, trDep.version)
+ }
+
+ def excluded = resolution
+ .dependenciesOf(
+ dep0.copy(exclusions = Set.empty),
+ withReconciledVersions = false
+ )
+ .sortBy { trDep =>
+ (trDep.module.organization, trDep.module.name, trDep.version)
+ }
+ .map(_.moduleVersion)
+ .filterNot(dependencies.map(_.moduleVersion).toSet).map {
+ case (mod, ver) =>
+ Elem(
+ Dependency(mod, ver, "", Set.empty, Attributes("", ""), false, false),
+ excluded = true
+ )
+ }
+
+ dependencies.map(Elem(_, excluded = false)) ++
+ (if (printExclusions) excluded else Nil)
+ }
+ }
+
+ if (reverse) {
+
+ case class Parent(
+ module: Module,
+ version: String,
+ dependsOn: Module,
+ wantVersion: String,
+ gotVersion: String,
+ excluding: Boolean
+ ) {
+ lazy val repr: String =
+ if (excluding)
+ s"${Console.YELLOW}(excluded by)${Console.RESET} $module:$version"
+ else if (wantVersion == gotVersion)
+ s"$module:$version"
+ else {
+ val assumeCompatibleVersions = compatibleVersions(wantVersion, gotVersion)
+
+ s"$module:$version " +
+ (if (assumeCompatibleVersions) Console.YELLOW else Console.RED) +
+ s"(wants $dependsOn:$wantVersion, got $gotVersion)" +
+ Console.RESET
+ }
+ }
+
+ val parents: Map[Module, Seq[Parent]] = {
+ val links = for {
+ dep <- resolution.dependencies.toVector
+ elem <- Elem(dep, excluded = false).children
+ }
+ yield elem.dep.module -> Parent(
+ dep.module,
+ dep.version,
+ elem.dep.module,
+ elem.dep.version,
+ elem.reconciledVersion,
+ elem.excluded
+ )
+
+ links
+ .groupBy(_._1)
+ .mapValues(_.map(_._2).distinct.sortBy(par => (par.module.organization, par.module.name)))
+ .iterator
+ .toMap
+ }
+
+ def children(par: Parent) =
+ if (par.excluding)
+ Nil
+ else
+ parents.getOrElse(par.module, Nil)
+
+ Tree(
+ resolution
+ .dependencies
+ .toVector
+ .sortBy(dep => (dep.module.organization, dep.module.name, dep.version))
+ .map(dep =>
+ Parent(dep.module, dep.version, dep.module, dep.version, dep.version, excluding = false)
+ )
+ )(children, _.repr)
+ } else
+ Tree(roots.toVector.map(Elem(_, excluded = false)))(_.children, _.repr)
+ }
+
}
diff --git a/core/shared/src/main/scala/coursier/util/Tree.scala b/core/shared/src/main/scala/coursier/util/Tree.scala
new file mode 100644
index 000000000..a2a985641
--- /dev/null
+++ b/core/shared/src/main/scala/coursier/util/Tree.scala
@@ -0,0 +1,30 @@
+package coursier.util
+
+import scala.collection.mutable.ArrayBuffer
+
+object Tree {
+
+ def apply[T](roots: IndexedSeq[T])(children: T => Seq[T], print: T => String): String = {
+
+ def helper(elems: Seq[T], prefix: String, acc: String => Unit): Unit =
+ for ((elem, idx) <- elems.zipWithIndex) {
+ val isLast = idx == elems.length - 1
+
+ val tee = if (isLast) "└─ " else "├─ "
+
+ acc(prefix + tee + print(elem))
+
+ val children0 = children(elem)
+
+ if (children0.nonEmpty) {
+ val extraPrefix = if (isLast) " " else "| "
+ helper(children0, prefix + extraPrefix, acc)
+ }
+ }
+
+ val b = new ArrayBuffer[String]
+ helper(roots, "", b += _)
+ b.mkString("\n")
+ }
+
+}
diff --git a/fetch-js/src/main/scala/coursier/Platform.scala b/fetch-js/src/main/scala/coursier/Platform.scala
index ccb91f8ed..f09ced57d 100644
--- a/fetch-js/src/main/scala/coursier/Platform.scala
+++ b/fetch-js/src/main/scala/coursier/Platform.scala
@@ -80,7 +80,7 @@ object Platform {
get(artifact.url)
.map(\/-(_))
.recover { case e: Exception =>
- -\/(e.getMessage)
+ -\/(e.toString + Option(e.getMessage).fold("")(" (" + _ + ")"))
}
}
)
@@ -104,9 +104,10 @@ object Platform {
.flatMap(_ => get(artifact.url))
.map { s => logger.fetched(artifact.url); \/-(s) }
.recover { case e: Exception =>
- logger.other(artifact.url, e.getMessage)
- -\/(e.getMessage)
- }
+ val msg = e.toString + Option(e.getMessage).fold("")(" (" + _ + ")")
+ logger.other(artifact.url, msg)
+ -\/(msg)
+ }
}
)
}
diff --git a/plugin/src/main/scala-2.10/coursier/CoursierPlugin.scala b/plugin/src/main/scala-2.10/coursier/CoursierPlugin.scala
index 436f20171..04c5979e5 100644
--- a/plugin/src/main/scala-2.10/coursier/CoursierPlugin.scala
+++ b/plugin/src/main/scala-2.10/coursier/CoursierPlugin.scala
@@ -16,18 +16,41 @@ object CoursierPlugin extends AutoPlugin {
val coursierArtifactsChecksums = Keys.coursierArtifactsChecksums
val coursierCachePolicies = Keys.coursierCachePolicies
val coursierVerbosity = Keys.coursierVerbosity
+ val coursierSourceRepositories = Keys.coursierSourceRepositories
val coursierResolvers = Keys.coursierResolvers
val coursierSbtResolvers = Keys.coursierSbtResolvers
+ val coursierCredentials = Keys.coursierCredentials
val coursierFallbackDependencies = Keys.coursierFallbackDependencies
val coursierCache = Keys.coursierCache
val coursierProject = Keys.coursierProject
val coursierProjects = Keys.coursierProjects
val coursierPublications = Keys.coursierPublications
val coursierSbtClassifiersModule = Keys.coursierSbtClassifiersModule
+
+ val coursierConfigurations = Keys.coursierConfigurations
+
+ val coursierResolution = Keys.coursierResolution
+ val coursierSbtClassifiersResolution = Keys.coursierSbtClassifiersResolution
+
+ val coursierDependencyTree = Keys.coursierDependencyTree
+ val coursierDependencyInverseTree = Keys.coursierDependencyInverseTree
+
+ val coursierExport = Keys.coursierExport
+ val coursierExportDirectory = Keys.coursierExportDirectory
+ val coursierExportJavadoc = Keys.coursierExportJavadoc
+ val coursierExportSources = Keys.coursierExportSources
}
import autoImport._
+ lazy val treeSettings = Seq(
+ coursierDependencyTree <<= Tasks.coursierDependencyTreeTask(
+ inverse = false
+ ),
+ coursierDependencyInverseTree <<= Tasks.coursierDependencyTreeTask(
+ inverse = true
+ )
+ )
override lazy val projectSettings = Seq(
coursierParallelDownloads := 6,
@@ -36,8 +59,10 @@ object CoursierPlugin extends AutoPlugin {
coursierArtifactsChecksums := Seq(None),
coursierCachePolicies := Settings.defaultCachePolicies,
coursierVerbosity := Settings.defaultVerbosityLevel,
+ coursierSourceRepositories := Nil,
coursierResolvers <<= Tasks.coursierResolversTask,
coursierSbtResolvers <<= externalResolvers in updateSbtClassifiers,
+ coursierCredentials := Map.empty,
coursierFallbackDependencies <<= Tasks.coursierFallbackDependenciesTask,
coursierCache := Cache.default,
update <<= Tasks.updateTask(withClassifiers = false),
@@ -53,7 +78,18 @@ object CoursierPlugin extends AutoPlugin {
coursierProject <<= Tasks.coursierProjectTask,
coursierProjects <<= Tasks.coursierProjectsTask,
coursierPublications <<= Tasks.coursierPublicationsTask,
- coursierSbtClassifiersModule <<= classifiersModule in updateSbtClassifiers
- )
+ coursierSbtClassifiersModule <<= classifiersModule in updateSbtClassifiers,
+ coursierConfigurations <<= Tasks.coursierConfigurationsTask,
+ coursierResolution <<= Tasks.resolutionTask(),
+ coursierSbtClassifiersResolution <<= Tasks.resolutionTask(
+ sbtClassifiers = true
+ ),
+ coursierExport <<= Tasks.coursierExportTask,
+ coursierExportDirectory := baseDirectory.in(ThisBuild).value / "target" / "repository",
+ coursierExportJavadoc := false,
+ coursierExportSources := false
+ ) ++
+ inConfig(Compile)(treeSettings) ++
+ inConfig(Test)(treeSettings)
}
diff --git a/plugin/src/main/scala-2.10/coursier/Credentials.scala b/plugin/src/main/scala-2.10/coursier/Credentials.scala
new file mode 100644
index 000000000..c3229fe17
--- /dev/null
+++ b/plugin/src/main/scala-2.10/coursier/Credentials.scala
@@ -0,0 +1,57 @@
+package coursier
+
+import java.io.{File, FileInputStream}
+import java.util.Properties
+
+import coursier.core.Authentication
+
+sealed abstract class Credentials extends Product with Serializable {
+ def user: String
+ def password: String
+
+ def authentication: Authentication =
+ Authentication(user, password)
+}
+
+object Credentials {
+
+ case class Direct(user: String, password: String) extends Credentials {
+ override def toString = s"Direct($user, ******)"
+ }
+
+ case class FromFile(file: File) extends Credentials {
+
+ private lazy val props = {
+ val p = new Properties()
+ p.load(new FileInputStream(file))
+ p
+ }
+
+ private def findKey(keys: Seq[String]) = keys
+ .iterator
+ .map(props.getProperty)
+ .filter(_ != null)
+ .toStream
+ .headOption
+ .getOrElse {
+ throw new NoSuchElementException(s"${keys.head} key in $file")
+ }
+
+ lazy val user: String = findKey(FromFile.fileUserKeys)
+ lazy val password: String = findKey(FromFile.filePasswordKeys)
+ }
+
+ object FromFile {
+ // from sbt.Credentials
+ private val fileUserKeys = Seq("user", "user.name", "username")
+ private val filePasswordKeys = Seq("password", "pwd", "pass", "passwd")
+ }
+
+
+ def apply(user: String, password: String): Credentials =
+ Direct(user, password)
+
+ def apply(file: File): Credentials =
+ FromFile(file)
+
+}
diff --git a/plugin/src/main/scala-2.10/coursier/FallbackDependenciesRepository.scala b/plugin/src/main/scala-2.10/coursier/FallbackDependenciesRepository.scala
index 3c196e9d6..d2bf6358d 100644
--- a/plugin/src/main/scala-2.10/coursier/FallbackDependenciesRepository.scala
+++ b/plugin/src/main/scala-2.10/coursier/FallbackDependenciesRepository.scala
@@ -18,7 +18,7 @@ case class FallbackDependenciesRepository(
case None => Nil
case Some((url, changing)) =>
Seq(
- Artifact(url.toString, Map.empty, Map.empty, Attributes("jar", ""), changing)
+ Artifact(url.toString, Map.empty, Map.empty, Attributes("jar", ""), changing, None)
)
}
}
diff --git a/plugin/src/main/scala-2.10/coursier/FromSbt.scala b/plugin/src/main/scala-2.10/coursier/FromSbt.scala
index db49a7943..28ac928da 100644
--- a/plugin/src/main/scala-2.10/coursier/FromSbt.scala
+++ b/plugin/src/main/scala-2.10/coursier/FromSbt.scala
@@ -4,6 +4,7 @@ import coursier.ivy.{ IvyXml, IvyRepository }
import java.net.{ MalformedURLException, URL }
+import coursier.core.Authentication
import sbt.{ Resolver, CrossVersion, ModuleID }
import sbt.mavenint.SbtPomExtraProperties
@@ -145,17 +146,27 @@ object FromSbt {
} else
None
- private def mavenRepositoryOpt(root: String, log: sbt.Logger): Option[MavenRepository] =
+ private def mavenRepositoryOpt(
+ root: String,
+ log: sbt.Logger,
+ authentication: Option[Authentication]
+ ): Option[MavenRepository] =
try {
Cache.url(root) // ensure root is a URL whose protocol can be handled here
val root0 = if (root.endsWith("/")) root else root + "/"
- Some(MavenRepository(root0, sbtAttrStub = true))
+ Some(
+ MavenRepository(
+ root0,
+ sbtAttrStub = true,
+ authentication = authentication
+ )
+ )
} catch {
case e: MalformedURLException =>
log.warn(
"Error parsing Maven repository base " +
root +
- Option(e.getMessage).map(" (" + _ + ")").mkString +
+ Option(e.getMessage).fold("")(" (" + _ + ")") +
", ignoring it"
)
@@ -165,11 +176,12 @@ object FromSbt {
def repository(
resolver: Resolver,
ivyProperties: Map[String, String],
- log: sbt.Logger
+ log: sbt.Logger,
+ authentication: Option[Authentication]
): Option[Repository] =
resolver match {
case sbt.MavenRepository(_, root) =>
- mavenRepositoryOpt(root, log)
+ mavenRepositoryOpt(root, log, authentication)
case sbt.FileRepository(_, _, patterns)
if patterns.ivyPatterns.lengthCompare(1) == 0 &&
@@ -184,10 +196,11 @@ object FromSbt {
metadataPatternOpt = Some("file://" + patterns.ivyPatterns.head),
changing = Some(true),
properties = ivyProperties,
- dropInfoAttributes = true
+ dropInfoAttributes = true,
+ authentication = authentication
))
case Some(mavenCompatibleBase) =>
- mavenRepositoryOpt("file://" + mavenCompatibleBase, log)
+ mavenRepositoryOpt("file://" + mavenCompatibleBase, log, authentication)
}
case sbt.URLRepository(_, patterns)
@@ -203,10 +216,11 @@ object FromSbt {
metadataPatternOpt = Some(patterns.ivyPatterns.head),
changing = None,
properties = ivyProperties,
- dropInfoAttributes = true
+ dropInfoAttributes = true,
+ authentication = authentication
))
case Some(mavenCompatibleBase) =>
- mavenRepositoryOpt(mavenCompatibleBase, log)
+ mavenRepositoryOpt(mavenCompatibleBase, log, authentication)
}
case other =>
diff --git a/plugin/src/main/scala-2.10/coursier/Keys.scala b/plugin/src/main/scala-2.10/coursier/Keys.scala
index 796031c87..995f74084 100644
--- a/plugin/src/main/scala-2.10/coursier/Keys.scala
+++ b/plugin/src/main/scala-2.10/coursier/Keys.scala
@@ -7,24 +7,57 @@ import coursier.core.Publication
import sbt.{ GetClassifiersModule, Resolver, SettingKey, TaskKey }
object Keys {
- val coursierParallelDownloads = SettingKey[Int]("coursier-parallel-downloads", "")
- val coursierMaxIterations = SettingKey[Int]("coursier-max-iterations", "")
- val coursierChecksums = SettingKey[Seq[Option[String]]]("coursier-checksums", "")
- val coursierArtifactsChecksums = SettingKey[Seq[Option[String]]]("coursier-artifacts-checksums", "")
- val coursierCachePolicies = SettingKey[Seq[CachePolicy]]("coursier-cache-policies", "")
+ val coursierParallelDownloads = SettingKey[Int]("coursier-parallel-downloads")
+ val coursierMaxIterations = SettingKey[Int]("coursier-max-iterations")
+ val coursierChecksums = SettingKey[Seq[Option[String]]]("coursier-checksums")
+ val coursierArtifactsChecksums = SettingKey[Seq[Option[String]]]("coursier-artifacts-checksums")
+ val coursierCachePolicies = SettingKey[Seq[CachePolicy]]("coursier-cache-policies")
- val coursierVerbosity = SettingKey[Int]("coursier-verbosity", "")
+ val coursierVerbosity = SettingKey[Int]("coursier-verbosity")
- val coursierResolvers = TaskKey[Seq[Resolver]]("coursier-resolvers", "")
- val coursierSbtResolvers = TaskKey[Seq[Resolver]]("coursier-sbt-resolvers", "")
+ val coursierSourceRepositories = SettingKey[Seq[File]]("coursier-source-repositories")
+ val coursierResolvers = TaskKey[Seq[Resolver]]("coursier-resolvers")
+ val coursierSbtResolvers = TaskKey[Seq[Resolver]]("coursier-sbt-resolvers")
+ val coursierCredentials = TaskKey[Map[String, Credentials]]("coursier-credentials")
- val coursierCache = SettingKey[File]("coursier-cache", "")
+ val coursierCache = SettingKey[File]("coursier-cache")
- val coursierFallbackDependencies = TaskKey[Seq[(Module, String, URL, Boolean)]]("coursier-fallback-dependencies", "")
+ val coursierFallbackDependencies = TaskKey[Seq[(Module, String, URL, Boolean)]]("coursier-fallback-dependencies")
- val coursierProject = TaskKey[Project]("coursier-project", "")
- val coursierProjects = TaskKey[Seq[Project]]("coursier-projects", "")
- val coursierPublications = TaskKey[Seq[(String, Publication)]]("coursier-publications", "")
+ val coursierProject = TaskKey[Project]("coursier-project")
+ val coursierProjects = TaskKey[Seq[Project]]("coursier-projects")
+ val coursierPublications = TaskKey[Seq[(String, Publication)]]("coursier-publications")
- val coursierSbtClassifiersModule = TaskKey[GetClassifiersModule]("coursier-sbt-classifiers-module", "")
+ val coursierSbtClassifiersModule = TaskKey[GetClassifiersModule]("coursier-sbt-classifiers-module")
+
+ val coursierConfigurations = TaskKey[Map[String, Set[String]]]("coursier-configurations")
+
+ val coursierResolution = TaskKey[Resolution]("coursier-resolution")
+ val coursierSbtClassifiersResolution = TaskKey[Resolution]("coursier-sbt-classifiers-resolution")
+
+ val coursierDependencyTree = TaskKey[Unit](
+ "coursier-dependency-tree",
+ "Prints dependencies and transitive dependencies as a tree"
+ )
+ val coursierDependencyInverseTree = TaskKey[Unit](
+ "coursier-dependency-inverse-tree",
+ "Prints dependencies and transitive dependencies as an inverted tree (dependees as children)"
+ )
+
+ val coursierExport = TaskKey[Option[File]](
+ "coursier-export",
+ "Generates files allowing using these sources as a source dependency repository"
+ )
+ val coursierExportDirectory = TaskKey[File](
+ "coursier-export-directory",
+ "Base directory for the products of coursierExport"
+ )
+ val coursierExportJavadoc = SettingKey[Boolean](
+ "coursier-export-javadoc",
+ "Build javadoc packages for the coursier source dependency repository"
+ )
+ val coursierExportSources = SettingKey[Boolean](
+ "coursier-export-sources",
+ "Build sources packages for the coursier source dependency repository"
+ )
}
diff --git a/plugin/src/main/scala-2.10/coursier/Tasks.scala b/plugin/src/main/scala-2.10/coursier/Tasks.scala
index 4b6809ed2..e8fe7551f 100644
--- a/plugin/src/main/scala-2.10/coursier/Tasks.scala
+++ b/plugin/src/main/scala-2.10/coursier/Tasks.scala
@@ -9,6 +9,7 @@ import coursier.core.Publication
import coursier.ivy.IvyRepository
import coursier.Keys._
import coursier.Structure._
+import coursier.maven.WritePom
import coursier.util.{ Config, Print }
import org.apache.ivy.core.module.id.ModuleRevisionId
@@ -166,16 +167,49 @@ object Tasks {
sbtArtifactsPublication ++ extraSbtArtifactsPublication
}
- // FIXME More things should possibly be put here too (resolvers, etc.)
- private case class CacheKey(
+ def coursierConfigurationsTask = Def.task {
+
+ val configs0 = ivyConfigurations.value.map { config =>
+ config.name -> config.extendsConfigs.map(_.name)
+ }.toMap
+
+ def allExtends(c: String) = {
+ // possibly bad complexity
+ def helper(current: Set[String]): Set[String] = {
+ val newSet = current ++ current.flatMap(configs0.getOrElse(_, Nil))
+ if ((newSet -- current).nonEmpty)
+ helper(newSet)
+ else
+ newSet
+ }
+
+ helper(Set(c))
+ }
+
+ configs0.map {
+ case (config, _) =>
+ config -> allExtends(config)
+ }
+ }
+
+ private case class ResolutionCacheKey(
project: Project,
repositories: Seq[Repository],
resolution: Resolution,
+ sbtClassifiers: Boolean
+ )
+
+ private case class ReportCacheKey(
+ project: Project,
+ resolution: Resolution,
withClassifiers: Boolean,
sbtClassifiers: Boolean
)
- private val resolutionsCache = new mutable.HashMap[CacheKey, UpdateReport]
+ private val resolutionsCache = new mutable.HashMap[ResolutionCacheKey, Resolution]
+ // these may actually not need to be cached any more, now that the resolutions
+ // are cached
+ private val reportsCache = new mutable.HashMap[ReportCacheKey, UpdateReport]
private def forcedScalaModules(scalaVersion: String): Map[Module, String] =
Map(
@@ -185,17 +219,12 @@ object Tasks {
Module("org.scala-lang", "scalap") -> scalaVersion
)
- def updateTask(
- withClassifiers: Boolean,
- sbtClassifiers: Boolean = false,
- ignoreArtifactErrors: Boolean = false
- ) = Def.task {
+ private def projectDescription(project: Project) =
+ s"${project.module.organization}:${project.module.name}:${project.version}"
- def grouped[K, V](map: Seq[(K, V)]): Map[K, Seq[V]] =
- map.groupBy { case (k, _) => k }.map {
- case (k, l) =>
- k -> l.map { case (_, v) => v }
- }
+ def resolutionTask(
+ sbtClassifiers: Boolean = false
+ ) = Def.task {
// let's update only one module at once, for a better output
// Downloads are already parallel, no need to parallelize further anyway
@@ -230,25 +259,10 @@ object Tasks {
(proj.copy(publications = publications), fallbackDeps)
}
- val ivySbt0 = ivySbt.value
- val ivyCacheManager = ivySbt0.withIvy(streams.value.log)(ivy =>
- ivy.getResolutionCacheManager
- )
-
- val ivyModule = ModuleRevisionId.newInstance(
- currentProject.module.organization,
- currentProject.module.name,
- currentProject.version,
- currentProject.module.attributes.asJava
- )
- val cacheIvyFile = ivyCacheManager.getResolvedIvyFileInCache(ivyModule)
- val cacheIvyPropertiesFile = ivyCacheManager.getResolvedIvyPropertiesInCache(ivyModule)
-
val projects = coursierProjects.value
val parallelDownloads = coursierParallelDownloads.value
val checksums = coursierChecksums.value
- val artifactsChecksums = coursierArtifactsChecksums.value
val maxIterations = coursierMaxIterations.value
val cachePolicies = coursierCachePolicies.value
val cache = coursierCache.value
@@ -286,6 +300,40 @@ object Tasks {
else
coursierResolvers.value
+ val sourceRepositories = coursierSourceRepositories.value.map { dir =>
+ // FIXME Don't hardcode this path?
+ new File(dir, "target/repository")
+ }
+
+ val sourceRepositoriesForcedDependencies = sourceRepositories.flatMap {
+ base =>
+
+ def pomDirComponents(f: File, components: Vector[String]): Stream[Vector[String]] =
+ if (f.isDirectory) {
+ val components0 = components :+ f.getName
+ Option(f.listFiles()).toStream.flatten.flatMap(pomDirComponents(_, components0))
+ } else if (f.getName.endsWith(".pom"))
+ Stream(components)
+ else
+ Stream.empty
+
+ Option(base.listFiles())
+ .toVector
+ .flatten
+ .flatMap(pomDirComponents(_, Vector()))
+ // at least 3 for org / name / version - the contrary should not happen, but who knows
+ .filter(_.length >= 3)
+ .map { components =>
+ val org = components.dropRight(2).mkString(".")
+ val name = components(components.length - 2)
+ val version = components.last
+
+ Module(org, name) -> version
+ }
+ }
+
+ // TODO Warn about possible duplicated modules from source repositories?
+
val verbosityLevel = coursierVerbosity.value
@@ -295,25 +343,14 @@ object Tasks {
dep.copy(exclusions = dep.exclusions ++ exclusions)
}.toSet,
filter = Some(dep => !dep.optional),
- forceVersions = userForceVersions ++ forcedScalaModules(sv) ++ projects.map(_.moduleVersion)
+ forceVersions =
+ // order matters here
+ userForceVersions ++
+ sourceRepositoriesForcedDependencies ++
+ forcedScalaModules(sv) ++
+ projects.map(_.moduleVersion)
)
- // required for publish to be fine, later on
- def writeIvyFiles() = {
- val printer = new scala.xml.PrettyPrinter(80, 2)
-
- val b = new StringBuilder
- b ++= """"""
- b += '\n'
- b ++= printer.format(MakeIvyXml(currentProject))
- cacheIvyFile.getParentFile.mkdirs()
- Files.write(cacheIvyFile.toPath, b.result().getBytes("UTF-8"))
-
- // Just writing an empty file here... Are these only used?
- cacheIvyPropertiesFile.getParentFile.mkdirs()
- Files.write(cacheIvyPropertiesFile.toPath, "".getBytes("UTF-8"))
- }
-
if (verbosityLevel >= 2) {
log.info("InterProjectRepository")
for (p <- projects)
@@ -334,12 +371,14 @@ object Tasks {
"ivy.home" -> (new File(sys.props("user.home")).toURI.getPath + ".ivy2")
) ++ sys.props
- val repositories = Seq(
- globalPluginsRepo,
- interProjectRepo
- ) ++ resolvers.flatMap(
- FromSbt.repository(_, ivyProperties, log)
- ) ++ {
+ val credentials = coursierCredentials.value
+
+ val sourceRepositories0 = sourceRepositories.map {
+ base =>
+ MavenRepository(base.toURI.toString, changing = Some(true))
+ }
+
+ val fallbackDependenciesRepositories =
if (fallbackDependencies.isEmpty)
Nil
else {
@@ -352,9 +391,21 @@ object Tasks {
FallbackDependenciesRepository(map)
)
}
- }
- def report = {
+ val repositories =
+ Seq(globalPluginsRepo, interProjectRepo) ++
+ sourceRepositories0 ++
+ resolvers.flatMap { resolver =>
+ FromSbt.repository(
+ resolver,
+ ivyProperties,
+ log,
+ credentials.get(resolver.name).map(_.authentication)
+ )
+ } ++
+ fallbackDependenciesRepositories
+
+ def resolution = {
val pool = Executors.newFixedThreadPool(parallelDownloads, Strategy.DefaultDaemonThreadFactory)
def createLogger() = new TermDisplay(new OutputStreamWriter(System.err))
@@ -374,11 +425,6 @@ object Tasks {
s"${dep.module}:${dep.version}:$config->${dep.configuration}"
}.sorted.distinct
- def depsRepr0(deps: Seq[Dependency]) =
- deps.map { dep =>
- s"${dep.module}:${dep.version}:${dep.configuration}"
- }.sorted.distinct
-
if (verbosityLevel >= 2) {
val repoReprs = repositories.map {
case r: IvyRepository =>
@@ -399,7 +445,10 @@ object Tasks {
}
if (verbosityLevel >= 0)
- log.info(s"Updating ${currentProject.module.organization}:${currentProject.module.name}:${currentProject.version}")
+ log.info(
+ s"Updating ${projectDescription(currentProject)}" +
+ (if (sbtClassifiers) " (sbt classifiers)" else "")
+ )
if (verbosityLevel >= 2)
for (depRepr <- depsRepr(currentProject.dependencies))
log.info(s" $depRepr")
@@ -443,34 +492,115 @@ object Tasks {
throw new Exception(s"Encountered ${res.errors.length} error(s) in dependency resolution")
}
- val depsByConfig = grouped(currentProject.dependencies)
+ if (verbosityLevel >= 0)
+ log.info(s"Resolved ${projectDescription(currentProject)} dependencies")
- val configs = {
- val configs0 = ivyConfigurations.value.map { config =>
- config.name -> config.extendsConfigs.map(_.name)
- }.toMap
+ res
+ }
- def allExtends(c: String) = {
- // possibly bad complexity
- def helper(current: Set[String]): Set[String] = {
- val newSet = current ++ current.flatMap(configs0.getOrElse(_, Nil))
- if ((newSet -- current).nonEmpty)
- helper(newSet)
- else
- newSet
- }
+ resolutionsCache.getOrElseUpdate(
+ ResolutionCacheKey(
+ currentProject,
+ repositories,
+ startRes.copy(filter = None),
+ sbtClassifiers
+ ),
+ resolution
+ )
+ }
+ }
- helper(Set(c))
- }
+ def updateTask(
+ withClassifiers: Boolean,
+ sbtClassifiers: Boolean = false,
+ ignoreArtifactErrors: Boolean = false
+ ) = Def.task {
- configs0.map {
- case (config, _) =>
- config -> allExtends(config)
- }
+ def grouped[K, V](map: Seq[(K, V)]): Map[K, Seq[V]] =
+ map.groupBy { case (k, _) => k }.map {
+ case (k, l) =>
+ k -> l.map { case (_, v) => v }
+ }
+
+ // let's update only one module at once, for a better output
+ // Downloads are already parallel, no need to parallelize further anyway
+ synchronized {
+
+ lazy val cm = coursierSbtClassifiersModule.value
+
+ val currentProject =
+ if (sbtClassifiers) {
+ val sv = scalaVersion.value
+ val sbv = scalaBinaryVersion.value
+
+ FromSbt.project(
+ cm.id,
+ cm.modules,
+ cm.configurations.map(cfg => cfg.name -> cfg.extendsConfigs.map(_.name)).toMap,
+ sv,
+ sbv
+ )
+ } else {
+ val proj = coursierProject.value
+ val publications = coursierPublications.value
+ proj.copy(publications = publications)
}
- if (verbosityLevel >= 0)
- log.info("Resolution done")
+ val ivySbt0 = ivySbt.value
+ val ivyCacheManager = ivySbt0.withIvy(streams.value.log)(ivy =>
+ ivy.getResolutionCacheManager
+ )
+
+ val ivyModule = ModuleRevisionId.newInstance(
+ currentProject.module.organization,
+ currentProject.module.name,
+ currentProject.version,
+ currentProject.module.attributes.asJava
+ )
+ val cacheIvyFile = ivyCacheManager.getResolvedIvyFileInCache(ivyModule)
+ val cacheIvyPropertiesFile = ivyCacheManager.getResolvedIvyPropertiesInCache(ivyModule)
+
+ val parallelDownloads = coursierParallelDownloads.value
+ val artifactsChecksums = coursierArtifactsChecksums.value
+ val cachePolicies = coursierCachePolicies.value
+ val cache = coursierCache.value
+
+ val log = streams.value.log
+
+ val verbosityLevel = coursierVerbosity.value
+
+ // required for publish to be fine, later on
+ def writeIvyFiles() = {
+ val printer = new scala.xml.PrettyPrinter(80, 2)
+
+ val b = new StringBuilder
+ b ++= """"""
+ b += '\n'
+ b ++= printer.format(MakeIvyXml(currentProject))
+ cacheIvyFile.getParentFile.mkdirs()
+ Files.write(cacheIvyFile.toPath, b.result().getBytes("UTF-8"))
+
+ // Just writing an empty file here... Are these only used?
+ cacheIvyPropertiesFile.getParentFile.mkdirs()
+ Files.write(cacheIvyPropertiesFile.toPath, "".getBytes("UTF-8"))
+ }
+
+ val res = {
+ if (withClassifiers && sbtClassifiers)
+ coursierSbtClassifiersResolution
+ else
+ coursierResolution
+ }.value
+
+ def report = {
+ val pool = Executors.newFixedThreadPool(parallelDownloads, Strategy.DefaultDaemonThreadFactory)
+
+ def createLogger() = new TermDisplay(new OutputStreamWriter(System.err))
+
+ val depsByConfig = grouped(currentProject.dependencies)
+
+ val configs = coursierConfigurations.value
+
if (verbosityLevel >= 2) {
val finalDeps = Config.dependenciesWithConfig(
res,
@@ -520,7 +650,10 @@ object Tasks {
}
if (verbosityLevel >= 0)
- log.info("Fetching artifacts")
+ log.info(
+ s"Fetching artifacts of ${projectDescription(currentProject)}" +
+ (if (sbtClassifiers) " (sbt classifiers)" else "")
+ )
artifactsLogger.init()
@@ -534,7 +667,10 @@ object Tasks {
artifactsLogger.stop()
if (verbosityLevel >= 0)
- log.info("Fetching artifacts: done")
+ log.info(
+ s"Fetched artifacts of ${projectDescription(currentProject)}" +
+ (if (sbtClassifiers) " (sbt classifiers)" else "")
+ )
val artifactFiles = artifactFilesOrErrors.collect {
case (artifact, \/-(file)) =>
@@ -602,11 +738,10 @@ object Tasks {
)
}
- resolutionsCache.getOrElseUpdate(
- CacheKey(
+ reportsCache.getOrElseUpdate(
+ ReportCacheKey(
currentProject,
- repositories,
- startRes.copy(filter = None),
+ res,
withClassifiers,
sbtClassifiers
),
@@ -615,4 +750,145 @@ object Tasks {
}
}
+ def coursierDependencyTreeTask(
+ inverse: Boolean,
+ sbtClassifiers: Boolean = false,
+ ignoreArtifactErrors: Boolean = false
+ ) = Def.task {
+
+ val currentProject =
+ if (sbtClassifiers) {
+ val cm = coursierSbtClassifiersModule.value
+ val sv = scalaVersion.value
+ val sbv = scalaBinaryVersion.value
+
+ FromSbt.project(
+ cm.id,
+ cm.modules,
+ cm.configurations.map(cfg => cfg.name -> cfg.extendsConfigs.map(_.name)).toMap,
+ sv,
+ sbv
+ )
+ } else {
+ val proj = coursierProject.value
+ val publications = coursierPublications.value
+ proj.copy(publications = publications)
+ }
+
+ val res = {
+ if (sbtClassifiers)
+ coursierSbtClassifiersResolution
+ else
+ coursierResolution
+ }.value
+
+ val config = classpathConfiguration.value.name
+ val configs = coursierConfigurations.value
+
+ val includedConfigs = configs.getOrElse(config, Set.empty) + config
+
+ val dependencies0 = currentProject.dependencies.collect {
+ case (cfg, dep) if includedConfigs(cfg) => dep
+ }.sortBy { dep =>
+ (dep.module.organization, dep.module.name, dep.version)
+ }
+
+ val subRes = res.subset(dependencies0.toSet)
+
+ // use sbt logging?
+ println(
+ projectDescription(currentProject) + "\n" +
+ Print.dependencyTree(dependencies0, subRes, printExclusions = true, inverse)
+ )
+ }
+
+ def coursierExportTask =
+ (
+ sbt.Keys.state,
+ sbt.Keys.thisProjectRef,
+ sbt.Keys.projectID,
+ sbt.Keys.scalaVersion,
+ sbt.Keys.scalaBinaryVersion,
+ sbt.Keys.ivyConfigurations,
+ streams,
+ coursierProject,
+ coursierExportDirectory,
+ coursierExportJavadoc,
+ coursierExportSources
+ ).flatMap { (state, projectRef, projId, sv, sbv, ivyConfs, streams, proj, exportDir, exportJavadoc, exportSources) =>
+
+ val javadocPackageTasks =
+ if (exportJavadoc)
+ Seq(Some("javadoc") -> packageDoc)
+ else
+ Nil
+
+ val sourcesPackageTasks =
+ if (exportJavadoc)
+ Seq(Some("sources") -> packageSrc)
+ else
+ Nil
+
+ val packageTasks = Seq(None -> packageBin) ++ javadocPackageTasks ++ sourcesPackageTasks
+
+ val configs = Seq(None -> Compile, Some("tests") -> Test)
+
+ val productTasks =
+ for {
+ (classifierOpt, pkgTask) <- packageTasks
+ (classifierPrefixOpt, config) <- configs
+ if publishArtifact.in(projectRef).in(pkgTask).in(config).getOrElse(state, false)
+ } yield {
+ val classifier = (classifierPrefixOpt.toSeq ++ classifierOpt.toSeq).mkString("-")
+ pkgTask.in(projectRef).in(config).get(state).map((classifier, _))
+ }
+
+ val productTask = sbt.std.TaskExtra.joinTasks(productTasks).join
+
+ val dir = new File(
+ exportDir,
+ s"${proj.module.organization.replace('.', '/')}/${proj.module.name}/${proj.version}"
+ )
+
+ def pom = "\n" + WritePom.project(proj, Some("jar"))
+
+ val log = streams.log
+
+ productTask.map { products =>
+
+ if (products.isEmpty)
+ None
+ else {
+
+ dir.mkdirs()
+
+ val pomFile = new File(dir, s"${proj.module.name}-${proj.version}.pom")
+ Files.write(pomFile.toPath, pom.getBytes("UTF-8"))
+ log.info(s"Wrote POM file to $pomFile")
+
+ for ((classifier, f) <- products) {
+
+ val suffix = if (classifier.isEmpty) "" else "-" + classifier
+
+ val jarPath = new File(dir, s"${proj.module.name}-${proj.version}$suffix.jar")
+
+ if (jarPath.exists()) {
+ if (!jarPath.delete())
+ log.warn(s"Cannot remove $jarPath")
+ }
+
+ Files.createSymbolicLink(
+ jarPath.toPath,
+ dir.toPath.relativize(f.toPath)
+ )
+ log.info(s"Created symbolic link $jarPath -> $f")
+ }
+
+ // TODO Clean extra files in dir
+
+ Some(exportDir)
+ }
+ }
+ }
+
}
diff --git a/plugin/src/sbt-test/sbt-coursier/credentials-from-file/build.sbt b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/build.sbt
new file mode 100644
index 000000000..ddec377a8
--- /dev/null
+++ b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/build.sbt
@@ -0,0 +1,14 @@
+scalaVersion := "2.11.8"
+
+resolvers += "authenticated" at "http://localhost:8080"
+
+coursierCredentials += "authenticated" -> coursier.Credentials(file("credentials"))
+
+coursierCachePolicies := {
+ if (sys.props("os.name").startsWith("Windows"))
+ coursierCachePolicies.value
+ else
+ Seq(coursier.CachePolicy.ForceDownload)
+}
+
+libraryDependencies += "com.abc" % "test" % "0.1"
diff --git a/plugin/src/sbt-test/sbt-coursier/credentials-from-file/credentials b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/credentials
new file mode 100644
index 000000000..03c71b2e5
--- /dev/null
+++ b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/credentials
@@ -0,0 +1,2 @@
+user=user
+password=pass
diff --git a/plugin/src/sbt-test/sbt-coursier/credentials-from-file/project/plugins.sbt b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/project/plugins.sbt
new file mode 100644
index 000000000..152225a9e
--- /dev/null
+++ b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/project/plugins.sbt
@@ -0,0 +1,11 @@
+{
+ val pluginVersion = sys.props.getOrElse(
+ "plugin.version",
+ throw new RuntimeException(
+ """|The system property 'plugin.version' is not defined.
+ |Specify this property using the scriptedLaunchOpts -D.""".stripMargin
+ )
+ )
+
+ addSbtPlugin("io.get-coursier" % "sbt-coursier" % pluginVersion)
+}
diff --git a/plugin/src/sbt-test/sbt-coursier/credentials-from-file/src/main/scala/Main.scala b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/src/main/scala/Main.scala
new file mode 100644
index 000000000..86ae9e9e3
--- /dev/null
+++ b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/src/main/scala/Main.scala
@@ -0,0 +1 @@
+object Main extends App
\ No newline at end of file
diff --git a/plugin/src/sbt-test/sbt-coursier/credentials-from-file/test b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/test
new file mode 100644
index 000000000..4d96ce4c9
--- /dev/null
+++ b/plugin/src/sbt-test/sbt-coursier/credentials-from-file/test
@@ -0,0 +1 @@
+> coursierResolution
diff --git a/plugin/src/sbt-test/sbt-coursier/credentials/build.sbt b/plugin/src/sbt-test/sbt-coursier/credentials/build.sbt
new file mode 100644
index 000000000..28a48f79a
--- /dev/null
+++ b/plugin/src/sbt-test/sbt-coursier/credentials/build.sbt
@@ -0,0 +1,14 @@
+scalaVersion := "2.11.8"
+
+resolvers += "authenticated" at "http://localhost:8080"
+
+coursierCredentials += "authenticated" -> coursier.Credentials("user", "pass")
+
+coursierCachePolicies := {
+ if (sys.props("os.name").startsWith("Windows"))
+ coursierCachePolicies.value
+ else
+ Seq(coursier.CachePolicy.ForceDownload)
+}
+
+libraryDependencies += "com.abc" % "test" % "0.1"
diff --git a/plugin/src/sbt-test/sbt-coursier/credentials/project/plugins.sbt b/plugin/src/sbt-test/sbt-coursier/credentials/project/plugins.sbt
new file mode 100644
index 000000000..152225a9e
--- /dev/null
+++ b/plugin/src/sbt-test/sbt-coursier/credentials/project/plugins.sbt
@@ -0,0 +1,11 @@
+{
+ val pluginVersion = sys.props.getOrElse(
+ "plugin.version",
+ throw new RuntimeException(
+ """|The system property 'plugin.version' is not defined.
+ |Specify this property using the scriptedLaunchOpts -D.""".stripMargin
+ )
+ )
+
+ addSbtPlugin("io.get-coursier" % "sbt-coursier" % pluginVersion)
+}
diff --git a/plugin/src/sbt-test/sbt-coursier/credentials/src/main/scala/Main.scala b/plugin/src/sbt-test/sbt-coursier/credentials/src/main/scala/Main.scala
new file mode 100644
index 000000000..86ae9e9e3
--- /dev/null
+++ b/plugin/src/sbt-test/sbt-coursier/credentials/src/main/scala/Main.scala
@@ -0,0 +1 @@
+object Main extends App
\ No newline at end of file
diff --git a/plugin/src/sbt-test/sbt-coursier/credentials/test b/plugin/src/sbt-test/sbt-coursier/credentials/test
new file mode 100644
index 000000000..4d96ce4c9
--- /dev/null
+++ b/plugin/src/sbt-test/sbt-coursier/credentials/test
@@ -0,0 +1 @@
+> coursierResolution
diff --git a/plugin/src/sbt-test/sbt-coursier/exclude-dependencies/build.sbt b/plugin/src/sbt-test/sbt-coursier/exclude-dependencies/build.sbt
index 75d83d382..d3a9c89b9 100644
--- a/plugin/src/sbt-test/sbt-coursier/exclude-dependencies/build.sbt
+++ b/plugin/src/sbt-test/sbt-coursier/exclude-dependencies/build.sbt
@@ -1,5 +1,12 @@
scalaVersion := "2.11.8"
+coursierCachePolicies := {
+ if (sys.props("os.name").startsWith("Windows"))
+ coursierCachePolicies.value
+ else
+ Seq(coursier.CachePolicy.ForceDownload)
+}
+
libraryDependencies += "com.github.alexarchambault" %% "argonaut-shapeless_6.1" % "1.0.0-RC1"
excludeDependencies += SbtExclusionRule("com.chuusai", "shapeless_2.11")
diff --git a/plugin/src/sbt-test/sbt-coursier/from/build.sbt b/plugin/src/sbt-test/sbt-coursier/from/build.sbt
index 0f861ab23..9640ce8da 100644
--- a/plugin/src/sbt-test/sbt-coursier/from/build.sbt
+++ b/plugin/src/sbt-test/sbt-coursier/from/build.sbt
@@ -1,5 +1,12 @@
scalaVersion := "2.11.8"
+coursierCachePolicies := {
+ if (sys.props("os.name").startsWith("Windows"))
+ coursierCachePolicies.value
+ else
+ Seq(coursier.CachePolicy.ForceDownload)
+}
+
libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.41" from {
val f = file(sys.props("sbttest.base")) / "sbt-coursier" / "from" / "shapeless_2.11-2.3.0.jar"
diff --git a/plugin/src/sbt-test/sbt-coursier/maven-compatible/build.sbt b/plugin/src/sbt-test/sbt-coursier/maven-compatible/build.sbt
index 58b00349c..7479343f5 100644
--- a/plugin/src/sbt-test/sbt-coursier/maven-compatible/build.sbt
+++ b/plugin/src/sbt-test/sbt-coursier/maven-compatible/build.sbt
@@ -1,5 +1,12 @@
scalaVersion := "2.11.8"
+coursierCachePolicies := {
+ if (sys.props("os.name").startsWith("Windows"))
+ coursierCachePolicies.value
+ else
+ Seq(coursier.CachePolicy.ForceDownload)
+}
+
resolvers += Resolver.url(
"webjars-bintray",
new URL("https://dl.bintray.com/scalaz/releases/")
diff --git a/plugin/src/sbt-test/sbt-coursier/simple/build.sbt b/plugin/src/sbt-test/sbt-coursier/simple/build.sbt
index c03b2c8be..9725a4306 100644
--- a/plugin/src/sbt-test/sbt-coursier/simple/build.sbt
+++ b/plugin/src/sbt-test/sbt-coursier/simple/build.sbt
@@ -1 +1,8 @@
scalaVersion := "2.11.8"
+
+coursierCachePolicies := {
+ if (sys.props("os.name").startsWith("Windows"))
+ coursierCachePolicies.value
+ else
+ Seq(coursier.CachePolicy.ForceDownload)
+}
diff --git a/simple-web-server/src/main/scala/coursier/SimpleHttpServer.scala b/simple-web-server/src/main/scala/coursier/SimpleHttpServer.scala
index 3bd6b8fdb..ef52696d5 100644
--- a/simple-web-server/src/main/scala/coursier/SimpleHttpServer.scala
+++ b/simple-web-server/src/main/scala/coursier/SimpleHttpServer.scala
@@ -1,15 +1,19 @@
package coursier
import java.io.{ File, FileOutputStream }
+import java.net.NetworkInterface
import java.nio.channels.{ FileLock, OverlappingFileLockException }
import org.http4s.dsl._
import org.http4s.headers.Authorization
+import org.http4s.server.HttpService
import org.http4s.server.blaze.BlazeBuilder
-import org.http4s.{ BasicCredentials, Challenge, HttpService, Request, Response }
+import org.http4s.{ BasicCredentials, Challenge, EmptyBody, Request, Response }
import caseapp._
+import scala.collection.JavaConverters._
+
import scalaz.concurrent.Task
case class SimpleHttpServerApp(
@@ -22,15 +26,17 @@ case class SimpleHttpServerApp(
@ExtraName("p")
@ValueDescription("port")
port: Int = 8080,
- @ExtraName("P")
+ @ExtraName("s")
acceptPost: Boolean,
@ExtraName("t")
acceptPut: Boolean,
@ExtraName("w")
- @HelpMessage("Accept write requests. Equivalent to -P -t")
+ @HelpMessage("Accept write requests. Equivalent to -s -t")
acceptWrite: Boolean,
@ExtraName("v")
verbose: Int @@ Counter,
+ @ExtraName("q")
+ quiet: Boolean,
@ExtraName("u")
@ValueDescription("user")
user: String,
@@ -44,7 +50,7 @@ case class SimpleHttpServerApp(
val baseDir = new File(if (directory.isEmpty) "." else directory)
- val verbosityLevel = Tag.unwrap(verbose)
+ val verbosityLevel = Tag.unwrap(verbose) - (if (quiet) 1 else 0)
def write(path: Seq[String], req: Request): Boolean = {
@@ -108,16 +114,37 @@ case class SimpleHttpServerApp(
else
HttpService {
case req =>
+ def warn(msg: => String) =
+ if (verbosityLevel >= 1)
+ Console.err.println(s"${req.method.name} ${req.uri.path}: $msg")
+
req.headers.get(Authorization) match {
case None =>
+ warn("no authentication provided")
unauthorized
case Some(auth) =>
auth.credentials match {
case basic: BasicCredentials =>
if (basic.username == user && basic.password == password)
- service.run(req)
- else
+ service.run(req).flatMap {
+ case Some(v) => Task.now(v)
+ case None => NotFound()
+ }
+ else {
+ warn {
+ val msg =
+ if (basic.username == user)
+ "wrong password"
+ else
+ s"unrecognized user ${basic.username}"
+
+ s"authentication failed ($msg)"
+ }
unauthorized
+ }
+ case _ =>
+ warn("no basic credentials found")
+ unauthorized
}
}
}
@@ -145,15 +172,22 @@ case class SimpleHttpServerApp(
}
def getService = authenticated {
- case GET -> path =>
+ case (method @ (GET | HEAD)) -> path =>
if (verbosityLevel >= 1)
- Console.err.println(s"GET $path")
+ Console.err.println(s"${method.name} $path")
val f = new File(baseDir, path.toList.mkString("/"))
- if (f.exists())
+ val resp = if (f.exists())
Ok(f)
else
NotFound()
+
+ method match {
+ case HEAD =>
+ resp.map(_.copy(body = EmptyBody))
+ case _ =>
+ resp
+ }
}
val builder = {
@@ -169,6 +203,16 @@ case class SimpleHttpServerApp(
b
}
+ if (verbosityLevel >= 0) {
+ Console.err.println(s"Listening on http://$host:$port")
+
+ if (verbosityLevel >= 1 && host == "0.0.0.0") {
+ Console.err.println(s"Listening on addresses")
+ for (itf <- NetworkInterface.getNetworkInterfaces.asScala; addr <- itf.getInetAddresses.asScala)
+ Console.err.println(s" ${addr.getHostAddress} (${itf.getName})")
+ }
+ }
+
builder
.run
.awaitShutdown()
diff --git a/tests/jvm/src/test/scala/coursier/test/CacheFetchTests.scala b/tests/jvm/src/test/scala/coursier/test/CacheFetchTests.scala
new file mode 100644
index 000000000..ab2cdc73e
--- /dev/null
+++ b/tests/jvm/src/test/scala/coursier/test/CacheFetchTests.scala
@@ -0,0 +1,147 @@
+package coursier
+package test
+
+import java.io.File
+import java.nio.file.Files
+
+import coursier.cache.protocol.TestprotocolHandler
+import coursier.core.Authentication
+
+import utest._
+
+import scala.util.Try
+
+object CacheFetchTests extends TestSuite {
+
+ val tests = TestSuite {
+
+ def check(extraRepo: Repository): Unit = {
+
+ val tmpDir = Files.createTempDirectory("coursier-cache-fetch-tests").toFile
+
+ def cleanTmpDir() = {
+ def delete(f: File): Boolean =
+ if (f.isDirectory) {
+ val removedContent = Option(f.listFiles()).toSeq.flatten.map(delete).forall(x => x)
+ val removedDir = f.delete()
+
+ removedContent && removedDir
+ } else
+ f.delete()
+
+ if (!delete(tmpDir))
+ Console.err.println(s"Warning: unable to remove temporary directory $tmpDir")
+ }
+
+ val res = try {
+ val fetch = Fetch.from(
+ Seq(
+ extraRepo,
+ MavenRepository("https://repo1.maven.org/maven2")
+ ),
+ Cache.fetch(
+ tmpDir
+ )
+ )
+
+ val startRes = Resolution(
+ Set(
+ Dependency(
+ Module("com.github.alexarchambault", "coursier_2.11"), "1.0.0-M9-test"
+ )
+ )
+ )
+
+ startRes.process.run(fetch).run
+ } finally {
+ cleanTmpDir()
+ }
+
+ val errors = res.errors
+
+ assert(errors.isEmpty)
+ }
+
+ // using scala-test would allow to put the below comments in the test names...
+
+ * - {
+ // test that everything's fine with basic file protocol
+ val repoPath = new File(getClass.getResource("/test-repo/http/abc.com").getPath)
+ check(MavenRepository(repoPath.toURI.toString))
+ }
+
+ 'customProtocol - {
+ * - {
+ // test the Cache.url method
+ val shouldFail = Try(Cache.url("notfoundzzzz://foo/bar"))
+ assert(shouldFail.isFailure)
+
+ Cache.url("testprotocol://foo/bar")
+ }
+
+ * - {
+ // the real custom protocol test
+ check(MavenRepository(s"${TestprotocolHandler.protocol}://foo/"))
+ }
+ }
+
+ 'httpAuthentication - {
+ // requires an authenticated HTTP server to be running on localhost:8080 with user 'user'
+ // and password 'pass'
+
+ val address = "localhost:8080"
+ val user = "user"
+ val password = "pass"
+
+ def printErrorMessage() =
+ Console.err.println(
+ Console.RED +
+ s"HTTP authentication tests require a running HTTP server on $address, requiring " +
+ s"basic authentication with user '$user' and password '$password', serving the right " +
+ "files.\n" + Console.RESET +
+ "Run one from the coursier sources with\n" +
+ " ./coursier launch -r http://dl.bintray.com/scalaz/releases " +
+ "io.get-coursier:simple-web-server_2.11:1.0.0-M12 -- " +
+ "-d tests/jvm/src/test/resources/test-repo/http/abc.com -u user -P pass -r realm -v"
+ )
+
+ * - {
+ // no authentication -> should fail
+
+ val failed = try {
+ check(
+ MavenRepository(
+ s"http://$address"
+ )
+ )
+
+ printErrorMessage()
+ false
+ } catch {
+ case e: Throwable =>
+ true
+ }
+
+ assert(failed)
+ }
+
+ * - {
+ // with authentication -> should work
+
+ try {
+ check(
+ MavenRepository(
+ s"http://$address",
+ authentication = Some(Authentication(user, password))
+ )
+ )
+ } catch {
+ case e: Throwable =>
+ printErrorMessage()
+ throw e
+ }
+ }
+ }
+ }
+
+}
diff --git a/tests/jvm/src/test/scala/coursier/test/ChecksumTests.scala b/tests/jvm/src/test/scala/coursier/test/ChecksumTests.scala
index ee11b1935..9b545448b 100644
--- a/tests/jvm/src/test/scala/coursier/test/ChecksumTests.scala
+++ b/tests/jvm/src/test/scala/coursier/test/ChecksumTests.scala
@@ -70,7 +70,8 @@ object ChecksumTests extends TestSuite {
),
Map.empty,
Attributes("jar"),
- changing = false
+ changing = false,
+ authentication = None
)
val artifacts = Seq(
diff --git a/tests/jvm/src/test/scala/coursier/test/CustomProtocolTests.scala b/tests/jvm/src/test/scala/coursier/test/CustomProtocolTests.scala
deleted file mode 100644
index 08597d8d9..000000000
--- a/tests/jvm/src/test/scala/coursier/test/CustomProtocolTests.scala
+++ /dev/null
@@ -1,85 +0,0 @@
-package coursier
-package test
-
-import java.io.File
-import java.nio.file.Files
-
-import coursier.cache.protocol.TestprotocolHandler
-import utest._
-
-import scala.util.Try
-
-object CustomProtocolTests extends TestSuite {
-
- val tests = TestSuite {
-
- def check(extraMavenRepo: String): Unit = {
-
- val tmpDir = Files.createTempDirectory("coursier-protocol-tests").toFile
-
- def cleanTmpDir() = {
- def delete(f: File): Boolean =
- if (f.isDirectory) {
- val removedContent = f.listFiles().map(delete).forall(x => x)
- val removedDir = f.delete()
-
- removedContent && removedDir
- } else
- f.delete()
-
- if (!delete(tmpDir))
- Console.err.println(s"Warning: unable to remove temporary directory $tmpDir")
- }
-
- val res = try {
- val fetch = Fetch.from(
- Seq(
- MavenRepository(extraMavenRepo),
- MavenRepository("https://repo1.maven.org/maven2")
- ),
- Cache.fetch(
- tmpDir
- )
- )
-
- val startRes = Resolution(
- Set(
- Dependency(
- Module("com.github.alexarchambault", "coursier_2.11"), "1.0.0-M9-test"
- )
- )
- )
-
- startRes.process.run(fetch).run
- } finally {
- cleanTmpDir()
- }
-
- val errors = res.errors
-
- assert(errors.isEmpty)
- }
-
- // using scala-test would allow to put the below comments in the test names...
-
- * - {
- // test that everything's fine with standard protocols
- val repoPath = new File(getClass.getResource("/test-repo/http/abc.com").getPath)
- check(repoPath.toURI.toString)
- }
-
- * - {
- // test the Cache.url method
- val shouldFail = Try(Cache.url("notfoundzzzz://foo/bar"))
- assert(shouldFail.isFailure)
-
- Cache.url("testprotocol://foo/bar")
- }
-
- * - {
- // the real custom protocol test
- check(s"${TestprotocolHandler.protocol}://foo/")
- }
- }
-
-}