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:
|
os:
|
||||||
- osx
|
- osx
|
||||||
script:
|
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
|
# Uncomment once https://github.com/scoverage/sbt-scoverage/issues/111 is fixed
|
||||||
# after_success:
|
# after_success:
|
||||||
# - bash <(curl -s https://codecov.io/bash)
|
# - bash <(curl -s https://codecov.io/bash)
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- env: TRAVIS_SCALA_VERSION=2.11.7 PUBLISH=1
|
- env: TRAVIS_SCALA_VERSION=2.11.8 PUBLISH=1
|
||||||
os: linux
|
os: linux
|
||||||
jdk: oraclejdk8
|
jdk: oraclejdk8
|
||||||
- env: TRAVIS_SCALA_VERSION=2.10.6 PUBLISH=1
|
- 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
|
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`.
|
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
|
## Roadmap
|
||||||
|
|
||||||
The first releases were milestones like `0.1.0-M?`. As a launcher, basic Ivy
|
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 SBT_OPTS=-XX:MaxPermSize=2g -Xmx4g
|
||||||
- cmd: SET COURSIER_NO_TERM=1
|
- cmd: SET COURSIER_NO_TERM=1
|
||||||
build_script:
|
build_script:
|
||||||
- sbt ++2.11.7 clean compile coreJVM/publishLocal
|
- sbt ++2.11.8 clean compile coreJVM/publishLocal
|
||||||
- sbt ++2.10.6 clean compile
|
- sbt ++2.10.6 clean compile
|
||||||
test_script:
|
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
|
- sbt ++2.10.6 testsJVM/test
|
||||||
cache:
|
cache:
|
||||||
- C:\sbt\
|
- C:\sbt\
|
||||||
|
|
|
||||||
10
build.sbt
10
build.sbt
|
|
@ -104,8 +104,8 @@ lazy val baseCommonSettings = Seq(
|
||||||
) ++ releaseSettings
|
) ++ releaseSettings
|
||||||
|
|
||||||
lazy val commonSettings = baseCommonSettings ++ Seq(
|
lazy val commonSettings = baseCommonSettings ++ Seq(
|
||||||
scalaVersion := "2.11.7",
|
scalaVersion := "2.11.8",
|
||||||
crossScalaVersions := Seq("2.11.7", "2.10.6"),
|
crossScalaVersions := Seq("2.11.8", "2.10.6"),
|
||||||
libraryDependencies ++= {
|
libraryDependencies ++= {
|
||||||
if (scalaVersion.value startsWith "2.10.")
|
if (scalaVersion.value startsWith "2.10.")
|
||||||
Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full))
|
Seq(compilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full))
|
||||||
|
|
@ -199,7 +199,8 @@ lazy val tests = crossProject
|
||||||
)
|
)
|
||||||
.jsSettings(
|
.jsSettings(
|
||||||
postLinkJSEnv := NodeJSEnv().value,
|
postLinkJSEnv := NodeJSEnv().value,
|
||||||
scalaJSStage in Global := FastOptStage
|
scalaJSStage in Global := FastOptStage,
|
||||||
|
scalaJSUseRhino in Global := false
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy val testsJvm = tests.jvm.dependsOn(cache % "test")
|
lazy val testsJvm = tests.jvm.dependsOn(cache % "test")
|
||||||
|
|
@ -221,6 +222,9 @@ lazy val cache = project
|
||||||
|
|
||||||
Seq(
|
Seq(
|
||||||
// Since 1.0.0-M10
|
// 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`
|
// cache argument type changed from `Seq[(String, File)]` to `File`
|
||||||
ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.Cache.file"),
|
ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.Cache.file"),
|
||||||
ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.Cache.fetch"),
|
ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.Cache.fetch"),
|
||||||
|
|
|
||||||
|
|
@ -107,11 +107,55 @@ object Cache {
|
||||||
helper(alreadyDownloaded)
|
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")
|
val lockFile = new File(file.getParentFile, s"${file.getName}.lock")
|
||||||
|
|
||||||
lockFile.getParentFile.mkdirs()
|
var out: FileOutputStream = null
|
||||||
var out = new FileOutputStream(lockFile)
|
|
||||||
|
withStructureLock(cache) {
|
||||||
|
lockFile.getParentFile.mkdirs()
|
||||||
|
out = new FileOutputStream(lockFile)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var lock: FileLock = null
|
var lock: FileLock = null
|
||||||
|
|
@ -378,7 +422,7 @@ object Cache {
|
||||||
def remote(file: File, url: String): EitherT[Task, FileError, Unit] =
|
def remote(file: File, url: String): EitherT[Task, FileError, Unit] =
|
||||||
EitherT {
|
EitherT {
|
||||||
Task {
|
Task {
|
||||||
withLockFor(file) {
|
withLockFor(cache, file) {
|
||||||
downloading(url, file, logger) {
|
downloading(url, file, logger) {
|
||||||
val tmp = temporaryFile(file)
|
val tmp = temporaryFile(file)
|
||||||
|
|
||||||
|
|
@ -416,14 +460,18 @@ object Cache {
|
||||||
|
|
||||||
val result =
|
val result =
|
||||||
try {
|
try {
|
||||||
tmp.getParentFile.mkdirs()
|
val out = withStructureLock(cache) {
|
||||||
val out = new FileOutputStream(tmp, partialDownload)
|
tmp.getParentFile.mkdirs()
|
||||||
|
new FileOutputStream(tmp, partialDownload)
|
||||||
|
}
|
||||||
try \/-(readFullyTo(in, out, logger, url, if (partialDownload) alreadyDownloaded else 0L))
|
try \/-(readFullyTo(in, out, logger, url, if (partialDownload) alreadyDownloaded else 0L))
|
||||||
finally out.close()
|
finally out.close()
|
||||||
} finally in.close()
|
} finally in.close()
|
||||||
|
|
||||||
file.getParentFile.mkdirs()
|
withStructureLock(cache) {
|
||||||
NioFiles.move(tmp.toPath, file.toPath, StandardCopyOption.ATOMIC_MOVE)
|
file.getParentFile.mkdirs()
|
||||||
|
NioFiles.move(tmp.toPath, file.toPath, StandardCopyOption.ATOMIC_MOVE)
|
||||||
|
}
|
||||||
|
|
||||||
for (lastModified <- Option(conn.getLastModified) if lastModified > 0L)
|
for (lastModified <- Option(conn.getLastModified) if lastModified > 0L)
|
||||||
file.setLastModified(lastModified)
|
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
|
} else
|
||||||
None
|
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)
|
private def control(n: Int, c: Char) = output.write(s"\033[" + n + c)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move up `n` squares
|
* 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
|
* 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
|
* 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
|
* Clear the current line
|
||||||
|
|
@ -50,196 +50,14 @@ object Terminal {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TermDisplay(
|
object TermDisplay {
|
||||||
out: Writer,
|
private def defaultFallbackMode: Boolean = {
|
||||||
var fallbackMode: Boolean = sys.env.get("COURSIER_NO_TERM").nonEmpty
|
val env = sys.env.get("COURSIER_NO_TERM").nonEmpty
|
||||||
) extends Cache.Logger {
|
def nonInteractive = System.console() == null
|
||||||
|
|
||||||
private val ansi = new Terminal.Ansi(out)
|
env || nonInteractive
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
private sealed abstract class Info extends Product with Serializable {
|
||||||
def fraction: Option[Double]
|
def fraction: Option[Double]
|
||||||
|
|
@ -327,73 +145,288 @@ class TermDisplay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val downloads = new ArrayBuffer[String]
|
private sealed abstract class Message extends Product with Serializable
|
||||||
private val doneQueue = new ArrayBuffer[(String, Info)]
|
private object Message {
|
||||||
private val infos = new ConcurrentHashMap[String, Info]
|
case object Update extends Message
|
||||||
|
case object Stop extends Message
|
||||||
private val q = new LinkedBlockingDeque[Either[Unit, Unit]]
|
|
||||||
def update(): Unit = {
|
|
||||||
if (q.size() == 0)
|
|
||||||
q.put(Right(()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def newEntry(
|
private val refreshInterval = 1000 / 60
|
||||||
url: String,
|
private val fallbackRefreshInterval = 1000
|
||||||
info: Info,
|
|
||||||
fallbackMessage: => String
|
|
||||||
): Unit = {
|
|
||||||
assert(!infos.containsKey(url))
|
|
||||||
val prev = infos.putIfAbsent(url, info)
|
|
||||||
assert(prev == null)
|
|
||||||
|
|
||||||
if (fallbackMode) {
|
private class UpdateDisplayThread(
|
||||||
// FIXME What about concurrent accesses to out from the thread above?
|
out: Writer,
|
||||||
out.write(fallbackMessage)
|
var fallbackMode: Boolean
|
||||||
out.flush()
|
) 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 {
|
def end(): Unit = {
|
||||||
downloads.append(url)
|
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)
|
class TermDisplay(
|
||||||
assert(info != null)
|
out: Writer,
|
||||||
|
val fallbackMode: Boolean = TermDisplay.defaultFallbackMode
|
||||||
|
) extends Cache.Logger {
|
||||||
|
|
||||||
if (success)
|
import TermDisplay._
|
||||||
doneQueue += (url -> update0(info))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fallbackMode && success) {
|
private val updateThread = new UpdateDisplayThread(out, fallbackMode)
|
||||||
// FIXME What about concurrent accesses to out from the thread above?
|
|
||||||
out.write(fallbackMessage)
|
|
||||||
out.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
update()
|
def init(): Unit = {
|
||||||
|
updateThread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
def stop(): Unit = {
|
||||||
|
updateThread.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
override def downloadingArtifact(url: String, file: File): Unit =
|
override def downloadingArtifact(url: String, file: File): Unit =
|
||||||
newEntry(
|
updateThread.newEntry(
|
||||||
url,
|
url,
|
||||||
DownloadInfo(0L, 0L, None, System.currentTimeMillis(), updateCheck = false),
|
DownloadInfo(0L, 0L, None, System.currentTimeMillis(), updateCheck = false),
|
||||||
s"Downloading $url\n"
|
s"Downloading $url\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
override def downloadLength(url: String, totalLength: Long, alreadyDownloaded: Long): Unit = {
|
override def downloadLength(url: String, totalLength: Long, alreadyDownloaded: Long): Unit = {
|
||||||
val info = infos.get(url)
|
val info = updateThread.infos.get(url)
|
||||||
assert(info != null)
|
assert(info != null)
|
||||||
val newInfo = info match {
|
val newInfo = info match {
|
||||||
case info0: DownloadInfo =>
|
case info0: DownloadInfo =>
|
||||||
|
|
@ -401,12 +434,12 @@ class TermDisplay(
|
||||||
case _ =>
|
case _ =>
|
||||||
throw new Exception(s"Incoherent display state for $url")
|
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 = {
|
override def downloadProgress(url: String, downloaded: Long): Unit = {
|
||||||
val info = infos.get(url)
|
val info = updateThread.infos.get(url)
|
||||||
assert(info != null)
|
assert(info != null)
|
||||||
val newInfo = info match {
|
val newInfo = info match {
|
||||||
case info0: DownloadInfo =>
|
case info0: DownloadInfo =>
|
||||||
|
|
@ -414,16 +447,16 @@ class TermDisplay(
|
||||||
case _ =>
|
case _ =>
|
||||||
throw new Exception(s"Incoherent display state for $url")
|
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 =
|
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 =
|
override def checkingUpdates(url: String, currentTimeOpt: Option[Long]): Unit =
|
||||||
newEntry(
|
updateThread.newEntry(
|
||||||
url,
|
url,
|
||||||
CheckUpdateInfo(currentTimeOpt, None, isDone = false),
|
CheckUpdateInfo(currentTimeOpt, None, isDone = false),
|
||||||
s"Checking $url\n"
|
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 =>
|
case info: CheckUpdateInfo =>
|
||||||
info.copy(remoteTimeOpt = remoteTimeOpt, isDone = true)
|
info.copy(remoteTimeOpt = remoteTimeOpt, isDone = true)
|
||||||
case _ =>
|
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 coursier
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import java.io.{ FileInputStream, ByteArrayOutputStream, File, IOException }
|
import caseapp._
|
||||||
import java.net.URLClassLoader
|
import caseapp.core.{ ArgsApp, CommandsMessages }
|
||||||
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.{ HelpMessage => Help, ValueDescription => Value, ExtraName => Short, _ }
|
import shapeless.union.Union
|
||||||
import coursier.util.Parse
|
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
// Temporary, see comment in Coursier below
|
||||||
import scala.language.reflectiveCalls
|
case class CoursierCommandHelper(
|
||||||
import scala.util.Try
|
command: CoursierCommandHelper.U
|
||||||
|
) extends ArgsApp {
|
||||||
case class CommonOptions(
|
def setRemainingArgs(remainingArgs: Seq[String]): Unit =
|
||||||
@Help("Keep optional dependencies (Maven)")
|
command.unify.setRemainingArgs(remainingArgs)
|
||||||
keepOptional: Boolean,
|
def remainingArgs: Seq[String] =
|
||||||
@Help("Download mode (default: missing, that is fetch things missing from cache)")
|
command.unify.remainingArgs
|
||||||
@Value("offline|update-changing|update|missing|force")
|
def apply(): Unit =
|
||||||
@Short("m")
|
command.unify.apply()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case class CacheOptions(
|
object CoursierCommandHelper {
|
||||||
@Help("Cache directory (defaults to environment variable COURSIER_CACHE or ~/.coursier/cache/v1)")
|
type U = Union.`'bootstrap -> Bootstrap, 'fetch -> Fetch, 'launch -> Launch, 'resolve -> Resolve`.T
|
||||||
@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)
|
|
||||||
|
|
||||||
|
implicit val commandParser: CommandParser[CoursierCommandHelper] =
|
||||||
|
CommandParser[U].map(CoursierCommandHelper(_))
|
||||||
|
implicit val commandsMessages: CommandsMessages[CoursierCommandHelper] =
|
||||||
|
CommandsMessages(CommandsMessages[U].messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Fetch(
|
object Coursier extends CommandAppOf[
|
||||||
@Help("Fetch source artifacts")
|
// Temporary using CoursierCommandHelper instead of the union type, until case-app
|
||||||
@Short("S")
|
// supports the latter directly.
|
||||||
sources: Boolean,
|
// Union.`'bootstrap -> Bootstrap, 'fetch -> Fetch, 'launch -> Launch, 'resolve -> Resolve`.T
|
||||||
@Help("Fetch javadoc artifacts")
|
CoursierCommandHelper
|
||||||
@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] {
|
|
||||||
override def appName = "Coursier"
|
override def appName = "Coursier"
|
||||||
override def progName = "coursier"
|
override def progName = "coursier"
|
||||||
override def appVersion = coursier.util.Properties.version
|
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 =
|
val logger =
|
||||||
if (verbose0 >= 0)
|
if (verbosityLevel >= 0)
|
||||||
Some(new TermDisplay(new OutputStreamWriter(System.err)))
|
Some(new TermDisplay(new OutputStreamWriter(System.err)))
|
||||||
else
|
else
|
||||||
None
|
None
|
||||||
|
|
@ -204,17 +204,17 @@ class Helper(
|
||||||
fetchs.tail: _*
|
fetchs.tail: _*
|
||||||
)
|
)
|
||||||
val fetch0 =
|
val fetch0 =
|
||||||
if (verbose0 <= 0) fetchQuiet
|
if (verbosityLevel >= 2) {
|
||||||
else {
|
|
||||||
modVers: Seq[(Module, String)] =>
|
modVers: Seq[(Module, String)] =>
|
||||||
val print = Task {
|
val print = Task {
|
||||||
errPrintln(s"Getting ${modVers.length} project definition(s)")
|
errPrintln(s"Getting ${modVers.length} project definition(s)")
|
||||||
}
|
}
|
||||||
|
|
||||||
print.flatMap(_ => fetchQuiet(modVers))
|
print.flatMap(_ => fetchQuiet(modVers))
|
||||||
}
|
} else
|
||||||
|
fetchQuiet
|
||||||
|
|
||||||
if (verbose0 >= 0) {
|
if (verbosityLevel >= 1) {
|
||||||
errPrintln(s" Dependencies:\n${Print.dependenciesUnknownConfigs(dependencies, Map.empty)}")
|
errPrintln(s" Dependencies:\n${Print.dependenciesUnknownConfigs(dependencies, Map.empty)}")
|
||||||
|
|
||||||
if (forceVersions.nonEmpty) {
|
if (forceVersions.nonEmpty) {
|
||||||
|
|
@ -237,8 +237,9 @@ class Helper(
|
||||||
|
|
||||||
lazy val projCache = res.projectCache.mapValues { case (_, p) => p }
|
lazy val projCache = res.projectCache.mapValues { case (_, p) => p }
|
||||||
|
|
||||||
if (printResultStdout || verbose0 >= 0) {
|
if (printResultStdout || verbosityLevel >= 1) {
|
||||||
errPrintln(s" Result:")
|
if ((printResultStdout && verbosityLevel >= 1) || verbosityLevel >= 2)
|
||||||
|
errPrintln(s" Result:")
|
||||||
val depsStr = Print.dependenciesUnknownConfigs(trDeps, projCache)
|
val depsStr = Print.dependenciesUnknownConfigs(trDeps, projCache)
|
||||||
if (printResultStdout)
|
if (printResultStdout)
|
||||||
println(depsStr)
|
println(depsStr)
|
||||||
|
|
@ -285,7 +286,7 @@ class Helper(
|
||||||
subset: Set[Dependency] = null
|
subset: Set[Dependency] = null
|
||||||
): Seq[Artifact] = {
|
): Seq[Artifact] = {
|
||||||
|
|
||||||
if (subset == null && verbose0 >= 0) {
|
if (subset == null && verbosityLevel >= 1) {
|
||||||
val msg = cachePolicies match {
|
val msg = cachePolicies match {
|
||||||
case Seq(CachePolicy.LocalOnly) =>
|
case Seq(CachePolicy.LocalOnly) =>
|
||||||
" Checking artifacts"
|
" Checking artifacts"
|
||||||
|
|
@ -319,12 +320,12 @@ class Helper(
|
||||||
val artifacts0 = artifacts(sources, javadoc, subset)
|
val artifacts0 = artifacts(sources, javadoc, subset)
|
||||||
|
|
||||||
val logger =
|
val logger =
|
||||||
if (verbose0 >= 0)
|
if (verbosityLevel >= 0)
|
||||||
Some(new TermDisplay(new OutputStreamWriter(System.err)))
|
Some(new TermDisplay(new OutputStreamWriter(System.err)))
|
||||||
else
|
else
|
||||||
None
|
None
|
||||||
|
|
||||||
if (verbose0 >= 1 && artifacts0.nonEmpty)
|
if (verbosityLevel >= 1 && artifacts0.nonEmpty)
|
||||||
println(s" Found ${artifacts0.length} artifacts")
|
println(s" Found ${artifacts0.length} artifacts")
|
||||||
|
|
||||||
val tasks = artifacts0.map(artifact =>
|
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:
|
* Configurations partial order based on configuration mapping `configurations`.
|
||||||
* Compile < Runtime < Test
|
*
|
||||||
*/
|
* @param configurations: for each configuration, the configurations it directly extends.
|
||||||
|
*/
|
||||||
def configurationPartialOrder(configurations: Map[String, Seq[String]]): PartialOrdering[String] =
|
def configurationPartialOrder(configurations: Map[String, Seq[String]]): PartialOrdering[String] =
|
||||||
new PartialOrdering[String] {
|
new PartialOrdering[String] {
|
||||||
val allParentsMap = allConfigurations(configurations)
|
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] =
|
def minDependencies: Set[Dependency] =
|
||||||
Orders.minDependencies(
|
Orders.minDependencies(
|
||||||
dependencies,
|
dependencies,
|
||||||
|
|
@ -897,6 +914,15 @@ final case class Resolution(
|
||||||
.toSeq
|
.toSeq
|
||||||
} yield (dep, err)
|
} 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 = {
|
def subset(dependencies: Set[Dependency]): Resolution = {
|
||||||
val (_, _, finalVersions) = nextDependenciesAndConflicts
|
val (_, _, finalVersions) = nextDependenciesAndConflicts
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ object Config {
|
||||||
.groupBy(_.copy(configuration = ""))
|
.groupBy(_.copy(configuration = ""))
|
||||||
.map {
|
.map {
|
||||||
case (dep, l) =>
|
case (dep, l) =>
|
||||||
dep.copy(configuration = l.map(_.configuration).mkString(","))
|
dep.copy(configuration = l.map(_.configuration).mkString(";"))
|
||||||
}
|
}
|
||||||
.toSet
|
.toSet
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ object Print {
|
||||||
.groupBy(_.copy(configuration = ""))
|
.groupBy(_.copy(configuration = ""))
|
||||||
.toVector
|
.toVector
|
||||||
.map { case (k, l) =>
|
.map { case (k, l) =>
|
||||||
k.copy(configuration = l.toVector.map(_.configuration).sorted.mkString(","))
|
k.copy(configuration = l.toVector.map(_.configuration).sorted.mkString(";"))
|
||||||
}
|
}
|
||||||
.sortBy { dep =>
|
.sortBy { dep =>
|
||||||
(dep.module.organization, dep.module.name, dep.module.toString, dep.version)
|
(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
|
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`.
|
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
|
## Roadmap
|
||||||
|
|
||||||
The first releases were milestones like `0.1.0-M?`. As a launcher, basic Ivy
|
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 coursierMaxIterations = Keys.coursierMaxIterations
|
||||||
val coursierChecksums = Keys.coursierChecksums
|
val coursierChecksums = Keys.coursierChecksums
|
||||||
val coursierArtifactsChecksums = Keys.coursierArtifactsChecksums
|
val coursierArtifactsChecksums = Keys.coursierArtifactsChecksums
|
||||||
val coursierCachePolicy = Keys.coursierCachePolicy
|
val coursierCachePolicies = Keys.coursierCachePolicies
|
||||||
val coursierVerbosity = Keys.coursierVerbosity
|
val coursierVerbosity = Keys.coursierVerbosity
|
||||||
val coursierResolvers = Keys.coursierResolvers
|
val coursierResolvers = Keys.coursierResolvers
|
||||||
val coursierSbtResolvers = Keys.coursierSbtResolvers
|
val coursierSbtResolvers = Keys.coursierSbtResolvers
|
||||||
|
|
@ -35,8 +35,8 @@ object CoursierPlugin extends AutoPlugin {
|
||||||
coursierMaxIterations := 50,
|
coursierMaxIterations := 50,
|
||||||
coursierChecksums := Seq(Some("SHA-1"), None),
|
coursierChecksums := Seq(Some("SHA-1"), None),
|
||||||
coursierArtifactsChecksums := Seq(None),
|
coursierArtifactsChecksums := Seq(None),
|
||||||
coursierCachePolicy := CachePolicy.FetchMissing,
|
coursierCachePolicies := Settings.defaultCachePolicies,
|
||||||
coursierVerbosity := 1,
|
coursierVerbosity := Settings.defaultVerbosityLevel,
|
||||||
coursierResolvers <<= Tasks.coursierResolversTask,
|
coursierResolvers <<= Tasks.coursierResolversTask,
|
||||||
coursierSbtResolvers <<= externalResolvers in updateSbtClassifiers,
|
coursierSbtResolvers <<= externalResolvers in updateSbtClassifiers,
|
||||||
coursierCache := Cache.default,
|
coursierCache := Cache.default,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,21 @@ object FromSbt {
|
||||||
!k.startsWith(SbtPomExtraProperties.POM_INFO_KEY_PREFIX)
|
!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,
|
module: ModuleID,
|
||||||
scalaVersion: String,
|
scalaVersion: String,
|
||||||
scalaBinaryVersion: String
|
scalaBinaryVersion: String
|
||||||
|
|
@ -42,11 +56,11 @@ object FromSbt {
|
||||||
|
|
||||||
// TODO Warn about unsupported properties in `module`
|
// TODO Warn about unsupported properties in `module`
|
||||||
|
|
||||||
val fullName = sbtModuleIdName(module, scalaVersion, scalaBinaryVersion)
|
val (module0, version) = moduleVersion(module, scalaVersion, scalaBinaryVersion)
|
||||||
|
|
||||||
val dep = Dependency(
|
val dep = Dependency(
|
||||||
Module(module.organization, fullName, FromSbt.attributes(module.extraDependencyAttributes)),
|
module0,
|
||||||
module.revision,
|
version,
|
||||||
exclusions = module.exclusions.map { rule =>
|
exclusions = module.exclusions.map { rule =>
|
||||||
// FIXME Other `rule` fields are ignored here
|
// FIXME Other `rule` fields are ignored here
|
||||||
(rule.organization, rule.name)
|
(rule.organization, rule.name)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ object Keys {
|
||||||
val coursierMaxIterations = SettingKey[Int]("coursier-max-iterations", "")
|
val coursierMaxIterations = SettingKey[Int]("coursier-max-iterations", "")
|
||||||
val coursierChecksums = SettingKey[Seq[Option[String]]]("coursier-checksums", "")
|
val coursierChecksums = SettingKey[Seq[Option[String]]]("coursier-checksums", "")
|
||||||
val coursierArtifactsChecksums = SettingKey[Seq[Option[String]]]("coursier-artifacts-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", "")
|
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 checksums = coursierChecksums.value
|
||||||
val artifactsChecksums = coursierArtifactsChecksums.value
|
val artifactsChecksums = coursierArtifactsChecksums.value
|
||||||
val maxIterations = coursierMaxIterations.value
|
val maxIterations = coursierMaxIterations.value
|
||||||
val cachePolicy = coursierCachePolicy.value
|
val cachePolicies = coursierCachePolicies.value
|
||||||
val cache = coursierCache.value
|
val cache = coursierCache.value
|
||||||
|
|
||||||
val sv = scalaVersion.value // is this always defined? (e.g. for Java only projects?)
|
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 =
|
val resolvers =
|
||||||
if (sbtClassifiers)
|
if (sbtClassifiers)
|
||||||
|
|
@ -227,13 +232,13 @@ object Tasks {
|
||||||
else
|
else
|
||||||
coursierResolvers.value
|
coursierResolvers.value
|
||||||
|
|
||||||
val verbosity = coursierVerbosity.value
|
val verbosityLevel = coursierVerbosity.value
|
||||||
|
|
||||||
|
|
||||||
val startRes = Resolution(
|
val startRes = Resolution(
|
||||||
currentProject.dependencies.map { case (_, dep) => dep }.toSet,
|
currentProject.dependencies.map { case (_, dep) => dep }.toSet,
|
||||||
filter = Some(dep => !dep.optional),
|
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
|
// required for publish to be fine, later on
|
||||||
|
|
@ -252,7 +257,7 @@ object Tasks {
|
||||||
Files.write(cacheIvyPropertiesFile.toPath, "".getBytes("UTF-8"))
|
Files.write(cacheIvyPropertiesFile.toPath, "".getBytes("UTF-8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (verbosity >= 2) {
|
if (verbosityLevel >= 2) {
|
||||||
println("InterProjectRepository")
|
println("InterProjectRepository")
|
||||||
for (p <- projects)
|
for (p <- projects)
|
||||||
println(s" ${p.module}:${p.version}")
|
println(s" ${p.module}:${p.version}")
|
||||||
|
|
@ -283,8 +288,10 @@ object Tasks {
|
||||||
|
|
||||||
val fetch = Fetch.from(
|
val fetch = Fetch.from(
|
||||||
repositories,
|
repositories,
|
||||||
Cache.fetch(cache, CachePolicy.LocalOnly, checksums = checksums, logger = Some(resLogger), pool = pool),
|
Cache.fetch(cache, cachePolicies.head, checksums = checksums, logger = Some(resLogger), pool = pool),
|
||||||
Cache.fetch(cache, cachePolicy, 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)]) =
|
def depsRepr(deps: Seq[(String, Dependency)]) =
|
||||||
|
|
@ -297,7 +304,7 @@ object Tasks {
|
||||||
s"${dep.module}:${dep.version}:${dep.configuration}"
|
s"${dep.module}:${dep.version}:${dep.configuration}"
|
||||||
}.sorted.distinct
|
}.sorted.distinct
|
||||||
|
|
||||||
if (verbosity >= 1) {
|
if (verbosityLevel >= 1) {
|
||||||
val repoReprs = repositories.map {
|
val repoReprs = repositories.map {
|
||||||
case r: IvyRepository =>
|
case r: IvyRepository =>
|
||||||
s"ivy:${r.pattern}"
|
s"ivy:${r.pattern}"
|
||||||
|
|
@ -313,9 +320,9 @@ object Tasks {
|
||||||
errPrintln(s"Repositories:\n${repoReprs.map(" "+_).mkString("\n")}")
|
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}")
|
errPrintln(s"Resolving ${currentProject.module.organization}:${currentProject.module.name}:${currentProject.version}")
|
||||||
if (verbosity >= 1)
|
if (verbosityLevel >= 1)
|
||||||
for (depRepr <- depsRepr(currentProject.dependencies))
|
for (depRepr <- depsRepr(currentProject.dependencies))
|
||||||
errPrintln(s" $depRepr")
|
errPrintln(s" $depRepr")
|
||||||
|
|
||||||
|
|
@ -374,9 +381,9 @@ object Tasks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (verbosity >= 0)
|
if (verbosityLevel >= 0)
|
||||||
errPrintln("Resolution done")
|
errPrintln("Resolution done")
|
||||||
if (verbosity >= 1) {
|
if (verbosityLevel >= 1) {
|
||||||
val finalDeps = Config.dependenciesWithConfig(
|
val finalDeps = Config.dependenciesWithConfig(
|
||||||
res,
|
res,
|
||||||
depsByConfig.map { case (k, l) => k -> l.toSet },
|
depsByConfig.map { case (k, l) => k -> l.toSet },
|
||||||
|
|
@ -408,10 +415,23 @@ object Tasks {
|
||||||
val artifactsLogger = createLogger()
|
val artifactsLogger = createLogger()
|
||||||
|
|
||||||
val artifactFileOrErrorTasks = allArtifacts.toVector.map { a =>
|
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")
|
errPrintln(s"Fetching artifacts")
|
||||||
|
|
||||||
artifactsLogger.init()
|
artifactsLogger.init()
|
||||||
|
|
@ -425,7 +445,7 @@ object Tasks {
|
||||||
|
|
||||||
artifactsLogger.stop()
|
artifactsLogger.stop()
|
||||||
|
|
||||||
if (verbosity >= 0)
|
if (verbosityLevel >= 0)
|
||||||
errPrintln(s"Fetching artifacts: done")
|
errPrintln(s"Fetching artifacts: done")
|
||||||
|
|
||||||
def artifactFileOpt(artifact: Artifact) = {
|
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.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("com.jsuereth" % "sbt-pgp" % "1.0.0")
|
||||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.1.0")
|
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.1.0")
|
||||||
addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.0")
|
addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.0")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
com.github.alexarchambault:coursier_2.11:1.0.0-SNAPSHOT:compile
|
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-parser-combinators_2.11:1.0.4:default
|
||||||
org.scala-lang.modules:scala-xml_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
|
org.scalaz:scalaz-core_2.11:7.1.2:default
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue