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, "")