Keep moving things around in cli module

In particular, put each command in a separate file...
This commit is contained in:
Alexandre Archambault 2016-03-13 22:57:25 +01:00
parent d4b2549c13
commit 4be4f761a6
6 changed files with 277 additions and 237 deletions

View File

@ -2,228 +2,17 @@ package coursier
package cli
import java.io.{ FileInputStream, ByteArrayOutputStream, File, IOException }
import java.net.URLClassLoader
import java.nio.file.{ Files => NIOFiles }
import java.nio.file.Files
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 scala.annotation.tailrec
import scala.language.reflectiveCalls
import scala.util.Try
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)
}
case class Fetch(
@Recurse
options: FetchOptions
) extends CoursierCommand {
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)
}
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(
@Recurse
options: LaunchOptions
) 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(
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.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 (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.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 (options.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 (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.verbose0 >= 1)
Helper.errPrintln(s"Launching $mainClass0 ${extraArgs.mkString(" ")}")
else if (options.common.verbose0 == 0)
Helper.errPrintln(s"Launching")
Thread.currentThread().setContextClassLoader(loader)
method.invoke(null, extraArgs.toArray)
}
import caseapp._
case class Bootstrap(
@Recurse
options: BootstrapOptions
) extends CoursierCommand {
) extends App {
import scala.collection.JavaConverters._
@ -402,7 +191,7 @@ case class Bootstrap(
"exec java -jar " + options.javaOpt.map(s => "'" + s.replace("'", "\\'") + "'").mkString(" ") + " \"$0\" \"$@\""
).mkString("", "\n", "\n")
try NIOFiles.write(output0.toPath, shellPreamble.getBytes("UTF-8") ++ buffer.toByteArray)
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)
@ -410,7 +199,7 @@ case class Bootstrap(
try {
val perms = NIOFiles.getPosixFilePermissions(output0.toPath).asScala.toSet
val perms = Files.getPosixFilePermissions(output0.toPath).asScala.toSet
var newPerms = perms
if (perms(PosixFilePermission.OWNER_READ))
@ -421,7 +210,7 @@ case class Bootstrap(
newPerms += PosixFilePermission.OTHERS_EXECUTE
if (newPerms != perms)
NIOFiles.setPosixFilePermissions(
Files.setPosixFilePermissions(
output0.toPath,
newPerms.asJava
)

View File

@ -2,8 +2,37 @@ package coursier
package cli
import caseapp._
import caseapp.core.{ ArgsApp, CommandsMessages }
object Coursier extends CommandAppOf[CoursierCommand] {
import shapeless.union.Union
// Temporary, see comment in Coursier below
case class CoursierCommandHelper(
command: CoursierCommandHelper.U
) extends ArgsApp {
def setRemainingArgs(remainingArgs: Seq[String]): Unit =
command.unify.setRemainingArgs(remainingArgs)
def remainingArgs: Seq[String] =
command.unify.remainingArgs
def apply(): Unit =
command.unify.apply()
}
object CoursierCommandHelper {
type U = Union.`'bootstrap -> Bootstrap, 'fetch -> Fetch, 'launch -> Launch, 'resolve -> Resolve`.T
implicit val commandParser: CommandParser[CoursierCommandHelper] =
CommandParser[U].map(CoursierCommandHelper(_))
implicit val commandsMessages: CommandsMessages[CoursierCommandHelper] =
CommandsMessages(CommandsMessages[U].messages)
}
object Coursier extends CommandAppOf[
// Temporary using CoursierCommandHelper instead of the union type, until case-app
// supports the latter directly.
// Union.`'bootstrap -> Bootstrap, 'fetch -> Fetch, 'launch -> Launch, 'resolve -> Resolve`.T
CoursierCommandHelper
] {
override def appName = "Coursier"
override def progName = "coursier"
override def appVersion = coursier.util.Properties.version

View File

@ -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)
}

View File

@ -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
}

View File

@ -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.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 (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.verbose0 >= 1) {
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.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 (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.verbose0 >= 1)
Helper.errPrintln(s"Launching $mainClass0 ${extraArgs.mkString(" ")}")
else if (options.common.verbose0 == 0)
Helper.errPrintln(s"Launching")
Thread.currentThread().setContextClassLoader(loader)
method.invoke(null, extraArgs.toArray)
}

View File

@ -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)
}