Merge pull request #124 from alexarchambault/develop

Latest developments
This commit is contained in:
Alexandre Archambault 2016-01-27 11:14:46 +01:00
commit c9c6b39e49
23 changed files with 450 additions and 339 deletions

View File

@ -14,15 +14,9 @@ script:
matrix:
include:
- env: TRAVIS_SCALA_VERSION=2.11.7 PUBLISH=1
os: linux
jdk: openjdk7
- env: TRAVIS_SCALA_VERSION=2.10.6 PUBLISH=1
os: linux
jdk: openjdk7
- env: TRAVIS_SCALA_VERSION=2.11.7
os: linux
jdk: oraclejdk8
- env: TRAVIS_SCALA_VERSION=2.10.6
- env: TRAVIS_SCALA_VERSION=2.10.6 PUBLISH=1
os: linux
jdk: oraclejdk8
env:

View File

@ -1,15 +1,9 @@
package coursier;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.*;
import java.nio.file.Files;
import java.security.CodeSource;
import java.security.ProtectionDomain;
@ -40,6 +34,7 @@ public class Bootstrap {
}
final static String defaultURLResource = "bootstrap-jar-urls";
final static String defaultJarResource = "bootstrap-jar-resources";
final static String isolationIDsResource = "bootstrap-isolation-ids";
static String[] readStringSequence(String resource) throws IOException {
@ -54,21 +49,16 @@ public class Bootstrap {
return content.split("\n");
}
static Map<String, URL[]> readIsolationContexts(File jarDir, String[] isolationIDs) throws IOException {
static Map<String, URL[]> readIsolationContexts(File jarDir, String[] isolationIDs, String bootstrapProtocol, ClassLoader loader) throws IOException {
final Map<String, URL[]> perContextURLs = new LinkedHashMap<>();
for (String isolationID: isolationIDs) {
String[] contextURLs = readStringSequence("bootstrap-isolation-" + isolationID + "-jar-urls");
List<URL> urls = new ArrayList<>();
for (String strURL : contextURLs) {
URL url = new URL(strURL);
File local = localFile(jarDir, url);
if (local.exists())
urls.add(local.toURI().toURL());
else
System.err.println("Warning: " + local + " not found.");
}
perContextURLs.put(isolationID, urls.toArray(new URL[urls.size()]));
String[] strUrls = readStringSequence("bootstrap-isolation-" + isolationID + "-jar-urls");
String[] resources = readStringSequence("bootstrap-isolation-" + isolationID + "-jar-resources");
List<URL> urls = getURLs(strUrls, resources, bootstrapProtocol, loader);
List<URL> localURLs = getLocalURLs(urls, jarDir, bootstrapProtocol);
perContextURLs.put(isolationID, localURLs.toArray(new URL[localURLs.size()]));
}
return perContextURLs;
@ -141,8 +131,7 @@ public class Bootstrap {
return new File(jarDir, fileName);
}
public static void main(String[] args) throws Throwable {
static List<URL> getLocalURLs(List<URL> urls, final File jarDir, String bootstrapProtocol) {
ThreadFactory threadFactory = new ThreadFactory() {
// from scalaz Strategy.DefaultDaemonThreadFactory
@ -156,61 +145,18 @@ public class Bootstrap {
ExecutorService pool = Executors.newFixedThreadPool(concurrentDownloadCount, threadFactory);
System.setProperty("coursier.mainJar", mainJarPath());
for (int i = 0; i < args.length; i++) {
System.setProperty("coursier.main.arg-" + i, args[i]);
}
Map<String,String> properties = loadPropertiesMap(Thread.currentThread().getContextClassLoader().getResourceAsStream("bootstrap.properties"));
for (Map.Entry<String, String> ent : properties.entrySet()) {
System.setProperty(ent.getKey(), ent.getValue());
}
String mainClass0 = System.getProperty("bootstrap.mainClass");
String jarDir0 = System.getProperty("bootstrap.jarDir");
boolean prependClasspath = Boolean.parseBoolean(System.getProperty("bootstrap.prependClasspath", "false"));
final File jarDir = new File(jarDir0);
if (jarDir.exists()) {
if (!jarDir.isDirectory())
exit("Error: " + jarDir0 + " is not a directory");
} else if (!jarDir.mkdirs())
System.err.println("Warning: cannot create " + jarDir0 + ", continuing anyway.");
String[] jarStrUrls = readStringSequence(defaultURLResource);
List<String> errors = new ArrayList<>();
List<URL> urls = new ArrayList<>();
for (String urlStr : jarStrUrls) {
try {
URL url = URI.create(urlStr).toURL();
urls.add(url);
} catch (Exception ex) {
String message = urlStr + ": " + ex.getMessage();
errors.add(message);
}
}
if (!errors.isEmpty()) {
StringBuilder builder = new StringBuilder("Error parsing " + errors.size() + " URL(s):");
for (String error: errors) {
builder.append('\n');
builder.append(error);
}
exit(builder.toString());
}
CompletionService<URL> completionService =
new ExecutorCompletionService<>(pool);
List<URL> localURLs = new ArrayList<>();
for (URL url : urls) {
if (!url.getProtocol().equals("file")) {
String protocol = url.getProtocol();
if (protocol.equals("file") || protocol.equals(bootstrapProtocol)) {
localURLs.add(url);
} else {
final URL url0 = url;
completionService.submit(new Callable<URL>() {
@ -236,8 +182,6 @@ public class Bootstrap {
return dest.toURI().toURL();
}
});
} else {
localURLs.add(url);
}
}
@ -256,8 +200,115 @@ public class Bootstrap {
exit("Interrupted");
}
final String[] isolationIDs = readStringSequence(isolationIDsResource);
final Map<String, URL[]> perIsolationContextURLs = readIsolationContexts(jarDir, isolationIDs);
return localURLs;
}
static void setMainProperties(String mainJarPath, String[] args) {
System.setProperty("coursier.mainJar", mainJarPath);
for (int i = 0; i < args.length; i++) {
System.setProperty("coursier.main.arg-" + i, args[i]);
}
}
static void setExtraProperties(String resource) throws IOException {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Map<String,String> properties = loadPropertiesMap(loader.getResourceAsStream(resource));
for (Map.Entry<String, String> ent : properties.entrySet()) {
System.setProperty(ent.getKey(), ent.getValue());
}
}
static List<URL> getURLs(String[] rawURLs, String[] resources, String bootstrapProtocol, ClassLoader loader) throws MalformedURLException {
List<String> errors = new ArrayList<>();
List<URL> urls = new ArrayList<>();
for (String urlStr : rawURLs) {
try {
URL url = URI.create(urlStr).toURL();
urls.add(url);
} catch (Exception ex) {
String message = urlStr + ": " + ex.getMessage();
errors.add(message);
}
}
for (String resource : resources) {
URL url = loader.getResource(resource);
if (url == null) {
String message = "Resource " + resource + " not found";
errors.add(message);
} else {
URL url0 = new URL(bootstrapProtocol, null, resource);
urls.add(url0);
}
}
if (!errors.isEmpty()) {
StringBuilder builder = new StringBuilder("Error:");
for (String error: errors) {
builder.append("\n ");
builder.append(error);
}
exit(builder.toString());
}
return urls;
}
// JARs from JARs can't be used directly, see:
// http://stackoverflow.com/questions/183292/classpath-including-jar-within-a-jar/2326775#2326775
// Loading them via a custom protocol, inspired by:
// http://stackoverflow.com/questions/26363573/registering-and-using-a-custom-java-net-url-protocol/26409796#26409796
static void registerBootstrapUnder(final String bootstrapProtocol, final ClassLoader loader) {
URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
public URLStreamHandler createURLStreamHandler(String protocol) {
return bootstrapProtocol.equals(protocol) ? new URLStreamHandler() {
protected URLConnection openConnection(URL url) throws IOException {
String path = url.getPath();
URL resURL = loader.getResource(path);
if (resURL == null)
throw new FileNotFoundException("Resource " + path);
return resURL.openConnection();
}
} : null;
}
});
}
public static void main(String[] args) throws Throwable {
setMainProperties(mainJarPath(), args);
setExtraProperties("bootstrap.properties");
String mainClass0 = System.getProperty("bootstrap.mainClass");
String jarDir0 = System.getProperty("bootstrap.jarDir");
final File jarDir = new File(jarDir0);
if (jarDir.exists()) {
if (!jarDir.isDirectory())
exit("Error: " + jarDir0 + " is not a directory");
} else if (!jarDir.mkdirs())
System.err.println("Warning: cannot create " + jarDir0 + ", continuing anyway.");
Random rng = new Random();
String protocol = "bootstrap" + rng.nextLong();
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
registerBootstrapUnder(protocol, contextLoader);
String[] strUrls = readStringSequence(defaultURLResource);
String[] resources = readStringSequence(defaultJarResource);
List<URL> urls = getURLs(strUrls, resources, protocol, contextLoader);
List<URL> localURLs = getLocalURLs(urls, jarDir, protocol);
String[] isolationIDs = readStringSequence(isolationIDsResource);
Map<String, URL[]> perIsolationContextURLs = readIsolationContexts(jarDir, isolationIDs, protocol, contextLoader);
Thread thread = Thread.currentThread();
ClassLoader parentClassLoader = thread.getContextClassLoader();
@ -267,7 +318,7 @@ public class Bootstrap {
parentClassLoader = new IsolatedClassLoader(contextURLs, parentClassLoader, new String[]{ isolationID });
}
URLClassLoader classLoader = new URLClassLoader(localURLs.toArray(new URL[localURLs.size()]), parentClassLoader);
ClassLoader classLoader = new BootstrapClassLoader(localURLs.toArray(new URL[localURLs.size()]), parentClassLoader);
Class<?> mainClass = null;
Method mainMethod = null;
@ -288,14 +339,6 @@ public class Bootstrap {
List<String> userArgs0 = new ArrayList<>();
if (prependClasspath) {
for (URL url : localURLs) {
assert url.getProtocol().equals("file");
userArgs0.add("-B");
userArgs0.add(url.getPath());
}
}
for (int i = 0; i < args.length; i++)
userArgs0.add(args[i]);

View File

@ -0,0 +1,25 @@
package coursier;
import java.net.URL;
import java.net.URLClassLoader;
public class BootstrapClassLoader extends URLClassLoader {
public BootstrapClassLoader(
URL[] urls,
ClassLoader parent
) {
super(urls, parent);
}
/**
* Can be called by reflection by launched applications, to find the "main" `ClassLoader`
* that loaded them, and possibly short-circuit it to load other things for example.
*
* The `launch` command of coursier does that.
*/
public boolean isBootstrapLoader() {
return true;
}
}

View File

@ -122,18 +122,23 @@ object Cache {
logger.foreach(_.downloadingArtifact(url, file))
val res =
try f
catch { case e: Exception =>
logger.foreach(_.downloadedArtifact(url, success = false))
throw e
try \/-(f)
catch {
case nfe: FileNotFoundException if nfe.getMessage != null =>
logger.foreach(_.downloadedArtifact(url, success = false))
-\/(-\/(FileError.NotFound(nfe.getMessage)))
case e: Exception =>
logger.foreach(_.downloadedArtifact(url, success = false))
throw e
}
finally {
urlLocks.remove(url)
}
logger.foreach(_.downloadedArtifact(url, success = true))
for (res0 <- res)
logger.foreach(_.downloadedArtifact(url, success = res0.isRight))
res
res.merge
} else
-\/(FileError.ConcurrentDownload(url))
}
@ -575,21 +580,21 @@ object FileError {
def message = s"Download error: $message0"
}
case class NotFound(file: String) extends FileError {
def message = s"$file: not found"
def message = s"Not found: $file"
}
case class ChecksumNotFound(sumType: String, file: String) extends FileError {
def message = s"$file: $sumType checksum not found"
def message = s"$sumType checksum not found: $file"
}
case class WrongChecksum(sumType: String, got: String, expected: String, file: String, sumFile: String) extends FileError {
def message = s"$file: $sumType checksum validation failed"
def message = s"$sumType checksum validation failed: $file"
}
sealed trait Recoverable extends FileError
case class Locked(file: File) extends Recoverable {
def message = s"$file: locked"
def message = s"Locked: $file"
}
case class ConcurrentDownload(url: String) extends Recoverable {
def message = s"$url: concurrent download"
def message = s"Concurrent download: $url"
}
}

View File

@ -1,7 +1,7 @@
package coursier
package cli
import java.io.{ ByteArrayOutputStream, File, IOException }
import java.io.{ FileInputStream, ByteArrayOutputStream, File, IOException }
import java.net.URLClassLoader
import java.nio.file.{ Files => NIOFiles }
import java.nio.file.attribute.PosixFilePermission
@ -9,7 +9,10 @@ import java.util.Properties
import java.util.zip.{ ZipEntry, ZipOutputStream, ZipInputStream }
import caseapp.{ HelpMessage => Help, ValueDescription => Value, ExtraName => Short, _ }
import coursier.util.{ Parse, ClasspathFilter }
import coursier.util.Parse
import scala.annotation.tailrec
import scala.language.reflectiveCalls
case class CommonOptions(
@Help("Keep optional dependencies (Maven)")
@ -61,7 +64,7 @@ case class Resolve(
) extends CoursierCommand {
// the `val helper = ` part is needed because of DelayedInit it seems
val helper = new Helper(common, remainingArgs)
val helper = new Helper(common, remainingArgs, printResultStdout = true)
}
@ -161,6 +164,32 @@ case class IsolatedLoaderOptions(
}
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")
@ -188,12 +217,20 @@ case class Launch(
val files0 = helper.fetch(sources = false, javadoc = false)
val contextLoader = Thread.currentThread().getContextClassLoader
val parentLoader0: ClassLoader = new ClasspathFilter(
Thread.currentThread().getContextClassLoader,
Coursier.baseCp.map(new File(_)).toSet,
exclude = true
)
val parentLoader0: ClassLoader = 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
}
val (parentLoader, filteredFiles) =
if (isolated.isolated.isEmpty)
@ -283,7 +320,7 @@ case class Launch(
}
val method =
try cls.getMethod("main", classOf[Array[String]])
catch { case e: NoSuchMethodError =>
catch { case e: NoSuchMethodException =>
Helper.errPrintln(s"Error: method main not found in $mainClass0")
sys.exit(255)
}
@ -307,10 +344,10 @@ case class Bootstrap(
downloadDir: String,
@Short("f")
force: Boolean,
@Help(s"Internal use - prepend base classpath options to arguments (like -B jar1 -B jar2 etc.)")
@Short("b")
prependClasspath: Boolean,
@Help("Set environment variables in the generated launcher. No escaping is done. Value is simply put between quotes in the launcher preamble.")
@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],
@ -327,7 +364,7 @@ case class Bootstrap(
sys.exit(255)
}
if (downloadDir.isEmpty) {
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)
@ -381,21 +418,46 @@ case class Bootstrap(
val helper = new Helper(common, remainingArgs)
val urls = helper.res.artifacts.map(_.url)
val (_, isolatedUrls) =
isolated.targets.foldLeft((Vector.empty[String], Map.empty[String, Seq[String]])) {
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 subUrls = subRes.artifacts.map(_.url)
val subArtifacts = subRes.artifacts.map(_.url)
val filteredSubUrls = subUrls.diff(done)
val filteredSubArtifacts = subArtifacts.diff(done)
val updatedAcc = acc + (target -> filteredSubUrls)
def subFiles0 = helper.fetch(
sources = false,
javadoc = false,
subset = isolated.isolatedDeps.getOrElse(target, Seq.empty).toSet
)
(done ++ filteredSubUrls, updatedAcc)
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.res.artifacts.map(_.url),
Seq.empty[File]
)
val isolatedUrls = isolatedArtifactFiles.map { case (k, (v, _)) => k -> v }
val isolatedFiles = isolatedArtifactFiles.map { case (k, (_, v)) => k -> v }
val unrecognized = urls.filter(s => !s.startsWith("http://") && !s.startsWith("https://"))
if (unrecognized.nonEmpty)
Console.err.println(s"Warning: non HTTP URLs:\n${unrecognized.mkString("\n")}")
@ -423,6 +485,15 @@ case class Bootstrap(
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) {
@ -430,17 +501,26 @@ case class Bootstrap(
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)
properties.setProperty("bootstrap.jarDir", downloadDir)
properties.setProperty("bootstrap.prependClasspath", prependClasspath.toString)
if (!standalone)
properties.setProperty("bootstrap.jarDir", downloadDir)
outputZip.putNextEntry(propsEntry)
properties.store(outputZip, "")
@ -487,27 +567,7 @@ case class Bootstrap(
}
case class BaseCommand(
@Hidden
@Short("B")
baseCp: List[String]
) extends Command {
Coursier.baseCp = baseCp
// FIXME Should be in a trait in case-app
override def setCommand(cmd: Option[Either[String, String]]): Unit = {
if (cmd.isEmpty) {
// FIXME Print available commands too?
Console.err.println("Error: no command specified")
sys.exit(255)
}
super.setCommand(cmd)
}
}
object Coursier extends CommandAppOfWithBase[BaseCommand, CoursierCommand] {
object Coursier extends CommandAppOf[CoursierCommand] {
override def appName = "Coursier"
override def progName = "coursier"
private[coursier] var baseCp = Seq.empty[String]
}

View File

@ -58,7 +58,8 @@ object Util {
class Helper(
common: CommonOptions,
rawDependencies: Seq[String]
rawDependencies: Seq[String],
printResultStdout: Boolean = false
) {
import common._
import Helper.errPrintln
@ -168,19 +169,13 @@ class Helper(
print.flatMap(_ => fetchQuiet(modVers))
}
def indent(s: String): String =
if (s.isEmpty)
s
else
s.split('\n').map(" "+_).mkString("\n")
if (verbose0 >= 0) {
errPrintln(s"Dependencies:\n${indent(Print.dependenciesUnknownConfigs(dependencies))}")
errPrintln(s" Dependencies:\n${Print.dependenciesUnknownConfigs(dependencies, Map.empty)}")
if (forceVersions.nonEmpty) {
errPrintln("Force versions:")
errPrintln(" Force versions:")
for ((mod, ver) <- forceVersions.toVector.sortBy { case (mod, _) => mod.toString })
errPrintln(s" $mod:$ver")
errPrintln(s"$mod:$ver")
}
}
@ -202,20 +197,28 @@ class Helper(
}
exitIf(res.errors.nonEmpty) {
s"\n${res.errors.size} error(s):\n" +
s"\nError:\n" +
res.errors.map { case (dep, errs) =>
s" ${dep.module}:${dep.version}:\n${errs.map(" " + _.replace("\n", " \n")).mkString("\n")}"
}.mkString("\n")
}
exitIf(res.conflicts.nonEmpty) {
s"${res.conflicts.size} conflict(s):\n${Print.dependenciesUnknownConfigs(res.conflicts.toVector)}"
s"\nConflict:\n${Print.dependenciesUnknownConfigs(res.conflicts.toVector, projCache)}"
}
val trDeps = res.minDependencies.toVector
if (verbose0 >= 0)
errPrintln(s"Result:\n${indent(Print.dependenciesUnknownConfigs(trDeps))}")
lazy val projCache = res.projectCache.mapValues { case (_, p) => p }
if (printResultStdout || verbose0 >= 0) {
errPrintln(s" Result:")
val depsStr = Print.dependenciesUnknownConfigs(trDeps, projCache)
if (printResultStdout)
println(depsStr)
else
errPrintln(depsStr)
}
def fetch(
sources: Boolean,
@ -226,9 +229,9 @@ class Helper(
if (verbose0 >= 0) {
val msg = cachePolicies match {
case Seq(CachePolicy.LocalOnly) =>
"Checking artifacts"
" Checking artifacts"
case _ =>
"Fetching artifacts"
" Fetching artifacts"
}
errPrintln(msg)
@ -255,7 +258,7 @@ class Helper(
None
if (verbose0 >= 1 && artifacts.nonEmpty)
println(s"Found ${artifacts.length} artifacts")
println(s" Found ${artifacts.length} artifacts")
val tasks = artifacts.map(artifact =>
(Cache.file(artifact, caches, cachePolicies.head, logger = logger, pool = pool) /: cachePolicies.tail)(
@ -274,9 +277,9 @@ class Helper(
logger.foreach(_.stop())
exitIf(errors.nonEmpty) {
s"${errors.size} error(s):\n" +
s" Error:\n" +
errors.map { case (artifact, error) =>
s" ${artifact.url}: $error"
s"${artifact.url}: $error"
}.mkString("\n")
}

View File

@ -1,37 +0,0 @@
package coursier
import java.io.File
import java.net.URLClassLoader
import coursier.util.ClasspathFilter
class ResolutionClassLoader(
val resolution: Resolution,
val artifacts: Seq[(Dependency, Artifact, File)],
parent: ClassLoader
) extends URLClassLoader(
artifacts.map { case (_, _, f) => f.toURI.toURL }.toArray,
parent
) {
/**
* Filtered version of this `ClassLoader`, exposing only `dependencies` and their
* their transitive dependencies, and filtering out the other dependencies from
* `resolution` - for `ClassLoader` isolation.
*
* An application launched by `coursier launch -C` has `ResolutionClassLoader` set as its
* context `ClassLoader` (can be obtain with `Thread.currentThread().getContextClassLoader`).
* If it aims at doing `ClassLoader` isolation, exposing only a dependency `dep` to the isolated
* things, `filter(dep)` provides a `ClassLoader` that loaded `dep` and all its transitive
* dependencies through the same loader as the contextual one, but that "exposes" only
* `dep` and its transitive dependencies, nothing more.
*/
def filter(dependencies: Set[Dependency]): ClassLoader = {
val subRes = resolution.subset(dependencies)
val subArtifacts = subRes.dependencyArtifacts.map { case (_, a) => a }.toSet
val subFiles = artifacts.collect { case (_, a, f) if subArtifacts(a) => f }
new ClasspathFilter(this, subFiles.toSet, exclude = false)
}
}

View File

@ -1,116 +0,0 @@
package coursier.util
// Extracted and adapted from SBT
import java.io.File
import java.net._
/**
* Doesn't load any classes itself, but instead verifies that all classes loaded through `parent`
* come from `classpath`.
*
* If `exclude` is `true`, does the opposite - exclude classes from `classpath`.
*/
class ClasspathFilter(parent: ClassLoader, classpath: Set[File], exclude: Boolean) extends ClassLoader(parent) {
override def toString = s"ClasspathFilter(parent = $parent, cp = $classpath)"
private val directories = classpath.toSeq.filter(_.isDirectory)
private def toFile(url: URL) =
try { new File(url.toURI) }
catch { case _: URISyntaxException => new File(url.getPath) }
private def uriToFile(uriString: String) = {
val uri = new URI(uriString)
assert(uri.getScheme == "file", s"Expected protocol to be 'file' in URI $uri")
if (uri.getAuthority == null)
new File(uri)
else {
/* https://github.com/sbt/sbt/issues/564
* http://blogs.msdn.com/b/ie/archive/2006/12/06/file-uris-in-windows.aspx
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5086147
* The specific problem here is that `uri` will have a defined authority component for UNC names like //foo/bar/some/path.jar
* but the File constructor requires URIs with an undefined authority component.
*/
new File(uri.getSchemeSpecificPart)
}
}
private def urlAsFile(url: URL) =
url.getProtocol match {
case "file" => Some(toFile(url))
case "jar" =>
val path = url.getPath
val end = path.indexOf('!')
Some(uriToFile(if (end < 0) path else path.substring(0, end)))
case _ => None
}
private def baseFileString(baseFile: File) =
Some(baseFile).filter(_.isDirectory).map { d =>
val cp = d.getAbsolutePath.ensuring(_.nonEmpty)
if (cp.last == File.separatorChar) cp else cp + File.separatorChar
}
private def relativize(base: File, file: File) =
baseFileString(base) .flatMap { baseString =>
Some(file.getAbsolutePath).filter(_ startsWith baseString).map(_ substring baseString.length)
}
private def fromClasspath(c: Class[_]): Boolean = {
val codeSource = c.getProtectionDomain.getCodeSource
(codeSource eq null) ||
onClasspath(codeSource.getLocation) ||
// workaround SBT classloader returning the target directory as sourcecode
// make us keep more class than expected
urlAsFile(codeSource.getLocation).exists(_.isDirectory)
}
private val onClasspath: URL => Boolean = {
if (exclude)
src =>
src == null || urlAsFile(src).forall(f =>
!classpath(f) &&
directories.forall(relativize(_, f).isEmpty)
)
else
src =>
src == null || urlAsFile(src).exists(f =>
classpath(f) ||
directories.exists(relativize(_, f).isDefined)
)
}
override def loadClass(className: String, resolve: Boolean): Class[_] = {
val c =
try super.loadClass(className, resolve)
catch {
case e: LinkageError =>
// Happens when trying to derive a shapeless.Generic
// from an Ammonite session launched like
// ./coursier launch com.lihaoyi:ammonite-repl_2.11.7:0.5.2
// For className == "shapeless.GenericMacros",
// the super.loadClass above - which would be filtered out below anyway,
// raises a NoClassDefFoundError.
null
}
if (c != null && fromClasspath(c))
c
else
throw new ClassNotFoundException(className)
}
override def getResource(name: String): URL = {
val res = super.getResource(name)
if (onClasspath(res)) res else null
}
override def getResources(name: String): java.util.Enumeration[URL] = {
import collection.JavaConverters._
val res = super.getResources(name)
if (res == null) null else res.asScala.filter(onClasspath).asJavaEnumeration
}
}

View File

@ -10,6 +10,21 @@ object Parse {
else Some(Version(s))
}
def ivyLatestSubRevisionInterval(s: String): Option[VersionInterval] =
if (s.endsWith(".+")) {
for {
from <- version(s.stripSuffix(".+"))
if from.rawItems.nonEmpty
last <- Some(from.rawItems.last).collect { case n: Version.Numeric => n }
// a bit loose, but should do the job
if from.repr.endsWith(last.repr)
to <- version(from.repr.stripSuffix(last.repr) + last.next.repr)
// the contrary would mean something went wrong in the loose substitution above
if from.rawItems.init == to.rawItems.init
} yield VersionInterval(Some(from), Some(to), fromIncluded = true, toIncluded = false)
} else
None
def versionInterval(s: String): Option[VersionInterval] = {
for {
fromIncluded <- if (s.startsWith("[")) Some(true) else if (s.startsWith("(")) Some(false) else None
@ -28,6 +43,7 @@ object Parse {
def noConstraint = if (s.isEmpty) Some(VersionConstraint.None) else None
noConstraint
.orElse(ivyLatestSubRevisionInterval(s).map(VersionConstraint.Interval))
.orElse(version(s).map(VersionConstraint.Preferred))
.orElse(versionInterval(s).map(VersionConstraint.Interval))
}

View File

@ -335,7 +335,14 @@ object Resolution {
"project.groupId" -> project.module.organization,
"project.artifactId" -> project.module.name,
"project.version" -> project.version
)
) ++ project.parent.toSeq.flatMap {
case (parModule, parVersion) =>
Seq(
"project.parent.groupId" -> parModule.organization,
"project.parent.artifactId" -> parModule.name,
"project.parent.version" -> parVersion
)
}
val properties = propertiesMap(properties0)

View File

@ -10,6 +10,10 @@ import coursier.core.compatibility._
*/
case class Version(repr: String) extends Ordered[Version] {
lazy val items = Version.items(repr)
lazy val rawItems: Seq[Version.Item] = {
val (first, tokens) = Version.Tokenizer(repr)
first +: tokens.toVector.map { case (_, item) => item }
}
def compare(other: Version) = Version.listCompare(items, other.items)
def isEmpty = items.forall(_.isEmpty)
}
@ -39,13 +43,20 @@ object Version {
def compareToEmpty: Int = 1
}
sealed trait Numeric extends Item
sealed trait Numeric extends Item {
def repr: String
def next: Numeric
}
case class Number(value: Int) extends Numeric {
val order = 0
def next: Number = Number(value + 1)
def repr: String = value.toString
override def compareToEmpty = value.compare(0)
}
case class BigNumber(value: BigInt) extends Numeric {
val order = 0
def next: BigNumber = BigNumber(value + 1)
def repr: String = value.toString
override def compareToEmpty = value.compare(0)
}
case class Qualifier(value: String, level: Int) extends Item {

View File

@ -61,9 +61,10 @@ case class IvyRepository(
case None =>
project.publications.collect {
case (conf, p)
if conf == "*" ||
conf == dependency.configuration ||
project.allConfigurations.getOrElse(dependency.configuration, Set.empty).contains(conf) =>
if (conf == "*" ||
conf == dependency.configuration ||
project.allConfigurations.getOrElse(dependency.configuration, Set.empty).contains(conf)
) && p.classifier.isEmpty =>
p
}
case Some(classifiers) =>

View File

@ -261,6 +261,7 @@ case class MavenRepository(
): EitherT[F, String, (Artifact.Source, Project)] = {
Parse.versionInterval(version)
.orElse(Parse.ivyLatestSubRevisionInterval(version))
.filter(_.isValid) match {
case None =>
findNoInterval(module, version, fetch).map((source, _))

View File

@ -1,20 +1,34 @@
package coursier.util
import coursier.core.{ Orders, Dependency }
import coursier.core.{Module, Project, Orders, Dependency}
object Print {
def dependency(dep: Dependency): String =
s"${dep.module}:${dep.version}:${dep.configuration}"
def dependency(dep: Dependency): String = {
val exclusionsStr = dep.exclusions.toVector.sorted.map {
case (org, name) =>
s"\n exclude($org, $name)"
}.mkString
def dependenciesUnknownConfigs(deps: Seq[Dependency]): String = {
s"${dep.module}:${dep.version}:${dep.configuration}$exclusionsStr"
}
def dependenciesUnknownConfigs(deps: Seq[Dependency], projects: Map[(Module, String), Project]): String = {
val deps0 = deps.map { dep =>
dep.copy(
version = projects
.get(dep.moduleVersion)
.fold(dep.version)(_.version)
)
}
val minDeps = Orders.minDependencies(
deps.toSet,
deps0.toSet,
_ => Map.empty
)
val deps0 = minDeps
val deps1 = minDeps
.groupBy(_.copy(configuration = ""))
.toVector
.map { case (k, l) =>
@ -24,7 +38,7 @@ object Print {
(dep.module.organization, dep.module.name, dep.module.toString, dep.version)
}
deps0.map(dependency).mkString("\n")
deps1.map(dependency).mkString("\n")
}
}

View File

@ -295,7 +295,8 @@ object Tasks {
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)}")
val projCache = res.projectCache.mapValues { case (_, p) => p }
println(s"${res.conflicts.size} conflict(s):\n ${Print.dependenciesUnknownConfigs(res.conflicts.toVector, projCache)}")
throw new Exception(s"Conflict(s) in dependency resolution")
}
@ -342,7 +343,8 @@ object Tasks {
configs
)
val repr = Print.dependenciesUnknownConfigs(finalDeps.toVector)
val projCache = res.projectCache.mapValues { case (_, p) => p }
val repr = Print.dependenciesUnknownConfigs(finalDeps.toVector, projCache)
println(repr.split('\n').map(" "+_).mkString("\n"))
}

View File

@ -8,7 +8,6 @@ CACHE_VERSION=v1
--no-default \
-r central \
-D "\${user.home}/.coursier/bootstrap/$VERSION" \
-b \
-f -o coursier \
-M coursier.cli.Coursier \
-P coursier.cache="\${user.home}/.coursier/cache/$CACHE_VERSION"

View File

@ -1,16 +1,41 @@
package coursier.test
import coursier.{ Module, Cache }
import coursier.{ Dependency, Module, Cache }
import coursier.test.compatibility._
import scala.async.Async.{ async, await }
import utest._
object IvyLocalTests extends TestSuite {
val tests = TestSuite{
'coursier{
val module = Module("com.github.alexarchambault", "coursier_2.11")
val version = "1.0.0-SNAPSHOT"
val extraRepo = Some(Cache.ivy2Local)
// Assume this module (and the sub-projects it depends on) is published locally
CentralTests.resolutionCheck(
Module("com.github.alexarchambault", "coursier_2.11"), "1.0.0-SNAPSHOT",
Some(Cache.ivy2Local))
module, version,
extraRepo
)
async {
val res = await(CentralTests.resolve(
Set(Dependency(module, version)),
extraRepo = extraRepo
))
val artifacts = res.artifacts.map(_.url)
val anyJavadoc = artifacts.exists(_.contains("-javadoc"))
val anySources = artifacts.exists(_.contains("-sources"))
assert(!anyJavadoc)
assert(!anySources)
}
}
}

View File

@ -0,0 +1,2 @@
com.chuusai:shapeless_2.11:jar:2.2.5
org.scala-lang:scala-library:jar:2.11.7

View File

@ -0,0 +1,2 @@
com.chuusai:shapeless_2.11:jar:2.2.5
org.scala-lang:scala-library:jar:2.11.7

View File

@ -0,0 +1,17 @@
com.github.fommil.netlib:all:jar:1.1.2
com.github.fommil.netlib:core:jar:1.1.2
com.github.fommil.netlib:native_ref-java:jar:1.1
com.github.fommil.netlib:native_system-java:jar:1.1
com.github.fommil.netlib:netlib-native_ref-linux-armhf:jar:natives:1.1
com.github.fommil.netlib:netlib-native_ref-linux-i686:jar:natives:1.1
com.github.fommil.netlib:netlib-native_ref-linux-x86_64:jar:natives:1.1
com.github.fommil.netlib:netlib-native_ref-osx-x86_64:jar:natives:1.1
com.github.fommil.netlib:netlib-native_ref-win-i686:jar:natives:1.1
com.github.fommil.netlib:netlib-native_ref-win-x86_64:jar:natives:1.1
com.github.fommil.netlib:netlib-native_system-linux-armhf:jar:natives:1.1
com.github.fommil.netlib:netlib-native_system-linux-i686:jar:natives:1.1
com.github.fommil.netlib:netlib-native_system-linux-x86_64:jar:natives:1.1
com.github.fommil.netlib:netlib-native_system-osx-x86_64:jar:natives:1.1
com.github.fommil.netlib:netlib-native_system-win-i686:jar:natives:1.1
com.github.fommil.netlib:netlib-native_system-win-x86_64:jar:natives:1.1
com.github.fommil:jniloader:jar:1.1

View File

@ -0,0 +1 @@
com.googlecode.libphonenumber:libphonenumber:jar:7.0.11

View File

@ -0,0 +1 @@
com.googlecode.libphonenumber:libphonenumber:jar:7.0.11

View File

@ -61,12 +61,20 @@ object CentralTests extends TestSuite {
val result = res
.dependencies
.toVector
.map(repr)
.map { dep =>
val projOpt = res.projectCache
.get(dep.moduleVersion)
.map { case (_, proj) => proj }
val dep0 = dep.copy(
version = projOpt.fold(dep.version)(_.version)
)
repr(dep0)
}
.sorted
.distinct
for (((e, r), idx) <- expected.zip(result).zipWithIndex if e != r)
println(s"Line $idx:\n expected: $e\n got:$r")
println(s"Line $idx:\n expected: $e\n got: $r")
assert(result == expected)
}
@ -143,6 +151,33 @@ object CentralTests extends TestSuite {
extraRepo = Some(MavenRepository("https://oss.sonatype.org/content/repositories/public/"))
)
}
'parentProjectProperties - {
resolutionCheck(
Module("com.github.fommil.netlib", "all"),
"1.1.2"
)
}
'latestRevision - {
resolutionCheck(
Module("com.chuusai", "shapeless_2.11"),
"[2.2.0,2.3.0)"
)
resolutionCheck(
Module("com.chuusai", "shapeless_2.11"),
"2.2.+"
)
resolutionCheck(
Module("com.googlecode.libphonenumber", "libphonenumber"),
"[7.0,7.1)"
)
resolutionCheck(
Module("com.googlecode.libphonenumber", "libphonenumber"),
"7.0.+"
)
}
}
}