mirror of https://github.com/sbt/sbt.git
Merge pull request #102 from alexarchambault/topic/launcher-isolation
Add ClassLoader isolation with launcher, and a resolve command
This commit is contained in:
commit
c48abf9330
|
|
@ -0,0 +1,54 @@
|
|||
package coursier
|
||||
|
||||
import coursier.ivy.IvyRepository
|
||||
import coursier.util.Parse
|
||||
|
||||
import scalaz._, Scalaz._
|
||||
|
||||
object CacheParse {
|
||||
|
||||
def repository(s: String): Validation[String, Repository] =
|
||||
if (s == "ivy2local" || s == "ivy2Local")
|
||||
Cache.ivy2Local.success
|
||||
else {
|
||||
val repo = Parse.repository(s)
|
||||
|
||||
val url = repo match {
|
||||
case m: MavenRepository =>
|
||||
m.root
|
||||
case i: IvyRepository =>
|
||||
i.pattern
|
||||
case r =>
|
||||
sys.error(s"Unrecognized repository: $r")
|
||||
}
|
||||
|
||||
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file:/"))
|
||||
repo.success
|
||||
else
|
||||
s"Unrecognized protocol in $url".failure
|
||||
}
|
||||
|
||||
def repositories(l: Seq[String]): ValidationNel[String, Seq[Repository]] =
|
||||
l.toVector.traverseU { s =>
|
||||
repository(s).leftMap(_.wrapNel)
|
||||
}
|
||||
|
||||
def cachePolicies(s: String): ValidationNel[String, Seq[CachePolicy]] =
|
||||
s.split(',').toVector.traverseU {
|
||||
case "offline" =>
|
||||
Seq(CachePolicy.LocalOnly).successNel
|
||||
case "update-changing" =>
|
||||
Seq(CachePolicy.UpdateChanging).successNel
|
||||
case "update" =>
|
||||
Seq(CachePolicy.Update).successNel
|
||||
case "missing" =>
|
||||
Seq(CachePolicy.FetchMissing).successNel
|
||||
case "force" =>
|
||||
Seq(CachePolicy.ForceDownload).successNel
|
||||
case "default" =>
|
||||
Seq(CachePolicy.LocalOnly, CachePolicy.FetchMissing).successNel
|
||||
case other =>
|
||||
s"Unrecognized mode: $other".failureNel
|
||||
}.map(_.flatten)
|
||||
|
||||
}
|
||||
|
|
@ -1,45 +1,45 @@
|
|||
package coursier
|
||||
package cli
|
||||
|
||||
import java.io.{ByteArrayOutputStream, FileOutputStream, File, IOException}
|
||||
import java.io.{ ByteArrayOutputStream, File, IOException }
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.{ Files => NIOFiles }
|
||||
import java.nio.file.attribute.{FileTime, PosixFilePermission}
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import java.util.Properties
|
||||
import java.util.zip.{ZipEntry, ZipOutputStream, ZipInputStream, ZipFile}
|
||||
import java.util.zip.{ ZipEntry, ZipOutputStream, ZipInputStream }
|
||||
|
||||
import caseapp._
|
||||
import coursier.util.ClasspathFilter
|
||||
import caseapp.{ HelpMessage => Help, ValueDescription => Value, ExtraName => Short, _ }
|
||||
import coursier.util.{ Parse, ClasspathFilter }
|
||||
|
||||
case class CommonOptions(
|
||||
@HelpMessage("Keep optional dependencies (Maven)")
|
||||
@Help("Keep optional dependencies (Maven)")
|
||||
keepOptional: Boolean,
|
||||
@HelpMessage("Download mode (default: missing, that is fetch things missing from cache)")
|
||||
@ValueDescription("offline|update-changing|update|missing|force")
|
||||
@ExtraName("m")
|
||||
mode: String = "missing",
|
||||
@HelpMessage("Quiet output")
|
||||
@ExtraName("q")
|
||||
@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,
|
||||
@HelpMessage("Increase verbosity (specify several times to increase more)")
|
||||
@ExtraName("v")
|
||||
@Help("Increase verbosity (specify several times to increase more)")
|
||||
@Short("v")
|
||||
verbose: List[Unit],
|
||||
@HelpMessage("Maximum number of resolution iterations (specify a negative value for unlimited, default: 100)")
|
||||
@ExtraName("N")
|
||||
@Help("Maximum number of resolution iterations (specify a negative value for unlimited, default: 100)")
|
||||
@Short("N")
|
||||
maxIterations: Int = 100,
|
||||
@HelpMessage("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)")
|
||||
@ExtraName("r")
|
||||
@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],
|
||||
@HelpMessage("Do not add default repositories (~/.ivy2/local, and Central)")
|
||||
@Help("Do not add default repositories (~/.ivy2/local, and Central)")
|
||||
noDefault: Boolean = false,
|
||||
@HelpMessage("Modify names in Maven repository paths for SBT plugins")
|
||||
@Help("Modify names in Maven repository paths for SBT plugins")
|
||||
sbtPluginHack: Boolean = false,
|
||||
@HelpMessage("Force module version")
|
||||
@ValueDescription("organization:name:forcedVersion")
|
||||
@ExtraName("V")
|
||||
@Help("Force module version")
|
||||
@Value("organization:name:forcedVersion")
|
||||
@Short("V")
|
||||
forceVersion: List[String],
|
||||
@HelpMessage("Maximum number of parallel downloads (default: 6)")
|
||||
@ExtraName("n")
|
||||
@Help("Maximum number of parallel downloads (default: 6)")
|
||||
@Short("n")
|
||||
parallel: Int = 6,
|
||||
@Recurse
|
||||
cacheOptions: CacheOptions
|
||||
|
|
@ -48,22 +48,32 @@ case class CommonOptions(
|
|||
}
|
||||
|
||||
case class CacheOptions(
|
||||
@HelpMessage("Cache directory (defaults to environment variable COURSIER_CACHE or ~/.coursier/cache/v1)")
|
||||
@ExtraName("C")
|
||||
@Help("Cache directory (defaults to environment variable COURSIER_CACHE or ~/.coursier/cache/v1)")
|
||||
@Short("C")
|
||||
cache: String = Cache.defaultBase.toString
|
||||
)
|
||||
|
||||
sealed trait 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)
|
||||
|
||||
}
|
||||
|
||||
case class Fetch(
|
||||
@HelpMessage("Fetch source artifacts")
|
||||
@ExtraName("S")
|
||||
@Help("Fetch source artifacts")
|
||||
@Short("S")
|
||||
sources: Boolean,
|
||||
@HelpMessage("Fetch javadoc artifacts")
|
||||
@ExtraName("D")
|
||||
@Help("Fetch javadoc artifacts")
|
||||
@Short("D")
|
||||
javadoc: Boolean,
|
||||
@HelpMessage("Print java -cp compatible output")
|
||||
@ExtraName("p")
|
||||
@Help("Print java -cp compatible output")
|
||||
@Short("p")
|
||||
classpath: Boolean,
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
|
|
@ -88,12 +98,15 @@ case class Fetch(
|
|||
}
|
||||
|
||||
case class Launch(
|
||||
@ExtraName("M")
|
||||
@ExtraName("main")
|
||||
@Short("M")
|
||||
@Short("main")
|
||||
mainClass: String,
|
||||
@ExtraName("c")
|
||||
@HelpMessage("Assume coursier is a dependency of the launched app, and share the coursier dependency of the launcher with it - allows the launched app to get the resolution that launched it via ResolutionClassLoader")
|
||||
addCoursier: Boolean,
|
||||
@Value("target:dependency")
|
||||
@Short("I")
|
||||
isolated: List[String],
|
||||
@Help("Comma-separated isolation targets")
|
||||
@Short("i")
|
||||
isolateTarget: List[String],
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
) extends CoursierCommand {
|
||||
|
|
@ -107,45 +120,119 @@ case class Launch(
|
|||
}
|
||||
}
|
||||
|
||||
val extraForceVersions =
|
||||
if (addCoursier)
|
||||
???
|
||||
val isolateTargets = {
|
||||
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
|
||||
Seq.empty[String]
|
||||
valid.toArray
|
||||
}
|
||||
|
||||
val dontFilterOut =
|
||||
if (addCoursier) {
|
||||
val url = classOf[coursier.core.Resolution].getProtectionDomain.getCodeSource.getLocation
|
||||
val (validIsolated, unrecognizedIsolated) = isolated.partition(s => isolateTargets.exists(t => s.startsWith(t + ":")))
|
||||
|
||||
if (url.getProtocol == "file")
|
||||
Seq(new File(url.getPath))
|
||||
else {
|
||||
Console.err.println(s"Cannot get the location of the JAR of coursier ($url not a file URL)")
|
||||
if (unrecognizedIsolated.nonEmpty) {
|
||||
Console.err.println(s"Unrecognized isolation targets in:")
|
||||
for (i <- unrecognizedIsolated)
|
||||
Console.err.println(s" $i")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
val rawIsolated = validIsolated.map { s =>
|
||||
val Array(target, dep) = s.split(":", 2)
|
||||
target -> dep
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
} else
|
||||
Seq.empty[File]
|
||||
|
||||
t -> modVers
|
||||
}
|
||||
|
||||
val isolatedDeps = isolatedModuleVersions.map {
|
||||
case (t, l) =>
|
||||
t -> l.map {
|
||||
case (mod, ver) =>
|
||||
Dependency(mod, ver, configuration = "runtime")
|
||||
}
|
||||
}
|
||||
|
||||
val helper = new Helper(
|
||||
common.copy(forceVersion = common.forceVersion ++ extraForceVersions),
|
||||
rawDependencies
|
||||
common.copy(forceVersion = common.forceVersion),
|
||||
rawDependencies ++ rawIsolated.map { case (_, dep) => dep }
|
||||
)
|
||||
|
||||
|
||||
val files0 = helper.fetch(sources = false, javadoc = false)
|
||||
|
||||
val cl = new URLClassLoader(
|
||||
files0.map(_.toURI.toURL).toArray,
|
||||
new ClasspathFilter(
|
||||
Thread.currentThread().getContextClassLoader,
|
||||
Coursier.baseCp.map(new File(_)).toSet -- dontFilterOut,
|
||||
exclude = true
|
||||
)
|
||||
|
||||
val parentLoader0: ClassLoader = new ClasspathFilter(
|
||||
Thread.currentThread().getContextClassLoader,
|
||||
Coursier.baseCp.map(new File(_)).toSet,
|
||||
exclude = true
|
||||
)
|
||||
|
||||
val (parentLoader, filteredFiles) =
|
||||
if (isolated.isEmpty)
|
||||
(parentLoader0, files0)
|
||||
else {
|
||||
val (isolatedLoader, filteredFiles0) = isolateTargets.foldLeft((parentLoader0, files0)) {
|
||||
case ((parent, files0), target) =>
|
||||
|
||||
// FIXME These were already fetched above
|
||||
val isolatedFiles = helper.fetch(
|
||||
sources = false,
|
||||
javadoc = false,
|
||||
subset = 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(cl)
|
||||
val mainClasses = Helper.mainClasses(loader)
|
||||
|
||||
val mainClass =
|
||||
if (mainClasses.isEmpty) {
|
||||
|
|
@ -178,7 +265,7 @@ case class Launch(
|
|||
}
|
||||
|
||||
val cls =
|
||||
try cl.loadClass(mainClass0)
|
||||
try loader.loadClass(mainClass0)
|
||||
catch { case e: ClassNotFoundException =>
|
||||
Helper.errPrintln(s"Error: class $mainClass0 not found")
|
||||
sys.exit(255)
|
||||
|
|
@ -195,26 +282,26 @@ case class Launch(
|
|||
else if (common.verbose0 == 0)
|
||||
Helper.errPrintln(s"Launching")
|
||||
|
||||
Thread.currentThread().setContextClassLoader(cl)
|
||||
Thread.currentThread().setContextClassLoader(loader)
|
||||
method.invoke(null, extraArgs.toArray)
|
||||
}
|
||||
|
||||
case class Bootstrap(
|
||||
@ExtraName("M")
|
||||
@ExtraName("main")
|
||||
@Short("M")
|
||||
@Short("main")
|
||||
mainClass: String,
|
||||
@ExtraName("o")
|
||||
@Short("o")
|
||||
output: String = "bootstrap",
|
||||
@ExtraName("D")
|
||||
@Short("D")
|
||||
downloadDir: String,
|
||||
@ExtraName("f")
|
||||
@Short("f")
|
||||
force: Boolean,
|
||||
@HelpMessage(s"Internal use - prepend base classpath options to arguments (like -B jar1 -B jar2 etc.)")
|
||||
@ExtraName("b")
|
||||
@Help(s"Internal use - prepend base classpath options to arguments (like -B jar1 -B jar2 etc.)")
|
||||
@Short("b")
|
||||
prependClasspath: Boolean,
|
||||
@HelpMessage("Set environment variables in the generated launcher. No escaping is done. Value is simply put between quotes in the launcher preamble.")
|
||||
@ValueDescription("key=value")
|
||||
@ExtraName("P")
|
||||
@Help("Set environment variables in the generated launcher. No escaping is done. Value is simply put between quotes in the launcher preamble.")
|
||||
@Value("key=value")
|
||||
@Short("P")
|
||||
property: List[String],
|
||||
@Recurse
|
||||
common: CommonOptions
|
||||
|
|
@ -365,7 +452,7 @@ case class Bootstrap(
|
|||
|
||||
case class BaseCommand(
|
||||
@Hidden
|
||||
@ExtraName("B")
|
||||
@Short("B")
|
||||
baseCp: List[String]
|
||||
) extends Command {
|
||||
Coursier.baseCp = baseCp
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ import java.io.{ OutputStreamWriter, File }
|
|||
import java.util.concurrent.Executors
|
||||
|
||||
import coursier.ivy.IvyRepository
|
||||
import coursier.util.{Print, Parse}
|
||||
|
||||
import scalaz.{ \/-, -\/ }
|
||||
import scalaz.{Failure, Success, \/-, -\/}
|
||||
import scalaz.concurrent.{ Task, Strategy }
|
||||
|
||||
object Helper {
|
||||
|
|
@ -33,29 +34,45 @@ object Helper {
|
|||
}
|
||||
}
|
||||
|
||||
object Util {
|
||||
|
||||
def prematureExit(msg: String): Nothing = {
|
||||
Console.err.println(msg)
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
def prematureExitIf(cond: Boolean)(msg: => String): Unit =
|
||||
if (cond)
|
||||
prematureExit(msg)
|
||||
|
||||
def exit(msg: String): Nothing = {
|
||||
Console.err.println(msg)
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
def exitIf(cond: Boolean)(msg: => String): Unit =
|
||||
if (cond)
|
||||
exit(msg)
|
||||
|
||||
}
|
||||
|
||||
class Helper(
|
||||
common: CommonOptions,
|
||||
remainingArgs: Seq[String]
|
||||
rawDependencies: Seq[String]
|
||||
) {
|
||||
import common._
|
||||
import Helper.errPrintln
|
||||
|
||||
val cachePolicies = mode match {
|
||||
case "offline" =>
|
||||
Seq(CachePolicy.LocalOnly)
|
||||
case "update-changing" =>
|
||||
Seq(CachePolicy.UpdateChanging)
|
||||
case "update" =>
|
||||
Seq(CachePolicy.Update)
|
||||
case "missing" =>
|
||||
Seq(CachePolicy.FetchMissing)
|
||||
case "force" =>
|
||||
Seq(CachePolicy.ForceDownload)
|
||||
case "default" =>
|
||||
Seq(CachePolicy.LocalOnly, CachePolicy.FetchMissing)
|
||||
case other =>
|
||||
errPrintln(s"Unrecognized mode: $other")
|
||||
sys.exit(255)
|
||||
import Util._
|
||||
|
||||
val cachePoliciesValidation = CacheParse.cachePolicies(common.mode)
|
||||
|
||||
val cachePolicies = cachePoliciesValidation match {
|
||||
case Success(cp) => cp
|
||||
case Failure(errors) =>
|
||||
prematureExit(
|
||||
s"Error parsing modes:\n${errors.list.map(" "+_).mkString("\n")}"
|
||||
)
|
||||
}
|
||||
|
||||
val caches =
|
||||
|
|
@ -66,125 +83,50 @@ class Helper(
|
|||
|
||||
val pool = Executors.newFixedThreadPool(parallel, Strategy.DefaultDaemonThreadFactory)
|
||||
|
||||
val central = MavenRepository("https://repo1.maven.org/maven2/")
|
||||
|
||||
val defaultRepositories = Seq(
|
||||
Cache.ivy2Local,
|
||||
central
|
||||
MavenRepository("https://repo1.maven.org/maven2")
|
||||
)
|
||||
|
||||
val repositories0 = common.repository.map { repo =>
|
||||
val repo0 = repo.toLowerCase
|
||||
if (repo0 == "central")
|
||||
Right(central)
|
||||
else if (repo0 == "ivy2local")
|
||||
Right(Cache.ivy2Local)
|
||||
else if (repo0.startsWith("sonatype:"))
|
||||
Right(
|
||||
MavenRepository(s"https://oss.sonatype.org/content/repositories/${repo.drop("sonatype:".length)}")
|
||||
)
|
||||
else {
|
||||
val (url, r) =
|
||||
if (repo.startsWith("ivy:")) {
|
||||
val url = repo.drop("ivy:".length)
|
||||
(url, IvyRepository(url))
|
||||
} else
|
||||
(repo, MavenRepository(repo))
|
||||
|
||||
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file:/"))
|
||||
Right(r)
|
||||
else
|
||||
Left(repo -> s"Unrecognized protocol or repository: $url")
|
||||
}
|
||||
}
|
||||
|
||||
val unrecognizedRepos = repositories0.collect { case Left(e) => e }
|
||||
if (unrecognizedRepos.nonEmpty) {
|
||||
errPrintln(s"${unrecognizedRepos.length} error(s) parsing repositories:")
|
||||
for ((repo, err) <- unrecognizedRepos)
|
||||
errPrintln(s"$repo: $err")
|
||||
sys.exit(255)
|
||||
}
|
||||
|
||||
val repositories1 =
|
||||
(if (common.noDefault) Nil else defaultRepositories) ++
|
||||
repositories0.collect { case Right(r) => r }
|
||||
|
||||
val repositories =
|
||||
val repositoriesValidation = CacheParse.repositories(common.repository).map { repos0 =>
|
||||
val repos = (if (common.noDefault) Nil else defaultRepositories) ++ repos0
|
||||
if (common.sbtPluginHack)
|
||||
repositories1.map {
|
||||
repos.map {
|
||||
case m: MavenRepository => m.copy(sbtAttrStub = true)
|
||||
case other => other
|
||||
}
|
||||
else
|
||||
repositories1
|
||||
|
||||
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)
|
||||
}
|
||||
repos
|
||||
}
|
||||
|
||||
val (splitDependencies, malformed) = rawDependencies.toList
|
||||
.map(_.split(":", 3).toSeq)
|
||||
.partition(_.length == 3)
|
||||
|
||||
val (splitForceVersions, malformedForceVersions) = forceVersion
|
||||
.map(_.split(":", 3).toSeq)
|
||||
.partition(_.length == 3)
|
||||
|
||||
if (splitDependencies.isEmpty) {
|
||||
Console.err.println(s"Error: no dependencies specified.")
|
||||
// CaseApp.printUsage[Coursier]()
|
||||
sys exit 1
|
||||
val repositories = repositoriesValidation match {
|
||||
case Success(repos) => repos
|
||||
case Failure(errors) =>
|
||||
prematureExit(
|
||||
s"Error parsing repositories:\n${errors.list.map(" "+_).mkString("\n")}"
|
||||
)
|
||||
}
|
||||
|
||||
if (malformed.nonEmpty || malformedForceVersions.nonEmpty) {
|
||||
if (malformed.nonEmpty) {
|
||||
errPrintln("Malformed dependency(ies), should be like org:name:version")
|
||||
for (s <- malformed)
|
||||
errPrintln(s" ${s.mkString(":")}")
|
||||
}
|
||||
|
||||
if (malformedForceVersions.nonEmpty) {
|
||||
errPrintln("Malformed force version(s), should be like org:name:forcedVersion")
|
||||
for (s <- malformedForceVersions)
|
||||
errPrintln(s" ${s.mkString(":")}")
|
||||
}
|
||||
val (modVerErrors, moduleVersions) = Parse.moduleVersions(rawDependencies)
|
||||
|
||||
sys.exit(1)
|
||||
prematureExitIf(modVerErrors.nonEmpty) {
|
||||
s"Cannot parse dependencies:\n" + modVerErrors.map(" "+_).mkString("\n")
|
||||
}
|
||||
|
||||
val moduleVersions = splitDependencies.map{
|
||||
case Seq(org, namePart, version) =>
|
||||
val p = namePart.split(';')
|
||||
val name = p.head
|
||||
val splitAttributes = p.tail.map(_.split("=", 2).toSeq).toSeq
|
||||
val malformedAttributes = splitAttributes.filter(_.length != 2)
|
||||
if (malformedAttributes.nonEmpty) {
|
||||
// FIXME Get these for all dependencies at once
|
||||
Console.err.println(s"Malformed attributes in ${splitDependencies.mkString(":")}")
|
||||
// :(
|
||||
sys.exit(255)
|
||||
}
|
||||
val attributes = splitAttributes.collect {
|
||||
case Seq(k, v) => k -> v
|
||||
}
|
||||
(Module(org, name, attributes.toMap), version)
|
||||
val dependencies = moduleVersions.map {
|
||||
case (module, version) =>
|
||||
Dependency(module, version, configuration = "default(compile)")
|
||||
}
|
||||
|
||||
val deps = moduleVersions.map{case (mod, ver) =>
|
||||
Dependency(mod, ver, configuration = "runtime")
|
||||
|
||||
val (forceVersionErrors, forceVersions0) = Parse.moduleVersions(forceVersion)
|
||||
|
||||
prematureExitIf(forceVersionErrors.nonEmpty) {
|
||||
s"Cannot parse forced versions:\n" + forceVersionErrors.map(" "+_).mkString("\n")
|
||||
}
|
||||
|
||||
val forceVersions = {
|
||||
val forceVersions0 = splitForceVersions.map {
|
||||
case Seq(org, name, version) => (Module(org, name), version)
|
||||
}
|
||||
|
||||
val grouped = forceVersions0
|
||||
.groupBy { case (mod, _) => mod }
|
||||
.map { case (mod, l) => mod -> l.map { case (_, version) => version } }
|
||||
|
|
@ -196,7 +138,7 @@ class Helper(
|
|||
}
|
||||
|
||||
val startRes = Resolution(
|
||||
deps.toSet,
|
||||
dependencies.toSet,
|
||||
forceVersions = forceVersions,
|
||||
filter = Some(dep => keepOptional || !dep.optional)
|
||||
)
|
||||
|
|
@ -219,17 +161,21 @@ class Helper(
|
|||
if (verbose0 <= 0) fetchQuiet
|
||||
else {
|
||||
modVers: Seq[(Module, String)] =>
|
||||
val print = Task{
|
||||
val print = Task {
|
||||
errPrintln(s"Getting ${modVers.length} project definition(s)")
|
||||
}
|
||||
|
||||
print.flatMap(_ => fetchQuiet(modVers))
|
||||
}
|
||||
|
||||
def indent(s: String): String =
|
||||
if (s.isEmpty)
|
||||
s
|
||||
else
|
||||
s.split('\n').map(" "+_).mkString("\n")
|
||||
|
||||
if (verbose0 >= 0) {
|
||||
errPrintln("Dependencies:")
|
||||
for ((mod, ver) <- moduleVersions)
|
||||
errPrintln(s" $mod:$ver")
|
||||
errPrintln(s"Dependencies:\n${indent(Print.dependenciesUnknownConfigs(dependencies))}")
|
||||
|
||||
if (forceVersions.nonEmpty) {
|
||||
errPrintln("Force versions:")
|
||||
|
|
@ -247,66 +193,36 @@ class Helper(
|
|||
|
||||
logger.foreach(_.stop())
|
||||
|
||||
if (!res.isDone) {
|
||||
// FIXME Better to print all the messages related to the exit conditions below, then exit
|
||||
// rather than exit at the first one
|
||||
|
||||
exitIf(!res.isDone) {
|
||||
errPrintln(s"Maximum number of iteration reached!")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
def repr(dep: Dependency) = {
|
||||
// dep.version can be an interval, whereas the one from project can't
|
||||
val version = res
|
||||
.projectCache
|
||||
.get(dep.moduleVersion)
|
||||
.map(_._2.version)
|
||||
.getOrElse(dep.version)
|
||||
val extra =
|
||||
if (version == dep.version) ""
|
||||
else s" ($version for ${dep.version})"
|
||||
|
||||
(
|
||||
Seq(
|
||||
dep.module.organization,
|
||||
dep.module.name,
|
||||
dep.attributes.`type`
|
||||
) ++
|
||||
Some(dep.attributes.classifier)
|
||||
.filter(_.nonEmpty)
|
||||
.toSeq ++
|
||||
Seq(
|
||||
version
|
||||
)
|
||||
).mkString(":") + extra
|
||||
exitIf(res.errors.nonEmpty) {
|
||||
s"\n${res.errors.size} error(s):\n" +
|
||||
res.errors.map { case (dep, errs) =>
|
||||
s" ${dep.module}:${dep.version}:\n${errs.map(" " + _.replace("\n", " \n")).mkString("\n")}"
|
||||
}.mkString("\n")
|
||||
}
|
||||
|
||||
val trDeps = res
|
||||
.minDependencies
|
||||
.toList
|
||||
.sortBy(repr)
|
||||
|
||||
if (verbose0 >= 1) {
|
||||
println("")
|
||||
println(
|
||||
trDeps
|
||||
.map(repr)
|
||||
.distinct
|
||||
.mkString("\n")
|
||||
)
|
||||
exitIf(res.conflicts.nonEmpty) {
|
||||
s"${res.conflicts.size} conflict(s):\n${Print.dependenciesUnknownConfigs(res.conflicts.toVector)}"
|
||||
}
|
||||
|
||||
if (res.conflicts.nonEmpty) {
|
||||
// Needs test
|
||||
println(s"${res.conflicts.size} conflict(s):\n ${res.conflicts.toList.map(repr).sorted.mkString(" \n")}")
|
||||
}
|
||||
val trDeps = res.minDependencies.toVector
|
||||
|
||||
val errors = res.errors
|
||||
if (errors.nonEmpty) {
|
||||
println(s"\n${errors.size} error(s):")
|
||||
for ((dep, errs) <- errors) {
|
||||
println(s" ${dep.module}:${dep.version}:\n${errs.map(" " + _.replace("\n", " \n")).mkString("\n")}")
|
||||
}
|
||||
}
|
||||
if (verbose0 >= 0)
|
||||
errPrintln(s"Result:\n${indent(Print.dependenciesUnknownConfigs(trDeps))}")
|
||||
|
||||
def fetch(
|
||||
sources: Boolean,
|
||||
javadoc: Boolean,
|
||||
subset: Set[Dependency] = null
|
||||
): Seq[File] = {
|
||||
|
||||
def fetch(sources: Boolean, javadoc: Boolean): Seq[File] = {
|
||||
if (verbose0 >= 0) {
|
||||
val msg = cachePolicies match {
|
||||
case Seq(CachePolicy.LocalOnly) =>
|
||||
|
|
@ -317,6 +233,9 @@ class Helper(
|
|||
|
||||
errPrintln(msg)
|
||||
}
|
||||
|
||||
val res0 = Option(subset).fold(res)(res.subset)
|
||||
|
||||
val artifacts =
|
||||
if (sources || javadoc) {
|
||||
var classifiers = Seq.empty[String]
|
||||
|
|
@ -325,9 +244,9 @@ class Helper(
|
|||
if (javadoc)
|
||||
classifiers = classifiers :+ "javadoc"
|
||||
|
||||
res.classifiersArtifacts(classifiers)
|
||||
res0.classifiersArtifacts(classifiers)
|
||||
} else
|
||||
res.artifacts
|
||||
res0.artifacts
|
||||
|
||||
val logger =
|
||||
if (verbose0 >= 0)
|
||||
|
|
@ -354,11 +273,11 @@ class Helper(
|
|||
|
||||
logger.foreach(_.stop())
|
||||
|
||||
if (errors.nonEmpty) {
|
||||
println(s"${errors.size} error(s):")
|
||||
for ((artifact, error) <- errors) {
|
||||
println(s" ${artifact.url}: $error")
|
||||
}
|
||||
exitIf(errors.nonEmpty) {
|
||||
s"${errors.size} error(s):\n" +
|
||||
errors.map { case (artifact, error) =>
|
||||
s" ${artifact.url}: $error"
|
||||
}.mkString("\n")
|
||||
}
|
||||
|
||||
files0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
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,49 @@
|
|||
package coursier.util
|
||||
|
||||
import coursier.core.{ Dependency, Resolution }
|
||||
|
||||
object Config {
|
||||
|
||||
// loose attempt at minimizing a set of dependencies from various configs
|
||||
// `configs` is assumed to be fully unfold
|
||||
def allDependenciesByConfig(
|
||||
res: Resolution,
|
||||
depsByConfig: Map[String, Set[Dependency]],
|
||||
configs: Map[String, Set[String]]
|
||||
): Map[String, Set[Dependency]] = {
|
||||
|
||||
val allDepsByConfig = depsByConfig.map {
|
||||
case (config, deps) =>
|
||||
config -> res.subset(deps).minDependencies
|
||||
}
|
||||
|
||||
val filteredAllDepsByConfig = depsByConfig.map {
|
||||
case (config, allDeps) =>
|
||||
val inherited = configs
|
||||
.getOrElse(config, Set.empty)
|
||||
.flatMap(allDepsByConfig.getOrElse(_, Set.empty))
|
||||
|
||||
config -> (allDeps -- inherited)
|
||||
}
|
||||
|
||||
filteredAllDepsByConfig
|
||||
}
|
||||
|
||||
def dependenciesWithConfig(
|
||||
res: Resolution,
|
||||
depsByConfig: Map[String, Set[Dependency]],
|
||||
configs: Map[String, Set[String]]
|
||||
): Set[Dependency] =
|
||||
allDependenciesByConfig(res, depsByConfig, configs)
|
||||
.flatMap {
|
||||
case (config, deps) =>
|
||||
deps.map(dep => dep.copy(configuration = s"$config->${dep.configuration}"))
|
||||
}
|
||||
.groupBy(_.copy(configuration = ""))
|
||||
.map {
|
||||
case (dep, l) =>
|
||||
dep.copy(configuration = l.map(_.configuration).mkString(","))
|
||||
}
|
||||
.toSet
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package coursier.util
|
||||
|
||||
import coursier.core.{Repository, Module}
|
||||
import coursier.ivy.IvyRepository
|
||||
import coursier.maven.MavenRepository
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
object Parse {
|
||||
|
||||
/**
|
||||
* Parses coordinates like
|
||||
* org:name:version
|
||||
* possibly with attributes, like
|
||||
* org:name;attr1=val1;attr2=val2:version
|
||||
*/
|
||||
def moduleVersion(s: String): Either[String, (Module, String)] = {
|
||||
|
||||
val parts = s.split(":", 3)
|
||||
|
||||
parts match {
|
||||
case Array(org, rawName, version) =>
|
||||
val splitName = rawName.split(';')
|
||||
|
||||
if (splitName.tail.exists(!_.contains("=")))
|
||||
Left(s"Malformed attribute in $s")
|
||||
else {
|
||||
val name = splitName.head
|
||||
val attributes = splitName.tail.map(_.split("=", 2)).map {
|
||||
case Array(key, value) => key -> value
|
||||
}.toMap
|
||||
|
||||
Right((Module(org, name, attributes), version))
|
||||
}
|
||||
|
||||
case _ =>
|
||||
Left(s"Malformed coordinates: $s")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a sequence of coordinates.
|
||||
*
|
||||
* @return Sequence of errors, and sequence of modules/versions
|
||||
*/
|
||||
def moduleVersions(l: Seq[String]): (Seq[String], Seq[(Module, String)]) = {
|
||||
|
||||
val errors = new ArrayBuffer[String]
|
||||
val moduleVersions = new ArrayBuffer[(Module, String)]
|
||||
|
||||
for (elem <- l)
|
||||
moduleVersion(elem) match {
|
||||
case Left(err) => errors += err
|
||||
case Right(modVer) => moduleVersions += modVer
|
||||
}
|
||||
|
||||
(errors.toSeq, moduleVersions.toSeq)
|
||||
}
|
||||
|
||||
def repository(s: String): Repository =
|
||||
if (s == "central")
|
||||
MavenRepository("https://repo1.maven.org/maven2")
|
||||
else if (s.startsWith("sonatype:"))
|
||||
MavenRepository(s"https://oss.sonatype.org/content/repositories/${s.stripPrefix("sonatype:")}")
|
||||
else if (s.startsWith("ivy:"))
|
||||
IvyRepository(s.stripPrefix("ivy:"))
|
||||
else
|
||||
MavenRepository(s)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package coursier.util
|
||||
|
||||
import coursier.core.{ Orders, Dependency }
|
||||
|
||||
object Print {
|
||||
|
||||
def dependency(dep: Dependency): String =
|
||||
s"${dep.module}:${dep.version}:${dep.configuration}"
|
||||
|
||||
def dependenciesUnknownConfigs(deps: Seq[Dependency]): String = {
|
||||
|
||||
val minDeps = Orders.minDependencies(
|
||||
deps.toSet,
|
||||
_ => Map.empty
|
||||
)
|
||||
|
||||
val deps0 = minDeps
|
||||
.groupBy(_.copy(configuration = ""))
|
||||
.toVector
|
||||
.map { case (k, l) =>
|
||||
k.copy(configuration = l.toVector.map(_.configuration).sorted.mkString(","))
|
||||
}
|
||||
.sortBy { dep =>
|
||||
(dep.module.organization, dep.module.name, dep.module.toString, dep.version)
|
||||
}
|
||||
|
||||
deps0.map(dependency).mkString("\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import coursier.core.Publication
|
|||
import coursier.ivy.IvyRepository
|
||||
import coursier.Keys._
|
||||
import coursier.Structure._
|
||||
import coursier.util.{ Config, Print }
|
||||
import org.apache.ivy.core.module.id.ModuleRevisionId
|
||||
|
||||
import sbt.{ UpdateReport, Classpaths, Resolver, Def }
|
||||
|
|
@ -204,7 +205,7 @@ object Tasks {
|
|||
}
|
||||
|
||||
def report = {
|
||||
if (verbosity >= 1) {
|
||||
if (verbosity >= 2) {
|
||||
println("InterProjectRepository")
|
||||
for (p <- projects)
|
||||
println(s" ${p.module}:${p.version}")
|
||||
|
|
@ -257,14 +258,19 @@ object Tasks {
|
|||
}.sorted.distinct
|
||||
|
||||
if (verbosity >= 1) {
|
||||
errPrintln(s"Repositories:")
|
||||
val repositories0 = repositories.map {
|
||||
case r: IvyRepository => r.copy(properties = Map.empty)
|
||||
case r: InterProjectRepository => r.copy(projects = Nil)
|
||||
case r => r
|
||||
val repoReprs = repositories.map {
|
||||
case r: IvyRepository =>
|
||||
s"ivy:${r.pattern}"
|
||||
case r: InterProjectRepository =>
|
||||
"inter-project"
|
||||
case r: MavenRepository =>
|
||||
r.root
|
||||
case r =>
|
||||
// should not happen
|
||||
r.toString
|
||||
}
|
||||
for (repo <- repositories0)
|
||||
errPrintln(s" $repo")
|
||||
|
||||
errPrintln(s"Repositories:\n${repoReprs.map(" "+_).mkString("\n")}")
|
||||
}
|
||||
|
||||
if (verbosity >= 0)
|
||||
|
|
@ -286,53 +292,58 @@ object Tasks {
|
|||
|
||||
|
||||
if (!res.isDone)
|
||||
throw new Exception(s"Maximum number of iteration reached!")
|
||||
throw new Exception(s"Maximum number of iteration of dependency resolution reached")
|
||||
|
||||
if (res.conflicts.nonEmpty) {
|
||||
println(s"${res.conflicts.size} conflict(s):\n ${Print.dependenciesUnknownConfigs(res.conflicts.toVector)}")
|
||||
throw new Exception(s"Conflict(s) in dependency resolution")
|
||||
}
|
||||
|
||||
if (res.errors.nonEmpty) {
|
||||
println(s"\n${res.errors.size} error(s):")
|
||||
for ((dep, errs) <- res.errors) {
|
||||
println(s" ${dep.module}:${dep.version}:\n${errs.map(" " + _.replace("\n", " \n")).mkString("\n")}")
|
||||
}
|
||||
throw new Exception(s"Encountered ${res.errors.length} error(s) in dependency resolution")
|
||||
}
|
||||
|
||||
val depsByConfig = grouped(currentProject.dependencies)
|
||||
|
||||
val configs = {
|
||||
val configs0 = ivyConfigurations.value.map { config =>
|
||||
config.name -> config.extendsConfigs.map(_.name)
|
||||
}.toMap
|
||||
|
||||
def allExtends(c: String) = {
|
||||
// possibly bad complexity
|
||||
def helper(current: Set[String]): Set[String] = {
|
||||
val newSet = current ++ current.flatMap(configs0.getOrElse(_, Nil))
|
||||
if ((newSet -- current).nonEmpty)
|
||||
helper(newSet)
|
||||
else
|
||||
newSet
|
||||
}
|
||||
|
||||
helper(Set(c))
|
||||
}
|
||||
|
||||
configs0.map {
|
||||
case (config, _) =>
|
||||
config -> allExtends(config)
|
||||
}
|
||||
}
|
||||
|
||||
if (verbosity >= 0)
|
||||
errPrintln("Resolution done")
|
||||
if (verbosity >= 1)
|
||||
for (depRepr <- depsRepr0(res.minDependencies.toSeq))
|
||||
errPrintln(s" $depRepr")
|
||||
if (verbosity >= 1) {
|
||||
val finalDeps = Config.dependenciesWithConfig(
|
||||
res,
|
||||
depsByConfig.map { case (k, l) => k -> l.toSet },
|
||||
configs
|
||||
)
|
||||
|
||||
def repr(dep: Dependency) = {
|
||||
// dep.version can be an interval, whereas the one from project can't
|
||||
val version = res
|
||||
.projectCache
|
||||
.get(dep.moduleVersion)
|
||||
.map(_._2.version)
|
||||
.getOrElse(dep.version)
|
||||
val extra =
|
||||
if (version == dep.version) ""
|
||||
else s" ($version for ${dep.version})"
|
||||
|
||||
(
|
||||
Seq(
|
||||
dep.module.organization,
|
||||
dep.module.name,
|
||||
dep.attributes.`type`
|
||||
) ++
|
||||
Some(dep.attributes.classifier)
|
||||
.filter(_.nonEmpty)
|
||||
.toSeq ++
|
||||
Seq(
|
||||
version
|
||||
)
|
||||
).mkString(":") + extra
|
||||
}
|
||||
|
||||
if (res.conflicts.nonEmpty) {
|
||||
// Needs test
|
||||
println(s"${res.conflicts.size} conflict(s):\n ${res.conflicts.toList.map(repr).sorted.mkString(" \n")}")
|
||||
}
|
||||
|
||||
val errors = res.errors
|
||||
|
||||
if (errors.nonEmpty) {
|
||||
println(s"\n${errors.size} error(s):")
|
||||
for ((dep, errs) <- errors) {
|
||||
println(s" ${dep.module}:${dep.version}:\n${errs.map(" " + _.replace("\n", " \n")).mkString("\n")}")
|
||||
}
|
||||
throw new Exception(s"Encountered ${errors.length} error(s)")
|
||||
val repr = Print.dependenciesUnknownConfigs(finalDeps.toVector)
|
||||
repr.split('\n').map(" "+_).mkString("\n")
|
||||
}
|
||||
|
||||
val classifiers =
|
||||
|
|
@ -375,30 +386,6 @@ object Tasks {
|
|||
if (verbosity >= 0)
|
||||
errPrintln(s"Fetching artifacts: done")
|
||||
|
||||
val configs = {
|
||||
val configs0 = ivyConfigurations.value.map { config =>
|
||||
config.name -> config.extendsConfigs.map(_.name)
|
||||
}.toMap
|
||||
|
||||
def allExtends(c: String) = {
|
||||
// possibly bad complexity
|
||||
def helper(current: Set[String]): Set[String] = {
|
||||
val newSet = current ++ current.flatMap(configs0.getOrElse(_, Nil))
|
||||
if ((newSet -- current).nonEmpty)
|
||||
helper(newSet)
|
||||
else
|
||||
newSet
|
||||
}
|
||||
|
||||
helper(Set(c))
|
||||
}
|
||||
|
||||
configs0.map {
|
||||
case (config, _) =>
|
||||
config -> allExtends(config)
|
||||
}
|
||||
}
|
||||
|
||||
def artifactFileOpt(artifact: Artifact) = {
|
||||
val fileOrError = artifactFilesOrErrors.getOrElse(artifact, -\/("Not downloaded"))
|
||||
|
||||
|
|
@ -413,8 +400,6 @@ object Tasks {
|
|||
}
|
||||
}
|
||||
|
||||
val depsByConfig = grouped(currentProject.dependencies)
|
||||
|
||||
writeIvyFiles()
|
||||
|
||||
ToSbt.updateReport(
|
||||
|
|
|
|||
Loading…
Reference in New Issue