Move things around in cli module

This commit is contained in:
Alexandre Archambault 2016-03-13 22:57:25 +01:00
parent ace927da5b
commit d4b2549c13
3 changed files with 622 additions and 597 deletions

View File

@ -0,0 +1,436 @@
package coursier
package cli
import java.io.{ FileInputStream, ByteArrayOutputStream, File, IOException }
import java.net.URLClassLoader
import java.nio.file.{ Files => NIOFiles }
import java.nio.file.attribute.PosixFilePermission
import java.util.Properties
import java.util.zip.{ ZipEntry, ZipOutputStream, ZipInputStream }
import caseapp.{ 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)
}
case class Bootstrap(
@Recurse
options: BootstrapOptions
) extends CoursierCommand {
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 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)
}
}

View File

@ -1,603 +1,7 @@
package coursier
package cli
import java.io.{ FileInputStream, ByteArrayOutputStream, File, IOException }
import java.net.URLClassLoader
import java.nio.file.{ Files => NIOFiles }
import java.nio.file.attribute.PosixFilePermission
import java.util.Properties
import java.util.zip.{ ZipEntry, ZipOutputStream, ZipInputStream }
import caseapp.{ HelpMessage => Help, ValueDescription => Value, ExtraName => Short, _ }
import coursier.util.Parse
import scala.annotation.tailrec
import scala.language.reflectiveCalls
import scala.util.Try
case class CommonOptions(
@Help("Keep optional dependencies (Maven)")
keepOptional: Boolean,
@Help("Download mode (default: missing, that is fetch things missing from cache)")
@Value("offline|update-changing|update|missing|force")
@Short("m")
mode: String = "default",
@Help("Quiet output")
@Short("q")
quiet: Boolean,
@Help("Increase verbosity (specify several times to increase more)")
@Short("v")
verbose: List[Unit],
@Help("Maximum number of resolution iterations (specify a negative value for unlimited, default: 100)")
@Short("N")
maxIterations: Int = 100,
@Help("Repositories - for multiple repositories, separate with comma and/or repeat this option (e.g. -r central,ivy2local -r sonatype-snapshots, or equivalently -r central,ivy2local,sonatype-snapshots)")
@Short("r")
repository: List[String],
@Help("Do not add default repositories (~/.ivy2/local, and Central)")
noDefault: Boolean = false,
@Help("Modify names in Maven repository paths for SBT plugins")
sbtPluginHack: Boolean = false,
@Help("Drop module attributes starting with 'info.' - these are sometimes used by projects built with SBT")
dropInfoAttr: Boolean = false,
@Help("Force module version")
@Value("organization:name:forcedVersion")
@Short("V")
forceVersion: List[String],
@Help("Exclude module")
@Value("organization:name")
@Short("E")
exclude: List[String],
@Help("Consider provided dependencies to be intransitive. Applies to all the provided dependencies.")
intransitive: Boolean,
@Help("Classifiers that should be fetched")
@Value("classifier1,classifier2,...")
@Short("C")
classifier: List[String],
@Help("Default configuration (default(compile) by default)")
@Value("configuration")
@Short("c")
defaultConfiguration: String = "default(compile)",
@Help("Maximum number of parallel downloads (default: 6)")
@Short("n")
parallel: Int = 6,
@Help("Checksums")
@Value("checksum1,checksum2,... - end with none to allow for no checksum validation if none are available")
checksum: List[String],
@Recurse
cacheOptions: CacheOptions
) {
val verbose0 = verbose.length - (if (quiet) 1 else 0)
lazy val classifier0 = classifier.flatMap(_.split(',')).filter(_.nonEmpty)
}
case class CacheOptions(
@Help("Cache directory (defaults to environment variable COURSIER_CACHE or ~/.coursier/cache/v1)")
@Short("C")
cache: String = Cache.default.toString
)
sealed abstract class CoursierCommand extends Command
case class Resolve(
@Recurse
common: CommonOptions
) extends CoursierCommand {
// the `val helper = ` part is needed because of DelayedInit it seems
val helper = new Helper(common, remainingArgs, printResultStdout = true)
}
case class Fetch(
@Help("Fetch source artifacts")
@Short("S")
sources: Boolean,
@Help("Fetch javadoc artifacts")
@Short("D")
javadoc: Boolean,
@Help("Print java -cp compatible output")
@Short("p")
classpath: Boolean,
@Help("Fetch artifacts even if the resolution is errored")
force: Boolean,
@Recurse
common: CommonOptions
) extends CoursierCommand {
val helper = new Helper(common, remainingArgs, ignoreErrors = force)
val files0 = helper.fetch(sources = sources, javadoc = javadoc)
val out =
if (classpath)
files0
.map(_.toString)
.mkString(File.pathSeparator)
else
files0
.map(_.toString)
.mkString("\n")
println(out)
}
case class IsolatedLoaderOptions(
@Value("target:dependency")
@Short("I")
isolated: List[String],
@Help("Comma-separated isolation targets")
@Short("i")
isolateTarget: List[String]
) {
def anyIsolatedDep = isolateTarget.nonEmpty || isolated.nonEmpty
lazy val targets = {
val l = isolateTarget.flatMap(_.split(',')).filter(_.nonEmpty)
val (invalid, valid) = l.partition(_.contains(":"))
if (invalid.nonEmpty) {
Console.err.println(s"Invalid target IDs:")
for (t <- invalid)
Console.err.println(s" $t")
sys.exit(255)
}
if (valid.isEmpty)
Array("default")
else
valid.toArray
}
lazy val (validIsolated, unrecognizedIsolated) = isolated.partition(s => targets.exists(t => s.startsWith(t + ":")))
def check() = {
if (unrecognizedIsolated.nonEmpty) {
Console.err.println(s"Unrecognized isolation targets in:")
for (i <- unrecognizedIsolated)
Console.err.println(s" $i")
sys.exit(255)
}
}
lazy val rawIsolated = validIsolated.map { s =>
val Array(target, dep) = s.split(":", 2)
target -> dep
}
lazy val isolatedModuleVersions = rawIsolated.groupBy { case (t, _) => t }.map {
case (t, l) =>
val (errors, modVers) = Parse.moduleVersions(l.map { case (_, d) => d })
if (errors.nonEmpty) {
errors.foreach(Console.err.println)
sys.exit(255)
}
t -> modVers
}
lazy val isolatedDeps = isolatedModuleVersions.map {
case (t, l) =>
t -> l.map {
case (mod, ver) =>
Dependency(mod, ver, configuration = "runtime")
}
}
}
object Launch {
@tailrec
def mainClassLoader(cl: ClassLoader): Option[ClassLoader] =
if (cl == null)
None
else {
val isMainLoader = try {
val cl0 = cl.asInstanceOf[Object {
def isBootstrapLoader: Boolean
}]
cl0.isBootstrapLoader
} catch {
case e: Exception =>
false
}
if (isMainLoader)
Some(cl)
else
mainClassLoader(cl.getParent)
}
}
case class Launch(
@Short("M")
@Short("main")
mainClass: String,
@Recurse
isolated: IsolatedLoaderOptions,
@Recurse
common: CommonOptions
) extends CoursierCommand {
val (rawDependencies, extraArgs) = {
val idxOpt = Some(remainingArgs.indexOf("--")).filter(_ >= 0)
idxOpt.fold((remainingArgs, Seq.empty[String])) { idx =>
val (l, r) = remainingArgs.splitAt(idx)
assert(r.nonEmpty)
(l, r.tail)
}
}
val helper = new Helper(
common,
rawDependencies ++ isolated.rawIsolated.map { case (_, dep) => dep }
)
val files0 = helper.fetch(sources = false, javadoc = false)
val contextLoader = Thread.currentThread().getContextClassLoader
val parentLoader0: ClassLoader =
if (Try(contextLoader.loadClass("coursier.cli.Launch")).isSuccess)
Launch.mainClassLoader(contextLoader)
.flatMap(cl => Option(cl.getParent))
.getOrElse {
if (common.verbose0 >= 0)
Console.err.println(
"Warning: cannot find the main ClassLoader that launched coursier. " +
"Was coursier launched by its main launcher? " +
"The ClassLoader of the application that is about to be launched will be intertwined " +
"with the one of coursier, which may be a problem if their dependencies conflict."
)
contextLoader
}
else
// proguarded -> no risk of conflicts, no need to find a specific ClassLoader
contextLoader
val (parentLoader, filteredFiles) =
if (isolated.isolated.isEmpty)
(parentLoader0, files0)
else {
val (isolatedLoader, filteredFiles0) = isolated.targets.foldLeft((parentLoader0, files0)) {
case ((parent, files0), target) =>
// FIXME These were already fetched above
val isolatedFiles = helper.fetch(
sources = false,
javadoc = false,
subset = isolated.isolatedDeps.getOrElse(target, Seq.empty).toSet
)
if (common.verbose0 >= 1) {
Console.err.println(s"Isolated loader files:")
for (f <- isolatedFiles.map(_.toString).sorted)
Console.err.println(s" $f")
}
val isolatedLoader = new IsolatedClassLoader(
isolatedFiles.map(_.toURI.toURL).toArray,
parent,
Array(target)
)
val filteredFiles0 = files0.filterNot(isolatedFiles.toSet)
(isolatedLoader, filteredFiles0)
}
if (common.verbose0 >= 1) {
Console.err.println(s"Remaining files:")
for (f <- filteredFiles0.map(_.toString).sorted)
Console.err.println(s" $f")
}
(isolatedLoader, filteredFiles0)
}
val loader = new URLClassLoader(
filteredFiles.map(_.toURI.toURL).toArray,
parentLoader
)
val mainClass0 =
if (mainClass.nonEmpty) mainClass
else {
val mainClasses = Helper.mainClasses(loader)
val mainClass =
if (mainClasses.isEmpty) {
Helper.errPrintln("No main class found. Specify one with -M or --main.")
sys.exit(255)
} else if (mainClasses.size == 1) {
val (_, mainClass) = mainClasses.head
mainClass
} else {
// Trying to get the main class of the first artifact
val mainClassOpt = for {
(module, _, _) <- helper.moduleVersionConfigs.headOption
mainClass <- mainClasses.collectFirst {
case ((org, name), mainClass)
if org == module.organization && (
module.name == name ||
module.name.startsWith(name + "_") // Ignore cross version suffix
) =>
mainClass
}
} yield mainClass
mainClassOpt.getOrElse {
Helper.errPrintln(s"Cannot find default main class. Specify one with -M or --main.")
sys.exit(255)
}
}
mainClass
}
val cls =
try loader.loadClass(mainClass0)
catch { case e: ClassNotFoundException =>
Helper.errPrintln(s"Error: class $mainClass0 not found")
sys.exit(255)
}
val method =
try cls.getMethod("main", classOf[Array[String]])
catch { case e: NoSuchMethodException =>
Helper.errPrintln(s"Error: method main not found in $mainClass0")
sys.exit(255)
}
if (common.verbose0 >= 1)
Helper.errPrintln(s"Launching $mainClass0 ${extraArgs.mkString(" ")}")
else if (common.verbose0 == 0)
Helper.errPrintln(s"Launching")
Thread.currentThread().setContextClassLoader(loader)
method.invoke(null, extraArgs.toArray)
}
case class Bootstrap(
@Short("M")
@Short("main")
mainClass: String,
@Short("o")
output: String = "bootstrap",
@Short("D")
downloadDir: String,
@Short("f")
force: Boolean,
@Help("Generate a standalone launcher, with all JARs included, instead of one downloading its dependencies on startup.")
@Short("s")
standalone: Boolean,
@Help("Set Java properties in the generated launcher.")
@Value("key=value")
@Short("P")
property: List[String],
@Help("Set Java command-line options in the generated launcher.")
@Value("option")
@Short("J")
javaOpt: List[String],
@Recurse
isolated: IsolatedLoaderOptions,
@Recurse
common: CommonOptions
) extends CoursierCommand {
import scala.collection.JavaConverters._
if (mainClass.isEmpty) {
Console.err.println(s"Error: no main class specified. Specify one with -M or --main")
sys.exit(255)
}
if (!standalone && downloadDir.isEmpty) {
Console.err.println(s"Error: no download dir specified. Specify one with -D or --download-dir")
Console.err.println("E.g. -D \"\\$HOME/.app-name/jars\"")
sys.exit(255)
}
val (validProperties, wrongProperties) = property.partition(_.contains("="))
if (wrongProperties.nonEmpty) {
Console.err.println(s"Wrong -P / --property option(s):\n${wrongProperties.mkString("\n")}")
sys.exit(255)
}
val properties0 = validProperties.map { s =>
val idx = s.indexOf('=')
assert(idx >= 0)
(s.take(idx), s.drop(idx + 1))
}
val bootstrapJar =
Option(Thread.currentThread().getContextClassLoader.getResourceAsStream("bootstrap.jar")) match {
case Some(is) => Cache.readFullySync(is)
case None =>
Console.err.println(s"Error: bootstrap JAR not found")
sys.exit(1)
}
val output0 = new File(output)
if (!force && output0.exists()) {
Console.err.println(s"Error: $output already exists, use -f option to force erasing it.")
sys.exit(1)
}
def zipEntries(zipStream: ZipInputStream): Iterator[(ZipEntry, Array[Byte])] =
new Iterator[(ZipEntry, Array[Byte])] {
var nextEntry = Option.empty[ZipEntry]
def update() =
nextEntry = Option(zipStream.getNextEntry)
update()
def hasNext = nextEntry.nonEmpty
def next() = {
val ent = nextEntry.get
val data = Platform.readFullySync(zipStream)
update()
(ent, data)
}
}
val helper = new Helper(common, remainingArgs)
val (_, isolatedArtifactFiles) =
isolated.targets.foldLeft((Vector.empty[String], Map.empty[String, (Seq[String], Seq[File])])) {
case ((done, acc), target) =>
val subRes = helper.res.subset(isolated.isolatedDeps.getOrElse(target, Nil).toSet)
val subArtifacts = subRes.artifacts.map(_.url)
val filteredSubArtifacts = subArtifacts.diff(done)
def subFiles0 = helper.fetch(
sources = false,
javadoc = false,
subset = isolated.isolatedDeps.getOrElse(target, Seq.empty).toSet
)
val (subUrls, subFiles) =
if (standalone)
(Nil, subFiles0)
else
(filteredSubArtifacts, Nil)
val updatedAcc = acc + (target -> (subUrls, subFiles))
(done ++ filteredSubArtifacts, updatedAcc)
}
val (urls, files) =
if (standalone)
(
Seq.empty[String],
helper.fetch(sources = false, javadoc = false)
)
else
(
helper.artifacts(sources = false, javadoc = false).map(_.url),
Seq.empty[File]
)
val isolatedUrls = isolatedArtifactFiles.map { case (k, (v, _)) => k -> v }
val isolatedFiles = isolatedArtifactFiles.map { case (k, (_, v)) => k -> v }
val nonHttpUrls = urls.filter(s => !s.startsWith("http://") && !s.startsWith("https://"))
if (nonHttpUrls.nonEmpty)
Console.err.println(s"Warning: non HTTP URLs:\n${nonHttpUrls.mkString("\n")}")
val buffer = new ByteArrayOutputStream()
val bootstrapZip = new ZipInputStream(Thread.currentThread().getContextClassLoader.getResourceAsStream("bootstrap.jar"))
val outputZip = new ZipOutputStream(buffer)
for ((ent, data) <- zipEntries(bootstrapZip)) {
outputZip.putNextEntry(ent)
outputZip.write(data)
outputZip.closeEntry()
}
val time = System.currentTimeMillis()
def putStringEntry(name: String, content: String): Unit = {
val entry = new ZipEntry(name)
entry.setTime(time)
outputZip.putNextEntry(entry)
outputZip.write(content.getBytes("UTF-8"))
outputZip.closeEntry()
}
def putEntryFromFile(name: String, f: File): Unit = {
val entry = new ZipEntry(name)
entry.setTime(f.lastModified())
outputZip.putNextEntry(entry)
outputZip.write(Cache.readFullySync(new FileInputStream(f)))
outputZip.closeEntry()
}
putStringEntry("bootstrap-jar-urls", urls.mkString("\n"))
if (isolated.anyIsolatedDep) {
putStringEntry("bootstrap-isolation-ids", isolated.targets.mkString("\n"))
for (target <- isolated.targets) {
val urls = isolatedUrls.getOrElse(target, Nil)
val files = isolatedFiles.getOrElse(target, Nil)
putStringEntry(s"bootstrap-isolation-$target-jar-urls", urls.mkString("\n"))
putStringEntry(s"bootstrap-isolation-$target-jar-resources", files.map(pathFor).mkString("\n"))
}
}
def pathFor(f: File) = s"jars/${f.getName}"
for (f <- files)
putEntryFromFile(pathFor(f), f)
putStringEntry("bootstrap-jar-resources", files.map(pathFor).mkString("\n"))
val propsEntry = new ZipEntry("bootstrap.properties")
propsEntry.setTime(time)
val properties = new Properties()
properties.setProperty("bootstrap.mainClass", mainClass)
if (!standalone)
properties.setProperty("bootstrap.jarDir", downloadDir)
outputZip.putNextEntry(propsEntry)
properties.store(outputZip, "")
outputZip.closeEntry()
outputZip.close()
// escaping of javaOpt possibly a bit loose :-|
val shellPreamble = Seq(
"#!/usr/bin/env sh",
"exec java -jar " + javaOpt.map(s => "'" + s.replace("'", "\\'") + "'").mkString(" ") + " \"$0\" \"$@\""
).mkString("", "\n", "\n")
try NIOFiles.write(output0.toPath, shellPreamble.getBytes("UTF-8") ++ buffer.toByteArray)
catch { case e: IOException =>
Console.err.println(s"Error while writing $output0: ${e.getMessage}")
sys.exit(1)
}
try {
val perms = NIOFiles.getPosixFilePermissions(output0.toPath).asScala.toSet
var newPerms = perms
if (perms(PosixFilePermission.OWNER_READ))
newPerms += PosixFilePermission.OWNER_EXECUTE
if (perms(PosixFilePermission.GROUP_READ))
newPerms += PosixFilePermission.GROUP_EXECUTE
if (perms(PosixFilePermission.OTHERS_READ))
newPerms += PosixFilePermission.OTHERS_EXECUTE
if (newPerms != perms)
NIOFiles.setPosixFilePermissions(
output0.toPath,
newPerms.asJava
)
} catch {
case e: UnsupportedOperationException =>
// Ignored
case e: IOException =>
Console.err.println(s"Error while making $output0 executable: ${e.getMessage}")
sys.exit(1)
}
}
import caseapp._
object Coursier extends CommandAppOf[CoursierCommand] {
override def appName = "Coursier"

View File

@ -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: 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(
@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
)