From 3e9a37edc358e1960ce607e9958c310afa2a01c8 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:06 +0100 Subject: [PATCH 01/14] Do not fetch artifacts with a classifier from Ivy repositories if not asked to do so --- .../scala/coursier/ivy/IvyRepository.scala | 7 +++-- .../scala/coursier/test/IvyLocalTests.scala | 31 +++++++++++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala index 599b5ceac..15cbfa30e 100644 --- a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala +++ b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala @@ -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) => diff --git a/tests/jvm/src/test/scala/coursier/test/IvyLocalTests.scala b/tests/jvm/src/test/scala/coursier/test/IvyLocalTests.scala index db4006289..4cf3b9669 100644 --- a/tests/jvm/src/test/scala/coursier/test/IvyLocalTests.scala +++ b/tests/jvm/src/test/scala/coursier/test/IvyLocalTests.scala @@ -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) + } } } From 92e5917af6ca2df1c4319cc0042ad1d0a60a9df2 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:06 +0100 Subject: [PATCH 02/14] Simplify ClassLoader isolation in launch command No more SBT-inspired ClasspathFilter. It now just tries to find the ClassLoader that loaded coursier, and use the parent as a base to load the launched application. Allows to remove the -b option of the bootstrap command, and remove BaseCommand (that was holding options common to all commands). --- .../src/main/java/coursier/Bootstrap.java | 13 +- .../java/coursier/BootstrapClassLoader.java | 25 ++++ .../scala-2.11/coursier/cli/Coursier.scala | 75 ++++++----- .../coursier/ResolutionClassLoader.scala | 37 ------ .../scala/coursier/util/ClasspathFilter.scala | 116 ------------------ project/generate-launcher.sh | 1 - 6 files changed, 70 insertions(+), 197 deletions(-) create mode 100644 bootstrap/src/main/java/coursier/BootstrapClassLoader.java delete mode 100644 core/jvm/src/main/scala/coursier/ResolutionClassLoader.scala delete mode 100644 core/jvm/src/main/scala/coursier/util/ClasspathFilter.scala diff --git a/bootstrap/src/main/java/coursier/Bootstrap.java b/bootstrap/src/main/java/coursier/Bootstrap.java index d30c3b1b3..76359d773 100644 --- a/bootstrap/src/main/java/coursier/Bootstrap.java +++ b/bootstrap/src/main/java/coursier/Bootstrap.java @@ -8,7 +8,6 @@ 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.nio.file.Files; import java.security.CodeSource; @@ -170,8 +169,6 @@ public class Bootstrap { 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()) { @@ -267,7 +264,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 +285,6 @@ public class Bootstrap { List 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]); diff --git a/bootstrap/src/main/java/coursier/BootstrapClassLoader.java b/bootstrap/src/main/java/coursier/BootstrapClassLoader.java new file mode 100644 index 000000000..3de06d173 --- /dev/null +++ b/bootstrap/src/main/java/coursier/BootstrapClassLoader.java @@ -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; + } + +} 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 af38c3003..bd9a8b06c 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala @@ -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)") @@ -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) @@ -307,9 +344,6 @@ 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.") @Value("key=value") @Short("P") @@ -440,7 +474,6 @@ case class Bootstrap( val properties = new Properties() properties.setProperty("bootstrap.mainClass", mainClass) properties.setProperty("bootstrap.jarDir", downloadDir) - properties.setProperty("bootstrap.prependClasspath", prependClasspath.toString) outputZip.putNextEntry(propsEntry) properties.store(outputZip, "") @@ -487,27 +520,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] } diff --git a/core/jvm/src/main/scala/coursier/ResolutionClassLoader.scala b/core/jvm/src/main/scala/coursier/ResolutionClassLoader.scala deleted file mode 100644 index 3f852bb9c..000000000 --- a/core/jvm/src/main/scala/coursier/ResolutionClassLoader.scala +++ /dev/null @@ -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) - } - -} diff --git a/core/jvm/src/main/scala/coursier/util/ClasspathFilter.scala b/core/jvm/src/main/scala/coursier/util/ClasspathFilter.scala deleted file mode 100644 index 7272a2723..000000000 --- a/core/jvm/src/main/scala/coursier/util/ClasspathFilter.scala +++ /dev/null @@ -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 - } -} diff --git a/project/generate-launcher.sh b/project/generate-launcher.sh index 65eeae6e8..706a31de6 100755 --- a/project/generate-launcher.sh +++ b/project/generate-launcher.sh @@ -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" From 63d0d7d1c3ec261cfe793c0951ba8613ebb5d9f8 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:07 +0100 Subject: [PATCH 03/14] Fix help message --- cli/src/main/scala-2.11/coursier/cli/Coursier.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bd9a8b06c..be2833cca 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala @@ -344,7 +344,7 @@ case class Bootstrap( downloadDir: String, @Short("f") force: Boolean, - @Help("Set environment variables in the generated launcher. No escaping is done. Value is simply put between quotes in the launcher preamble.") + @Help("Set Java properties in the generated launcher.") @Value("key=value") @Short("P") property: List[String], From 58e6375c337dd222a0477fdaa1cfb5047ef9bc25 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:07 +0100 Subject: [PATCH 04/14] Add support for standalone launchers in bootstrap command --- .../src/main/java/coursier/Bootstrap.java | 198 +++++++++++------- .../scala-2.11/coursier/cli/Coursier.scala | 69 +++++- 2 files changed, 184 insertions(+), 83 deletions(-) diff --git a/bootstrap/src/main/java/coursier/Bootstrap.java b/bootstrap/src/main/java/coursier/Bootstrap.java index 76359d773..a0494fa00 100644 --- a/bootstrap/src/main/java/coursier/Bootstrap.java +++ b/bootstrap/src/main/java/coursier/Bootstrap.java @@ -1,14 +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.URLConnection; +import java.net.*; import java.nio.file.Files; import java.security.CodeSource; import java.security.ProtectionDomain; @@ -39,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 { @@ -53,21 +49,16 @@ public class Bootstrap { return content.split("\n"); } - static Map readIsolationContexts(File jarDir, String[] isolationIDs) throws IOException { + static Map readIsolationContexts(File jarDir, String[] isolationIDs, String bootstrapProtocol, ClassLoader loader) throws IOException { final Map perContextURLs = new LinkedHashMap<>(); for (String isolationID: isolationIDs) { - String[] contextURLs = readStringSequence("bootstrap-isolation-" + isolationID + "-jar-urls"); - List 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 urls = getURLs(strUrls, resources, bootstrapProtocol, loader); + List localURLs = getLocalURLs(urls, jarDir, bootstrapProtocol); + + perContextURLs.put(isolationID, localURLs.toArray(new URL[localURLs.size()])); } return perContextURLs; @@ -140,8 +131,7 @@ public class Bootstrap { return new File(jarDir, fileName); } - - public static void main(String[] args) throws Throwable { + static List getLocalURLs(List urls, final File jarDir, String bootstrapProtocol) { ThreadFactory threadFactory = new ThreadFactory() { // from scalaz Strategy.DefaultDaemonThreadFactory @@ -155,59 +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 properties = loadPropertiesMap(Thread.currentThread().getContextClassLoader().getResourceAsStream("bootstrap.properties")); - for (Map.Entry ent : properties.entrySet()) { - System.setProperty(ent.getKey(), ent.getValue()); - } - - 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."); - - String[] jarStrUrls = readStringSequence(defaultURLResource); - - List errors = new ArrayList<>(); - List 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 completionService = new ExecutorCompletionService<>(pool); List 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() { @@ -233,8 +182,6 @@ public class Bootstrap { return dest.toURI().toURL(); } }); - } else { - localURLs.add(url); } } @@ -253,8 +200,115 @@ public class Bootstrap { exit("Interrupted"); } - final String[] isolationIDs = readStringSequence(isolationIDsResource); - final Map 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 properties = loadPropertiesMap(loader.getResourceAsStream(resource)); + for (Map.Entry ent : properties.entrySet()) { + System.setProperty(ent.getKey(), ent.getValue()); + } + } + + static List getURLs(String[] rawURLs, String[] resources, String bootstrapProtocol, ClassLoader loader) throws MalformedURLException { + + List errors = new ArrayList<>(); + List 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 urls = getURLs(strUrls, resources, protocol, contextLoader); + List localURLs = getLocalURLs(urls, jarDir, protocol); + + String[] isolationIDs = readStringSequence(isolationIDsResource); + Map perIsolationContextURLs = readIsolationContexts(jarDir, isolationIDs, protocol, contextLoader); Thread thread = Thread.currentThread(); ClassLoader parentClassLoader = thread.getContextClassLoader(); 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 be2833cca..b1f0a8cae 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala @@ -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 @@ -344,6 +344,9 @@ case class Bootstrap( 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") @@ -361,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) @@ -415,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")}") @@ -457,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) { @@ -464,16 +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) + if (!standalone) + properties.setProperty("bootstrap.jarDir", downloadDir) outputZip.putNextEntry(propsEntry) properties.store(outputZip, "") From 08312b9c2e15d2d77076274a8fcf7f3f565de38e Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:07 +0100 Subject: [PATCH 05/14] Catch NoSuchMethodExceptions in launcher Fixes https://github.com/alexarchambault/coursier/issues/76 --- cli/src/main/scala-2.11/coursier/cli/Coursier.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b1f0a8cae..b010e28d5 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala @@ -320,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) } From e74984da5d75f0c83d4c3b427ccabc10cf9995e0 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:07 +0100 Subject: [PATCH 06/14] Minor fix in tests --- tests/shared/src/test/scala/coursier/test/CentralTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shared/src/test/scala/coursier/test/CentralTests.scala b/tests/shared/src/test/scala/coursier/test/CentralTests.scala index f8b4b85fc..7cd86c54e 100644 --- a/tests/shared/src/test/scala/coursier/test/CentralTests.scala +++ b/tests/shared/src/test/scala/coursier/test/CentralTests.scala @@ -66,7 +66,7 @@ object CentralTests extends TestSuite { .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) } From a254b9c21cc1ca75909e7a80b9b4ef10323e2d45 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:08 +0100 Subject: [PATCH 07/14] Add support for parent project POM properties Fixes https://github.com/alexarchambault/coursier/issues/120 --- .../main/scala/coursier/core/Resolution.scala | 9 ++++++++- .../com.github.fommil.netlib/all/1.1.2 | 17 +++++++++++++++++ .../test/scala/coursier/test/CentralTests.scala | 6 ++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/shared/src/test/resources/resolutions/com.github.fommil.netlib/all/1.1.2 diff --git a/core/shared/src/main/scala/coursier/core/Resolution.scala b/core/shared/src/main/scala/coursier/core/Resolution.scala index f8bbaf818..ed3304d5e 100644 --- a/core/shared/src/main/scala/coursier/core/Resolution.scala +++ b/core/shared/src/main/scala/coursier/core/Resolution.scala @@ -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) diff --git a/tests/shared/src/test/resources/resolutions/com.github.fommil.netlib/all/1.1.2 b/tests/shared/src/test/resources/resolutions/com.github.fommil.netlib/all/1.1.2 new file mode 100644 index 000000000..de65a7709 --- /dev/null +++ b/tests/shared/src/test/resources/resolutions/com.github.fommil.netlib/all/1.1.2 @@ -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 diff --git a/tests/shared/src/test/scala/coursier/test/CentralTests.scala b/tests/shared/src/test/scala/coursier/test/CentralTests.scala index 7cd86c54e..8da90698c 100644 --- a/tests/shared/src/test/scala/coursier/test/CentralTests.scala +++ b/tests/shared/src/test/scala/coursier/test/CentralTests.scala @@ -143,6 +143,12 @@ 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" + ) + } } } From bf7386a21872a155b99bd948a72f538cb61b0c8c Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:08 +0100 Subject: [PATCH 08/14] Print results of the resolution command on stdout and unindented For easier piping to other processes --- cli/src/main/scala-2.11/coursier/cli/Coursier.scala | 2 +- cli/src/main/scala-2.11/coursier/cli/Helper.scala | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) 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 b010e28d5..8dfbaf506 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala @@ -64,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) } diff --git a/cli/src/main/scala-2.11/coursier/cli/Helper.scala b/cli/src/main/scala-2.11/coursier/cli/Helper.scala index 62eca141c..169e5643b 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Helper.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Helper.scala @@ -58,7 +58,8 @@ object Util { class Helper( common: CommonOptions, - rawDependencies: Seq[String] + rawDependencies: Seq[String], + printResultStdout: Boolean = false ) { import common._ import Helper.errPrintln @@ -214,7 +215,10 @@ class Helper( val trDeps = res.minDependencies.toVector - if (verbose0 >= 0) + if (printResultStdout) { + errPrintln(s"Result:") + println(Print.dependenciesUnknownConfigs(trDeps)) + } else if (verbose0 >= 0) errPrintln(s"Result:\n${indent(Print.dependenciesUnknownConfigs(trDeps))}") def fetch( From 061dbe2f91d4552769a8a6783fa85a75842640b4 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:08 +0100 Subject: [PATCH 09/14] Slightly different indentation in output --- .../main/scala-2.11/coursier/cli/Helper.scala | 39 +++++++++---------- .../src/main/scala/coursier/util/Print.scala | 18 ++++++--- .../src/main/scala-2.10/coursier/Tasks.scala | 6 ++- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/cli/src/main/scala-2.11/coursier/cli/Helper.scala b/cli/src/main/scala-2.11/coursier/cli/Helper.scala index 169e5643b..e5cf2c2eb 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Helper.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Helper.scala @@ -169,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") } } @@ -210,16 +204,21 @@ class Helper( } exitIf(res.conflicts.nonEmpty) { - s"${res.conflicts.size} conflict(s):\n${Print.dependenciesUnknownConfigs(res.conflicts.toVector)}" + s"${res.conflicts.size} conflict(s):\n${Print.dependenciesUnknownConfigs(res.conflicts.toVector, projCache)}" } val trDeps = res.minDependencies.toVector - if (printResultStdout) { - errPrintln(s"Result:") - println(Print.dependenciesUnknownConfigs(trDeps)) - } else 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, @@ -230,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) @@ -259,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)( @@ -278,9 +277,9 @@ class Helper( logger.foreach(_.stop()) exitIf(errors.nonEmpty) { - s"${errors.size} error(s):\n" + + s" ${errors.size} error(s):\n" + errors.map { case (artifact, error) => - s" ${artifact.url}: $error" + s"${artifact.url}: $error" }.mkString("\n") } diff --git a/core/shared/src/main/scala/coursier/util/Print.scala b/core/shared/src/main/scala/coursier/util/Print.scala index 6680a8d28..23309aacc 100644 --- a/core/shared/src/main/scala/coursier/util/Print.scala +++ b/core/shared/src/main/scala/coursier/util/Print.scala @@ -1,20 +1,28 @@ 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 dependenciesUnknownConfigs(deps: Seq[Dependency]): String = { + 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 +32,7 @@ object Print { (dep.module.organization, dep.module.name, dep.module.toString, dep.version) } - deps0.map(dependency).mkString("\n") + deps1.map(dependency).mkString("\n") } } diff --git a/plugin/src/main/scala-2.10/coursier/Tasks.scala b/plugin/src/main/scala-2.10/coursier/Tasks.scala index 421ed0f5f..5790e0a25 100644 --- a/plugin/src/main/scala-2.10/coursier/Tasks.scala +++ b/plugin/src/main/scala-2.10/coursier/Tasks.scala @@ -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")) } From 733049487336c002914505fc3983d895eee86fb5 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:08 +0100 Subject: [PATCH 10/14] Add support for Ivy latest revision syntax Like 2.2.+ for [2.2.0,2.3.0) Fixes https://github.com/alexarchambault/coursier/issues/104 --- .../src/main/scala/coursier/core/Parse.scala | 16 ++++++++++ .../main/scala/coursier/core/Version.scala | 13 +++++++- .../coursier/maven/MavenRepository.scala | 1 + .../com.chuusai/shapeless_2.11/2.2.+ | 2 ++ .../com.chuusai/shapeless_2.11/[2.2.0,2.3.0) | 2 ++ .../libphonenumber/7.0.+ | 1 + .../libphonenumber/[7.0,7.1) | 1 + .../scala/coursier/test/CentralTests.scala | 31 ++++++++++++++++++- 8 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 tests/shared/src/test/resources/resolutions/com.chuusai/shapeless_2.11/2.2.+ create mode 100644 tests/shared/src/test/resources/resolutions/com.chuusai/shapeless_2.11/[2.2.0,2.3.0) create mode 100644 tests/shared/src/test/resources/resolutions/com.googlecode.libphonenumber/libphonenumber/7.0.+ create mode 100644 tests/shared/src/test/resources/resolutions/com.googlecode.libphonenumber/libphonenumber/[7.0,7.1) diff --git a/core/shared/src/main/scala/coursier/core/Parse.scala b/core/shared/src/main/scala/coursier/core/Parse.scala index a813b26f1..eacc42788 100644 --- a/core/shared/src/main/scala/coursier/core/Parse.scala +++ b/core/shared/src/main/scala/coursier/core/Parse.scala @@ -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)) } diff --git a/core/shared/src/main/scala/coursier/core/Version.scala b/core/shared/src/main/scala/coursier/core/Version.scala index e01ebfb77..b15d3f9ff 100644 --- a/core/shared/src/main/scala/coursier/core/Version.scala +++ b/core/shared/src/main/scala/coursier/core/Version.scala @@ -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 { diff --git a/core/shared/src/main/scala/coursier/maven/MavenRepository.scala b/core/shared/src/main/scala/coursier/maven/MavenRepository.scala index 3eddb4b7c..f08bfcb97 100644 --- a/core/shared/src/main/scala/coursier/maven/MavenRepository.scala +++ b/core/shared/src/main/scala/coursier/maven/MavenRepository.scala @@ -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, _)) diff --git a/tests/shared/src/test/resources/resolutions/com.chuusai/shapeless_2.11/2.2.+ b/tests/shared/src/test/resources/resolutions/com.chuusai/shapeless_2.11/2.2.+ new file mode 100644 index 000000000..be25eaf98 --- /dev/null +++ b/tests/shared/src/test/resources/resolutions/com.chuusai/shapeless_2.11/2.2.+ @@ -0,0 +1,2 @@ +com.chuusai:shapeless_2.11:jar:2.2.5 +org.scala-lang:scala-library:jar:2.11.7 diff --git a/tests/shared/src/test/resources/resolutions/com.chuusai/shapeless_2.11/[2.2.0,2.3.0) b/tests/shared/src/test/resources/resolutions/com.chuusai/shapeless_2.11/[2.2.0,2.3.0) new file mode 100644 index 000000000..be25eaf98 --- /dev/null +++ b/tests/shared/src/test/resources/resolutions/com.chuusai/shapeless_2.11/[2.2.0,2.3.0) @@ -0,0 +1,2 @@ +com.chuusai:shapeless_2.11:jar:2.2.5 +org.scala-lang:scala-library:jar:2.11.7 diff --git a/tests/shared/src/test/resources/resolutions/com.googlecode.libphonenumber/libphonenumber/7.0.+ b/tests/shared/src/test/resources/resolutions/com.googlecode.libphonenumber/libphonenumber/7.0.+ new file mode 100644 index 000000000..2c9c7326a --- /dev/null +++ b/tests/shared/src/test/resources/resolutions/com.googlecode.libphonenumber/libphonenumber/7.0.+ @@ -0,0 +1 @@ +com.googlecode.libphonenumber:libphonenumber:jar:7.0.11 diff --git a/tests/shared/src/test/resources/resolutions/com.googlecode.libphonenumber/libphonenumber/[7.0,7.1) b/tests/shared/src/test/resources/resolutions/com.googlecode.libphonenumber/libphonenumber/[7.0,7.1) new file mode 100644 index 000000000..2c9c7326a --- /dev/null +++ b/tests/shared/src/test/resources/resolutions/com.googlecode.libphonenumber/libphonenumber/[7.0,7.1) @@ -0,0 +1 @@ +com.googlecode.libphonenumber:libphonenumber:jar:7.0.11 diff --git a/tests/shared/src/test/scala/coursier/test/CentralTests.scala b/tests/shared/src/test/scala/coursier/test/CentralTests.scala index 8da90698c..3e0704f39 100644 --- a/tests/shared/src/test/scala/coursier/test/CentralTests.scala +++ b/tests/shared/src/test/scala/coursier/test/CentralTests.scala @@ -61,7 +61,15 @@ 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 @@ -149,6 +157,27 @@ object CentralTests extends TestSuite { "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.+" + ) + } } } From 22fd372b0e95523acd0cd297c2d622ca5660b6cc Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:09 +0100 Subject: [PATCH 11/14] Print exclusions in output Fixes https://github.com/alexarchambault/coursier/issues/118 --- core/shared/src/main/scala/coursier/util/Print.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/shared/src/main/scala/coursier/util/Print.scala b/core/shared/src/main/scala/coursier/util/Print.scala index 23309aacc..bbde82229 100644 --- a/core/shared/src/main/scala/coursier/util/Print.scala +++ b/core/shared/src/main/scala/coursier/util/Print.scala @@ -4,8 +4,14 @@ 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 + + s"${dep.module}:${dep.version}:${dep.configuration}$exclusionsStr" + } def dependenciesUnknownConfigs(deps: Seq[Dependency], projects: Map[(Module, String), Project]): String = { From 8b2f3fcc0404cbf8463d6b4f4646fc88ba32502b Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:09 +0100 Subject: [PATCH 12/14] Better error message on 404 Not Found Fixes https://github.com/alexarchambault/coursier/issues/111 --- cache/src/main/scala/coursier/Cache.scala | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cache/src/main/scala/coursier/Cache.scala b/cache/src/main/scala/coursier/Cache.scala index f1383745e..1675bb890 100644 --- a/cache/src/main/scala/coursier/Cache.scala +++ b/cache/src/main/scala/coursier/Cache.scala @@ -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)) } From 3d1beadea934aa869a49e27bfd9ff5f6f51298e3 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sat, 23 Jan 2016 15:42:09 +0100 Subject: [PATCH 13/14] Slightly better error printing --- cache/src/main/scala/coursier/Cache.scala | 10 +++++----- cli/src/main/scala-2.11/coursier/cli/Helper.scala | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cache/src/main/scala/coursier/Cache.scala b/cache/src/main/scala/coursier/Cache.scala index 1675bb890..6cc16b6af 100644 --- a/cache/src/main/scala/coursier/Cache.scala +++ b/cache/src/main/scala/coursier/Cache.scala @@ -580,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" } } diff --git a/cli/src/main/scala-2.11/coursier/cli/Helper.scala b/cli/src/main/scala-2.11/coursier/cli/Helper.scala index e5cf2c2eb..1305f382e 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Helper.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Helper.scala @@ -197,14 +197,14 @@ 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, projCache)}" + s"\nConflict:\n${Print.dependenciesUnknownConfigs(res.conflicts.toVector, projCache)}" } val trDeps = res.minDependencies.toVector @@ -277,7 +277,7 @@ 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" }.mkString("\n") From a9b205e7418a89788fe7224277324a7e79bd4eb1 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Tue, 26 Jan 2016 18:16:55 +0100 Subject: [PATCH 14/14] Remove JDK 7 CI jobs --- .travis.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index ce58cc754..c183ff96c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: