mirror of https://github.com/sbt/sbt.git
Merge pull request #197 from alexarchambault/topic/develop
Bunch of things
This commit is contained in:
commit
e2d75c3796
|
|
@ -6,13 +6,13 @@ install:
|
|||
os:
|
||||
- osx
|
||||
script:
|
||||
- project/travis.sh "${TRAVIS_SCALA_VERSION:-2.11.7}" "$TRAVIS_PULL_REQUEST" "$TRAVIS_BRANCH" "$PUBLISH"
|
||||
- project/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)
|
||||
matrix:
|
||||
include:
|
||||
- env: TRAVIS_SCALA_VERSION=2.11.7 PUBLISH=1
|
||||
- env: TRAVIS_SCALA_VERSION=2.11.8 PUBLISH=1
|
||||
os: linux
|
||||
jdk: oraclejdk8
|
||||
- env: TRAVIS_SCALA_VERSION=2.10.6 PUBLISH=1
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -640,6 +640,21 @@ Set `scalaVersion` to `2.10.6` in `build.sbt`. Then re-open / reload the coursie
|
|||
They require `npm install` to have been run once from the `coursier` directory or a subdirectory of
|
||||
it. They can then be run with `sbt testsJS/test`.
|
||||
|
||||
#### Quickly running the CLI app from the sources
|
||||
|
||||
Run
|
||||
```
|
||||
$ sbt "~cli/pack"
|
||||
```
|
||||
|
||||
This generates and updates a runnable distribution of coursier in `target/pack`, via
|
||||
the [sbt-pack](https://github.com/xerial/sbt-pack/) plugin.
|
||||
|
||||
It can be run from another terminal with
|
||||
```
|
||||
$ cli/target/pack/bin/coursier
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
The first releases were milestones like `0.1.0-M?`. As a launcher, basic Ivy
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ install:
|
|||
- cmd: SET SBT_OPTS=-XX:MaxPermSize=2g -Xmx4g
|
||||
- cmd: SET COURSIER_NO_TERM=1
|
||||
build_script:
|
||||
- sbt ++2.11.7 clean compile coreJVM/publishLocal
|
||||
- sbt ++2.11.8 clean compile coreJVM/publishLocal
|
||||
- sbt ++2.10.6 clean compile
|
||||
test_script:
|
||||
- sbt ++2.11.7 testsJVM/test # Would node be around for testsJS/test?
|
||||
- sbt ++2.11.8 testsJVM/test # Would node be around for testsJS/test?
|
||||
- sbt ++2.10.6 testsJVM/test
|
||||
cache:
|
||||
- C:\sbt\
|
||||
|
|
|
|||
10
build.sbt
10
build.sbt
|
|
@ -104,8 +104,8 @@ lazy val baseCommonSettings = Seq(
|
|||
) ++ releaseSettings
|
||||
|
||||
lazy val commonSettings = baseCommonSettings ++ Seq(
|
||||
scalaVersion := "2.11.7",
|
||||
crossScalaVersions := Seq("2.11.7", "2.10.6"),
|
||||
scalaVersion := "2.11.8",
|
||||
crossScalaVersions := Seq("2.11.8", "2.10.6"),
|
||||
libraryDependencies ++= {
|
||||
if (scalaVersion.value startsWith "2.10.")
|
||||
Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full))
|
||||
|
|
@ -199,7 +199,8 @@ lazy val tests = crossProject
|
|||
)
|
||||
.jsSettings(
|
||||
postLinkJSEnv := NodeJSEnv().value,
|
||||
scalaJSStage in Global := FastOptStage
|
||||
scalaJSStage in Global := FastOptStage,
|
||||
scalaJSUseRhino in Global := false
|
||||
)
|
||||
|
||||
lazy val testsJvm = tests.jvm.dependsOn(cache % "test")
|
||||
|
|
@ -221,6 +222,9 @@ lazy val cache = project
|
|||
|
||||
Seq(
|
||||
// Since 1.0.0-M10
|
||||
// methods that should have been private anyway
|
||||
ProblemFilters.exclude[MissingMethodProblem]("coursier.TermDisplay.update"),
|
||||
ProblemFilters.exclude[MissingMethodProblem]("coursier.TermDisplay.fallbackMode_="),
|
||||
// cache argument type changed from `Seq[(String, File)]` to `File`
|
||||
ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.Cache.file"),
|
||||
ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.Cache.fetch"),
|
||||
|
|
|
|||
|
|
@ -107,11 +107,55 @@ object Cache {
|
|||
helper(alreadyDownloaded)
|
||||
}
|
||||
|
||||
private def withLockFor[T](file: File)(f: => FileError \/ T): FileError \/ T = {
|
||||
private val processStructureLocks = new ConcurrentHashMap[File, AnyRef]
|
||||
|
||||
/**
|
||||
* Should be acquired when doing operations changing the file structure of the cache (creating
|
||||
* new directories, creating / acquiring locks, ...), so that these don't hinder each other.
|
||||
*
|
||||
* Should hopefully address some transient errors seen on the CI of ensime-server.
|
||||
*/
|
||||
private def withStructureLock[T](cache: File)(f: => T): T = {
|
||||
|
||||
val intraProcessLock = Option(processStructureLocks.get(cache)).getOrElse {
|
||||
val lock = new AnyRef
|
||||
val prev = Option(processStructureLocks.putIfAbsent(cache, lock))
|
||||
prev.getOrElse(lock)
|
||||
}
|
||||
|
||||
intraProcessLock.synchronized {
|
||||
val lockFile = new File(cache, ".structure.lock")
|
||||
lockFile.getParentFile.mkdirs()
|
||||
var out = new FileOutputStream(lockFile)
|
||||
|
||||
try {
|
||||
var lock: FileLock = null
|
||||
try {
|
||||
lock = out.getChannel.lock()
|
||||
|
||||
try f
|
||||
finally {
|
||||
lock.release()
|
||||
lock = null
|
||||
out.close()
|
||||
out = null
|
||||
lockFile.delete()
|
||||
}
|
||||
}
|
||||
finally if (lock != null) lock.release()
|
||||
} finally if (out != null) out.close()
|
||||
}
|
||||
}
|
||||
|
||||
private def withLockFor[T](cache: File, file: File)(f: => FileError \/ T): FileError \/ T = {
|
||||
val lockFile = new File(file.getParentFile, s"${file.getName}.lock")
|
||||
|
||||
lockFile.getParentFile.mkdirs()
|
||||
var out = new FileOutputStream(lockFile)
|
||||
var out: FileOutputStream = null
|
||||
|
||||
withStructureLock(cache) {
|
||||
lockFile.getParentFile.mkdirs()
|
||||
out = new FileOutputStream(lockFile)
|
||||
}
|
||||
|
||||
try {
|
||||
var lock: FileLock = null
|
||||
|
|
@ -378,7 +422,7 @@ object Cache {
|
|||
def remote(file: File, url: String): EitherT[Task, FileError, Unit] =
|
||||
EitherT {
|
||||
Task {
|
||||
withLockFor(file) {
|
||||
withLockFor(cache, file) {
|
||||
downloading(url, file, logger) {
|
||||
val tmp = temporaryFile(file)
|
||||
|
||||
|
|
@ -416,14 +460,18 @@ object Cache {
|
|||
|
||||
val result =
|
||||
try {
|
||||
tmp.getParentFile.mkdirs()
|
||||
val out = new FileOutputStream(tmp, partialDownload)
|
||||
val out = withStructureLock(cache) {
|
||||
tmp.getParentFile.mkdirs()
|
||||
new FileOutputStream(tmp, partialDownload)
|
||||
}
|
||||
try \/-(readFullyTo(in, out, logger, url, if (partialDownload) alreadyDownloaded else 0L))
|
||||
finally out.close()
|
||||
} finally in.close()
|
||||
|
||||
file.getParentFile.mkdirs()
|
||||
NioFiles.move(tmp.toPath, file.toPath, StandardCopyOption.ATOMIC_MOVE)
|
||||
withStructureLock(cache) {
|
||||
file.getParentFile.mkdirs()
|
||||
NioFiles.move(tmp.toPath, file.toPath, StandardCopyOption.ATOMIC_MOVE)
|
||||
}
|
||||
|
||||
for (lastModified <- Option(conn.getLastModified) if lastModified > 0L)
|
||||
file.setLastModified(lastModified)
|
||||
|
|
@ -802,35 +850,3 @@ object Cache {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
sealed abstract class FileError(val message: String) extends Product with Serializable
|
||||
|
||||
object FileError {
|
||||
|
||||
final case class DownloadError(reason: String) extends FileError(s"Download error: $reason")
|
||||
|
||||
final case class NotFound(file: String, permanent: Option[Boolean] = None) extends FileError(s"Not found: $file")
|
||||
|
||||
final case class ChecksumNotFound(
|
||||
sumType: String,
|
||||
file: String
|
||||
) extends FileError(s"$sumType checksum not found: $file")
|
||||
|
||||
final case class ChecksumFormatError(
|
||||
sumType: String,
|
||||
file: String
|
||||
) extends FileError(s"Unrecognized $sumType checksum format in $file")
|
||||
|
||||
final case class WrongChecksum(
|
||||
sumType: String,
|
||||
got: String,
|
||||
expected: String,
|
||||
file: String,
|
||||
sumFile: String
|
||||
) extends FileError(s"$sumType checksum validation failed: $file")
|
||||
|
||||
sealed abstract class Recoverable(message: String) extends FileError(message)
|
||||
final case class Locked(file: File) extends Recoverable(s"Locked: $file")
|
||||
final case class ConcurrentDownload(url: String) extends Recoverable(s"Concurrent download: $url")
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package coursier
|
||||
|
||||
import java.io.File
|
||||
|
||||
sealed abstract class FileError(val message: String) extends Product with Serializable
|
||||
|
||||
object FileError {
|
||||
|
||||
final case class DownloadError(reason: String) extends FileError(s"Download error: $reason")
|
||||
|
||||
final case class NotFound(
|
||||
file: String,
|
||||
permanent: Option[Boolean] = None
|
||||
) extends FileError(s"Not found: $file")
|
||||
|
||||
final case class ChecksumNotFound(
|
||||
sumType: String,
|
||||
file: String
|
||||
) extends FileError(s"$sumType checksum not found: $file")
|
||||
|
||||
final case class ChecksumFormatError(
|
||||
sumType: String,
|
||||
file: String
|
||||
) extends FileError(s"Unrecognized $sumType checksum format in $file")
|
||||
|
||||
final case class WrongChecksum(
|
||||
sumType: String,
|
||||
got: String,
|
||||
expected: String,
|
||||
file: String,
|
||||
sumFile: String
|
||||
) extends FileError(s"$sumType checksum validation failed: $file")
|
||||
|
||||
sealed abstract class Recoverable(message: String) extends FileError(message)
|
||||
final case class Locked(file: File) extends Recoverable(s"Locked: $file")
|
||||
final case class ConcurrentDownload(url: String) extends Recoverable(s"Concurrent download: $url")
|
||||
|
||||
}
|
||||
|
|
@ -22,21 +22,21 @@ object Terminal {
|
|||
} else
|
||||
None
|
||||
|
||||
class Ansi(val output: Writer) extends AnyVal {
|
||||
implicit class Ansi(val output: Writer) extends AnyVal {
|
||||
private def control(n: Int, c: Char) = output.write(s"\033[" + n + c)
|
||||
|
||||
/**
|
||||
* Move up `n` squares
|
||||
*/
|
||||
def up(n: Int): Unit = if (n == 0) "" else control(n, 'A')
|
||||
def up(n: Int): Unit = if (n > 0) control(n, 'A')
|
||||
/**
|
||||
* Move down `n` squares
|
||||
*/
|
||||
def down(n: Int): Unit = if (n == 0) "" else control(n, 'B')
|
||||
def down(n: Int): Unit = if (n > 0) control(n, 'B')
|
||||
/**
|
||||
* Move left `n` squares
|
||||
*/
|
||||
def left(n: Int): Unit = if (n == 0) "" else control(n, 'D')
|
||||
def left(n: Int): Unit = if (n > 0) control(n, 'D')
|
||||
|
||||
/**
|
||||
* Clear the current line
|
||||
|
|
@ -50,196 +50,14 @@ object Terminal {
|
|||
|
||||
}
|
||||
|
||||
class TermDisplay(
|
||||
out: Writer,
|
||||
var fallbackMode: Boolean = sys.env.get("COURSIER_NO_TERM").nonEmpty
|
||||
) extends Cache.Logger {
|
||||
object TermDisplay {
|
||||
private def defaultFallbackMode: Boolean = {
|
||||
val env = sys.env.get("COURSIER_NO_TERM").nonEmpty
|
||||
def nonInteractive = System.console() == null
|
||||
|
||||
private val ansi = new Terminal.Ansi(out)
|
||||
private var width = 80
|
||||
private val refreshInterval = 1000 / 60
|
||||
private val fallbackRefreshInterval = 1000
|
||||
|
||||
private val lock = new AnyRef
|
||||
private var currentHeight = 0
|
||||
private val t = new Thread("TermDisplay") {
|
||||
override def run() = lock.synchronized {
|
||||
|
||||
val baseExtraWidth = width / 5
|
||||
|
||||
def reflowed(url: String, info: Info) = {
|
||||
val extra = info match {
|
||||
case downloadInfo: DownloadInfo =>
|
||||
val pctOpt = downloadInfo.fraction.map(100.0 * _)
|
||||
|
||||
if (downloadInfo.length.isEmpty && downloadInfo.downloaded == 0L)
|
||||
""
|
||||
else
|
||||
s"(${pctOpt.map(pct => f"$pct%.2f %%, ").mkString}${downloadInfo.downloaded}${downloadInfo.length.map(" / " + _).mkString})"
|
||||
|
||||
case updateInfo: CheckUpdateInfo =>
|
||||
"Checking for updates"
|
||||
}
|
||||
|
||||
val total = url.length + 1 + extra.length
|
||||
val (url0, extra0) =
|
||||
if (total >= width) { // or > ? If equal, does it go down 2 lines?
|
||||
val overflow = total - width + 1
|
||||
|
||||
val extra0 =
|
||||
if (extra.length > baseExtraWidth)
|
||||
extra.take((baseExtraWidth max (extra.length - overflow)) - 1) + "…"
|
||||
else
|
||||
extra
|
||||
|
||||
val total0 = url.length + 1 + extra0.length
|
||||
val overflow0 = total0 - width + 1
|
||||
|
||||
val url0 =
|
||||
if (total0 >= width)
|
||||
url.take(((width - baseExtraWidth - 1) max (url.length - overflow0)) - 1) + "…"
|
||||
else
|
||||
url
|
||||
|
||||
(url0, extra0)
|
||||
} else
|
||||
(url, extra)
|
||||
|
||||
(url0, extra0)
|
||||
}
|
||||
|
||||
def truncatedPrintln(s: String): Unit = {
|
||||
|
||||
ansi.clearLine(2)
|
||||
|
||||
if (s.length <= width)
|
||||
out.write(s + "\n")
|
||||
else
|
||||
out.write(s.take(width - 1) + "…\n")
|
||||
}
|
||||
|
||||
@tailrec def helper(lineCount: Int): Unit = {
|
||||
currentHeight = lineCount
|
||||
|
||||
Option(q.poll(100L, TimeUnit.MILLISECONDS)) match {
|
||||
case None => helper(lineCount)
|
||||
case Some(Left(())) => // poison pill
|
||||
case Some(Right(())) =>
|
||||
// update display
|
||||
|
||||
val (done0, downloads0) = downloads.synchronized {
|
||||
val q = doneQueue
|
||||
.toVector
|
||||
.filter {
|
||||
case (url, _) =>
|
||||
!url.endsWith(".sha1") && !url.endsWith(".md5")
|
||||
}
|
||||
.sortBy { case (url, _) => url }
|
||||
|
||||
doneQueue.clear()
|
||||
|
||||
val dw = downloads
|
||||
.toVector
|
||||
.map { url => url -> infos.get(url) }
|
||||
.sortBy { case (_, info) => - info.fraction.sum }
|
||||
|
||||
(q, dw)
|
||||
}
|
||||
|
||||
for ((url, info) <- done0 ++ downloads0) {
|
||||
assert(info != null, s"Incoherent state ($url)")
|
||||
|
||||
truncatedPrintln(url)
|
||||
ansi.clearLine(2)
|
||||
out.write(s" ${info.display()}\n")
|
||||
}
|
||||
|
||||
val displayedCount = (done0 ++ downloads0).length
|
||||
|
||||
if (displayedCount < lineCount) {
|
||||
for (_ <- 1 to 2; _ <- displayedCount until lineCount) {
|
||||
ansi.clearLine(2)
|
||||
ansi.down(1)
|
||||
}
|
||||
|
||||
for (_ <- displayedCount until lineCount)
|
||||
ansi.up(2)
|
||||
}
|
||||
|
||||
for (_ <- downloads0.indices)
|
||||
ansi.up(2)
|
||||
|
||||
ansi.left(10000)
|
||||
|
||||
out.flush()
|
||||
Thread.sleep(refreshInterval)
|
||||
helper(downloads0.length)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@tailrec def fallbackHelper(previous: Set[String]): Unit =
|
||||
Option(q.poll(100L, TimeUnit.MILLISECONDS)) match {
|
||||
case None => fallbackHelper(previous)
|
||||
case Some(Left(())) => // poison pill
|
||||
case Some(Right(())) =>
|
||||
val downloads0 = downloads.synchronized {
|
||||
downloads
|
||||
.toVector
|
||||
.map { url => url -> infos.get(url) }
|
||||
.sortBy { case (_, info) => - info.fraction.sum }
|
||||
}
|
||||
|
||||
var displayedSomething = false
|
||||
for ((url, info) <- downloads0 if previous(url)) {
|
||||
assert(info != null, s"Incoherent state ($url)")
|
||||
|
||||
val (url0, extra0) = reflowed(url, info)
|
||||
|
||||
displayedSomething = true
|
||||
out.write(s"$url0 $extra0\n")
|
||||
}
|
||||
|
||||
if (displayedSomething)
|
||||
out.write("\n")
|
||||
|
||||
out.flush()
|
||||
Thread.sleep(fallbackRefreshInterval)
|
||||
fallbackHelper(previous ++ downloads0.map { case (url, _) => url })
|
||||
}
|
||||
|
||||
if (fallbackMode)
|
||||
fallbackHelper(Set.empty)
|
||||
else
|
||||
helper(0)
|
||||
}
|
||||
env || nonInteractive
|
||||
}
|
||||
|
||||
t.setDaemon(true)
|
||||
|
||||
def init(): Unit = {
|
||||
Terminal.consoleDim("cols") match {
|
||||
case Some(cols) =>
|
||||
width = cols
|
||||
ansi.clearLine(2)
|
||||
case None =>
|
||||
fallbackMode = true
|
||||
}
|
||||
|
||||
t.start()
|
||||
}
|
||||
|
||||
def stop(): Unit = {
|
||||
for (_ <- 1 to 2; _ <- 0 until currentHeight) {
|
||||
ansi.clearLine(2)
|
||||
ansi.down(1)
|
||||
}
|
||||
for (_ <- 0 until currentHeight) {
|
||||
ansi.up(2)
|
||||
}
|
||||
q.put(Left(()))
|
||||
lock.synchronized(())
|
||||
}
|
||||
|
||||
private sealed abstract class Info extends Product with Serializable {
|
||||
def fraction: Option[Double]
|
||||
|
|
@ -327,73 +145,288 @@ class TermDisplay(
|
|||
}
|
||||
}
|
||||
|
||||
private val downloads = new ArrayBuffer[String]
|
||||
private val doneQueue = new ArrayBuffer[(String, Info)]
|
||||
private val infos = new ConcurrentHashMap[String, Info]
|
||||
|
||||
private val q = new LinkedBlockingDeque[Either[Unit, Unit]]
|
||||
def update(): Unit = {
|
||||
if (q.size() == 0)
|
||||
q.put(Right(()))
|
||||
private sealed abstract class Message extends Product with Serializable
|
||||
private object Message {
|
||||
case object Update extends Message
|
||||
case object Stop extends Message
|
||||
}
|
||||
|
||||
private def newEntry(
|
||||
url: String,
|
||||
info: Info,
|
||||
fallbackMessage: => String
|
||||
): Unit = {
|
||||
assert(!infos.containsKey(url))
|
||||
val prev = infos.putIfAbsent(url, info)
|
||||
assert(prev == null)
|
||||
private val refreshInterval = 1000 / 60
|
||||
private val fallbackRefreshInterval = 1000
|
||||
|
||||
if (fallbackMode) {
|
||||
// FIXME What about concurrent accesses to out from the thread above?
|
||||
out.write(fallbackMessage)
|
||||
out.flush()
|
||||
private class UpdateDisplayThread(
|
||||
out: Writer,
|
||||
var fallbackMode: Boolean
|
||||
) extends Thread("TermDisplay") {
|
||||
|
||||
import Terminal.Ansi
|
||||
|
||||
setDaemon(true)
|
||||
|
||||
private var width = 80
|
||||
private var currentHeight = 0
|
||||
|
||||
private val q = new LinkedBlockingDeque[Message]
|
||||
|
||||
|
||||
def update(): Unit = {
|
||||
if (q.size() == 0)
|
||||
q.put(Message.Update)
|
||||
}
|
||||
|
||||
downloads.synchronized {
|
||||
downloads.append(url)
|
||||
def end(): Unit = {
|
||||
q.put(Message.Stop)
|
||||
join()
|
||||
}
|
||||
|
||||
update()
|
||||
private val downloads = new ArrayBuffer[String]
|
||||
private val doneQueue = new ArrayBuffer[(String, Info)]
|
||||
val infos = new ConcurrentHashMap[String, Info]
|
||||
|
||||
def newEntry(
|
||||
url: String,
|
||||
info: Info,
|
||||
fallbackMessage: => String
|
||||
): Unit = {
|
||||
assert(!infos.containsKey(url))
|
||||
val prev = infos.putIfAbsent(url, info)
|
||||
assert(prev == null)
|
||||
|
||||
if (fallbackMode) {
|
||||
// FIXME What about concurrent accesses to out from the thread above?
|
||||
out.write(fallbackMessage)
|
||||
out.flush()
|
||||
}
|
||||
|
||||
downloads.synchronized {
|
||||
downloads.append(url)
|
||||
}
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
def removeEntry(
|
||||
url: String,
|
||||
success: Boolean,
|
||||
fallbackMessage: => String
|
||||
)(
|
||||
update0: Info => Info
|
||||
): Unit = {
|
||||
downloads.synchronized {
|
||||
downloads -= url
|
||||
|
||||
val info = infos.remove(url)
|
||||
assert(info != null)
|
||||
|
||||
if (success)
|
||||
doneQueue += (url -> update0(info))
|
||||
}
|
||||
|
||||
if (fallbackMode && success) {
|
||||
// FIXME What about concurrent accesses to out from the thread above?
|
||||
out.write(fallbackMessage)
|
||||
out.flush()
|
||||
}
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
private def reflowed(url: String, info: Info) = {
|
||||
val extra = info match {
|
||||
case downloadInfo: DownloadInfo =>
|
||||
val pctOpt = downloadInfo.fraction.map(100.0 * _)
|
||||
|
||||
if (downloadInfo.length.isEmpty && downloadInfo.downloaded == 0L)
|
||||
""
|
||||
else
|
||||
s"(${pctOpt.map(pct => f"$pct%.2f %%, ").mkString}${downloadInfo.downloaded}${downloadInfo.length.map(" / " + _).mkString})"
|
||||
|
||||
case updateInfo: CheckUpdateInfo =>
|
||||
"Checking for updates"
|
||||
}
|
||||
|
||||
val baseExtraWidth = width / 5
|
||||
|
||||
val total = url.length + 1 + extra.length
|
||||
val (url0, extra0) =
|
||||
if (total >= width) { // or > ? If equal, does it go down 2 lines?
|
||||
val overflow = total - width + 1
|
||||
|
||||
val extra0 =
|
||||
if (extra.length > baseExtraWidth)
|
||||
extra.take((baseExtraWidth max (extra.length - overflow)) - 1) + "…"
|
||||
else
|
||||
extra
|
||||
|
||||
val total0 = url.length + 1 + extra0.length
|
||||
val overflow0 = total0 - width + 1
|
||||
|
||||
val url0 =
|
||||
if (total0 >= width)
|
||||
url.take(((width - baseExtraWidth - 1) max (url.length - overflow0)) - 1) + "…"
|
||||
else
|
||||
url
|
||||
|
||||
(url0, extra0)
|
||||
} else
|
||||
(url, extra)
|
||||
|
||||
(url0, extra0)
|
||||
}
|
||||
|
||||
private def truncatedPrintln(s: String): Unit = {
|
||||
|
||||
out.clearLine(2)
|
||||
|
||||
if (s.length <= width)
|
||||
out.write(s + "\n")
|
||||
else
|
||||
out.write(s.take(width - 1) + "…\n")
|
||||
}
|
||||
|
||||
@tailrec private def updateDisplayLoop(lineCount: Int): Unit = {
|
||||
currentHeight = lineCount
|
||||
|
||||
Option(q.poll(100L, TimeUnit.MILLISECONDS)) match {
|
||||
case None => updateDisplayLoop(lineCount)
|
||||
case Some(Message.Stop) => // poison pill
|
||||
case Some(Message.Update) =>
|
||||
|
||||
val (done0, downloads0) = downloads.synchronized {
|
||||
val q = doneQueue
|
||||
.toVector
|
||||
.filter {
|
||||
case (url, _) =>
|
||||
!url.endsWith(".sha1") && !url.endsWith(".md5")
|
||||
}
|
||||
.sortBy { case (url, _) => url }
|
||||
|
||||
doneQueue.clear()
|
||||
|
||||
val dw = downloads
|
||||
.toVector
|
||||
.map { url => url -> infos.get(url) }
|
||||
.sortBy { case (_, info) => - info.fraction.sum }
|
||||
|
||||
(q, dw)
|
||||
}
|
||||
|
||||
for ((url, info) <- done0 ++ downloads0) {
|
||||
assert(info != null, s"Incoherent state ($url)")
|
||||
|
||||
truncatedPrintln(url)
|
||||
out.clearLine(2)
|
||||
out.write(s" ${info.display()}\n")
|
||||
}
|
||||
|
||||
val displayedCount = (done0 ++ downloads0).length
|
||||
|
||||
if (displayedCount < lineCount) {
|
||||
for (_ <- 1 to 2; _ <- displayedCount until lineCount) {
|
||||
out.clearLine(2)
|
||||
out.down(1)
|
||||
}
|
||||
|
||||
for (_ <- displayedCount until lineCount)
|
||||
out.up(2)
|
||||
}
|
||||
|
||||
for (_ <- downloads0.indices)
|
||||
out.up(2)
|
||||
|
||||
out.left(10000)
|
||||
|
||||
out.flush()
|
||||
Thread.sleep(refreshInterval)
|
||||
updateDisplayLoop(downloads0.length)
|
||||
}
|
||||
}
|
||||
|
||||
@tailrec private def fallbackDisplayLoop(previous: Set[String]): Unit =
|
||||
Option(q.poll(100L, TimeUnit.MILLISECONDS)) match {
|
||||
case None => fallbackDisplayLoop(previous)
|
||||
case Some(Message.Stop) => // poison pill
|
||||
|
||||
// clean up display
|
||||
for (_ <- 1 to 2; _ <- 0 until currentHeight) {
|
||||
out.clearLine(2)
|
||||
out.down(1)
|
||||
}
|
||||
for (_ <- 0 until currentHeight) {
|
||||
out.up(2)
|
||||
}
|
||||
|
||||
case Some(Message.Update) =>
|
||||
val downloads0 = downloads.synchronized {
|
||||
downloads
|
||||
.toVector
|
||||
.map { url => url -> infos.get(url) }
|
||||
.sortBy { case (_, info) => - info.fraction.sum }
|
||||
}
|
||||
|
||||
var displayedSomething = false
|
||||
for ((url, info) <- downloads0 if previous(url)) {
|
||||
assert(info != null, s"Incoherent state ($url)")
|
||||
|
||||
val (url0, extra0) = reflowed(url, info)
|
||||
|
||||
displayedSomething = true
|
||||
out.write(s"$url0 $extra0\n")
|
||||
}
|
||||
|
||||
if (displayedSomething)
|
||||
out.write("\n")
|
||||
|
||||
out.flush()
|
||||
Thread.sleep(fallbackRefreshInterval)
|
||||
fallbackDisplayLoop(previous ++ downloads0.map { case (url, _) => url })
|
||||
}
|
||||
|
||||
override def run(): Unit = {
|
||||
|
||||
Terminal.consoleDim("cols") match {
|
||||
case Some(cols) =>
|
||||
width = cols
|
||||
out.clearLine(2)
|
||||
case None =>
|
||||
fallbackMode = true
|
||||
}
|
||||
|
||||
if (fallbackMode)
|
||||
fallbackDisplayLoop(Set.empty)
|
||||
else
|
||||
updateDisplayLoop(0)
|
||||
}
|
||||
}
|
||||
|
||||
private def removeEntry(
|
||||
url: String,
|
||||
success: Boolean,
|
||||
fallbackMessage: => String
|
||||
)(
|
||||
update0: Info => Info
|
||||
): Unit = {
|
||||
downloads.synchronized {
|
||||
downloads -= url
|
||||
}
|
||||
|
||||
val info = infos.remove(url)
|
||||
assert(info != null)
|
||||
class TermDisplay(
|
||||
out: Writer,
|
||||
val fallbackMode: Boolean = TermDisplay.defaultFallbackMode
|
||||
) extends Cache.Logger {
|
||||
|
||||
if (success)
|
||||
doneQueue += (url -> update0(info))
|
||||
}
|
||||
import TermDisplay._
|
||||
|
||||
if (fallbackMode && success) {
|
||||
// FIXME What about concurrent accesses to out from the thread above?
|
||||
out.write(fallbackMessage)
|
||||
out.flush()
|
||||
}
|
||||
private val updateThread = new UpdateDisplayThread(out, fallbackMode)
|
||||
|
||||
update()
|
||||
def init(): Unit = {
|
||||
updateThread.start()
|
||||
}
|
||||
|
||||
def stop(): Unit = {
|
||||
updateThread.end()
|
||||
}
|
||||
|
||||
override def downloadingArtifact(url: String, file: File): Unit =
|
||||
newEntry(
|
||||
updateThread.newEntry(
|
||||
url,
|
||||
DownloadInfo(0L, 0L, None, System.currentTimeMillis(), updateCheck = false),
|
||||
s"Downloading $url\n"
|
||||
)
|
||||
|
||||
override def downloadLength(url: String, totalLength: Long, alreadyDownloaded: Long): Unit = {
|
||||
val info = infos.get(url)
|
||||
val info = updateThread.infos.get(url)
|
||||
assert(info != null)
|
||||
val newInfo = info match {
|
||||
case info0: DownloadInfo =>
|
||||
|
|
@ -401,12 +434,12 @@ class TermDisplay(
|
|||
case _ =>
|
||||
throw new Exception(s"Incoherent display state for $url")
|
||||
}
|
||||
infos.put(url, newInfo)
|
||||
updateThread.infos.put(url, newInfo)
|
||||
|
||||
update()
|
||||
updateThread.update()
|
||||
}
|
||||
override def downloadProgress(url: String, downloaded: Long): Unit = {
|
||||
val info = infos.get(url)
|
||||
val info = updateThread.infos.get(url)
|
||||
assert(info != null)
|
||||
val newInfo = info match {
|
||||
case info0: DownloadInfo =>
|
||||
|
|
@ -414,16 +447,16 @@ class TermDisplay(
|
|||
case _ =>
|
||||
throw new Exception(s"Incoherent display state for $url")
|
||||
}
|
||||
infos.put(url, newInfo)
|
||||
updateThread.infos.put(url, newInfo)
|
||||
|
||||
update()
|
||||
updateThread.update()
|
||||
}
|
||||
|
||||
override def downloadedArtifact(url: String, success: Boolean): Unit =
|
||||
removeEntry(url, success, s"Downloaded $url\n")(x => x)
|
||||
updateThread.removeEntry(url, success, s"Downloaded $url\n")(x => x)
|
||||
|
||||
override def checkingUpdates(url: String, currentTimeOpt: Option[Long]): Unit =
|
||||
newEntry(
|
||||
updateThread.newEntry(
|
||||
url,
|
||||
CheckUpdateInfo(currentTimeOpt, None, isDone = false),
|
||||
s"Checking $url\n"
|
||||
|
|
@ -442,7 +475,7 @@ class TermDisplay(
|
|||
}
|
||||
}
|
||||
|
||||
removeEntry(url, !newUpdate, s"Checked $url") {
|
||||
updateThread.removeEntry(url, !newUpdate, s"Checked $url") {
|
||||
case info: CheckUpdateInfo =>
|
||||
info.copy(remoteTimeOpt = remoteTimeOpt, isDone = true)
|
||||
case _ =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,225 @@
|
|||
package coursier
|
||||
package cli
|
||||
|
||||
import java.io.{ FileInputStream, ByteArrayOutputStream, File, IOException }
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import java.util.Properties
|
||||
import java.util.zip.{ ZipEntry, ZipOutputStream, ZipInputStream }
|
||||
|
||||
import caseapp._
|
||||
|
||||
case class Bootstrap(
|
||||
@Recurse
|
||||
options: BootstrapOptions
|
||||
) extends App {
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
if (options.mainClass.isEmpty) {
|
||||
Console.err.println(s"Error: no main class specified. Specify one with -M or --main")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
if (!options.standalone && options.downloadDir.isEmpty) {
|
||||
Console.err.println(s"Error: no download dir specified. Specify one with -D or --download-dir")
|
||||
Console.err.println("E.g. -D \"\\$HOME/.app-name/jars\"")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
val (validProperties, wrongProperties) = options.property.partition(_.contains("="))
|
||||
if (wrongProperties.nonEmpty) {
|
||||
Console.err.println(s"Wrong -P / --property option(s):\n${wrongProperties.mkString("\n")}")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
val properties0 = validProperties.map { s =>
|
||||
val idx = s.indexOf('=')
|
||||
assert(idx >= 0)
|
||||
(s.take(idx), s.drop(idx + 1))
|
||||
}
|
||||
|
||||
val bootstrapJar =
|
||||
Option(Thread.currentThread().getContextClassLoader.getResourceAsStream("bootstrap.jar")) match {
|
||||
case Some(is) => Cache.readFullySync(is)
|
||||
case None =>
|
||||
Console.err.println(s"Error: bootstrap JAR not found")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
val output0 = new File(options.output)
|
||||
if (!options.force && output0.exists()) {
|
||||
Console.err.println(s"Error: ${options.output} already exists, use -f option to force erasing it.")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
def zipEntries(zipStream: ZipInputStream): Iterator[(ZipEntry, Array[Byte])] =
|
||||
new Iterator[(ZipEntry, Array[Byte])] {
|
||||
var nextEntry = Option.empty[ZipEntry]
|
||||
def update() =
|
||||
nextEntry = Option(zipStream.getNextEntry)
|
||||
|
||||
update()
|
||||
|
||||
def hasNext = nextEntry.nonEmpty
|
||||
def next() = {
|
||||
val ent = nextEntry.get
|
||||
val data = Platform.readFullySync(zipStream)
|
||||
|
||||
update()
|
||||
|
||||
(ent, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val helper = new Helper(options.common, remainingArgs)
|
||||
|
||||
val (_, isolatedArtifactFiles) =
|
||||
options.isolated.targets.foldLeft((Vector.empty[String], Map.empty[String, (Seq[String], Seq[File])])) {
|
||||
case ((done, acc), target) =>
|
||||
val subRes = helper.res.subset(options.isolated.isolatedDeps.getOrElse(target, Nil).toSet)
|
||||
val subArtifacts = subRes.artifacts.map(_.url)
|
||||
|
||||
val filteredSubArtifacts = subArtifacts.diff(done)
|
||||
|
||||
def subFiles0 = helper.fetch(
|
||||
sources = false,
|
||||
javadoc = false,
|
||||
subset = options.isolated.isolatedDeps.getOrElse(target, Seq.empty).toSet
|
||||
)
|
||||
|
||||
val (subUrls, subFiles) =
|
||||
if (options.standalone)
|
||||
(Nil, subFiles0)
|
||||
else
|
||||
(filteredSubArtifacts, Nil)
|
||||
|
||||
val updatedAcc = acc + (target -> (subUrls, subFiles))
|
||||
|
||||
(done ++ filteredSubArtifacts, updatedAcc)
|
||||
}
|
||||
|
||||
val (urls, files) =
|
||||
if (options.standalone)
|
||||
(
|
||||
Seq.empty[String],
|
||||
helper.fetch(sources = false, javadoc = false)
|
||||
)
|
||||
else
|
||||
(
|
||||
helper.artifacts(sources = false, javadoc = false).map(_.url),
|
||||
Seq.empty[File]
|
||||
)
|
||||
|
||||
val isolatedUrls = isolatedArtifactFiles.map { case (k, (v, _)) => k -> v }
|
||||
val isolatedFiles = isolatedArtifactFiles.map { case (k, (_, v)) => k -> v }
|
||||
|
||||
val nonHttpUrls = urls.filter(s => !s.startsWith("http://") && !s.startsWith("https://"))
|
||||
if (nonHttpUrls.nonEmpty)
|
||||
Console.err.println(s"Warning: non HTTP URLs:\n${nonHttpUrls.mkString("\n")}")
|
||||
|
||||
val buffer = new ByteArrayOutputStream()
|
||||
|
||||
val bootstrapZip = new ZipInputStream(Thread.currentThread().getContextClassLoader.getResourceAsStream("bootstrap.jar"))
|
||||
val outputZip = new ZipOutputStream(buffer)
|
||||
|
||||
for ((ent, data) <- zipEntries(bootstrapZip)) {
|
||||
outputZip.putNextEntry(ent)
|
||||
outputZip.write(data)
|
||||
outputZip.closeEntry()
|
||||
}
|
||||
|
||||
|
||||
val time = System.currentTimeMillis()
|
||||
|
||||
def putStringEntry(name: String, content: String): Unit = {
|
||||
val entry = new ZipEntry(name)
|
||||
entry.setTime(time)
|
||||
|
||||
outputZip.putNextEntry(entry)
|
||||
outputZip.write(content.getBytes("UTF-8"))
|
||||
outputZip.closeEntry()
|
||||
}
|
||||
|
||||
def putEntryFromFile(name: String, f: File): Unit = {
|
||||
val entry = new ZipEntry(name)
|
||||
entry.setTime(f.lastModified())
|
||||
|
||||
outputZip.putNextEntry(entry)
|
||||
outputZip.write(Cache.readFullySync(new FileInputStream(f)))
|
||||
outputZip.closeEntry()
|
||||
}
|
||||
|
||||
putStringEntry("bootstrap-jar-urls", urls.mkString("\n"))
|
||||
|
||||
if (options.isolated.anyIsolatedDep) {
|
||||
putStringEntry("bootstrap-isolation-ids", options.isolated.targets.mkString("\n"))
|
||||
|
||||
for (target <- options.isolated.targets) {
|
||||
val urls = isolatedUrls.getOrElse(target, Nil)
|
||||
val files = isolatedFiles.getOrElse(target, Nil)
|
||||
putStringEntry(s"bootstrap-isolation-$target-jar-urls", urls.mkString("\n"))
|
||||
putStringEntry(s"bootstrap-isolation-$target-jar-resources", files.map(pathFor).mkString("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
def pathFor(f: File) = s"jars/${f.getName}"
|
||||
|
||||
for (f <- files)
|
||||
putEntryFromFile(pathFor(f), f)
|
||||
|
||||
putStringEntry("bootstrap-jar-resources", files.map(pathFor).mkString("\n"))
|
||||
|
||||
val propsEntry = new ZipEntry("bootstrap.properties")
|
||||
propsEntry.setTime(time)
|
||||
|
||||
val properties = new Properties()
|
||||
properties.setProperty("bootstrap.mainClass", options.mainClass)
|
||||
if (!options.standalone)
|
||||
properties.setProperty("bootstrap.jarDir", options.downloadDir)
|
||||
|
||||
outputZip.putNextEntry(propsEntry)
|
||||
properties.store(outputZip, "")
|
||||
outputZip.closeEntry()
|
||||
|
||||
outputZip.close()
|
||||
|
||||
// escaping of javaOpt possibly a bit loose :-|
|
||||
val shellPreamble = Seq(
|
||||
"#!/usr/bin/env sh",
|
||||
"exec java -jar " + options.javaOpt.map(s => "'" + s.replace("'", "\\'") + "'").mkString(" ") + " \"$0\" \"$@\""
|
||||
).mkString("", "\n", "\n")
|
||||
|
||||
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}")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
val perms = Files.getPosixFilePermissions(output0.toPath).asScala.toSet
|
||||
|
||||
var newPerms = perms
|
||||
if (perms(PosixFilePermission.OWNER_READ))
|
||||
newPerms += PosixFilePermission.OWNER_EXECUTE
|
||||
if (perms(PosixFilePermission.GROUP_READ))
|
||||
newPerms += PosixFilePermission.GROUP_EXECUTE
|
||||
if (perms(PosixFilePermission.OTHERS_READ))
|
||||
newPerms += PosixFilePermission.OTHERS_EXECUTE
|
||||
|
||||
if (newPerms != perms)
|
||||
Files.setPosixFilePermissions(
|
||||
output0.toPath,
|
||||
newPerms.asJava
|
||||
)
|
||||
} catch {
|
||||
case e: UnsupportedOperationException =>
|
||||
// Ignored
|
||||
case e: IOException =>
|
||||
Console.err.println(s"Error while making $output0 executable: ${e.getMessage}")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,605 +1,38 @@
|
|||
package coursier
|
||||
package cli
|
||||
|
||||
import java.io.{ FileInputStream, ByteArrayOutputStream, File, IOException }
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.{ Files => NIOFiles }
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import java.util.Properties
|
||||
import java.util.zip.{ ZipEntry, ZipOutputStream, ZipInputStream }
|
||||
import caseapp._
|
||||
import caseapp.core.{ ArgsApp, CommandsMessages }
|
||||
|
||||
import caseapp.{ HelpMessage => Help, ValueDescription => Value, ExtraName => Short, _ }
|
||||
import coursier.util.Parse
|
||||
import shapeless.union.Union
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.language.reflectiveCalls
|
||||
import scala.util.Try
|
||||
|
||||
case class CommonOptions(
|
||||
@Help("Keep optional dependencies (Maven)")
|
||||
keepOptional: Boolean,
|
||||
@Help("Download mode (default: missing, that is fetch things missing from cache)")
|
||||
@Value("offline|update-changing|update|missing|force")
|
||||
@Short("m")
|
||||
mode: String = "default",
|
||||
@Help("Quiet output")
|
||||
@Short("q")
|
||||
quiet: Boolean,
|
||||
@Help("Increase verbosity (specify several times to increase more)")
|
||||
@Short("v")
|
||||
verbose: List[Unit],
|
||||
@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)")
|
||||
@Short("r")
|
||||
repository: 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")
|
||||
sbtPluginHack: Boolean = false,
|
||||
@Help("Drop module attributes starting with 'info.' - these are sometimes used by projects built with SBT")
|
||||
dropInfoAttr: Boolean = false,
|
||||
@Help("Force module version")
|
||||
@Value("organization:name:forcedVersion")
|
||||
@Short("V")
|
||||
forceVersion: List[String],
|
||||
@Help("Exclude module")
|
||||
@Value("organization:name")
|
||||
@Short("E")
|
||||
exclude: List[String],
|
||||
@Help("Consider provided dependencies to be intransitive. Applies to all the provided dependencies.")
|
||||
intransitive: Boolean,
|
||||
@Help("Classifiers that should be fetched")
|
||||
@Value("classifier1,classifier2,...")
|
||||
@Short("C")
|
||||
classifier: List[String],
|
||||
@Help("Default configuration (default(compile) by default)")
|
||||
@Value("configuration")
|
||||
@Short("c")
|
||||
defaultConfiguration: String = "default(compile)",
|
||||
@Help("Maximum number of parallel downloads (default: 6)")
|
||||
@Short("n")
|
||||
parallel: Int = 6,
|
||||
@Help("Checksums")
|
||||
@Value("checksum1,checksum2,... - end with none to allow for no checksum validation if none are available")
|
||||
checksum: List[String],
|
||||
@Recurse
|
||||
cacheOptions: CacheOptions
|
||||
) {
|
||||
val verbose0 = verbose.length - (if (quiet) 1 else 0)
|
||||
lazy val classifier0 = classifier.flatMap(_.split(',')).filter(_.nonEmpty)
|
||||
// Temporary, see comment in Coursier below
|
||||
case class CoursierCommandHelper(
|
||||
command: CoursierCommandHelper.U
|
||||
) extends ArgsApp {
|
||||
def setRemainingArgs(remainingArgs: Seq[String]): Unit =
|
||||
command.unify.setRemainingArgs(remainingArgs)
|
||||
def remainingArgs: Seq[String] =
|
||||
command.unify.remainingArgs
|
||||
def apply(): Unit =
|
||||
command.unify.apply()
|
||||
}
|
||||
|
||||
case class CacheOptions(
|
||||
@Help("Cache directory (defaults to environment variable COURSIER_CACHE or ~/.coursier/cache/v1)")
|
||||
@Short("C")
|
||||
cache: String = Cache.default.toString
|
||||
)
|
||||
|
||||
sealed abstract class CoursierCommand extends Command
|
||||
|
||||
case class Resolve(
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
) extends CoursierCommand {
|
||||
|
||||
// the `val helper = ` part is needed because of DelayedInit it seems
|
||||
val helper = new Helper(common, remainingArgs, printResultStdout = true)
|
||||
object CoursierCommandHelper {
|
||||
type U = Union.`'bootstrap -> Bootstrap, 'fetch -> Fetch, 'launch -> Launch, 'resolve -> Resolve`.T
|
||||
|
||||
implicit val commandParser: CommandParser[CoursierCommandHelper] =
|
||||
CommandParser[U].map(CoursierCommandHelper(_))
|
||||
implicit val commandsMessages: CommandsMessages[CoursierCommandHelper] =
|
||||
CommandsMessages(CommandsMessages[U].messages)
|
||||
}
|
||||
|
||||
case class Fetch(
|
||||
@Help("Fetch source artifacts")
|
||||
@Short("S")
|
||||
sources: Boolean,
|
||||
@Help("Fetch javadoc artifacts")
|
||||
@Short("D")
|
||||
javadoc: Boolean,
|
||||
@Help("Print java -cp compatible output")
|
||||
@Short("p")
|
||||
classpath: Boolean,
|
||||
@Help("Fetch artifacts even if the resolution is errored")
|
||||
force: Boolean,
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
) extends CoursierCommand {
|
||||
|
||||
val helper = new Helper(common, remainingArgs, ignoreErrors = force)
|
||||
|
||||
val files0 = helper.fetch(sources = sources, javadoc = javadoc)
|
||||
|
||||
val out =
|
||||
if (classpath)
|
||||
files0
|
||||
.map(_.toString)
|
||||
.mkString(File.pathSeparator)
|
||||
else
|
||||
files0
|
||||
.map(_.toString)
|
||||
.mkString("\n")
|
||||
|
||||
println(out)
|
||||
|
||||
}
|
||||
|
||||
case class IsolatedLoaderOptions(
|
||||
@Value("target:dependency")
|
||||
@Short("I")
|
||||
isolated: List[String],
|
||||
@Help("Comma-separated isolation targets")
|
||||
@Short("i")
|
||||
isolateTarget: List[String]
|
||||
) {
|
||||
|
||||
def anyIsolatedDep = isolateTarget.nonEmpty || isolated.nonEmpty
|
||||
|
||||
lazy val targets = {
|
||||
val l = isolateTarget.flatMap(_.split(',')).filter(_.nonEmpty)
|
||||
val (invalid, valid) = l.partition(_.contains(":"))
|
||||
if (invalid.nonEmpty) {
|
||||
Console.err.println(s"Invalid target IDs:")
|
||||
for (t <- invalid)
|
||||
Console.err.println(s" $t")
|
||||
sys.exit(255)
|
||||
}
|
||||
if (valid.isEmpty)
|
||||
Array("default")
|
||||
else
|
||||
valid.toArray
|
||||
}
|
||||
|
||||
lazy val (validIsolated, unrecognizedIsolated) = isolated.partition(s => targets.exists(t => s.startsWith(t + ":")))
|
||||
|
||||
def check() = {
|
||||
if (unrecognizedIsolated.nonEmpty) {
|
||||
Console.err.println(s"Unrecognized isolation targets in:")
|
||||
for (i <- unrecognizedIsolated)
|
||||
Console.err.println(s" $i")
|
||||
sys.exit(255)
|
||||
}
|
||||
}
|
||||
|
||||
lazy val rawIsolated = validIsolated.map { s =>
|
||||
val Array(target, dep) = s.split(":", 2)
|
||||
target -> dep
|
||||
}
|
||||
|
||||
lazy val isolatedModuleVersions = rawIsolated.groupBy { case (t, _) => t }.map {
|
||||
case (t, l) =>
|
||||
val (errors, modVers) = Parse.moduleVersions(l.map { case (_, d) => d })
|
||||
|
||||
if (errors.nonEmpty) {
|
||||
errors.foreach(Console.err.println)
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
t -> modVers
|
||||
}
|
||||
|
||||
lazy val isolatedDeps = isolatedModuleVersions.map {
|
||||
case (t, l) =>
|
||||
t -> l.map {
|
||||
case (mod, ver) =>
|
||||
Dependency(mod, ver, configuration = "runtime")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Launch {
|
||||
|
||||
@tailrec
|
||||
def mainClassLoader(cl: ClassLoader): Option[ClassLoader] =
|
||||
if (cl == null)
|
||||
None
|
||||
else {
|
||||
val isMainLoader = try {
|
||||
val cl0 = cl.asInstanceOf[Object {
|
||||
def isBootstrapLoader: Boolean
|
||||
}]
|
||||
|
||||
cl0.isBootstrapLoader
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
false
|
||||
}
|
||||
|
||||
if (isMainLoader)
|
||||
Some(cl)
|
||||
else
|
||||
mainClassLoader(cl.getParent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case class Launch(
|
||||
@Short("M")
|
||||
@Short("main")
|
||||
mainClass: String,
|
||||
@Recurse
|
||||
isolated: IsolatedLoaderOptions,
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
) extends CoursierCommand {
|
||||
|
||||
val (rawDependencies, extraArgs) = {
|
||||
val idxOpt = Some(remainingArgs.indexOf("--")).filter(_ >= 0)
|
||||
idxOpt.fold((remainingArgs, Seq.empty[String])) { idx =>
|
||||
val (l, r) = remainingArgs.splitAt(idx)
|
||||
assert(r.nonEmpty)
|
||||
(l, r.tail)
|
||||
}
|
||||
}
|
||||
|
||||
val helper = new Helper(
|
||||
common,
|
||||
rawDependencies ++ isolated.rawIsolated.map { case (_, dep) => dep }
|
||||
)
|
||||
|
||||
|
||||
val files0 = helper.fetch(sources = false, javadoc = false)
|
||||
|
||||
val contextLoader = Thread.currentThread().getContextClassLoader
|
||||
|
||||
val parentLoader0: ClassLoader =
|
||||
if (Try(contextLoader.loadClass("coursier.cli.Launch")).isSuccess)
|
||||
Launch.mainClassLoader(contextLoader)
|
||||
.flatMap(cl => Option(cl.getParent))
|
||||
.getOrElse {
|
||||
if (common.verbose0 >= 0)
|
||||
Console.err.println(
|
||||
"Warning: cannot find the main ClassLoader that launched coursier. " +
|
||||
"Was coursier launched by its main launcher? " +
|
||||
"The ClassLoader of the application that is about to be launched will be intertwined " +
|
||||
"with the one of coursier, which may be a problem if their dependencies conflict."
|
||||
)
|
||||
contextLoader
|
||||
}
|
||||
else
|
||||
// proguarded -> no risk of conflicts, no need to find a specific ClassLoader
|
||||
contextLoader
|
||||
|
||||
val (parentLoader, filteredFiles) =
|
||||
if (isolated.isolated.isEmpty)
|
||||
(parentLoader0, files0)
|
||||
else {
|
||||
val (isolatedLoader, filteredFiles0) = isolated.targets.foldLeft((parentLoader0, files0)) {
|
||||
case ((parent, files0), target) =>
|
||||
|
||||
// FIXME These were already fetched above
|
||||
val isolatedFiles = helper.fetch(
|
||||
sources = false,
|
||||
javadoc = false,
|
||||
subset = isolated.isolatedDeps.getOrElse(target, Seq.empty).toSet
|
||||
)
|
||||
|
||||
if (common.verbose0 >= 1) {
|
||||
Console.err.println(s"Isolated loader files:")
|
||||
for (f <- isolatedFiles.map(_.toString).sorted)
|
||||
Console.err.println(s" $f")
|
||||
}
|
||||
|
||||
val isolatedLoader = new IsolatedClassLoader(
|
||||
isolatedFiles.map(_.toURI.toURL).toArray,
|
||||
parent,
|
||||
Array(target)
|
||||
)
|
||||
|
||||
val filteredFiles0 = files0.filterNot(isolatedFiles.toSet)
|
||||
|
||||
(isolatedLoader, filteredFiles0)
|
||||
}
|
||||
|
||||
if (common.verbose0 >= 1) {
|
||||
Console.err.println(s"Remaining files:")
|
||||
for (f <- filteredFiles0.map(_.toString).sorted)
|
||||
Console.err.println(s" $f")
|
||||
}
|
||||
|
||||
(isolatedLoader, filteredFiles0)
|
||||
}
|
||||
|
||||
val loader = new URLClassLoader(
|
||||
filteredFiles.map(_.toURI.toURL).toArray,
|
||||
parentLoader
|
||||
)
|
||||
|
||||
val mainClass0 =
|
||||
if (mainClass.nonEmpty) mainClass
|
||||
else {
|
||||
val mainClasses = Helper.mainClasses(loader)
|
||||
|
||||
val mainClass =
|
||||
if (mainClasses.isEmpty) {
|
||||
Helper.errPrintln("No main class found. Specify one with -M or --main.")
|
||||
sys.exit(255)
|
||||
} else if (mainClasses.size == 1) {
|
||||
val (_, mainClass) = mainClasses.head
|
||||
mainClass
|
||||
} else {
|
||||
// Trying to get the main class of the first artifact
|
||||
val mainClassOpt = for {
|
||||
(module, _, _) <- helper.moduleVersionConfigs.headOption
|
||||
mainClass <- mainClasses.collectFirst {
|
||||
case ((org, name), mainClass)
|
||||
if org == module.organization && (
|
||||
module.name == name ||
|
||||
module.name.startsWith(name + "_") // Ignore cross version suffix
|
||||
) =>
|
||||
mainClass
|
||||
}
|
||||
} yield mainClass
|
||||
|
||||
mainClassOpt.getOrElse {
|
||||
Helper.errPrintln(s"Cannot find default main class. Specify one with -M or --main.")
|
||||
sys.exit(255)
|
||||
}
|
||||
}
|
||||
|
||||
mainClass
|
||||
}
|
||||
|
||||
val cls =
|
||||
try loader.loadClass(mainClass0)
|
||||
catch { case e: ClassNotFoundException =>
|
||||
Helper.errPrintln(s"Error: class $mainClass0 not found")
|
||||
sys.exit(255)
|
||||
}
|
||||
val method =
|
||||
try cls.getMethod("main", classOf[Array[String]])
|
||||
catch { case e: NoSuchMethodException =>
|
||||
Helper.errPrintln(s"Error: method main not found in $mainClass0")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
if (common.verbose0 >= 1)
|
||||
Helper.errPrintln(s"Launching $mainClass0 ${extraArgs.mkString(" ")}")
|
||||
else if (common.verbose0 == 0)
|
||||
Helper.errPrintln(s"Launching")
|
||||
|
||||
Thread.currentThread().setContextClassLoader(loader)
|
||||
method.invoke(null, extraArgs.toArray)
|
||||
}
|
||||
|
||||
case class Bootstrap(
|
||||
@Short("M")
|
||||
@Short("main")
|
||||
mainClass: String,
|
||||
@Short("o")
|
||||
output: String = "bootstrap",
|
||||
@Short("D")
|
||||
downloadDir: String,
|
||||
@Short("f")
|
||||
force: Boolean,
|
||||
@Help("Generate a standalone launcher, with all JARs included, instead of one downloading its dependencies on startup.")
|
||||
@Short("s")
|
||||
standalone: Boolean,
|
||||
@Help("Set Java properties in the generated launcher.")
|
||||
@Value("key=value")
|
||||
@Short("P")
|
||||
property: List[String],
|
||||
@Help("Set Java command-line options in the generated launcher.")
|
||||
@Value("option")
|
||||
@Short("J")
|
||||
javaOpt: List[String],
|
||||
@Recurse
|
||||
isolated: IsolatedLoaderOptions,
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
) extends CoursierCommand {
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
if (mainClass.isEmpty) {
|
||||
Console.err.println(s"Error: no main class specified. Specify one with -M or --main")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
if (!standalone && downloadDir.isEmpty) {
|
||||
Console.err.println(s"Error: no download dir specified. Specify one with -D or --download-dir")
|
||||
Console.err.println("E.g. -D \"\\$HOME/.app-name/jars\"")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
val (validProperties, wrongProperties) = property.partition(_.contains("="))
|
||||
if (wrongProperties.nonEmpty) {
|
||||
Console.err.println(s"Wrong -P / --property option(s):\n${wrongProperties.mkString("\n")}")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
val properties0 = validProperties.map { s =>
|
||||
val idx = s.indexOf('=')
|
||||
assert(idx >= 0)
|
||||
(s.take(idx), s.drop(idx + 1))
|
||||
}
|
||||
|
||||
val bootstrapJar =
|
||||
Option(Thread.currentThread().getContextClassLoader.getResourceAsStream("bootstrap.jar")) match {
|
||||
case Some(is) => Cache.readFullySync(is)
|
||||
case None =>
|
||||
Console.err.println(s"Error: bootstrap JAR not found")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
val output0 = new File(output)
|
||||
if (!force && output0.exists()) {
|
||||
Console.err.println(s"Error: $output already exists, use -f option to force erasing it.")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
def zipEntries(zipStream: ZipInputStream): Iterator[(ZipEntry, Array[Byte])] =
|
||||
new Iterator[(ZipEntry, Array[Byte])] {
|
||||
var nextEntry = Option.empty[ZipEntry]
|
||||
def update() =
|
||||
nextEntry = Option(zipStream.getNextEntry)
|
||||
|
||||
update()
|
||||
|
||||
def hasNext = nextEntry.nonEmpty
|
||||
def next() = {
|
||||
val ent = nextEntry.get
|
||||
val data = Platform.readFullySync(zipStream)
|
||||
|
||||
update()
|
||||
|
||||
(ent, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val helper = new Helper(common, remainingArgs)
|
||||
|
||||
val (_, isolatedArtifactFiles) =
|
||||
isolated.targets.foldLeft((Vector.empty[String], Map.empty[String, (Seq[String], Seq[File])])) {
|
||||
case ((done, acc), target) =>
|
||||
val subRes = helper.res.subset(isolated.isolatedDeps.getOrElse(target, Nil).toSet)
|
||||
val subArtifacts = subRes.artifacts.map(_.url)
|
||||
|
||||
val filteredSubArtifacts = subArtifacts.diff(done)
|
||||
|
||||
def subFiles0 = helper.fetch(
|
||||
sources = false,
|
||||
javadoc = false,
|
||||
subset = isolated.isolatedDeps.getOrElse(target, Seq.empty).toSet
|
||||
)
|
||||
|
||||
val (subUrls, subFiles) =
|
||||
if (standalone)
|
||||
(Nil, subFiles0)
|
||||
else
|
||||
(filteredSubArtifacts, Nil)
|
||||
|
||||
val updatedAcc = acc + (target -> (subUrls, subFiles))
|
||||
|
||||
(done ++ filteredSubArtifacts, updatedAcc)
|
||||
}
|
||||
|
||||
val (urls, files) =
|
||||
if (standalone)
|
||||
(
|
||||
Seq.empty[String],
|
||||
helper.fetch(sources = false, javadoc = false)
|
||||
)
|
||||
else
|
||||
(
|
||||
helper.artifacts(sources = false, javadoc = false).map(_.url),
|
||||
Seq.empty[File]
|
||||
)
|
||||
|
||||
val isolatedUrls = isolatedArtifactFiles.map { case (k, (v, _)) => k -> v }
|
||||
val isolatedFiles = isolatedArtifactFiles.map { case (k, (_, v)) => k -> v }
|
||||
|
||||
val nonHttpUrls = urls.filter(s => !s.startsWith("http://") && !s.startsWith("https://"))
|
||||
if (nonHttpUrls.nonEmpty)
|
||||
Console.err.println(s"Warning: non HTTP URLs:\n${nonHttpUrls.mkString("\n")}")
|
||||
|
||||
val buffer = new ByteArrayOutputStream()
|
||||
|
||||
val bootstrapZip = new ZipInputStream(Thread.currentThread().getContextClassLoader.getResourceAsStream("bootstrap.jar"))
|
||||
val outputZip = new ZipOutputStream(buffer)
|
||||
|
||||
for ((ent, data) <- zipEntries(bootstrapZip)) {
|
||||
outputZip.putNextEntry(ent)
|
||||
outputZip.write(data)
|
||||
outputZip.closeEntry()
|
||||
}
|
||||
|
||||
|
||||
val time = System.currentTimeMillis()
|
||||
|
||||
def putStringEntry(name: String, content: String): Unit = {
|
||||
val entry = new ZipEntry(name)
|
||||
entry.setTime(time)
|
||||
|
||||
outputZip.putNextEntry(entry)
|
||||
outputZip.write(content.getBytes("UTF-8"))
|
||||
outputZip.closeEntry()
|
||||
}
|
||||
|
||||
def putEntryFromFile(name: String, f: File): Unit = {
|
||||
val entry = new ZipEntry(name)
|
||||
entry.setTime(f.lastModified())
|
||||
|
||||
outputZip.putNextEntry(entry)
|
||||
outputZip.write(Cache.readFullySync(new FileInputStream(f)))
|
||||
outputZip.closeEntry()
|
||||
}
|
||||
|
||||
putStringEntry("bootstrap-jar-urls", urls.mkString("\n"))
|
||||
|
||||
if (isolated.anyIsolatedDep) {
|
||||
putStringEntry("bootstrap-isolation-ids", isolated.targets.mkString("\n"))
|
||||
|
||||
for (target <- isolated.targets) {
|
||||
val urls = isolatedUrls.getOrElse(target, Nil)
|
||||
val files = isolatedFiles.getOrElse(target, Nil)
|
||||
putStringEntry(s"bootstrap-isolation-$target-jar-urls", urls.mkString("\n"))
|
||||
putStringEntry(s"bootstrap-isolation-$target-jar-resources", files.map(pathFor).mkString("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
def pathFor(f: File) = s"jars/${f.getName}"
|
||||
|
||||
for (f <- files)
|
||||
putEntryFromFile(pathFor(f), f)
|
||||
|
||||
putStringEntry("bootstrap-jar-resources", files.map(pathFor).mkString("\n"))
|
||||
|
||||
val propsEntry = new ZipEntry("bootstrap.properties")
|
||||
propsEntry.setTime(time)
|
||||
|
||||
val properties = new Properties()
|
||||
properties.setProperty("bootstrap.mainClass", mainClass)
|
||||
if (!standalone)
|
||||
properties.setProperty("bootstrap.jarDir", downloadDir)
|
||||
|
||||
outputZip.putNextEntry(propsEntry)
|
||||
properties.store(outputZip, "")
|
||||
outputZip.closeEntry()
|
||||
|
||||
outputZip.close()
|
||||
|
||||
// escaping of javaOpt possibly a bit loose :-|
|
||||
val shellPreamble = Seq(
|
||||
"#!/usr/bin/env sh",
|
||||
"exec java -jar " + javaOpt.map(s => "'" + s.replace("'", "\\'") + "'").mkString(" ") + " \"$0\" \"$@\""
|
||||
).mkString("", "\n", "\n")
|
||||
|
||||
try NIOFiles.write(output0.toPath, shellPreamble.getBytes("UTF-8") ++ buffer.toByteArray)
|
||||
catch { case e: IOException =>
|
||||
Console.err.println(s"Error while writing $output0: ${e.getMessage}")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
val perms = NIOFiles.getPosixFilePermissions(output0.toPath).asScala.toSet
|
||||
|
||||
var newPerms = perms
|
||||
if (perms(PosixFilePermission.OWNER_READ))
|
||||
newPerms += PosixFilePermission.OWNER_EXECUTE
|
||||
if (perms(PosixFilePermission.GROUP_READ))
|
||||
newPerms += PosixFilePermission.GROUP_EXECUTE
|
||||
if (perms(PosixFilePermission.OTHERS_READ))
|
||||
newPerms += PosixFilePermission.OTHERS_EXECUTE
|
||||
|
||||
if (newPerms != perms)
|
||||
NIOFiles.setPosixFilePermissions(
|
||||
output0.toPath,
|
||||
newPerms.asJava
|
||||
)
|
||||
} catch {
|
||||
case e: UnsupportedOperationException =>
|
||||
// Ignored
|
||||
case e: IOException =>
|
||||
Console.err.println(s"Error while making $output0 executable: ${e.getMessage}")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Coursier extends CommandAppOf[CoursierCommand] {
|
||||
object Coursier extends CommandAppOf[
|
||||
// Temporary using CoursierCommandHelper instead of the union type, until case-app
|
||||
// supports the latter directly.
|
||||
// Union.`'bootstrap -> Bootstrap, 'fetch -> Fetch, 'launch -> Launch, 'resolve -> Resolve`.T
|
||||
CoursierCommandHelper
|
||||
] {
|
||||
override def appName = "Coursier"
|
||||
override def progName = "coursier"
|
||||
override def appVersion = coursier.util.Properties.version
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package coursier
|
||||
package cli
|
||||
|
||||
import java.io.File
|
||||
|
||||
import caseapp._
|
||||
|
||||
import scala.language.reflectiveCalls
|
||||
|
||||
case class Fetch(
|
||||
@Recurse
|
||||
options: FetchOptions
|
||||
) extends App {
|
||||
|
||||
val helper = new Helper(options.common, remainingArgs, ignoreErrors = options.force)
|
||||
|
||||
val files0 = helper.fetch(sources = options.sources, javadoc = options.javadoc)
|
||||
|
||||
val out =
|
||||
if (options.classpath)
|
||||
files0
|
||||
.map(_.toString)
|
||||
.mkString(File.pathSeparator)
|
||||
else
|
||||
files0
|
||||
.map(_.toString)
|
||||
.mkString("\n")
|
||||
|
||||
println(out)
|
||||
|
||||
}
|
||||
|
|
@ -190,7 +190,7 @@ class Helper(
|
|||
)
|
||||
|
||||
val logger =
|
||||
if (verbose0 >= 0)
|
||||
if (verbosityLevel >= 0)
|
||||
Some(new TermDisplay(new OutputStreamWriter(System.err)))
|
||||
else
|
||||
None
|
||||
|
|
@ -204,17 +204,17 @@ class Helper(
|
|||
fetchs.tail: _*
|
||||
)
|
||||
val fetch0 =
|
||||
if (verbose0 <= 0) fetchQuiet
|
||||
else {
|
||||
if (verbosityLevel >= 2) {
|
||||
modVers: Seq[(Module, String)] =>
|
||||
val print = Task {
|
||||
errPrintln(s"Getting ${modVers.length} project definition(s)")
|
||||
}
|
||||
|
||||
print.flatMap(_ => fetchQuiet(modVers))
|
||||
}
|
||||
} else
|
||||
fetchQuiet
|
||||
|
||||
if (verbose0 >= 0) {
|
||||
if (verbosityLevel >= 1) {
|
||||
errPrintln(s" Dependencies:\n${Print.dependenciesUnknownConfigs(dependencies, Map.empty)}")
|
||||
|
||||
if (forceVersions.nonEmpty) {
|
||||
|
|
@ -237,8 +237,9 @@ class Helper(
|
|||
|
||||
lazy val projCache = res.projectCache.mapValues { case (_, p) => p }
|
||||
|
||||
if (printResultStdout || verbose0 >= 0) {
|
||||
errPrintln(s" Result:")
|
||||
if (printResultStdout || verbosityLevel >= 1) {
|
||||
if ((printResultStdout && verbosityLevel >= 1) || verbosityLevel >= 2)
|
||||
errPrintln(s" Result:")
|
||||
val depsStr = Print.dependenciesUnknownConfigs(trDeps, projCache)
|
||||
if (printResultStdout)
|
||||
println(depsStr)
|
||||
|
|
@ -285,7 +286,7 @@ class Helper(
|
|||
subset: Set[Dependency] = null
|
||||
): Seq[Artifact] = {
|
||||
|
||||
if (subset == null && verbose0 >= 0) {
|
||||
if (subset == null && verbosityLevel >= 1) {
|
||||
val msg = cachePolicies match {
|
||||
case Seq(CachePolicy.LocalOnly) =>
|
||||
" Checking artifacts"
|
||||
|
|
@ -319,12 +320,12 @@ class Helper(
|
|||
val artifacts0 = artifacts(sources, javadoc, subset)
|
||||
|
||||
val logger =
|
||||
if (verbose0 >= 0)
|
||||
if (verbosityLevel >= 0)
|
||||
Some(new TermDisplay(new OutputStreamWriter(System.err)))
|
||||
else
|
||||
None
|
||||
|
||||
if (verbose0 >= 1 && artifacts0.nonEmpty)
|
||||
if (verbosityLevel >= 1 && artifacts0.nonEmpty)
|
||||
println(s" Found ${artifacts0.length} artifacts")
|
||||
|
||||
val tasks = artifacts0.map(artifact =>
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package coursier.cli
|
||||
|
||||
import java.net.{ URL, URLClassLoader }
|
||||
|
||||
class IsolatedClassLoader(
|
||||
urls: Array[URL],
|
||||
parent: ClassLoader,
|
||||
isolationTargets: Array[String]
|
||||
) extends URLClassLoader(urls, parent) {
|
||||
|
||||
/**
|
||||
* Applications wanting to access an isolated `ClassLoader` should inspect the hierarchy of
|
||||
* loaders, and look into each of them for this method, by reflection. Then they should
|
||||
* call it (still by reflection), and look for an agreed in advance target in it. If it is found,
|
||||
* then the corresponding `ClassLoader` is the one with isolated dependencies.
|
||||
*/
|
||||
def getIsolationTargets: Array[String] = isolationTargets
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package coursier
|
||||
package cli
|
||||
|
||||
import java.net.{ URL, URLClassLoader }
|
||||
|
||||
import caseapp._
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.language.reflectiveCalls
|
||||
import scala.util.Try
|
||||
|
||||
object Launch {
|
||||
|
||||
@tailrec
|
||||
def mainClassLoader(cl: ClassLoader): Option[ClassLoader] =
|
||||
if (cl == null)
|
||||
None
|
||||
else {
|
||||
val isMainLoader = try {
|
||||
val cl0 = cl.asInstanceOf[Object {
|
||||
def isBootstrapLoader: Boolean
|
||||
}]
|
||||
|
||||
cl0.isBootstrapLoader
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
false
|
||||
}
|
||||
|
||||
if (isMainLoader)
|
||||
Some(cl)
|
||||
else
|
||||
mainClassLoader(cl.getParent)
|
||||
}
|
||||
|
||||
class IsolatedClassLoader(
|
||||
urls: Array[URL],
|
||||
parent: ClassLoader,
|
||||
isolationTargets: Array[String]
|
||||
) extends URLClassLoader(urls, parent) {
|
||||
|
||||
/**
|
||||
* Applications wanting to access an isolated `ClassLoader` should inspect the hierarchy of
|
||||
* loaders, and look into each of them for this method, by reflection. Then they should
|
||||
* call it (still by reflection), and look for an agreed in advance target in it. If it is found,
|
||||
* then the corresponding `ClassLoader` is the one with isolated dependencies.
|
||||
*/
|
||||
def getIsolationTargets: Array[String] = isolationTargets
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case class Launch(
|
||||
@Recurse
|
||||
options: LaunchOptions
|
||||
) extends App {
|
||||
|
||||
val (rawDependencies, extraArgs) = {
|
||||
val idxOpt = Some(remainingArgs.indexOf("--")).filter(_ >= 0)
|
||||
idxOpt.fold((remainingArgs, Seq.empty[String])) { idx =>
|
||||
val (l, r) = remainingArgs.splitAt(idx)
|
||||
assert(r.nonEmpty)
|
||||
(l, r.tail)
|
||||
}
|
||||
}
|
||||
|
||||
val helper = new Helper(
|
||||
options.common,
|
||||
rawDependencies ++ options.isolated.rawIsolated.map { case (_, dep) => dep }
|
||||
)
|
||||
|
||||
|
||||
val files0 = helper.fetch(sources = false, javadoc = false)
|
||||
|
||||
val contextLoader = Thread.currentThread().getContextClassLoader
|
||||
|
||||
val parentLoader0: ClassLoader =
|
||||
if (Try(contextLoader.loadClass("coursier.cli.Launch")).isSuccess)
|
||||
Launch.mainClassLoader(contextLoader)
|
||||
.flatMap(cl => Option(cl.getParent))
|
||||
.getOrElse {
|
||||
if (options.common.verbosityLevel >= 0)
|
||||
Console.err.println(
|
||||
"Warning: cannot find the main ClassLoader that launched coursier.\n" +
|
||||
"Was coursier launched by its main launcher? " +
|
||||
"The ClassLoader of the application that is about to be launched will be intertwined " +
|
||||
"with the one of coursier, which may be a problem if their dependencies conflict."
|
||||
)
|
||||
contextLoader
|
||||
}
|
||||
else
|
||||
// proguarded -> no risk of conflicts, no need to find a specific ClassLoader
|
||||
contextLoader
|
||||
|
||||
val (parentLoader, filteredFiles) =
|
||||
if (options.isolated.isolated.isEmpty)
|
||||
(parentLoader0, files0)
|
||||
else {
|
||||
val (isolatedLoader, filteredFiles0) = options.isolated.targets.foldLeft((parentLoader0, files0)) {
|
||||
case ((parent, files0), target) =>
|
||||
|
||||
// FIXME These were already fetched above
|
||||
val isolatedFiles = helper.fetch(
|
||||
sources = false,
|
||||
javadoc = false,
|
||||
subset = options.isolated.isolatedDeps.getOrElse(target, Seq.empty).toSet
|
||||
)
|
||||
|
||||
if (options.common.verbosityLevel >= 2) {
|
||||
Console.err.println(s"Isolated loader files:")
|
||||
for (f <- isolatedFiles.map(_.toString).sorted)
|
||||
Console.err.println(s" $f")
|
||||
}
|
||||
|
||||
val isolatedLoader = new Launch.IsolatedClassLoader(
|
||||
isolatedFiles.map(_.toURI.toURL).toArray,
|
||||
parent,
|
||||
Array(target)
|
||||
)
|
||||
|
||||
val filteredFiles0 = files0.filterNot(isolatedFiles.toSet)
|
||||
|
||||
(isolatedLoader, filteredFiles0)
|
||||
}
|
||||
|
||||
if (options.common.verbosityLevel >= 2) {
|
||||
Console.err.println(s"Remaining files:")
|
||||
for (f <- filteredFiles0.map(_.toString).sorted)
|
||||
Console.err.println(s" $f")
|
||||
}
|
||||
|
||||
(isolatedLoader, filteredFiles0)
|
||||
}
|
||||
|
||||
val loader = new URLClassLoader(
|
||||
filteredFiles.map(_.toURI.toURL).toArray,
|
||||
parentLoader
|
||||
)
|
||||
|
||||
val mainClass0 =
|
||||
if (options.mainClass.nonEmpty) options.mainClass
|
||||
else {
|
||||
val mainClasses = Helper.mainClasses(loader)
|
||||
|
||||
val mainClass =
|
||||
if (mainClasses.isEmpty) {
|
||||
Helper.errPrintln("No main class found. Specify one with -M or --main.")
|
||||
sys.exit(255)
|
||||
} else if (mainClasses.size == 1) {
|
||||
val (_, mainClass) = mainClasses.head
|
||||
mainClass
|
||||
} else {
|
||||
// Trying to get the main class of the first artifact
|
||||
val mainClassOpt = for {
|
||||
(module, _, _) <- helper.moduleVersionConfigs.headOption
|
||||
mainClass <- mainClasses.collectFirst {
|
||||
case ((org, name), mainClass)
|
||||
if org == module.organization && (
|
||||
module.name == name ||
|
||||
module.name.startsWith(name + "_") // Ignore cross version suffix
|
||||
) =>
|
||||
mainClass
|
||||
}
|
||||
} yield mainClass
|
||||
|
||||
mainClassOpt.getOrElse {
|
||||
Helper.errPrintln(s"Cannot find default main class. Specify one with -M or --main.")
|
||||
sys.exit(255)
|
||||
}
|
||||
}
|
||||
|
||||
mainClass
|
||||
}
|
||||
|
||||
val cls =
|
||||
try loader.loadClass(mainClass0)
|
||||
catch { case e: ClassNotFoundException =>
|
||||
Helper.errPrintln(s"Error: class $mainClass0 not found")
|
||||
sys.exit(255)
|
||||
}
|
||||
val method =
|
||||
try cls.getMethod("main", classOf[Array[String]])
|
||||
catch { case e: NoSuchMethodException =>
|
||||
Helper.errPrintln(s"Error: method main not found in $mainClass0")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
if (options.common.verbosityLevel >= 2)
|
||||
Helper.errPrintln(s"Launching $mainClass0 ${extraArgs.mkString(" ")}")
|
||||
else if (options.common.verbosityLevel == 1)
|
||||
Helper.errPrintln(s"Launching")
|
||||
|
||||
Thread.currentThread().setContextClassLoader(loader)
|
||||
method.invoke(null, extraArgs.toArray)
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
package coursier
|
||||
package cli
|
||||
|
||||
import caseapp.{ HelpMessage => Help, ValueDescription => Value, ExtraName => Short, _ }
|
||||
|
||||
import coursier.util.Parse
|
||||
|
||||
case class CommonOptions(
|
||||
@Help("Keep optional dependencies (Maven)")
|
||||
keepOptional: Boolean,
|
||||
@Help("Download mode (default: missing, that is fetch things missing from cache)")
|
||||
@Value("offline|update-changing|update|missing|force")
|
||||
@Short("m")
|
||||
mode: String = "default",
|
||||
@Help("Quiet output")
|
||||
@Short("q")
|
||||
quiet: Boolean,
|
||||
@Help("Increase verbosity (specify several times to increase more)")
|
||||
@Short("v")
|
||||
verbose: Int @@ Counter,
|
||||
@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)")
|
||||
@Short("r")
|
||||
repository: 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")
|
||||
sbtPluginHack: Boolean = false,
|
||||
@Help("Drop module attributes starting with 'info.' - these are sometimes used by projects built with SBT")
|
||||
dropInfoAttr: Boolean = false,
|
||||
@Help("Force module version")
|
||||
@Value("organization:name:forcedVersion")
|
||||
@Short("V")
|
||||
forceVersion: List[String],
|
||||
@Help("Exclude module")
|
||||
@Value("organization:name")
|
||||
@Short("E")
|
||||
exclude: List[String],
|
||||
@Help("Consider provided dependencies to be intransitive. Applies to all the provided dependencies.")
|
||||
intransitive: Boolean,
|
||||
@Help("Classifiers that should be fetched")
|
||||
@Value("classifier1,classifier2,...")
|
||||
@Short("C")
|
||||
classifier: List[String],
|
||||
@Help("Default configuration (default(compile) by default)")
|
||||
@Value("configuration")
|
||||
@Short("c")
|
||||
defaultConfiguration: String = "default(compile)",
|
||||
@Help("Maximum number of parallel downloads (default: 6)")
|
||||
@Short("n")
|
||||
parallel: Int = 6,
|
||||
@Help("Checksums")
|
||||
@Value("checksum1,checksum2,... - end with none to allow for no checksum validation if none are available")
|
||||
checksum: List[String],
|
||||
@Recurse
|
||||
cacheOptions: CacheOptions
|
||||
) {
|
||||
val verbosityLevel = Tag.unwrap(verbose) - (if (quiet) 1 else 0)
|
||||
lazy val classifier0 = classifier.flatMap(_.split(',')).filter(_.nonEmpty)
|
||||
}
|
||||
|
||||
case class CacheOptions(
|
||||
@Help("Cache directory (defaults to environment variable COURSIER_CACHE or ~/.coursier/cache/v1)")
|
||||
@Short("C")
|
||||
cache: String = Cache.default.toString
|
||||
)
|
||||
|
||||
case class IsolatedLoaderOptions(
|
||||
@Value("target:dependency")
|
||||
@Short("I")
|
||||
isolated: List[String],
|
||||
@Help("Comma-separated isolation targets")
|
||||
@Short("i")
|
||||
isolateTarget: List[String]
|
||||
) {
|
||||
|
||||
def anyIsolatedDep = isolateTarget.nonEmpty || isolated.nonEmpty
|
||||
|
||||
lazy val targets = {
|
||||
val l = isolateTarget.flatMap(_.split(',')).filter(_.nonEmpty)
|
||||
val (invalid, valid) = l.partition(_.contains(":"))
|
||||
if (invalid.nonEmpty) {
|
||||
Console.err.println(s"Invalid target IDs:")
|
||||
for (t <- invalid)
|
||||
Console.err.println(s" $t")
|
||||
sys.exit(255)
|
||||
}
|
||||
if (valid.isEmpty)
|
||||
Array("default")
|
||||
else
|
||||
valid.toArray
|
||||
}
|
||||
|
||||
lazy val (validIsolated, unrecognizedIsolated) = isolated.partition(s => targets.exists(t => s.startsWith(t + ":")))
|
||||
|
||||
def check() = {
|
||||
if (unrecognizedIsolated.nonEmpty) {
|
||||
Console.err.println(s"Unrecognized isolation targets in:")
|
||||
for (i <- unrecognizedIsolated)
|
||||
Console.err.println(s" $i")
|
||||
sys.exit(255)
|
||||
}
|
||||
}
|
||||
|
||||
lazy val rawIsolated = validIsolated.map { s =>
|
||||
val Array(target, dep) = s.split(":", 2)
|
||||
target -> dep
|
||||
}
|
||||
|
||||
lazy val isolatedModuleVersions = rawIsolated.groupBy { case (t, _) => t }.map {
|
||||
case (t, l) =>
|
||||
val (errors, modVers) = Parse.moduleVersions(l.map { case (_, d) => d })
|
||||
|
||||
if (errors.nonEmpty) {
|
||||
errors.foreach(Console.err.println)
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
t -> modVers
|
||||
}
|
||||
|
||||
lazy val isolatedDeps = isolatedModuleVersions.map {
|
||||
case (t, l) =>
|
||||
t -> l.map {
|
||||
case (mod, ver) =>
|
||||
Dependency(mod, ver, configuration = "runtime")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case class FetchOptions(
|
||||
@Help("Fetch source artifacts")
|
||||
@Short("S")
|
||||
sources: Boolean,
|
||||
@Help("Fetch javadoc artifacts")
|
||||
@Short("D")
|
||||
javadoc: Boolean,
|
||||
@Help("Print java -cp compatible output")
|
||||
@Short("p")
|
||||
classpath: Boolean,
|
||||
@Help("Fetch artifacts even if the resolution is errored")
|
||||
force: Boolean,
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
)
|
||||
|
||||
case class LaunchOptions(
|
||||
@Short("M")
|
||||
@Short("main")
|
||||
mainClass: String,
|
||||
@Recurse
|
||||
isolated: IsolatedLoaderOptions,
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
)
|
||||
|
||||
case class BootstrapOptions(
|
||||
@Short("M")
|
||||
@Short("main")
|
||||
mainClass: String,
|
||||
@Short("o")
|
||||
output: String = "bootstrap",
|
||||
@Short("D")
|
||||
downloadDir: String,
|
||||
@Short("f")
|
||||
force: Boolean,
|
||||
@Help("Generate a standalone launcher, with all JARs included, instead of one downloading its dependencies on startup.")
|
||||
@Short("s")
|
||||
standalone: Boolean,
|
||||
@Help("Set Java properties in the generated launcher.")
|
||||
@Value("key=value")
|
||||
@Short("P")
|
||||
property: List[String],
|
||||
@Help("Set Java command-line options in the generated launcher.")
|
||||
@Value("option")
|
||||
@Short("J")
|
||||
javaOpt: List[String],
|
||||
@Recurse
|
||||
isolated: IsolatedLoaderOptions,
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package coursier
|
||||
package cli
|
||||
|
||||
import caseapp._
|
||||
|
||||
case class Resolve(
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
) extends App {
|
||||
|
||||
// the `val helper = ` part is needed because of DelayedInit it seems
|
||||
val helper = new Helper(common, remainingArgs, printResultStdout = true)
|
||||
|
||||
}
|
||||
|
|
@ -35,9 +35,10 @@ object Orders {
|
|||
}
|
||||
|
||||
/**
|
||||
* Only relations:
|
||||
* Compile < Runtime < Test
|
||||
*/
|
||||
* Configurations partial order based on configuration mapping `configurations`.
|
||||
*
|
||||
* @param configurations: for each configuration, the configurations it directly extends.
|
||||
*/
|
||||
def configurationPartialOrder(configurations: Map[String, Seq[String]]): PartialOrdering[String] =
|
||||
new PartialOrdering[String] {
|
||||
val allParentsMap = allConfigurations(configurations)
|
||||
|
|
|
|||
|
|
@ -847,6 +847,23 @@ final case class Resolution(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimized dependency set. Returns `dependencies` with no redundancy.
|
||||
*
|
||||
* E.g. `dependencies` may contains several dependencies towards module org:name:version,
|
||||
* a first one excluding A and B, and a second one excluding A and C. In practice, B and C will
|
||||
* be brought anyway, because the first dependency doesn't exclude C, and the second one doesn't
|
||||
* exclude B. So having both dependencies is equivalent to having only one dependency towards
|
||||
* org:name:version, excluding just A.
|
||||
*
|
||||
* The same kind of substitution / filtering out can be applied with configurations. If
|
||||
* `dependencies` contains several dependencies towards org:name:version, a first one bringing
|
||||
* its configuration "runtime", a second one "compile", and the configuration mapping of
|
||||
* org:name:version says that "runtime" extends "compile", then all the dependencies brought
|
||||
* by the latter will be brought anyway by the former, so that the latter can be removed.
|
||||
*
|
||||
* @return A minimized `dependencies`, applying this kind of substitutions.
|
||||
*/
|
||||
def minDependencies: Set[Dependency] =
|
||||
Orders.minDependencies(
|
||||
dependencies,
|
||||
|
|
@ -897,6 +914,15 @@ final case class Resolution(
|
|||
.toSeq
|
||||
} yield (dep, err)
|
||||
|
||||
/**
|
||||
* Removes from this `Resolution` dependencies that are not in `dependencies` neither brought
|
||||
* transitively by them.
|
||||
*
|
||||
* This keeps the versions calculated by this `Resolution`. The common dependencies of different
|
||||
* subsets will thus be guaranteed to have the same versions.
|
||||
*
|
||||
* @param dependencies: the dependencies to keep from this `Resolution`
|
||||
*/
|
||||
def subset(dependencies: Set[Dependency]): Resolution = {
|
||||
val (_, _, finalVersions) = nextDependenciesAndConflicts
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ object Config {
|
|||
.groupBy(_.copy(configuration = ""))
|
||||
.map {
|
||||
case (dep, l) =>
|
||||
dep.copy(configuration = l.map(_.configuration).mkString(","))
|
||||
dep.copy(configuration = l.map(_.configuration).mkString(";"))
|
||||
}
|
||||
.toSet
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ object Print {
|
|||
.groupBy(_.copy(configuration = ""))
|
||||
.toVector
|
||||
.map { case (k, l) =>
|
||||
k.copy(configuration = l.toVector.map(_.configuration).sorted.mkString(","))
|
||||
k.copy(configuration = l.toVector.map(_.configuration).sorted.mkString(";"))
|
||||
}
|
||||
.sortBy { dep =>
|
||||
(dep.module.organization, dep.module.name, dep.module.toString, dep.version)
|
||||
|
|
|
|||
|
|
@ -664,6 +664,21 @@ Set `scalaVersion` to `2.10.6` in `build.sbt`. Then re-open / reload the coursie
|
|||
They require `npm install` to have been run once from the `coursier` directory or a subdirectory of
|
||||
it. They can then be run with `sbt testsJS/test`.
|
||||
|
||||
#### Quickly running the CLI app from the sources
|
||||
|
||||
Run
|
||||
```
|
||||
$ sbt "~cli/pack"
|
||||
```
|
||||
|
||||
This generates and updates a runnable distribution of coursier in `target/pack`, via
|
||||
the [sbt-pack](https://github.com/xerial/sbt-pack/) plugin.
|
||||
|
||||
It can be run from another terminal with
|
||||
```
|
||||
$ cli/target/pack/bin/coursier
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
The first releases were milestones like `0.1.0-M?`. As a launcher, basic Ivy
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ object CoursierPlugin extends AutoPlugin {
|
|||
val coursierMaxIterations = Keys.coursierMaxIterations
|
||||
val coursierChecksums = Keys.coursierChecksums
|
||||
val coursierArtifactsChecksums = Keys.coursierArtifactsChecksums
|
||||
val coursierCachePolicy = Keys.coursierCachePolicy
|
||||
val coursierCachePolicies = Keys.coursierCachePolicies
|
||||
val coursierVerbosity = Keys.coursierVerbosity
|
||||
val coursierResolvers = Keys.coursierResolvers
|
||||
val coursierSbtResolvers = Keys.coursierSbtResolvers
|
||||
|
|
@ -35,8 +35,8 @@ object CoursierPlugin extends AutoPlugin {
|
|||
coursierMaxIterations := 50,
|
||||
coursierChecksums := Seq(Some("SHA-1"), None),
|
||||
coursierArtifactsChecksums := Seq(None),
|
||||
coursierCachePolicy := CachePolicy.FetchMissing,
|
||||
coursierVerbosity := 1,
|
||||
coursierCachePolicies := Settings.defaultCachePolicies,
|
||||
coursierVerbosity := Settings.defaultVerbosityLevel,
|
||||
coursierResolvers <<= Tasks.coursierResolversTask,
|
||||
coursierSbtResolvers <<= externalResolvers in updateSbtClassifiers,
|
||||
coursierCache := Cache.default,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,21 @@ object FromSbt {
|
|||
!k.startsWith(SbtPomExtraProperties.POM_INFO_KEY_PREFIX)
|
||||
}
|
||||
|
||||
def dependencies(
|
||||
def moduleVersion(
|
||||
module: ModuleID,
|
||||
scalaVersion: String,
|
||||
scalaBinaryVersion: String
|
||||
): (Module, String) = {
|
||||
|
||||
val fullName = sbtModuleIdName(module, scalaVersion, scalaBinaryVersion)
|
||||
|
||||
val module0 = Module(module.organization, fullName, FromSbt.attributes(module.extraDependencyAttributes))
|
||||
val version = module.revision
|
||||
|
||||
(module0, version)
|
||||
}
|
||||
|
||||
def dependencies(
|
||||
module: ModuleID,
|
||||
scalaVersion: String,
|
||||
scalaBinaryVersion: String
|
||||
|
|
@ -42,11 +56,11 @@ object FromSbt {
|
|||
|
||||
// TODO Warn about unsupported properties in `module`
|
||||
|
||||
val fullName = sbtModuleIdName(module, scalaVersion, scalaBinaryVersion)
|
||||
val (module0, version) = moduleVersion(module, scalaVersion, scalaBinaryVersion)
|
||||
|
||||
val dep = Dependency(
|
||||
Module(module.organization, fullName, FromSbt.attributes(module.extraDependencyAttributes)),
|
||||
module.revision,
|
||||
module0,
|
||||
version,
|
||||
exclusions = module.exclusions.map { rule =>
|
||||
// FIXME Other `rule` fields are ignored here
|
||||
(rule.organization, rule.name)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ object Keys {
|
|||
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 coursierCachePolicy = SettingKey[CachePolicy]("coursier-cache-policy", "")
|
||||
val coursierCachePolicies = SettingKey[Seq[CachePolicy]]("coursier-cache-policies", "")
|
||||
|
||||
val coursierVerbosity = SettingKey[Int]("coursier-verbosity", "")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package coursier
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
object Settings {
|
||||
|
||||
private val baseDefaultVerbosityLevel = 0
|
||||
|
||||
def defaultVerbosityLevel: Int = {
|
||||
|
||||
def fromOption(value: Option[String], description: String): Option[Int] =
|
||||
value.filter(_.nonEmpty).flatMap {
|
||||
str =>
|
||||
Try(str.toInt) match {
|
||||
case Success(level) => Some(level)
|
||||
case Failure(ex) =>
|
||||
Console.err.println(
|
||||
s"Warning: unrecognized $description value (should be an integer), ignoring it."
|
||||
)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
val fromEnv = fromOption(
|
||||
sys.env.get("COURSIER_VERBOSITY"),
|
||||
"COURSIER_VERBOSITY environment variable"
|
||||
)
|
||||
|
||||
def fromProps = fromOption(
|
||||
sys.props.get("coursier.verbosity"),
|
||||
"Java property coursier.verbosity"
|
||||
)
|
||||
|
||||
fromEnv
|
||||
.orElse(fromProps)
|
||||
.getOrElse(baseDefaultVerbosityLevel)
|
||||
}
|
||||
|
||||
|
||||
private val baseDefaultCachePolicies = Seq(
|
||||
CachePolicy.LocalOnly,
|
||||
CachePolicy.FetchMissing
|
||||
)
|
||||
|
||||
def defaultCachePolicies: Seq[CachePolicy] = {
|
||||
|
||||
def fromOption(value: Option[String], description: String): Option[Seq[CachePolicy]] =
|
||||
value.filter(_.nonEmpty).flatMap {
|
||||
str =>
|
||||
CacheParse.cachePolicies(str) match {
|
||||
case scalaz.Success(Seq()) =>
|
||||
Console.err.println(
|
||||
s"Warning: no mode found in $description, ignoring it."
|
||||
)
|
||||
None
|
||||
case scalaz.Success(policies) =>
|
||||
Some(policies)
|
||||
case scalaz.Failure(errors) =>
|
||||
Console.err.println(
|
||||
s"Warning: unrecognized mode in $description, ignoring it."
|
||||
)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
val fromEnv = fromOption(
|
||||
sys.env.get("COURSIER_MODE"),
|
||||
"COURSIER_MODE environment variable"
|
||||
)
|
||||
|
||||
def fromProps = fromOption(
|
||||
sys.props.get("coursier.mode"),
|
||||
"Java property coursier.mode"
|
||||
)
|
||||
|
||||
fromEnv
|
||||
.orElse(fromProps)
|
||||
.getOrElse(baseDefaultCachePolicies)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -216,10 +216,15 @@ object Tasks {
|
|||
val checksums = coursierChecksums.value
|
||||
val artifactsChecksums = coursierArtifactsChecksums.value
|
||||
val maxIterations = coursierMaxIterations.value
|
||||
val cachePolicy = coursierCachePolicy.value
|
||||
val cachePolicies = coursierCachePolicies.value
|
||||
val cache = coursierCache.value
|
||||
|
||||
val sv = scalaVersion.value // is this always defined? (e.g. for Java only projects?)
|
||||
val sbv = scalaBinaryVersion.value
|
||||
|
||||
val userForceVersions = dependencyOverrides.value.map(
|
||||
FromSbt.moduleVersion(_, sv, sbv)
|
||||
).toMap
|
||||
|
||||
val resolvers =
|
||||
if (sbtClassifiers)
|
||||
|
|
@ -227,13 +232,13 @@ object Tasks {
|
|||
else
|
||||
coursierResolvers.value
|
||||
|
||||
val verbosity = coursierVerbosity.value
|
||||
val verbosityLevel = coursierVerbosity.value
|
||||
|
||||
|
||||
val startRes = Resolution(
|
||||
currentProject.dependencies.map { case (_, dep) => dep }.toSet,
|
||||
filter = Some(dep => !dep.optional),
|
||||
forceVersions = forcedScalaModules(sv) ++ projects.map(_.moduleVersion)
|
||||
forceVersions = userForceVersions ++ forcedScalaModules(sv) ++ projects.map(_.moduleVersion)
|
||||
)
|
||||
|
||||
// required for publish to be fine, later on
|
||||
|
|
@ -252,7 +257,7 @@ object Tasks {
|
|||
Files.write(cacheIvyPropertiesFile.toPath, "".getBytes("UTF-8"))
|
||||
}
|
||||
|
||||
if (verbosity >= 2) {
|
||||
if (verbosityLevel >= 2) {
|
||||
println("InterProjectRepository")
|
||||
for (p <- projects)
|
||||
println(s" ${p.module}:${p.version}")
|
||||
|
|
@ -283,8 +288,10 @@ object Tasks {
|
|||
|
||||
val fetch = Fetch.from(
|
||||
repositories,
|
||||
Cache.fetch(cache, CachePolicy.LocalOnly, checksums = checksums, logger = Some(resLogger), pool = pool),
|
||||
Cache.fetch(cache, cachePolicy, checksums = checksums, logger = Some(resLogger), pool = pool)
|
||||
Cache.fetch(cache, cachePolicies.head, checksums = checksums, logger = Some(resLogger), pool = pool),
|
||||
cachePolicies.tail.map(p =>
|
||||
Cache.fetch(cache, p, checksums = checksums, logger = Some(resLogger), pool = pool)
|
||||
): _*
|
||||
)
|
||||
|
||||
def depsRepr(deps: Seq[(String, Dependency)]) =
|
||||
|
|
@ -297,7 +304,7 @@ object Tasks {
|
|||
s"${dep.module}:${dep.version}:${dep.configuration}"
|
||||
}.sorted.distinct
|
||||
|
||||
if (verbosity >= 1) {
|
||||
if (verbosityLevel >= 1) {
|
||||
val repoReprs = repositories.map {
|
||||
case r: IvyRepository =>
|
||||
s"ivy:${r.pattern}"
|
||||
|
|
@ -313,9 +320,9 @@ object Tasks {
|
|||
errPrintln(s"Repositories:\n${repoReprs.map(" "+_).mkString("\n")}")
|
||||
}
|
||||
|
||||
if (verbosity >= 0)
|
||||
if (verbosityLevel >= 0)
|
||||
errPrintln(s"Resolving ${currentProject.module.organization}:${currentProject.module.name}:${currentProject.version}")
|
||||
if (verbosity >= 1)
|
||||
if (verbosityLevel >= 1)
|
||||
for (depRepr <- depsRepr(currentProject.dependencies))
|
||||
errPrintln(s" $depRepr")
|
||||
|
||||
|
|
@ -374,9 +381,9 @@ object Tasks {
|
|||
}
|
||||
}
|
||||
|
||||
if (verbosity >= 0)
|
||||
if (verbosityLevel >= 0)
|
||||
errPrintln("Resolution done")
|
||||
if (verbosity >= 1) {
|
||||
if (verbosityLevel >= 1) {
|
||||
val finalDeps = Config.dependenciesWithConfig(
|
||||
res,
|
||||
depsByConfig.map { case (k, l) => k -> l.toSet },
|
||||
|
|
@ -408,10 +415,23 @@ object Tasks {
|
|||
val artifactsLogger = createLogger()
|
||||
|
||||
val artifactFileOrErrorTasks = allArtifacts.toVector.map { a =>
|
||||
Cache.file(a, cache, cachePolicy, checksums = artifactsChecksums, logger = Some(artifactsLogger), pool = pool).run.map((a, _))
|
||||
def f(p: CachePolicy) =
|
||||
Cache.file(
|
||||
a,
|
||||
cache,
|
||||
p,
|
||||
checksums = artifactsChecksums,
|
||||
logger = Some(artifactsLogger),
|
||||
pool = pool
|
||||
)
|
||||
|
||||
cachePolicies.tail
|
||||
.foldLeft(f(cachePolicies.head))(_ orElse f(_))
|
||||
.run
|
||||
.map((a, _))
|
||||
}
|
||||
|
||||
if (verbosity >= 0)
|
||||
if (verbosityLevel >= 0)
|
||||
errPrintln(s"Fetching artifacts")
|
||||
|
||||
artifactsLogger.init()
|
||||
|
|
@ -425,7 +445,7 @@ object Tasks {
|
|||
|
||||
artifactsLogger.stop()
|
||||
|
||||
if (verbosity >= 0)
|
||||
if (verbosityLevel >= 0)
|
||||
errPrintln(s"Fetching artifacts: done")
|
||||
|
||||
def artifactFileOpt(artifact: Artifact) = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ ! -e cli/target/scala-2.11/proguard/coursier-standalone.jar ]; then
|
||||
echo "Generating proguarded JAR..." 1>&2
|
||||
sbt cli/proguard:proguard
|
||||
fi
|
||||
|
||||
cat > coursier-standalone << EOF
|
||||
#!/bin/sh
|
||||
exec java -noverify -cp "\$0" coursier.cli.Coursier "\$@"
|
||||
EOF
|
||||
cat cli/target/scala-2.11/proguard/coursier-standalone.jar >> coursier-standalone
|
||||
chmod +x coursier-standalone
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.6.8")
|
||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5")
|
||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.7")
|
||||
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.1.0")
|
||||
addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.0")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
com.github.alexarchambault:coursier_2.11:1.0.0-SNAPSHOT:compile
|
||||
org.scala-lang:scala-library:2.11.7:default
|
||||
org.scala-lang:scala-library:2.11.8:default
|
||||
org.scala-lang.modules:scala-parser-combinators_2.11:1.0.4:default
|
||||
org.scala-lang.modules:scala-xml_2.11:1.0.4:default
|
||||
org.scalaz:scalaz-core_2.11:7.1.2:default
|
||||
|
|
|
|||
Loading…
Reference in New Issue