diff --git a/bootstrap/src/main/java/coursier/Bootstrap.java b/bootstrap/src/main/java/coursier/Bootstrap.java index 4c1035d38..a86b7ef2f 100644 --- a/bootstrap/src/main/java/coursier/Bootstrap.java +++ b/bootstrap/src/main/java/coursier/Bootstrap.java @@ -11,9 +11,10 @@ import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.nio.file.Files; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import java.util.concurrent.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class Bootstrap { @@ -36,10 +37,55 @@ public class Bootstrap { return buffer.toByteArray(); } - final static String usage = "Usage: bootstrap main-class JAR-directory JAR-URLs..."; + static String[] readJarUrls() throws IOException { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + InputStream is = loader.getResourceAsStream("bootstrap-jar-urls"); + byte[] rawContent = readFullySync(is); + String content = new String(rawContent, "UTF-8"); + return content.split("\n"); + } final static int concurrentDownloadCount = 6; + // http://stackoverflow.com/questions/872272/how-to-reference-another-property-in-java-util-properties/27724276#27724276 + public static Map loadPropertiesMap(InputStream s) throws IOException { + final Map ordered = new LinkedHashMap<>(); + //Hack to use properties class to parse but our map for preserved order + Properties bp = new Properties() { + @Override + public synchronized Object put(Object key, Object value) { + ordered.put((String)key, (String)value); + return super.put(key, value); + } + }; + bp.load(s); + + + final Pattern propertyRegex = Pattern.compile(Pattern.quote("${") + "[^" + Pattern.quote("{[()]}") + "]*" + Pattern.quote("}")); + + final Map resolved = new LinkedHashMap<>(ordered.size()); + + for (String k : ordered.keySet()) { + String value = ordered.get(k); + + Matcher matcher = propertyRegex.matcher(value); + + // cycles would loop indefinitely here :-| + while (matcher.find()) { + int start = matcher.start(0); + int end = matcher.end(0); + String subKey = value.substring(start + 2, end - 1); + String subValue = resolved.get(subKey); + if (subValue == null) + subValue = System.getProperty(subKey); + value = value.substring(0, start) + subValue + value.substring(end); + } + + resolved.put(k, value); + } + return resolved; + } + public static void main(String[] args) throws Throwable { ThreadFactory threadFactory = new ThreadFactory() { @@ -54,25 +100,15 @@ public class Bootstrap { ExecutorService pool = Executors.newFixedThreadPool(concurrentDownloadCount, threadFactory); - boolean prependClasspath = false; - - if (args.length > 0 && args[0].equals("-B")) - prependClasspath = true; - - if (args.length < 2 || (prependClasspath && args.length < 3)) { - exit(usage); + Map properties = loadPropertiesMap(Thread.currentThread().getContextClassLoader().getResourceAsStream("bootstrap.properties")); + for (Map.Entry ent : properties.entrySet()) { + System.setProperty(ent.getKey(), ent.getValue()); } - int offset = 0; - if (prependClasspath) - offset += 1; + String mainClass0 = System.getProperty("bootstrap.mainClass"); + String jarDir0 = System.getProperty("bootstrap.jarDir"); - String mainClass0 = args[offset]; - String jarDir0 = args[offset + 1]; - - List remainingArgs = new ArrayList<>(); - for (int i = offset + 2; i < args.length; i++) - remainingArgs.add(args[i]); + boolean prependClasspath = Boolean.parseBoolean(System.getProperty("bootstrap.prependClasspath", "false")); final File jarDir = new File(jarDir0); @@ -82,17 +118,7 @@ public class Bootstrap { } else if (!jarDir.mkdirs()) System.err.println("Warning: cannot create " + jarDir0 + ", continuing anyway."); - int splitIdx = remainingArgs.indexOf("--"); - List jarStrUrls; - List userArgs; - - if (splitIdx < 0) { - jarStrUrls = remainingArgs; - userArgs = new ArrayList<>(); - } else { - jarStrUrls = remainingArgs.subList(0, splitIdx); - userArgs = remainingArgs.subList(splitIdx + 1, remainingArgs.size()); - } + String[] jarStrUrls = readJarUrls(); List errors = new ArrayList<>(); List urls = new ArrayList<>(); @@ -204,7 +230,8 @@ public class Bootstrap { } } - userArgs0.addAll(userArgs); + for (int i = 0; i < args.length; i++) + userArgs0.add(args[i]); thread.setContextClassLoader(classLoader); try { 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 4f0dba5b6..7ea1b733f 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala @@ -1,10 +1,12 @@ package coursier package cli -import java.io.{ File, IOException } +import java.io.{ByteArrayOutputStream, FileOutputStream, File, IOException} import java.net.URLClassLoader import java.nio.file.{ Files => NIOFiles } -import java.nio.file.attribute.PosixFilePermission +import java.nio.file.attribute.{FileTime, PosixFilePermission} +import java.util.Properties +import java.util.zip.{ZipEntry, ZipOutputStream, ZipInputStream, ZipFile} import caseapp._ import coursier.util.ClasspathFilter @@ -211,9 +213,9 @@ case class Bootstrap( @ExtraName("b") prependClasspath: Boolean, @HelpMessage("Set environment variables in the generated launcher. No escaping is done. Value is simply put between quotes in the launcher preamble.") - @ValueDescription("NAME=VALUE") - @ExtraName("e") - env: List[String], + @ValueDescription("key=value") + @ExtraName("P") + property: List[String], @Recurse common: CommonOptions ) extends CoursierCommand { @@ -231,24 +233,18 @@ case class Bootstrap( sys.exit(255) } - val (validEnv, wrongEnv) = env.partition(_.contains("=")) - if (wrongEnv.nonEmpty) { - Console.err.println(s"Wrong -e / --env option(s):\n${wrongEnv.mkString("\n")}") + val (validProperties, wrongProperties) = property.partition(_.contains("=")) + if (wrongProperties.nonEmpty) { + Console.err.println(s"Wrong -P / --property option(s):\n${wrongProperties.mkString("\n")}") sys.exit(255) } - val env0 = validEnv.map { s => + val properties0 = validProperties.map { s => val idx = s.indexOf('=') assert(idx >= 0) (s.take(idx), s.drop(idx + 1)) } - val downloadDir0 = - if (downloadDir.isEmpty) - "$HOME/" - else - downloadDir - val bootstrapJar = Option(Thread.currentThread().getContextClassLoader.getResourceAsStream("bootstrap.jar")) match { case Some(is) => Cache.readFullySync(is) @@ -257,6 +253,32 @@ case class Bootstrap( sys.exit(1) } + val output0 = new File(output) + if (!force && output0.exists()) { + Console.err.println(s"Error: $output already exists, use -f option to force erasing it.") + sys.exit(1) + } + + def zipEntries(zipStream: ZipInputStream): Iterator[(ZipEntry, Array[Byte])] = + new Iterator[(ZipEntry, Array[Byte])] { + var nextEntry = Option.empty[ZipEntry] + def update() = + nextEntry = Option(zipStream.getNextEntry) + + update() + + def hasNext = nextEntry.nonEmpty + def next() = { + val ent = nextEntry.get + val data = Platform.readFullySync(zipStream) + + update() + + (ent, data) + } + } + + val helper = new Helper(common, remainingArgs) val artifacts = helper.res.artifacts @@ -267,29 +289,57 @@ case class Bootstrap( if (unrecognized.nonEmpty) Console.err.println(s"Warning: non HTTP URLs:\n${unrecognized.mkString("\n")}") - val output0 = new File(output) - if (!force && output0.exists()) { - Console.err.println(s"Error: $output already exists, use -f option to force erasing it.") - sys.exit(1) + val buffer = new ByteArrayOutputStream() + + val bootstrapZip = new ZipInputStream(Thread.currentThread().getContextClassLoader.getResourceAsStream("bootstrap.jar")) + val outputZip = new ZipOutputStream(buffer) + + for ((ent, data) <- zipEntries(bootstrapZip)) { + outputZip.putNextEntry(ent) + outputZip.write(data) + outputZip.closeEntry() } - val shellPreamble = { - Seq( - "#!/usr/bin/env sh" - ) ++ - env0.map { case (k, v) => "export " + k + "=\"" + v + "\"" } ++ - Seq( - "exec java -jar \"$0\" " + (if (prependClasspath) "-B " else "") + "\"" + mainClass + "\" \"" + downloadDir + "\" " + urls.map("\"" + _ + "\"").mkString(" ") + " -- \"$@\"", - "" - ) - }.mkString("\n") + val time = FileTime.fromMillis(System.currentTimeMillis()) - try NIOFiles.write(output0.toPath, shellPreamble.getBytes("UTF-8") ++ bootstrapJar) + val jarListEntry = new ZipEntry("bootstrap-jar-urls") + jarListEntry.setCreationTime(time) + jarListEntry.setLastAccessTime(time) + jarListEntry.setLastModifiedTime(time) + + outputZip.putNextEntry(jarListEntry) + outputZip.write(urls.mkString("\n").getBytes("UTF-8")) + outputZip.closeEntry() + + val propsEntry = new ZipEntry("bootstrap.properties") + propsEntry.setCreationTime(time) + propsEntry.setLastAccessTime(time) + propsEntry.setLastModifiedTime(time) + + 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, "") + outputZip.closeEntry() + + outputZip.close() + + + val shellPreamble = Seq( + "#!/usr/bin/env sh", + "exec java -jar \"$0\" \"$@\"" + ).mkString("", "\n", "\n") + + try NIOFiles.write(output0.toPath, shellPreamble.getBytes("UTF-8") ++ buffer.toByteArray) catch { case e: IOException => Console.err.println(s"Error while writing $output0: ${e.getMessage}") sys.exit(1) } + try { val perms = NIOFiles.getPosixFilePermissions(output0.toPath).asScala.toSet diff --git a/project/generate-launcher.sh b/project/generate-launcher.sh index 6b470f339..e19814491 100755 --- a/project/generate-launcher.sh +++ b/project/generate-launcher.sh @@ -7,11 +7,11 @@ CACHE_VERSION=v1 com.github.alexarchambault:coursier-cli_2.11:$VERSION \ -V com.github.alexarchambault:coursier_2.11:$VERSION \ -V com.github.alexarchambault:coursier-cache_2.11:$VERSION \ - -D "\$HOME/.coursier/bootstrap/$VERSION" \ + -D "\${user.home}/.coursier/bootstrap/$VERSION" \ -r https://repo1.maven.org/maven2 \ -r https://oss.sonatype.org/content/repositories/releases \ -r https://oss.sonatype.org/content/repositories/snapshots \ -b \ -f -o coursier \ -M coursier.cli.Coursier \ - -e COURSIER_CACHE="\$HOME/.coursier/cache/$CACHE_VERSION" + -P coursier.cache="\${user.home}/.coursier/cache/$CACHE_VERSION"