Add support for standalone launchers in bootstrap command

This commit is contained in:
Alexandre Archambault 2016-01-23 15:42:07 +01:00
parent 63d0d7d1c3
commit 58e6375c33
2 changed files with 184 additions and 83 deletions

View File

@ -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<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;
@ -140,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
@ -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<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");
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>() {
@ -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<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();

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