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/") - } - } - -}