From 4be4f761a6415fa3eb449f3a55868627bc4f3ee6 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sun, 13 Mar 2016 22:57:25 +0100 Subject: [PATCH] Keep moving things around in cli module In particular, put each command in a separate file... --- .../cli/{Commands.scala => Bootstrap.scala} | 223 +----------------- .../scala-2.11/coursier/cli/Coursier.scala | 31 ++- .../main/scala-2.11/coursier/cli/Fetch.scala | 31 +++ .../coursier/cli/IsolatedClassLoader.scala | 19 -- .../main/scala-2.11/coursier/cli/Launch.scala | 196 +++++++++++++++ .../scala-2.11/coursier/cli/Resolve.scala | 14 ++ 6 files changed, 277 insertions(+), 237 deletions(-) rename cli/src/main/scala-2.11/coursier/cli/{Commands.scala => Bootstrap.scala} (51%) create mode 100644 cli/src/main/scala-2.11/coursier/cli/Fetch.scala delete mode 100644 cli/src/main/scala-2.11/coursier/cli/IsolatedClassLoader.scala create mode 100644 cli/src/main/scala-2.11/coursier/cli/Launch.scala create mode 100644 cli/src/main/scala-2.11/coursier/cli/Resolve.scala diff --git a/cli/src/main/scala-2.11/coursier/cli/Commands.scala b/cli/src/main/scala-2.11/coursier/cli/Bootstrap.scala similarity index 51% rename from cli/src/main/scala-2.11/coursier/cli/Commands.scala rename to cli/src/main/scala-2.11/coursier/cli/Bootstrap.scala index 25ec6c882..c74da9917 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Commands.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Bootstrap.scala @@ -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 ) diff --git a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala index b9801fba1..a2be793d4 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala @@ -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 diff --git a/cli/src/main/scala-2.11/coursier/cli/Fetch.scala b/cli/src/main/scala-2.11/coursier/cli/Fetch.scala new file mode 100644 index 000000000..3677313be --- /dev/null +++ b/cli/src/main/scala-2.11/coursier/cli/Fetch.scala @@ -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) + +} diff --git a/cli/src/main/scala-2.11/coursier/cli/IsolatedClassLoader.scala b/cli/src/main/scala-2.11/coursier/cli/IsolatedClassLoader.scala deleted file mode 100644 index c07184672..000000000 --- a/cli/src/main/scala-2.11/coursier/cli/IsolatedClassLoader.scala +++ /dev/null @@ -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 - -} diff --git a/cli/src/main/scala-2.11/coursier/cli/Launch.scala b/cli/src/main/scala-2.11/coursier/cli/Launch.scala new file mode 100644 index 000000000..dbd4844e0 --- /dev/null +++ b/cli/src/main/scala-2.11/coursier/cli/Launch.scala @@ -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) +} \ No newline at end of file diff --git a/cli/src/main/scala-2.11/coursier/cli/Resolve.scala b/cli/src/main/scala-2.11/coursier/cli/Resolve.scala new file mode 100644 index 000000000..fd1a78e86 --- /dev/null +++ b/cli/src/main/scala-2.11/coursier/cli/Resolve.scala @@ -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) + +}