1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package tests;
  24 
  25 import java.io.ByteArrayInputStream;
  26 import java.io.ByteArrayOutputStream;
  27 import java.io.File;
  28 import java.io.FileOutputStream;
  29 import java.io.IOException;
  30 import java.io.InputStream;
  31 import java.io.OutputStream;
  32 import java.io.PrintStream;
  33 import java.io.PrintWriter;
  34 import java.io.StringWriter;
  35 import java.nio.file.Files;
  36 import java.nio.file.Path;
  37 import java.nio.file.Paths;
  38 import java.nio.file.StandardCopyOption;
  39 import java.util.ArrayList;
  40 import java.util.Arrays;
  41 import java.util.Collections;
  42 import java.util.HashSet;
  43 import java.util.List;
  44 import java.util.Objects;
  45 import java.util.Set;
  46 import java.util.jar.JarEntry;
  47 import java.util.jar.JarInputStream;
  48 import java.util.jar.JarOutputStream;
  49 import java.util.stream.Collectors;
  50 import java.util.stream.Stream;
  51 import java.util.zip.ZipEntry;
  52 
  53 import javax.tools.JavaCompiler;
  54 import javax.tools.StandardJavaFileManager;
  55 import javax.tools.StandardLocation;
  56 import javax.tools.ToolProvider;
  57 
  58 /**
  59  *
  60  * A generator for jmods, jars and images.
  61  */
  62 public class JImageGenerator {
  63 
  64     public static final String LOAD_ALL_CLASSES_TEMPLATE = "package PACKAGE;\n"
  65             + "\n"
  66             + "import java.net.URI;\n"
  67             + "import java.nio.file.FileSystems;\n"
  68             + "import java.nio.file.Files;\n"
  69             + "import java.nio.file.Path;\n"
  70             + "import java.util.function.Function;\n"
  71             + "\n"
  72             + "public class CLASS {\n"
  73             + "    private static long total_time;\n"
  74             + "    private static long num_classes;\n"
  75             + "    public static void main(String[] args) throws Exception {\n"
  76             + "        Function<Path, String> formatter = (path) -> {\n"
  77             + "            String clazz = path.toString().substring(\"modules/\".length()+1, path.toString().lastIndexOf(\".\"));\n"
  78             + "            clazz = clazz.substring(clazz.indexOf(\"/\") + 1);\n"
  79             + "            return clazz.replaceAll(\"/\", \"\\\\.\");\n"
  80             + "        };\n"
  81             + "        Files.walk(FileSystems.getFileSystem(URI.create(\"jrt:/\")).getPath(\"/modules/\")).\n"
  82             + "                filter((p) -> {\n"
  83             + "                    return Files.isRegularFile(p) && p.toString().endsWith(\".class\")\n"
  84             + "                    && !p.toString().endsWith(\"module-info.class\");\n"
  85             + "                }).\n"
  86             + "                map(formatter).forEach((clazz) -> {\n"
  87             + "                    try {\n"
  88             + "                        long t = System.currentTimeMillis();\n"
  89             + "                        Class.forName(clazz, false, Thread.currentThread().getContextClassLoader());\n"
  90             + "                        total_time+= System.currentTimeMillis()-t;\n"
  91             + "                        num_classes+=1;\n"
  92             + "                    } catch (IllegalAccessError ex) {\n"
  93             + "                        // Security exceptions can occur, this is not what we are testing\n"
  94             + "                        System.err.println(\"Access error, OK \" + clazz);\n"
  95             + "                    } catch (Exception ex) {\n"
  96             + "                        System.err.println(\"ERROR \" + clazz);\n"
  97             + "                        throw new RuntimeException(ex);\n"
  98             + "                    }\n"
  99             + "                });\n"
 100             + "    double res = (double) total_time / num_classes;\n"
 101             + "    // System.out.println(\"Total time \" + total_time + \" num classes \" + num_classes + \" average \" + res);\n"
 102             + "    }\n"
 103             + "}\n";
 104 
 105     private static final String OUTPUT_OPTION = "--output";
 106     private static final String POST_PROCESS_OPTION = "--post-process-path";
 107     private static final String MAIN_CLASS_OPTION = "--main-class";
 108     private static final String CLASS_PATH_OPTION = "--class-path";
 109     private static final String MODULE_PATH_OPTION = "--module-path";
 110     private static final String ADD_MODULES_OPTION = "--add-modules";
 111     private static final String LIMIT_MODULES_OPTION = "--limit-modules";
 112     private static final String PLUGIN_MODULE_PATH = "--plugin-module-path";
 113 
 114     private static final String CMDS_OPTION = "--cmds";
 115     private static final String CONFIG_OPTION = "--config";
 116     private static final String HASH_MODULES_OPTION = "--hash-modules";
 117     private static final String LIBS_OPTION = "--libs";
 118     private static final String MODULE_VERSION_OPTION = "--module-version";
 119 
 120     private JImageGenerator() {}
 121 
 122     private static String optionsPrettyPrint(String... args) {
 123         return Stream.of(args).collect(Collectors.joining(" "));
 124     }
 125 
 126     public static File getJModsDir(File jdkHome) {
 127         File jdkjmods = new File(jdkHome, "jmods");
 128         if (!jdkjmods.exists()) {
 129             return null;
 130         }
 131         return jdkjmods;
 132     }
 133 
 134     public static Path addFiles(Path module, InMemoryFile... resources) throws IOException {
 135         Path tempFile = Files.createTempFile("jlink-test", "");
 136         try (JarInputStream in = new JarInputStream(Files.newInputStream(module));
 137              JarOutputStream out = new JarOutputStream(new FileOutputStream(tempFile.toFile()))) {
 138             ZipEntry entry;
 139             while ((entry = in.getNextEntry()) != null) {
 140                 String name = entry.getName();
 141                 out.putNextEntry(new ZipEntry(name));
 142                 copy(in, out);
 143                 out.closeEntry();
 144             }
 145             for (InMemoryFile r : resources) {
 146                 addFile(r, out);
 147             }
 148         }
 149         Files.move(tempFile, module, StandardCopyOption.REPLACE_EXISTING);
 150         return module;
 151     }
 152 
 153     private static void copy(InputStream in, OutputStream out) throws IOException {
 154         int len;
 155         byte[] buf = new byte[4096];
 156         while ((len = in.read(buf)) > 0) {
 157             out.write(buf, 0, len);
 158         }
 159     }
 160 
 161     public static JModTask getJModTask() {
 162         return new JModTask();
 163     }
 164 
 165     public static JLinkTask getJLinkTask() {
 166         return new JLinkTask();
 167     }
 168 
 169     public static JImageTask getJImageTask() {
 170         return new JImageTask();
 171     }
 172 
 173     private static void addFile(InMemoryFile resource, JarOutputStream target) throws IOException {
 174         String fileName = resource.getPath();
 175         fileName = fileName.replace("\\", "/");
 176         String[] ss = fileName.split("/");
 177         Path p = Paths.get("");
 178         for (int i = 0; i < ss.length; ++i) {
 179             if (i < ss.length - 1) {
 180                 if (!ss[i].isEmpty()) {
 181                     p = p.resolve(ss[i]);
 182                     JarEntry entry = new JarEntry(p.toString() + "/");
 183                     target.putNextEntry(entry);
 184                     target.closeEntry();
 185                 }
 186             } else {
 187                 p = p.resolve(ss[i]);
 188                 JarEntry entry = new JarEntry(p.toString());
 189                 target.putNextEntry(entry);
 190                 copy(resource.getBytes(), target);
 191                 target.closeEntry();
 192             }
 193         }
 194     }
 195 
 196     public static Path createNewFile(Path root, String pathName, String extension) {
 197         Path out = root.resolve(pathName + extension);
 198         int i = 1;
 199         while (Files.exists(out)) {
 200             out = root.resolve(pathName + "-" + (++i) + extension);
 201         }
 202         return out;
 203     }
 204 
 205     public static Path generateSources(Path output, String moduleName, List<InMemorySourceFile> sources) throws IOException {
 206         Path moduleDir = output.resolve(moduleName);
 207         Files.createDirectory(moduleDir);
 208         for (InMemorySourceFile source : sources) {
 209             Path fileDir = moduleDir;
 210             if (!source.packageName.isEmpty()) {
 211                 String dir = source.packageName.replace('.', File.separatorChar);
 212                 fileDir = moduleDir.resolve(dir);
 213                 Files.createDirectories(fileDir);
 214             }
 215             Files.write(fileDir.resolve(source.className + ".java"), source.source.getBytes());
 216         }
 217         return moduleDir;
 218     }
 219 
 220     public static Path generateSourcesFromTemplate(Path output, String moduleName, String... classNames) throws IOException {
 221         List<InMemorySourceFile> sources = new ArrayList<>();
 222         for (String className : classNames) {
 223             String packageName = getPackageName(className);
 224             String simpleName = getSimpleName(className);
 225             String content = LOAD_ALL_CLASSES_TEMPLATE
 226                     .replace("CLASS", simpleName);
 227             if (packageName.isEmpty()) {
 228                 content = content.replace("package PACKAGE;", packageName);
 229             } else {
 230                 content = content.replace("PACKAGE", packageName);
 231             }
 232             sources.add(new InMemorySourceFile(packageName, simpleName, content));
 233         }
 234         return generateSources(output, moduleName, sources);
 235     }
 236 
 237     public static void generateModuleInfo(Path moduleDir, List<String> packages, String... dependencies) throws IOException {
 238         StringBuilder moduleInfoBuilder = new StringBuilder();
 239         Path file = moduleDir.resolve("module-info.java");
 240         String moduleName = moduleDir.getFileName().toString();
 241         moduleInfoBuilder.append("module ").append(moduleName).append("{\n");
 242         for (String dep : dependencies) {
 243             moduleInfoBuilder.append("requires ").append(dep).append(";\n");
 244         }
 245         for (String pkg : packages) {
 246             if (!pkg.trim().isEmpty()) {
 247                 moduleInfoBuilder.append("exports ").append(pkg).append(";\n");
 248             }
 249         }
 250         moduleInfoBuilder.append("}");
 251         Files.write(file, moduleInfoBuilder.toString().getBytes());
 252     }
 253 
 254     public static void compileSuccess(Path source, Path destination, String... options) throws IOException {
 255         if (!compile(source, destination, options)) {
 256             throw new AssertionError("Compilation failed.");
 257         }
 258     }
 259 
 260     public static boolean compile(Path source, Path destination, String... options) throws IOException {
 261         JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
 262         try (StandardJavaFileManager jfm = compiler.getStandardFileManager(null, null, null)) {
 263             List<Path> sources
 264                     = Files.find(source, Integer.MAX_VALUE,
 265                     (file, attrs) -> file.toString().endsWith(".java"))
 266                     .collect(Collectors.toList());
 267 
 268             Files.createDirectories(destination);
 269             jfm.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Collections.singleton(destination));
 270 
 271             List<String> opts = Arrays.asList(options);
 272             JavaCompiler.CompilationTask task
 273                     = compiler.getTask(null, jfm, null, opts, null,
 274                     jfm.getJavaFileObjectsFromPaths(sources));
 275             List<String> list = new ArrayList<>(opts);
 276             list.addAll(sources.stream()
 277                     .map(Path::toString)
 278                     .collect(Collectors.toList()));
 279             System.err.println("javac options: " + optionsPrettyPrint(list.toArray(new String[list.size()])));
 280             return task.call();
 281         }
 282     }
 283 
 284     public static Path createJarFile(Path jarfile, Path dir) throws IOException {
 285         return createJarFile(jarfile, dir, Paths.get("."));
 286     }
 287 
 288     public static Path createJarFile(Path jarfile, Path dir, Path file) throws IOException {
 289         // create the target directory
 290         Path parent = jarfile.getParent();
 291         if (parent != null)
 292             Files.createDirectories(parent);
 293 
 294         List<Path> entries = Files.find(dir.resolve(file), Integer.MAX_VALUE,
 295                 (p, attrs) -> attrs.isRegularFile())
 296                 .map(dir::relativize)
 297                 .collect(Collectors.toList());
 298 
 299         try (OutputStream out = Files.newOutputStream(jarfile);
 300              JarOutputStream jos = new JarOutputStream(out)) {
 301             for (Path entry : entries) {
 302                 // map the file path to a name in the JAR file
 303                 Path normalized = entry.normalize();
 304                 String name = normalized
 305                         .subpath(0, normalized.getNameCount())  // drop root
 306                         .toString()
 307                         .replace(File.separatorChar, '/');
 308 
 309                 jos.putNextEntry(new JarEntry(name));
 310                 Files.copy(dir.resolve(entry), jos);
 311             }
 312         }
 313         return jarfile;
 314     }
 315 
 316     public static Set<String> getModuleContent(Path module) {
 317         Result result = JImageGenerator.getJModTask()
 318                 .jmod(module)
 319                 .list();
 320         result.assertSuccess();
 321         return Stream.of(result.getMessage().split("\r?\n"))
 322                 .collect(Collectors.toSet());
 323     }
 324 
 325     public static void checkModule(Path module, Set<String> expected) throws IOException {
 326         Set<String> actual = getModuleContent(module);
 327         if (!Objects.equals(actual, expected)) {
 328             Set<String> unexpected = new HashSet<>(actual);
 329             unexpected.removeAll(expected);
 330             Set<String> notFound = new HashSet<>(expected);
 331             notFound.removeAll(actual);
 332             System.err.println("Unexpected files:");
 333             unexpected.forEach(s -> System.err.println("\t" + s));
 334             System.err.println("Not found files:");
 335             notFound.forEach(s -> System.err.println("\t" + s));
 336             throw new AssertionError("Module check failed.");
 337         }
 338     }
 339 
 340     public static class JModTask {
 341         static final java.util.spi.ToolProvider JMOD_TOOL =
 342             java.util.spi.ToolProvider.findFirst("jmod").get();
 343 
 344         private final List<Path> classpath = new ArrayList<>();
 345         private final List<Path> libs = new ArrayList<>();
 346         private final List<Path> cmds = new ArrayList<>();
 347         private final List<Path> config = new ArrayList<>();
 348         private final List<Path> jars = new ArrayList<>();
 349         private final List<Path> jmods = new ArrayList<>();
 350         private final List<String> options = new ArrayList<>();
 351         private Path output;
 352         private String hashModules;
 353         private String mainClass;
 354         private String moduleVersion;
 355 
 356         public JModTask addNativeLibraries(Path cp) {
 357             this.libs.add(cp);
 358             return this;
 359         }
 360 
 361         public JModTask hashModules(String hash) {
 362             this.hashModules = hash;
 363             return this;
 364         }
 365 
 366         public JModTask addCmds(Path cp) {
 367             this.cmds.add(cp);
 368             return this;
 369         }
 370 
 371         public JModTask addClassPath(Path cp) {
 372             this.classpath.add(cp);
 373             return this;
 374         }
 375 
 376         public JModTask addConfig(Path cp) {
 377             this.config.add(cp);
 378             return this;
 379         }
 380 
 381         public JModTask addJars(Path jars) {
 382             this.jars.add(jars);
 383             return this;
 384         }
 385 
 386         public JModTask addJmods(Path jmods) {
 387             this.jmods.add(jmods);
 388             return this;
 389         }
 390 
 391         public JModTask jmod(Path output) {
 392             this.output = output;
 393             return this;
 394         }
 395 
 396         public JModTask moduleVersion(String moduleVersion) {
 397             this.moduleVersion = moduleVersion;
 398             return this;
 399         }
 400 
 401         public JModTask mainClass(String mainClass) {
 402             this.mainClass = mainClass;
 403             return this;
 404         }
 405 
 406         public JModTask option(String o) {
 407             this.options.add(o);
 408             return this;
 409         }
 410 
 411         private String modulePath() {
 412             // This is expect FIRST jmods THEN jars, if you change this, some tests could fail
 413             String jmods = toPath(this.jmods);
 414             String jars = toPath(this.jars);
 415             return jmods + File.pathSeparator + jars;
 416         }
 417 
 418         private String toPath(List<Path> paths) {
 419             return paths.stream()
 420                     .map(Path::toString)
 421                     .collect(Collectors.joining(File.pathSeparator));
 422         }
 423 
 424         private String[] optionsJMod(String cmd) {
 425             List<String> options = new ArrayList<>();
 426             options.add(cmd);
 427             if (!cmds.isEmpty()) {
 428                 options.add(CMDS_OPTION);
 429                 options.add(toPath(cmds));
 430             }
 431             if (!config.isEmpty()) {
 432                 options.add(CONFIG_OPTION);
 433                 options.add(toPath(config));
 434             }
 435             if (hashModules != null) {
 436                 options.add(HASH_MODULES_OPTION);
 437                 options.add(hashModules);
 438             }
 439             if (mainClass != null) {
 440                 options.add(MAIN_CLASS_OPTION);
 441                 options.add(mainClass);
 442             }
 443             if (!libs.isEmpty()) {
 444                 options.add(LIBS_OPTION);
 445                 options.add(toPath(libs));
 446             }
 447             if (!classpath.isEmpty()) {
 448                 options.add(CLASS_PATH_OPTION);
 449                 options.add(toPath(classpath));
 450             }
 451             if (!jars.isEmpty() || !jmods.isEmpty()) {
 452                 options.add(MODULE_PATH_OPTION);
 453                 options.add(modulePath());
 454             }
 455             if (moduleVersion != null) {
 456                 options.add(MODULE_VERSION_OPTION);
 457                 options.add(moduleVersion);
 458             }
 459             options.addAll(this.options);
 460             if (output != null) {
 461                 options.add(output.toString());
 462             }
 463             return options.toArray(new String[options.size()]);
 464         }
 465 
 466         public Result create() {
 467             return cmd("create");
 468         }
 469 
 470         public Result list() {
 471             return cmd("list");
 472         }
 473 
 474         public Result call() {
 475             return cmd("");
 476         }
 477 
 478         private Result cmd(String cmd) {
 479             String[] args = optionsJMod(cmd);
 480             System.err.println("jmod options: " + optionsPrettyPrint(args));
 481             ByteArrayOutputStream baos = new ByteArrayOutputStream();
 482             PrintStream ps = new PrintStream(baos);
 483             int exitCode = JMOD_TOOL.run(ps, ps, args);
 484             String msg = new String(baos.toByteArray());
 485             return new Result(exitCode, msg, output);
 486         }
 487     }
 488 
 489     public static String getPackageName(String canonicalName) {
 490         int index = canonicalName.lastIndexOf('.');
 491         return index > 0 ? canonicalName.substring(0, index) : "";
 492     }
 493 
 494     public static String getSimpleName(String canonicalName) {
 495         int index = canonicalName.lastIndexOf('.');
 496         return canonicalName.substring(index + 1);
 497     }
 498 
 499     public static class JImageTask {
 500 
 501         private final List<Path> pluginModulePath = new ArrayList<>();
 502         private final List<String> options = new ArrayList<>();
 503         private Path dir;
 504         private Path image;
 505 
 506         public JImageTask pluginModulePath(Path p) {
 507             this.pluginModulePath.add(p);
 508             return this;
 509         }
 510 
 511         public JImageTask image(Path image) {
 512             this.image = image;
 513             return this;
 514         }
 515 
 516         public JImageTask dir(Path dir) {
 517             this.dir = dir;
 518             return this;
 519         }
 520 
 521         public JImageTask option(String o) {
 522             this.options.add(o);
 523             return this;
 524         }
 525 
 526         private String toPath(List<Path> paths) {
 527             return paths.stream()
 528                     .map(Path::toString)
 529                     .collect(Collectors.joining(File.pathSeparator));
 530         }
 531 
 532         private String[] optionsJImage(String cmd) {
 533             List<String> options = new ArrayList<>();
 534             options.add(cmd);
 535             if (dir != null) {
 536                 options.add("--dir");
 537                 options.add(dir.toString());
 538             }
 539             if (!pluginModulePath.isEmpty()) {
 540                 options.add(PLUGIN_MODULE_PATH);
 541                 options.add(toPath(pluginModulePath));
 542             }
 543             options.addAll(this.options);
 544             options.add(image.toString());
 545             return options.toArray(new String[options.size()]);
 546         }
 547 
 548         private Result cmd(String cmd, Path returnPath) {
 549             String[] args = optionsJImage(cmd);
 550             System.err.println("jimage options: " + optionsPrettyPrint(args));
 551             StringWriter writer = new StringWriter();
 552             int exitCode = jdk.tools.jimage.Main.run(args, new PrintWriter(writer));
 553             return new Result(exitCode, writer.toString(), returnPath);
 554         }
 555 
 556         public Result extract() {
 557             return cmd("extract", dir);
 558         }
 559     }
 560 
 561     public static class JLinkTask {
 562         static final java.util.spi.ToolProvider JLINK_TOOL =
 563             java.util.spi.ToolProvider.findFirst("jlink").get();
 564 
 565         private final List<Path> jars = new ArrayList<>();
 566         private final List<Path> jmods = new ArrayList<>();
 567         private final List<Path> pluginModulePath = new ArrayList<>();
 568         private final List<String> addMods = new ArrayList<>();
 569         private final List<String> limitMods = new ArrayList<>();
 570         private final List<String> options = new ArrayList<>();
 571         private String modulePath;
 572         // if you want to specifiy repeated --module-path option
 573         private String repeatedModulePath;
 574         // if you want to specifiy repeated --limit-modules option
 575         private String repeatedLimitMods;
 576         private Path output;
 577         private Path existing;
 578 
 579         public JLinkTask modulePath(String modulePath) {
 580             this.modulePath = modulePath;
 581             return this;
 582         }
 583 
 584         public JLinkTask repeatedModulePath(String modulePath) {
 585             this.repeatedModulePath = modulePath;
 586             return this;
 587         }
 588 
 589         public JLinkTask addJars(Path jars) {
 590             this.jars.add(jars);
 591             return this;
 592         }
 593 
 594         public JLinkTask addJmods(Path jmods) {
 595             this.jmods.add(jmods);
 596             return this;
 597         }
 598 
 599         public JLinkTask pluginModulePath(Path p) {
 600             this.pluginModulePath.add(p);
 601             return this;
 602         }
 603 
 604         public JLinkTask addMods(String moduleName) {
 605             this.addMods.add(moduleName);
 606             return this;
 607         }
 608 
 609         public JLinkTask limitMods(String moduleName) {
 610             this.limitMods.add(moduleName);
 611             return this;
 612         }
 613 
 614         public JLinkTask repeatedLimitMods(String modules) {
 615             this.repeatedLimitMods = modules;
 616             return this;
 617         }
 618 
 619         public JLinkTask output(Path output) {
 620             this.output = output;
 621             return this;
 622         }
 623 
 624         public JLinkTask existing(Path existing) {
 625             this.existing = existing;
 626             return this;
 627         }
 628 
 629         public JLinkTask option(String o) {
 630             this.options.add(o);
 631             return this;
 632         }
 633 
 634         private String modulePath() {
 635             // This is expect FIRST jmods THEN jars, if you change this, some tests could fail
 636             String jmods = toPath(this.jmods);
 637             String jars = toPath(this.jars);
 638             return jmods + File.pathSeparator + jars;
 639         }
 640 
 641         private String toPath(List<Path> paths) {
 642             return paths.stream()
 643                     .map(Path::toString)
 644                     .collect(Collectors.joining(File.pathSeparator));
 645         }
 646 
 647         private String[] optionsJLink() {
 648             List<String> options = new ArrayList<>();
 649             if (output != null) {
 650                 options.add(OUTPUT_OPTION);
 651                 options.add(output.toString());
 652             }
 653             if (!addMods.isEmpty()) {
 654                 options.add(ADD_MODULES_OPTION);
 655                 options.add(addMods.stream().collect(Collectors.joining(",")));
 656             }
 657             if (!limitMods.isEmpty()) {
 658                 options.add(LIMIT_MODULES_OPTION);
 659                 options.add(limitMods.stream().collect(Collectors.joining(",")));
 660             }
 661             if (repeatedLimitMods != null) {
 662                 options.add(LIMIT_MODULES_OPTION);
 663                 options.add(repeatedLimitMods);
 664             }
 665             if (!jars.isEmpty() || !jmods.isEmpty()) {
 666                 options.add(MODULE_PATH_OPTION);
 667                 options.add(modulePath());
 668             }
 669             if (modulePath != null) {
 670                 options.add(MODULE_PATH_OPTION);
 671                 options.add(modulePath);
 672             }
 673             if (repeatedModulePath != null) {
 674                 options.add(MODULE_PATH_OPTION);
 675                 options.add(repeatedModulePath);
 676             }
 677             if (!pluginModulePath.isEmpty()) {
 678                 options.add(PLUGIN_MODULE_PATH);
 679                 options.add(toPath(pluginModulePath));
 680             }
 681             options.addAll(this.options);
 682             return options.toArray(new String[options.size()]);
 683         }
 684 
 685         private String[] optionsPostProcessJLink() {
 686             List<String> options = new ArrayList<>();
 687             if (existing != null) {
 688                 options.add(POST_PROCESS_OPTION);
 689                 options.add(existing.toString());
 690             }
 691             options.addAll(this.options);
 692             return options.toArray(new String[options.size()]);
 693         }
 694 
 695         public Result call() {
 696             String[] args = optionsJLink();
 697             System.err.println("jlink options: " + optionsPrettyPrint(args));
 698             StringWriter writer = new StringWriter();
 699             PrintWriter pw = new PrintWriter(writer);
 700             int exitCode = JLINK_TOOL.run(pw, pw, args);
 701             return new Result(exitCode, writer.toString(), output);
 702         }
 703 
 704         public Result callPostProcess() {
 705             String[] args = optionsPostProcessJLink();
 706             System.err.println("jlink options: " + optionsPrettyPrint(args));
 707             StringWriter writer = new StringWriter();
 708             PrintWriter pw = new PrintWriter(writer);
 709             int exitCode = JLINK_TOOL.run(pw, pw, args);
 710             return new Result(exitCode, writer.toString(), output);
 711         }
 712     }
 713 
 714     public static class InMemorySourceFile {
 715         public final String packageName;
 716         public final String className;
 717         public final String source;
 718 
 719         public InMemorySourceFile(String packageName, String simpleName, String source) {
 720             this.packageName = packageName;
 721             this.className = simpleName;
 722             this.source = source;
 723         }
 724     }
 725 
 726     public static class InMemoryFile {
 727         private final String path;
 728         private final byte[] bytes;
 729 
 730         public String getPath() {
 731             return path;
 732         }
 733 
 734         public InputStream getBytes() {
 735             return new ByteArrayInputStream(bytes);
 736         }
 737 
 738         public InMemoryFile(String path, byte[] bytes) {
 739             this.path = path;
 740             this.bytes = bytes;
 741         }
 742 
 743         public InMemoryFile(String path, InputStream is) throws IOException {
 744             this(path, readAllBytes(is));
 745         }
 746     }
 747 
 748     public static byte[] readAllBytes(InputStream is) throws IOException {
 749         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 750         byte[] buf = new byte[1024];
 751         while (true) {
 752             int n = is.read(buf);
 753             if (n < 0) {
 754                 break;
 755             }
 756             baos.write(buf, 0, n);
 757         }
 758         return baos.toByteArray();
 759     }
 760 }